From 799c3f544881306cb79609bf002140edb396da95 Mon Sep 17 00:00:00 2001 From: chrmc40 Date: Mon, 22 Jun 2026 20:18:51 -0700 Subject: [PATCH] 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 --- .comfyignore | 24 + .gitignore | 2 + __init__.py | 4 +- .../ComfyUI-Frame-Interpolation/LICENSE | 21 + .../ComfyUI-Frame-Interpolation/__init__.py | 22 + .../ComfyUI-Frame-Interpolation/config.yaml | 3 + .../other_nodes.py | 88 + .../pyproject.toml | 13 + .../requirements.txt | 1 + .../vfi_models/rife/__init__.py | 239 ++ .../vfi_models/rife/rife_arch.py | 732 ++++ .../ComfyUI-Frame-Interpolation/vfi_utils.py | 428 +++ .../ComfyUI-PuLID-Flux2/requirements.txt | 2 +- .../ComfyUI_PuLID_Flux_ll/requirements.txt | 2 +- bundled_packs/PuLID_ComfyUI/requirements.txt | 2 +- core/civitai.py | 144 +- core/extend_api.py | 110 + core/install_sections.py | 37 +- core/model_settings.py | 270 +- core/node_install_api.py | 158 +- core/subject_api.py | 16 +- core/thumbs.py | 107 +- data/models/ideogram_4.json | 12 +- data/models/wan_2_2_5b_5b.json | 141 +- data/models/wan_2_2_5b_5b_gguf.json | 52 + data/models/wan_2_2_i2v_14b.json | 187 +- data/models/wan_2_2_i2v_14b_gguf.json | 65 + data/models/wan_2_2_t2v_14b.json | 187 +- data/models/wan_2_2_t2v_14b_gguf.json | 65 + data/prompts/ideogram.json | 2 +- data/prompts/wan-video.json | 19 + data/templates/flux-t2i-regionbox.json | 92 + data/templates/ideogram-t2i-regionbox.json | 148 + data/templates/ideogram-t2i.json | 5 +- data/templates/sdxl-t2i-regionbox.json | 86 + data/templates/wan22-5b-i2v-gguf.json | 324 ++ data/templates/wan22-5b-i2v.json | 434 ++- data/templates/wan22-5b-t2v-gguf.json | 300 ++ data/templates/wan22-5b-t2v.json | 401 +- data/templates/wan22-flf2v-fast.json | 505 +++ data/templates/wan22-flf2v-gguf-fast.json | 503 +++ data/templates/wan22-flf2v-gguf.json | 455 +++ data/templates/wan22-flf2v.json | 457 +++ data/templates/wan22-i2v-fast.json | 479 +++ data/templates/wan22-i2v-gguf-fast.json | 477 +++ data/templates/wan22-i2v-gguf.json | 429 +++ data/templates/wan22-i2v.json | 584 ++- data/templates/wan22-t2v-fast.json | 437 +++ data/templates/wan22-t2v-gguf-fast.json | 435 +++ data/templates/wan22-t2v-gguf.json | 387 ++ data/templates/wan22-t2v.json | 531 ++- js/lib/asset-drop.js | 28 +- js/lib/extend-background.js | 152 + js/lib/extend-from-image.js | 247 ++ js/lib/model-bridge.js | 81 +- js/lib/model-indicator-bridge.js | 7 +- js/lib/model-settings-bridge.js | 66 +- js/lib/offscreen-graph.js | 29 + js/lib/region-box-registry.js | 62 + js/lib/regional-binding.js | 185 + js/lib/repose-from-image.js | 2 + js/lib/smooth-background.js | 147 + js/lib/smooth-from-image.js | 80 + ...0CC3-N.js => GeneratedGallery-D5XbkMk7.js} | 27 +- ...enubar-DI-z1NzN.js => Menubar-DC12j8gE.js} | 54 +- js/lib/svelte/assets/promptchain-svelte.css | 259 +- ...OP7d958.js => model-constants-cVxhUf51.js} | 31 +- js/lib/svelte/promptchain-fullscreen.js | 2 +- js/lib/svelte/promptchain-gallery.js | 2 +- js/lib/svelte/promptchain-model-indicator.js | 708 ++-- js/lib/svelte/promptchain-model-settings.js | 103 +- js/lib/svelte/promptchain-node.js | 407 +- js/lib/svelte/promptchain-sidebar.js | 60 +- js/lib/svelte/promptchain-viewer.js | 3286 ++++++++++++----- js/lib/viewer-bridge.js | 143 + js/main.js | 38 +- js/pose-studio.js | 120 +- js/region-box.js | 983 +++++ nodes/ideogram_caption.py | 278 +- nodes/region_box.py | 153 + nodes/regional_detailer.py | 6 +- pyproject.toml | 34 + requirements.txt | 27 + scripts/natlang_decompose_probe.py | 2 +- scripts/natlang_scene_composer.py | 2 +- svelte-src/components/ExtendModal.svelte | 350 ++ svelte-src/components/ImageViewer.svelte | 469 ++- .../components/LineageContextMenu.svelte | 4 +- svelte-src/components/SmoothModal.svelte | 170 + .../gallery/GeneratedGallery.svelte | 39 +- .../components/model/SettingsTab.svelte | 72 +- svelte-src/components/node/ImagePanel.svelte | 34 +- svelte-src/components/node/Menubar.svelte | 14 + svelte-src/components/node/NodeWidget.svelte | 14 + svelte-src/components/node/RegionPanel.svelte | 299 ++ .../picker/CatalogDownloadModal.svelte | 7 +- .../components/picker/ModelPicker.svelte | 266 +- svelte-src/components/sidebar/GridView.svelte | 14 +- .../components/sidebar/ItemThumbnail.svelte | 13 + svelte-src/components/sidebar/ListView.svelte | 13 + svelte-src/lib/lineage-lanes.js | 7 +- svelte-src/lib/model-constants.js | 48 +- svelte-src/node-main.svelte.js | 4 + tests/test_compact_history.py | 194 - tests/test_region_binding.py | 200 - vendor/rtmlib/visualization/draw.py | 2 +- 106 files changed, 17538 insertions(+), 3130 deletions(-) create mode 100644 .comfyignore create mode 100644 bundled_packs/ComfyUI-Frame-Interpolation/LICENSE create mode 100644 bundled_packs/ComfyUI-Frame-Interpolation/__init__.py create mode 100644 bundled_packs/ComfyUI-Frame-Interpolation/config.yaml create mode 100644 bundled_packs/ComfyUI-Frame-Interpolation/other_nodes.py create mode 100644 bundled_packs/ComfyUI-Frame-Interpolation/pyproject.toml create mode 100644 bundled_packs/ComfyUI-Frame-Interpolation/requirements.txt create mode 100644 bundled_packs/ComfyUI-Frame-Interpolation/vfi_models/rife/__init__.py create mode 100644 bundled_packs/ComfyUI-Frame-Interpolation/vfi_models/rife/rife_arch.py create mode 100644 bundled_packs/ComfyUI-Frame-Interpolation/vfi_utils.py create mode 100644 core/extend_api.py create mode 100644 data/models/wan_2_2_5b_5b_gguf.json create mode 100644 data/models/wan_2_2_i2v_14b_gguf.json create mode 100644 data/models/wan_2_2_t2v_14b_gguf.json create mode 100644 data/prompts/wan-video.json create mode 100644 data/templates/flux-t2i-regionbox.json create mode 100644 data/templates/ideogram-t2i-regionbox.json create mode 100644 data/templates/sdxl-t2i-regionbox.json create mode 100644 data/templates/wan22-5b-i2v-gguf.json create mode 100644 data/templates/wan22-5b-t2v-gguf.json create mode 100644 data/templates/wan22-flf2v-fast.json create mode 100644 data/templates/wan22-flf2v-gguf-fast.json create mode 100644 data/templates/wan22-flf2v-gguf.json create mode 100644 data/templates/wan22-flf2v.json create mode 100644 data/templates/wan22-i2v-fast.json create mode 100644 data/templates/wan22-i2v-gguf-fast.json create mode 100644 data/templates/wan22-i2v-gguf.json create mode 100644 data/templates/wan22-t2v-fast.json create mode 100644 data/templates/wan22-t2v-gguf-fast.json create mode 100644 data/templates/wan22-t2v-gguf.json create mode 100644 js/lib/extend-background.js create mode 100644 js/lib/extend-from-image.js create mode 100644 js/lib/offscreen-graph.js create mode 100644 js/lib/region-box-registry.js create mode 100644 js/lib/regional-binding.js create mode 100644 js/lib/smooth-background.js create mode 100644 js/lib/smooth-from-image.js rename js/lib/svelte/{GeneratedGallery-Dw0CC3-N.js => GeneratedGallery-D5XbkMk7.js} (97%) rename js/lib/svelte/{Menubar-DI-z1NzN.js => Menubar-DC12j8gE.js} (92%) rename js/lib/svelte/{model-constants-DOP7d958.js => model-constants-cVxhUf51.js} (67%) create mode 100644 js/region-box.js create mode 100644 nodes/region_box.py create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 svelte-src/components/ExtendModal.svelte create mode 100644 svelte-src/components/SmoothModal.svelte create mode 100644 svelte-src/components/node/RegionPanel.svelte delete mode 100644 tests/test_compact_history.py delete mode 100644 tests/test_region_binding.py diff --git a/.comfyignore b/.comfyignore new file mode 100644 index 0000000..74d2368 --- /dev/null +++ b/.comfyignore @@ -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 diff --git a/.gitignore b/.gitignore index ab96ff2..0baa87f 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/__init__.py b/__init__.py index 6e41740..93783fc 100644 --- a/__init__.py +++ b/__init__.py @@ -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: diff --git a/bundled_packs/ComfyUI-Frame-Interpolation/LICENSE b/bundled_packs/ComfyUI-Frame-Interpolation/LICENSE new file mode 100644 index 0000000..098ab64 --- /dev/null +++ b/bundled_packs/ComfyUI-Frame-Interpolation/LICENSE @@ -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. \ No newline at end of file diff --git a/bundled_packs/ComfyUI-Frame-Interpolation/__init__.py b/bundled_packs/ComfyUI-Frame-Interpolation/__init__.py new file mode 100644 index 0000000..3a3df6f --- /dev/null +++ b/bundled_packs/ComfyUI-Frame-Interpolation/__init__.py @@ -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+)" +} diff --git a/bundled_packs/ComfyUI-Frame-Interpolation/config.yaml b/bundled_packs/ComfyUI-Frame-Interpolation/config.yaml new file mode 100644 index 0000000..9302942 --- /dev/null +++ b/bundled_packs/ComfyUI-Frame-Interpolation/config.yaml @@ -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" \ No newline at end of file diff --git a/bundled_packs/ComfyUI-Frame-Interpolation/other_nodes.py b/bundled_packs/ComfyUI-Frame-Interpolation/other_nodes.py new file mode 100644 index 0000000..b52f913 --- /dev/null +++ b/bundled_packs/ComfyUI-Frame-Interpolation/other_nodes.py @@ -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) \ No newline at end of file diff --git a/bundled_packs/ComfyUI-Frame-Interpolation/pyproject.toml b/bundled_packs/ComfyUI-Frame-Interpolation/pyproject.toml new file mode 100644 index 0000000..3d4d6ae --- /dev/null +++ b/bundled_packs/ComfyUI-Frame-Interpolation/pyproject.toml @@ -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 = "" diff --git a/bundled_packs/ComfyUI-Frame-Interpolation/requirements.txt b/bundled_packs/ComfyUI-Frame-Interpolation/requirements.txt new file mode 100644 index 0000000..d27fa26 --- /dev/null +++ b/bundled_packs/ComfyUI-Frame-Interpolation/requirements.txt @@ -0,0 +1 @@ +einops diff --git a/bundled_packs/ComfyUI-Frame-Interpolation/vfi_models/rife/__init__.py b/bundled_packs/ComfyUI-Frame-Interpolation/vfi_models/rife/__init__.py new file mode 100644 index 0000000..ed4cc9b --- /dev/null +++ b/bundled_packs/ComfyUI-Frame-Interpolation/vfi_models/rife/__init__.py @@ -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),) diff --git a/bundled_packs/ComfyUI-Frame-Interpolation/vfi_models/rife/rife_arch.py b/bundled_packs/ComfyUI-Frame-Interpolation/vfi_models/rife/rife_arch.py new file mode 100644 index 0000000..f97a663 --- /dev/null +++ b/bundled_packs/ComfyUI-Frame-Interpolation/vfi_models/rife/rife_arch.py @@ -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] \ No newline at end of file diff --git a/bundled_packs/ComfyUI-Frame-Interpolation/vfi_utils.py b/bundled_packs/ComfyUI-Frame-Interpolation/vfi_utils.py new file mode 100644 index 0000000..3a981af --- /dev/null +++ b/bundled_packs/ComfyUI-Frame-Interpolation/vfi_utils.py @@ -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] """ \ No newline at end of file diff --git a/bundled_packs/ComfyUI-PuLID-Flux2/requirements.txt b/bundled_packs/ComfyUI-PuLID-Flux2/requirements.txt index 9a14607..e3a0c70 100644 --- a/bundled_packs/ComfyUI-PuLID-Flux2/requirements.txt +++ b/bundled_packs/ComfyUI-PuLID-Flux2/requirements.txt @@ -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 diff --git a/bundled_packs/ComfyUI_PuLID_Flux_ll/requirements.txt b/bundled_packs/ComfyUI_PuLID_Flux_ll/requirements.txt index db8c4fe..f8e2281 100644 --- a/bundled_packs/ComfyUI_PuLID_Flux_ll/requirements.txt +++ b/bundled_packs/ComfyUI_PuLID_Flux_ll/requirements.txt @@ -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 diff --git a/bundled_packs/PuLID_ComfyUI/requirements.txt b/bundled_packs/PuLID_ComfyUI/requirements.txt index ae3f1b1..22ef156 100644 --- a/bundled_packs/PuLID_ComfyUI/requirements.txt +++ b/bundled_packs/PuLID_ComfyUI/requirements.txt @@ -1,5 +1,5 @@ facexlib -insightface +insightface>=1.0 onnxruntime onnxruntime-gpu; sys_platform != 'darwin' and platform_machine == 'x86_64' ftfy diff --git a/core/civitai.py b/core/civitai.py index e02b5cb..c5a49b1 100644 --- a/core/civitai.py +++ b/core/civitai.py @@ -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: diff --git a/core/extend_api.py b/core/extend_api.py new file mode 100644 index 0000000..c7d7733 --- /dev/null +++ b/core/extend_api.py @@ -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 = 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, + }) diff --git a/core/install_sections.py b/core/install_sections.py index 44e8b9e..6e92111 100644 --- a/core/install_sections.py +++ b/core/install_sections.py @@ -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: diff --git a/core/model_settings.py b/core/model_settings.py index e070507..b7fbdea 100644 --- a/core/model_settings.py +++ b/core/model_settings.py @@ -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 diff --git a/core/node_install_api.py b/core/node_install_api.py index dab3726..c86c42c 100644 --- a/core/node_install_api.py +++ b/core/node_install_api.py @@ -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: diff --git a/core/subject_api.py b/core/subject_api.py index cdd6acc..fe049ce 100644 --- a/core/subject_api.py +++ b/core/subject_api.py @@ -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}) diff --git a/core/thumbs.py b/core/thumbs.py index 39c1035..8eb03f4 100644 --- a/core/thumbs.py +++ b/core/thumbs.py @@ -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 diff --git a/data/models/ideogram_4.json b/data/models/ideogram_4.json index 3d63d5e..343e7cc 100644 --- a/data/models/ideogram_4.json +++ b/data/models/ideogram_4.json @@ -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, diff --git a/data/models/wan_2_2_5b_5b.json b/data/models/wan_2_2_5b_5b.json index 03a6697..8b2fef9 100644 --- a/data/models/wan_2_2_5b_5b.json +++ b/data/models/wan_2_2_5b_5b.json @@ -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"] + } + } +} diff --git a/data/models/wan_2_2_5b_5b_gguf.json b/data/models/wan_2_2_5b_5b_gguf.json new file mode 100644 index 0000000..6b0c7aa --- /dev/null +++ b/data/models/wan_2_2_5b_5b_gguf.json @@ -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"] + } + } +} diff --git a/data/models/wan_2_2_i2v_14b.json b/data/models/wan_2_2_i2v_14b.json index 70990a6..0ee93c8 100644 --- a/data/models/wan_2_2_i2v_14b.json +++ b/data/models/wan_2_2_i2v_14b.json @@ -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"] + } + } +} diff --git a/data/models/wan_2_2_i2v_14b_gguf.json b/data/models/wan_2_2_i2v_14b_gguf.json new file mode 100644 index 0000000..43e1a63 --- /dev/null +++ b/data/models/wan_2_2_i2v_14b_gguf.json @@ -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"] + } + } +} diff --git a/data/models/wan_2_2_t2v_14b.json b/data/models/wan_2_2_t2v_14b.json index 6043ffa..67a0b7d 100644 --- a/data/models/wan_2_2_t2v_14b.json +++ b/data/models/wan_2_2_t2v_14b.json @@ -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"] + } + } +} diff --git a/data/models/wan_2_2_t2v_14b_gguf.json b/data/models/wan_2_2_t2v_14b_gguf.json new file mode 100644 index 0000000..10cb647 --- /dev/null +++ b/data/models/wan_2_2_t2v_14b_gguf.json @@ -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"] + } + } +} diff --git a/data/prompts/ideogram.json b/data/prompts/ideogram.json index da3ef4a..03e3ba3 100644 --- a/data/prompts/ideogram.json +++ b/data/prompts/ideogram.json @@ -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" diff --git a/data/prompts/wan-video.json b/data/prompts/wan-video.json new file mode 100644 index 0000000..6c78ae0 --- /dev/null +++ b/data/prompts/wan-video.json @@ -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" } + } + ] +} diff --git a/data/templates/flux-t2i-regionbox.json b/data/templates/flux-t2i-regionbox.json new file mode 100644 index 0000000..61fda71 --- /dev/null +++ b/data/templates/flux-t2i-regionbox.json @@ -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 } + ] +} diff --git a/data/templates/ideogram-t2i-regionbox.json b/data/templates/ideogram-t2i-regionbox.json new file mode 100644 index 0000000..bef6af5 --- /dev/null +++ b/data/templates/ideogram-t2i-regionbox.json @@ -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 } + ] +} diff --git a/data/templates/ideogram-t2i.json b/data/templates/ideogram-t2i.json index 7e8b1dd..017411f 100644 --- a/data/templates/ideogram-t2i.json +++ b/data/templates/ideogram-t2i.json @@ -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 } ] } diff --git a/data/templates/sdxl-t2i-regionbox.json b/data/templates/sdxl-t2i-regionbox.json new file mode 100644 index 0000000..e563f9f --- /dev/null +++ b/data/templates/sdxl-t2i-regionbox.json @@ -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 } + ] +} diff --git a/data/templates/wan22-5b-i2v-gguf.json b/data/templates/wan22-5b-i2v-gguf.json new file mode 100644 index 0000000..2078a20 --- /dev/null +++ b/data/templates/wan22-5b-i2v-gguf.json @@ -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 + } + ] +} diff --git a/data/templates/wan22-5b-i2v.json b/data/templates/wan22-5b-i2v.json index 24da32b..5d8a992 100644 --- a/data/templates/wan22-5b-i2v.json +++ b/data/templates/wan22-5b-i2v.json @@ -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 + } + ] +} diff --git a/data/templates/wan22-5b-t2v-gguf.json b/data/templates/wan22-5b-t2v-gguf.json new file mode 100644 index 0000000..18078ce --- /dev/null +++ b/data/templates/wan22-5b-t2v-gguf.json @@ -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 + } + ] +} diff --git a/data/templates/wan22-5b-t2v.json b/data/templates/wan22-5b-t2v.json index adf322d..e1b2354 100644 --- a/data/templates/wan22-5b-t2v.json +++ b/data/templates/wan22-5b-t2v.json @@ -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 + } + ] +} diff --git a/data/templates/wan22-flf2v-fast.json b/data/templates/wan22-flf2v-fast.json new file mode 100644 index 0000000..d9b22ec --- /dev/null +++ b/data/templates/wan22-flf2v-fast.json @@ -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 + } + ] +} \ No newline at end of file diff --git a/data/templates/wan22-flf2v-gguf-fast.json b/data/templates/wan22-flf2v-gguf-fast.json new file mode 100644 index 0000000..a90f045 --- /dev/null +++ b/data/templates/wan22-flf2v-gguf-fast.json @@ -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 + } + ] +} \ No newline at end of file diff --git a/data/templates/wan22-flf2v-gguf.json b/data/templates/wan22-flf2v-gguf.json new file mode 100644 index 0000000..59b25d4 --- /dev/null +++ b/data/templates/wan22-flf2v-gguf.json @@ -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 + } + ] +} \ No newline at end of file diff --git a/data/templates/wan22-flf2v.json b/data/templates/wan22-flf2v.json new file mode 100644 index 0000000..d4908a4 --- /dev/null +++ b/data/templates/wan22-flf2v.json @@ -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 + } + ] +} \ No newline at end of file diff --git a/data/templates/wan22-i2v-fast.json b/data/templates/wan22-i2v-fast.json new file mode 100644 index 0000000..c92acb4 --- /dev/null +++ b/data/templates/wan22-i2v-fast.json @@ -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 + } + ] +} diff --git a/data/templates/wan22-i2v-gguf-fast.json b/data/templates/wan22-i2v-gguf-fast.json new file mode 100644 index 0000000..5b18c1a --- /dev/null +++ b/data/templates/wan22-i2v-gguf-fast.json @@ -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 + } + ] +} diff --git a/data/templates/wan22-i2v-gguf.json b/data/templates/wan22-i2v-gguf.json new file mode 100644 index 0000000..ae0aaa1 --- /dev/null +++ b/data/templates/wan22-i2v-gguf.json @@ -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 + } + ] +} diff --git a/data/templates/wan22-i2v.json b/data/templates/wan22-i2v.json index a6adbeb..c604022 100644 --- a/data/templates/wan22-i2v.json +++ b/data/templates/wan22-i2v.json @@ -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 + } + ] +} diff --git a/data/templates/wan22-t2v-fast.json b/data/templates/wan22-t2v-fast.json new file mode 100644 index 0000000..5e17f7f --- /dev/null +++ b/data/templates/wan22-t2v-fast.json @@ -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 + } + ] +} diff --git a/data/templates/wan22-t2v-gguf-fast.json b/data/templates/wan22-t2v-gguf-fast.json new file mode 100644 index 0000000..592d48a --- /dev/null +++ b/data/templates/wan22-t2v-gguf-fast.json @@ -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 + } + ] +} diff --git a/data/templates/wan22-t2v-gguf.json b/data/templates/wan22-t2v-gguf.json new file mode 100644 index 0000000..0187068 --- /dev/null +++ b/data/templates/wan22-t2v-gguf.json @@ -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 + } + ] +} diff --git a/data/templates/wan22-t2v.json b/data/templates/wan22-t2v.json index ce0c99b..6c94233 100644 --- a/data/templates/wan22-t2v.json +++ b/data/templates/wan22-t2v.json @@ -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 + } + ] +} diff --git a/js/lib/asset-drop.js b/js/lib/asset-drop.js index 06ecb00..2639237 100644 --- a/js/lib/asset-drop.js +++ b/js/lib/asset-drop.js @@ -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)}` ); diff --git a/js/lib/extend-background.js b/js/lib/extend-background.js new file mode 100644 index 0000000..aa939fa --- /dev/null +++ b/js/lib/extend-background.js @@ -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; +} diff --git a/js/lib/extend-from-image.js b/js/lib/extend-from-image.js new file mode 100644 index 0000000..d88c863 --- /dev/null +++ b/js/lib/extend-from-image.js @@ -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 }; +} diff --git a/js/lib/model-bridge.js b/js/lib/model-bridge.js index 5fe7f92..d19456c 100644 --- a/js/lib/model-bridge.js +++ b/js/lib/model-bridge.js @@ -89,6 +89,39 @@ function ensureComboOption(widget, val) { } } +// A model filename is "-.", where 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; + } + } } diff --git a/js/lib/model-indicator-bridge.js b/js/lib/model-indicator-bridge.js index 7c4a358..6807408 100644 --- a/js/lib/model-indicator-bridge.js +++ b/js/lib/model-indicator-bridge.js @@ -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(); diff --git a/js/lib/model-settings-bridge.js b/js/lib/model-settings-bridge.js index 7dda018..689e457 100644 --- a/js/lib/model-settings-bridge.js +++ b/js/lib/model-settings-bridge.js @@ -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, diff --git a/js/lib/offscreen-graph.js b/js/lib/offscreen-graph.js new file mode 100644 index 0000000..8bd8ef0 --- /dev/null +++ b/js/lib/offscreen-graph.js @@ -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; +} diff --git a/js/lib/region-box-registry.js b/js/lib/region-box-registry.js new file mode 100644 index 0000000..b1008f1 --- /dev/null +++ b/js/lib/region-box-registry.js @@ -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); + }, +}; diff --git a/js/lib/regional-binding.js b/js/lib/regional-binding.js new file mode 100644 index 0000000..91af114 --- /dev/null +++ b/js/lib/regional-binding.js @@ -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); +} diff --git a/js/lib/repose-from-image.js b/js/lib/repose-from-image.js index fc5ca65..3828f57 100644 --- a/js/lib/repose-from-image.js +++ b/js/lib/repose-from-image.js @@ -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; diff --git a/js/lib/smooth-background.js b/js/lib/smooth-background.js new file mode 100644 index 0000000..6081f34 --- /dev/null +++ b/js/lib/smooth-background.js @@ -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; +} diff --git a/js/lib/smooth-from-image.js b/js/lib/smooth-from-image.js new file mode 100644 index 0000000..9cfed5a --- /dev/null +++ b/js/lib/smooth-from-image.js @@ -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 }; +} diff --git a/js/lib/svelte/GeneratedGallery-Dw0CC3-N.js b/js/lib/svelte/GeneratedGallery-D5XbkMk7.js similarity index 97% rename from js/lib/svelte/GeneratedGallery-Dw0CC3-N.js rename to js/lib/svelte/GeneratedGallery-D5XbkMk7.js index bd98fdf..f4b7a53 100644 --- a/js/lib/svelte/GeneratedGallery-Dw0CC3-N.js +++ b/js/lib/svelte/GeneratedGallery-D5XbkMk7.js @@ -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 diff --git a/js/lib/svelte/Menubar-DI-z1NzN.js b/js/lib/svelte/Menubar-DC12j8gE.js similarity index 92% rename from js/lib/svelte/Menubar-DI-z1NzN.js rename to js/lib/svelte/Menubar-DC12j8gE.js index fee1a6e..301dc86 100644 --- a/js/lib/svelte/Menubar-DI-z1NzN.js +++ b/js/lib/svelte/Menubar-DC12j8gE.js @@ -283,7 +283,7 @@ function ModeMenu($$anchor, $$props) { delegate(["click"]); var root_1 = from_html(``); var root_2 = from_html(``); -var root_3 = from_html(` `, 1); +var root_3 = from_html(` `, 1); var root_4 = from_html(` `, 1); var root = from_html(`
`, 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, () => '', true); - action(span_6, ($$node, $$action_arg) => actionClick == null ? void 0 : actionClick($$node, $$action_arg), () => "image"); + html(span_6, () => '', 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, () => '', true); - action(span_7, ($$node, $$action_arg) => actionClick == null ? void 0 : actionClick($$node, $$action_arg), () => "output"); + html(span_7, () => '', 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, () => '', 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, () => '', 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, () => '', 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, () => '', 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, () => '', 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 diff --git a/js/lib/svelte/assets/promptchain-svelte.css b/js/lib/svelte/assets/promptchain-svelte.css index cb25f1b..57d556c 100644 --- a/js/lib/svelte/assets/promptchain-svelte.css +++ b/js/lib/svelte/assets/promptchain-svelte.css @@ -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 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; diff --git a/js/lib/svelte/model-constants-DOP7d958.js b/js/lib/svelte/model-constants-cVxhUf51.js similarity index 67% rename from js/lib/svelte/model-constants-DOP7d958.js rename to js/lib/svelte/model-constants-cVxhUf51.js index 628266b..77cb6e2 100644 --- a/js/lib/svelte/model-constants-DOP7d958.js +++ b/js/lib/svelte/model-constants-cVxhUf51.js @@ -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 diff --git a/js/lib/svelte/promptchain-fullscreen.js b/js/lib/svelte/promptchain-fullscreen.js index 30ab2d8..32c57b5 100644 --- a/js/lib/svelte/promptchain-fullscreen.js +++ b/js/lib/svelte/promptchain-fullscreen.js @@ -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(``); var root$5 = from_html(`
Run 0 active
`); function Topbar($$anchor, $$props) { diff --git a/js/lib/svelte/promptchain-gallery.js b/js/lib/svelte/promptchain-gallery.js index bbbfc2e..70c9827 100644 --- a/js/lib/svelte/promptchain-gallery.js +++ b/js/lib/svelte/promptchain-gallery.js @@ -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 }); } diff --git a/js/lib/svelte/promptchain-model-indicator.js b/js/lib/svelte/promptchain-model-indicator.js index c62657b..d1e1c60 100644 --- a/js/lib/svelte/promptchain-model-indicator.js +++ b/js/lib/svelte/promptchain-model-indicator.js @@ -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(`
`); @@ -104,31 +104,34 @@ var root_1$3 = from_html(`
Loading...
`); var root_3$3 = from_html(`
`); var root_5$3 = from_html(` `); var root_6$3 = from_html(` `); -var root_4$2 = from_html(`
▸
`); var root_8$2 = from_html(` `); -var root_7$2 = from_html(`
›
`); -var root_11$1 = from_html(` `); -var root_12 = from_html(` `); -var root_10$2 = from_html(`
`); -var root_9$2 = from_html(`
AVAILABLE FOR DOWNLOAD
`, 1); -var root_14 = from_html(`
↓
`); -var root_15 = from_html(`
Searching CivitAI…
`); -var root_16 = from_html(`
Loading more…
`); -var root_17 = from_html(`
No results on CivitAI
`); -var root_13 = from_html(`
CIVITAI
`, 1); -var root_18 = from_html(`
No matches
`); +var root_7$2 = from_html(`
`); +var root_4$2 = from_html(`
▸
`); +var root_10$2 = from_html(` `); +var root_9$2 = from_html(`
›
`); +var root_13 = from_html(` `); +var root_15 = from_html(` resume ↻`, 1); +var root_14 = from_html(` `); +var root_12 = from_html(`
`); +var root_11$1 = from_html(`
AVAILABLE FOR DOWNLOAD
`, 1); +var root_17 = from_html(`
↓
`); +var root_18 = from_html(`
Searching CivitAI…
`); +var root_19 = from_html(`
Loading more…
`); +var root_20 = from_html(`
No results on CivitAI
`); +var root_16 = from_html(`
CIVITAI
`, 1); +var root_21 = from_html(`
No matches
`); var root_2$3 = from_html(`
`, 1); -var root_20 = from_html(`
▶
`); -var root_19 = from_html(`
SELECT VERSION
`); -var root_22 = from_html(`
↓
`); -var root_21 = from_html(`
SELECT VERSION
`); -var root_25 = from_html(`
TEMPLATES
`); -var root_24 = from_html(`
(Current Workflow)
`, 1); -var root_29 = from_html(`
`); -var root_30 = from_html(`
`); -var root_28 = from_html(` `, 1); -var root_31 = from_html(`
No templates available
`); -var root_23 = from_html(`
`); +var root_23 = from_html(`
▶
`); +var root_22 = from_html(`
SELECT VERSION
`); +var root_25 = from_html(`
↓
`); +var root_24 = from_html(`
SELECT VERSION
`); +var root_28 = from_html(`
TEMPLATES
`); +var root_27 = from_html(`
(Current Workflow)
`, 1); +var root_32 = from_html(`
`); +var root_33 = from_html(`
`); +var root_31 = from_html(` `, 1); +var root_34 = from_html(`
No templates available
`); +var root_26 = from_html(`
`); var root$3 = from_html(`
`, 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(`
`, 1); +var root_2$9 = from_html(`
`); +var root$a = from_html(`
`, 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(``); -var root_2$7 = from_html(`
`); -var root_4$5 = from_html(` `); -var root_3$4 = from_html(`
`); -var root_5$4 = from_html(` `); -var root$8 = from_html(`
`, 1); +var root_1$5 = from_html(``, 2); +var root_2$8 = from_html(``); +var root_3$4 = from_html(`
`); +var root_5$4 = from_html(` `); +var root_4$5 = from_html(`
`); +var root_6$3 = from_html(` `); +var root$9 = from_html(`
`, 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(`
No 3D Poser node in the graph
`); -var root$7 = from_html(`
`, 1); +var root_2$7 = from_html(`
No 3D Poser node in the graph
`); +var root$8 = from_html(`
`, 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(`
No Region Box node in the graph
`); +var root$7 = from_html(`
`, 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(`
`, 1); +var root = from_html(`
`, 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(); diff --git a/js/lib/svelte/promptchain-sidebar.js b/js/lib/svelte/promptchain-sidebar.js index 11f7bb4..c77b542 100644 --- a/js/lib/svelte/promptchain-sidebar.js +++ b/js/lib/svelte/promptchain-sidebar.js @@ -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(); diff --git a/js/lib/svelte/promptchain-viewer.js b/js/lib/svelte/promptchain-viewer.js index 065112e..72a2401 100644 --- a/js/lib/svelte/promptchain-viewer.js +++ b/js/lib/svelte/promptchain-viewer.js @@ -75,11 +75,11 @@ async function saveModalSetup(fetchApi, hash, kind, data, dims = null, planes = return false; } } -var root_1$7 = from_html(``); -var root_3$8 = from_html(` /`, 1); -var root_2$8 = from_html(` `, 1); -var root_5$7 = from_html(`
`); -var root_4$7 = from_html(`
`); +var root_1$9 = from_html(``); +var root_3$a = from_html(` /`, 1); +var root_2$a = from_html(` `, 1); +var root_5$9 = from_html(`
`); +var root_4$9 = from_html(`
`); var root$2 = from_html(`
`); function SavePathInput($$anchor, $$props) { push($$props, true); @@ -237,7 +237,7 @@ function SavePathInput($$anchor, $$props) { var node = child(div_1); { var consequent = ($$anchor2) => { - var input = root_1$7(); + var input = root_1$9(); bind_this(input, ($$value) => editEl = $$value, () => editEl); template_effect(() => set_value(input, value())); delegated("input", input, (e) => { @@ -280,10 +280,10 @@ function SavePathInput($$anchor, $$props) { append($$anchor2, input); }; var alternate = ($$anchor2) => { - var fragment = root_2$8(); + var fragment = root_2$a(); var node_1 = first_child(fragment); each(node_1, 17, () => get(chips), index, ($$anchor3, chip, i) => { - var fragment_1 = root_3$8(); + var fragment_1 = root_3$a(); var button = first_child(fragment_1); let classes_1; var text_1 = child(button); @@ -329,9 +329,9 @@ function SavePathInput($$anchor, $$props) { var node_2 = sibling(div_1, 2); { var consequent_1 = ($$anchor2) => { - var div_2 = root_4$7(); + var div_2 = root_4$9(); each(div_2, 21, () => get(matches), index, ($$anchor3, m, i) => { - var div_3 = root_5$7(); + var div_3 = root_5$9(); let classes_2; var text_2 = sibling(child(div_3)); template_effect(() => { @@ -358,12 +358,12 @@ function SavePathInput($$anchor, $$props) { pop(); } delegate(["click", "input", "keydown", "mousedown"]); -var root_3$7 = from_html(`
No matching models
`); -var root_6$6 = from_html(`
`); -var root_8$6 = from_html(``); -var root_7$6 = from_html(`
`); -var root_5$6 = from_html(` `, 1); -var root_2$7 = from_html(`
`, 1); +var root_3$9 = from_html(`
No matching models
`); +var root_6$8 = from_html(`
`); +var root_8$8 = from_html(``); +var root_7$8 = from_html(`
`); +var root_5$8 = from_html(` `, 1); +var root_2$9 = from_html(`
`, 1); var root$1 = from_html(` `, 1); function SearchableSelect($$anchor, $$props) { push($$props, true); @@ -478,7 +478,7 @@ function SearchableSelect($$anchor, $$props) { set(open, false); }, children: ($$anchor3, $$slotProps) => { - var fragment_2 = root_2$7(); + var fragment_2 = root_2$9(); var div = first_child(fragment_2); var input = child(div); bind_this(input, ($$value) => set(searchEl, $$value), () => get(searchEl)); @@ -486,18 +486,18 @@ function SearchableSelect($$anchor, $$props) { var node_1 = child(div_1); { var consequent = ($$anchor4) => { - var div_2 = root_3$7(); + var div_2 = root_3$9(); append($$anchor4, div_2); }; var alternate = ($$anchor4) => { var fragment_3 = comment(); var node_2 = first_child(fragment_3); each(node_2, 17, () => get(filteredGroups), index, ($$anchor5, g) => { - var fragment_4 = root_5$6(); + var fragment_4 = root_5$8(); var node_3 = first_child(fragment_4); { var consequent_1 = ($$anchor6) => { - var div_3 = root_6$6(); + var div_3 = root_6$8(); var text_2 = child(div_3); template_effect(() => set_text(text_2, get(g).label)); append($$anchor6, div_3); @@ -509,14 +509,14 @@ function SearchableSelect($$anchor, $$props) { var node_4 = sibling(node_3, 2); each(node_4, 17, () => get(g).options, index, ($$anchor6, opt) => { const i = user_derived(() => get(flat).indexOf(get(opt))); - var div_4 = root_7$6(); + var div_4 = root_7$8(); let classes; var span_2 = child(div_4); html(span_2, () => highlightText(get(opt).label), true); var node_5 = sibling(span_2, 2); { var consequent_2 = ($$anchor7) => { - var span_3 = root_8$6(); + var span_3 = root_8$8(); span_3.textContent = "✓"; append($$anchor7, span_3); }; @@ -568,29 +568,29 @@ function SearchableSelect($$anchor, $$props) { pop(); } delegate(["click", "keydown"]); -var root_2$6 = from_html(``); -var root_3$6 = from_html(``); -var root_5$5 = from_html(`
Before
After
`, 1); -var root_7$5 = from_html(``); -var root_6$5 = from_html(`
`); -var root_4$6 = from_html(`
`, 1); -var root_9$4 = from_html(`
`); -var root_10$4 = from_html(`
`); -var root_12$4 = from_html(`
`); -var root_11$3 = from_html(`
`, 1); -var root_8$5 = from_html(`
`); -var root_13$4 = from_html(``); -var root_14$4 = from_html(`

This image has no usable prompt metadata — a plain model upscale (ESRGAN) will be used.

`); -var root_16$4 = from_html(` `); +var root_2$8 = from_html(``); +var root_3$8 = from_html(``); +var root_5$7 = from_html(`
Before
After
`, 1); +var root_7$7 = from_html(``); +var root_6$7 = from_html(`
`); +var root_4$8 = from_html(`
`, 1); +var root_9$6 = from_html(`
`); +var root_10$6 = from_html(`
`); +var root_12$6 = from_html(`
`); +var root_11$5 = from_html(`
`, 1); +var root_8$7 = from_html(`
`); +var root_13$6 = from_html(``); +var root_14$6 = from_html(`

This image has no usable prompt metadata — a plain model upscale (ESRGAN) will be used.

`); +var root_16$5 = from_html(` `); var root_17$4 = from_html(`re-renders the whole frame at ~2MP from your instruction — composition can shift slightly; the climb model below pushes to the target size`); -var root_18$3 = from_html(`deterministic enlargement with the climb model below — add a Restore step for degraded sources (webcam, jpeg, blur)`); -var root_15$4 = from_html(`
Engine
`); +var root_18$4 = from_html(`deterministic enlargement with the climb model below — add a Restore step for degraded sources (webcam, jpeg, blur)`); +var root_15$5 = from_html(`
Engine
`); var root_20$3 = from_html(``); var root_21$3 = from_html(`
Depth from
3D pose
This image
`); var root_19$4 = from_html(`
Mode
`); var root_23$2 = from_html(``); var root_24$3 = from_html(``); -var root_22$3 = from_html(`
Character lock
keeps the character from drifting while tiles re-detail — the image itself is the reference
`); +var root_22$2 = from_html(`
Character lock
keeps the character from drifting while tiles re-detail — the image itself is the reference
`); var root_27$3 = from_html(`
`); var root_28$2 = from_html(``); var root_29$3 = from_html(`Workflow prompt (reference) `, 1); @@ -620,7 +620,7 @@ var root_54$2 = from_html(` `, 1); var root_56$2 = from_html(``); var root_55$2 = from_html(` `, 1); -var root_1$6 = from_html(`
`); +var root_1$8 = from_html(`
`); function UpscaleOptionsModal($$anchor, $$props) { push($$props, true); let caps = prop($$props, "caps", 3, null), fetchApi = prop($$props, "fetchApi", 3, null), filename = prop($$props, "filename", 3, ""), width = prop($$props, "width", 3, 0), height = prop($$props, "height", 3, 0), previewUrl = prop($$props, "previewUrl", 3, ""), progress = prop($$props, "progress", 3, null), onCancelRun = prop($$props, "onCancelRun", 3, null), onViewResult = prop($$props, "onViewResult", 3, null), onUseInEdit = prop( @@ -1377,7 +1377,7 @@ function UpscaleOptionsModal($$anchor, $$props) { var node_1 = first_child(fragment); { var consequent_43 = ($$anchor2) => { - var div = root_1$6(); + var div = root_1$8(); let styles; var div_1 = child(div); var div_2 = child(div_1); @@ -1386,7 +1386,7 @@ function UpscaleOptionsModal($$anchor, $$props) { var node_2 = sibling(span, 2); { var consequent = ($$anchor3) => { - var button = root_2$6(); + var button = root_2$8(); delegated("click", button, function(...$$args) { var _a; (_a = $$props.onCancel) == null ? void 0 : _a.apply(this, $$args); @@ -1404,18 +1404,18 @@ function UpscaleOptionsModal($$anchor, $$props) { var node_3 = child(div_5); { var consequent_1 = ($$anchor3) => { - var img = root_3$6(); + var img = root_3$8(); template_effect(() => set_attribute(img, "src", get(liveTile))); append($$anchor3, img); }; var consequent_5 = ($$anchor3) => { - var fragment_1 = root_4$6(); + var fragment_1 = root_4$8(); var div_6 = first_child(fragment_1); var img_1 = child(div_6); var node_4 = sibling(div_6, 2); { var consequent_2 = ($$anchor4) => { - var fragment_2 = root_5$5(); + var fragment_2 = root_5$7(); var div_7 = first_child(fragment_2); var div_8 = child(div_7); var img_2 = child(div_8); @@ -1441,13 +1441,13 @@ function UpscaleOptionsModal($$anchor, $$props) { var node_5 = sibling(node_4, 2); { var consequent_4 = ($$anchor4) => { - var div_10 = root_6$5(); + var div_10 = root_6$7(); var span_1 = child(div_10); var text_2 = child(span_1); var node_6 = sibling(span_1, 2); { var consequent_3 = ($$anchor5) => { - var button_1 = root_7$5(); + var button_1 = root_7$7(); let classes_1; template_effect(() => classes_1 = set_class(button_1, 1, "pcr-up-fit-btn svelte-xw7bpl", null, classes_1, { on: get(compareSplit) })); delegated("click", button_1, toggleCompare); @@ -1482,23 +1482,23 @@ function UpscaleOptionsModal($$anchor, $$props) { var node_7 = sibling(div_5, 2); { var consequent_9 = ($$anchor3) => { - var div_11 = root_8$5(); + var div_11 = root_8$7(); var node_8 = child(div_11); { var consequent_6 = ($$anchor4) => { - var div_12 = root_9$4(); + var div_12 = root_9$6(); var text_3 = child(div_12); template_effect(() => set_text(text_3, `Done${progress().filename ? ` — ${progress().filename}` : ""}`)); append($$anchor4, div_12); }; var consequent_7 = ($$anchor4) => { - var div_13 = root_10$4(); + var div_13 = root_10$6(); var text_4 = child(div_13); template_effect(() => set_text(text_4, progress().message || "Failed")); append($$anchor4, div_13); }; var alternate = ($$anchor4) => { - var fragment_3 = root_11$3(); + var fragment_3 = root_11$5(); var div_14 = first_child(fragment_3); var text_5 = child(div_14); var div_15 = sibling(div_14, 2); @@ -1507,7 +1507,7 @@ function UpscaleOptionsModal($$anchor, $$props) { var node_9 = sibling(div_15, 2); { var consequent_8 = ($$anchor5) => { - var div_17 = root_12$4(); + var div_17 = root_12$6(); var text_6 = child(div_17); template_effect(() => set_text(text_6, get(progressLabel))); append($$anchor5, div_17); @@ -1540,7 +1540,7 @@ function UpscaleOptionsModal($$anchor, $$props) { var node_10 = child(div_18); { var consequent_10 = ($$anchor3) => { - var button_3 = root_13$4(); + var button_3 = root_13$6(); delegated("click", button_3, applySavedSetup); append($$anchor3, button_3); }; @@ -1551,7 +1551,7 @@ function UpscaleOptionsModal($$anchor, $$props) { var node_11 = sibling(node_10, 2); { var consequent_11 = ($$anchor3) => { - var p = root_14$4(); + var p = root_14$6(); append($$anchor3, p); }; if_block(node_11, ($$render) => { @@ -1562,7 +1562,7 @@ function UpscaleOptionsModal($$anchor, $$props) { var node_12 = sibling(node_11, 2); { var consequent_15 = ($$anchor3) => { - var div_19 = root_15$4(); + var div_19 = root_15$5(); var div_20 = sibling(child(div_19), 2); var node_13 = child(div_20); SearchableSelect(node_13, { @@ -1582,7 +1582,7 @@ function UpscaleOptionsModal($$anchor, $$props) { var node_14 = sibling(node_13, 2); { var consequent_12 = ($$anchor4) => { - var span_2 = root_16$4(); + var span_2 = root_16$5(); var text_7 = child(span_2); template_effect(() => set_text(text_7, get(engineKind) === "flux1" ? "the climb model below pushes to the target, then this FLUX.1 model re-details every tile with the prompt below — low denoise holds structure without a ControlNet" : "the climb model below pushes to the target, then this checkpoint re-details every tile (structure locked by a tile ControlNet) with the prompt below")); append($$anchor4, span_2); @@ -1592,7 +1592,7 @@ function UpscaleOptionsModal($$anchor, $$props) { append($$anchor4, span_3); }; var consequent_14 = ($$anchor4) => { - var span_4 = root_18$3(); + var span_4 = root_18$4(); append($$anchor4, span_4); }; if_block(node_14, ($$render) => { @@ -1681,7 +1681,7 @@ function UpscaleOptionsModal($$anchor, $$props) { var node_17 = sibling(node_15, 2); { var consequent_20 = ($$anchor3) => { - var div_27 = root_22$3(); + var div_27 = root_22$2(); var div_28 = sibling(child(div_27), 2); var select_1 = child(div_28); var option_1 = child(select_1); @@ -2353,30 +2353,30 @@ delegate([ "contextmenu", "change" ]); -var root_2$5 = from_html(``); -var root_3$5 = from_html(`
`, 1); -var root_4$5 = from_html(`
`); -var root_5$4 = from_html(`
`); -var root_7$4 = from_html(`
`); -var root_9$3 = from_html(`
Before
After
`, 1); -var root_8$4 = from_html(`
`, 1); -var root_10$3 = from_html(`
`); -var root_12$3 = from_html(` `); -var root_11$2 = from_html(`
`); -var root_6$4 = from_html(` `, 1); -var root_13$3 = from_html(``); -var root_15$3 = from_html(`
renders the painted region with this checkpoint — no render metadata needed, and PuLID / Style Reference work on any source
`); -var root_16$3 = from_html(`
re-renders the painted region with this FLUX.1 model at the denoise below — describe the content, strong on faces and eyes
`); +var root_2$7 = from_html(``); +var root_3$7 = from_html(`
`, 1); +var root_4$7 = from_html(`
`); +var root_5$6 = from_html(`
`); +var root_7$6 = from_html(`
`); +var root_9$5 = from_html(`
Before
After
`, 1); +var root_8$6 = from_html(`
`, 1); +var root_10$5 = from_html(`
`); +var root_12$5 = from_html(` `); +var root_11$4 = from_html(`
`); +var root_6$6 = from_html(` `, 1); +var root_13$5 = from_html(``); +var root_15$4 = from_html(`
renders the painted region with this checkpoint — no render metadata needed, and PuLID / Style Reference work on any source
`); +var root_16$4 = from_html(`
re-renders the painted region with this FLUX.1 model at the denoise below — describe the content, strong on faces and eyes
`); var root_17$3 = from_html(`
follows your instruction inside the mask only — unmasked pixels stay untouched; the region renders at full model resolution
`); -var root_14$3 = from_html(`
Engine
`); +var root_14$5 = from_html(`
Engine
`); var root_19$3 = from_html(`
Installing pack…
`); var root_21$2 = from_html(`reference`); -var root_22$2 = from_html(`this image this image (default)`, 1); +var root_22$1 = from_html(`this image this image (default)`, 1); var root_25$2 = from_html(``); var root_24$2 = from_html(`
Start
End
`, 1); var root_26$2 = from_html(`
Fidelity
Start
End
`, 1); var root_20$2 = from_html(`
Strength
`, 1); -var root_18$2 = from_html(`
Condition
`); +var root_18$3 = from_html(`
Condition
`); var root_28$1 = from_html(``); var root_27$2 = from_html(`
Mode
`); var root_30$2 = from_html(` `); @@ -2396,7 +2396,7 @@ var root_43$2 = from_html(``); var root_46$2 = from_html(``); var root_44$2 = from_html(` `, 1); -var root_1$5 = from_html(`
`); +var root_1$7 = from_html(`
`); function InpaintModal($$anchor, $$props) { push($$props, true); let sourceUrl = prop($$props, "sourceUrl", 3, ""), width = prop($$props, "width", 3, 0), height = prop($$props, "height", 3, 0), filename = prop($$props, "filename", 3, ""), caps = prop($$props, "caps", 3, null), prefillPrompt = prop($$props, "prefillPrompt", 3, ""), referencePrompt = prop($$props, "referencePrompt", 3, ""), fetchApi = prop($$props, "fetchApi", 3, null), apiURL = prop($$props, "apiURL", 3, (p) => p), onRun = prop( @@ -3316,7 +3316,7 @@ function InpaintModal($$anchor, $$props) { var node_1 = first_child(fragment); { var consequent_32 = ($$anchor2) => { - var div = root_1$5(); + var div = root_1$7(); let styles; var div_1 = child(div); var div_2 = child(div_1); @@ -3325,7 +3325,7 @@ function InpaintModal($$anchor, $$props) { var node_2 = sibling(span, 2); { var consequent = ($$anchor3) => { - var button = root_2$5(); + var button = root_2$7(); delegated("click", button, close); append($$anchor3, button); }; @@ -3344,7 +3344,7 @@ function InpaintModal($$anchor, $$props) { var node_3 = sibling(div_6, 2); { var consequent_1 = ($$anchor3) => { - var fragment_1 = root_3$5(); + var fragment_1 = root_3$7(); var label = first_child(fragment_1); var input = sibling(child(label)); var div_9 = sibling(label, 2); @@ -3374,7 +3374,7 @@ function InpaintModal($$anchor, $$props) { append($$anchor3, fragment_1); }; var alternate = ($$anchor3) => { - var div_11 = root_4$5(); + var div_11 = root_4$7(); var button_5 = child(div_11); var button_6 = sibling(button_5, 2); let classes_4; @@ -3402,7 +3402,7 @@ function InpaintModal($$anchor, $$props) { var node_4 = sibling(div_13, 2); { var consequent_2 = ($$anchor3) => { - var div_14 = root_5$4(); + var div_14 = root_5$6(); let classes_6; template_effect(() => { classes_6 = set_class(div_14, 1, "pcr-ip-brush-ring svelte-ch4fw9", null, classes_6, { eraser: get(eraser) }); @@ -3417,23 +3417,23 @@ function InpaintModal($$anchor, $$props) { var node_5 = sibling(node_4, 2); { var consequent_8 = ($$anchor3) => { - var fragment_2 = root_6$4(); + var fragment_2 = root_6$6(); var node_6 = first_child(fragment_2); { var consequent_3 = ($$anchor4) => { - var div_15 = root_7$4(); + var div_15 = root_7$6(); var img_2 = child(div_15); template_effect(() => set_attribute(img_2, "src", get(progress).previewUrl)); append($$anchor4, div_15); }; var consequent_5 = ($$anchor4) => { - var fragment_3 = root_8$4(); + var fragment_3 = root_8$6(); var div_16 = first_child(fragment_3); var img_3 = child(div_16); var node_7 = sibling(div_16, 2); { var consequent_4 = ($$anchor5) => { - var fragment_4 = root_9$3(); + var fragment_4 = root_9$5(); var div_17 = first_child(fragment_4); var div_18 = child(div_17); var img_4 = child(div_18); @@ -3465,7 +3465,7 @@ function InpaintModal($$anchor, $$props) { append($$anchor4, fragment_3); }; var alternate_1 = ($$anchor4) => { - var div_20 = root_10$3(); + var div_20 = root_10$5(); var div_21 = child(div_20); var text_2 = child(div_21); template_effect(() => set_text(text_2, get(running) ? "Waiting for the sampler…" : "No output yet — paint a mask and Apply.")); @@ -3481,14 +3481,14 @@ function InpaintModal($$anchor, $$props) { var node_8 = sibling(node_6, 2); { var consequent_7 = ($$anchor4) => { - var div_22 = root_11$2(); + var div_22 = root_11$4(); var div_23 = child(div_22); var div_24 = child(div_23); let classes_7; var node_9 = sibling(div_23, 2); { var consequent_6 = ($$anchor5) => { - var span_1 = root_12$3(); + var span_1 = root_12$5(); var text_3 = child(span_1); template_effect(() => set_text(text_3, `step ${get(progress).value ?? ""}/${get(progress).max ?? ""}`)); append($$anchor5, span_1); @@ -3518,7 +3518,7 @@ function InpaintModal($$anchor, $$props) { var node_10 = child(div_25); { var consequent_9 = ($$anchor3) => { - var button_7 = root_13$3(); + var button_7 = root_13$5(); delegated("click", button_7, applySavedSetup); append($$anchor3, button_7); }; @@ -3529,7 +3529,7 @@ function InpaintModal($$anchor, $$props) { var node_11 = sibling(node_10, 2); { var consequent_13 = ($$anchor3) => { - var div_26 = root_14$3(); + var div_26 = root_14$5(); var node_12 = sibling(child(div_26), 2); { let $0 = user_derived(() => get(installing) || get(running)); @@ -3551,11 +3551,11 @@ function InpaintModal($$anchor, $$props) { var node_13 = sibling(node_12, 2); { var consequent_10 = ($$anchor4) => { - var div_27 = root_15$3(); + var div_27 = root_15$4(); append($$anchor4, div_27); }; var consequent_11 = ($$anchor4) => { - var div_28 = root_16$3(); + var div_28 = root_16$4(); append($$anchor4, div_28); }; var consequent_12 = ($$anchor4) => { @@ -3578,7 +3578,7 @@ function InpaintModal($$anchor, $$props) { var node_14 = sibling(node_11, 2); { var consequent_19 = ($$anchor3) => { - var div_30 = root_18$2(); + var div_30 = root_18$3(); var select = sibling(child(div_30), 2); var option = child(select); option.value = option.__value = "none"; @@ -3606,7 +3606,7 @@ function InpaintModal($$anchor, $$props) { append($$anchor5, img_5); }; var alternate_2 = ($$anchor5) => { - var fragment_6 = root_22$2(); + var fragment_6 = root_22$1(); var img_6 = first_child(fragment_6); template_effect(() => set_attribute(img_6, "src", sourceUrl())); append($$anchor5, fragment_6); @@ -4203,10 +4203,10 @@ delegate([ "contextmenu", "change" ]); -var root_4$4 = from_html(`
`); -var root_3$4 = from_html(`
`); -var root_2$4 = from_html(`
`); -var root_1$4 = from_html(`
`); +var root_4$6 = from_html(`
`); +var root_3$6 = from_html(`
`); +var root_2$6 = from_html(`
`); +var root_1$6 = from_html(`
`); function AdjustModal($$anchor, $$props) { push($$props, true); let sourceCanvas = prop( @@ -4865,7 +4865,7 @@ function AdjustModal($$anchor, $$props) { var node = first_child(fragment); { var consequent_1 = ($$anchor2) => { - var div = root_1$4(); + var div = root_1$6(); var div_1 = child(div); var div_2 = child(div_1); var span_1 = child(div_2); @@ -4880,7 +4880,7 @@ function AdjustModal($$anchor, $$props) { bind_this(div_4, ($$value) => viewportEl = $$value, () => viewportEl); var div_6 = sibling(div_4, 2); each(div_6, 21, () => GROUPS, index, ($$anchor3, g) => { - var div_7 = root_2$4(); + var div_7 = root_2$6(); var div_8 = child(div_7); var svg = child(div_8); let classes; @@ -4890,9 +4890,9 @@ function AdjustModal($$anchor, $$props) { var node_1 = sibling(div_8, 2); { var consequent = ($$anchor4) => { - var div_9 = root_3$4(); + var div_9 = root_3$6(); each(div_9, 21, () => get(g).sliders, index, ($$anchor5, sl) => { - var div_10 = root_4$4(); + var div_10 = root_4$6(); var span_4 = child(div_10); var text_2 = child(span_4); var node_2 = sibling(span_4, 2); @@ -4986,28 +4986,28 @@ delegate([ "contextmenu", "dblclick" ]); -var root_2$3 = from_html(` `); -var root_3$3 = from_svg(``); -var root_4$3 = from_svg(``); -var root_5$3 = from_html(`
`); -var root_6$3 = from_html(`Transform drag a corner to scale · the knob above rotates (Shift = 15° steps) · drag inside to move · Shift on a corner keeps proportions · Enter applies · Esc cancels`, 1); -var root_7$3 = from_html(`
Tolerance
click to flood-fill with the foreground color · constrained to the selection · Alt/Ctrl+Backspace fills`, 1); -var root_8$3 = from_html(`
Blur
Feather
lasso a region — it blurs and feather-blends on release`, 1); -var root_9$2 = from_html(`
Size
Hardness
Opacity
Flow
`, 1); -var root_11$1 = from_html(`
Opacity
Flow
`, 1); -var root_12$2 = from_html(`paint over a blemish — it heals from the surroundings on release`); -var root_13$2 = from_html(` `); -var root_14$2 = from_html(` `); -var root_10$2 = from_html(`
Size
Hardness
`, 1); -var root_16$2 = from_html(``); +var root_2$5 = from_html(` `); +var root_3$5 = from_svg(``); +var root_4$5 = from_svg(``); +var root_5$5 = from_html(`
`); +var root_6$5 = from_html(`Transform drag a corner to scale · the knob above rotates (Shift = 15° steps) · drag inside to move · Shift on a corner keeps proportions · Enter applies · Esc cancels`, 1); +var root_7$5 = from_html(`
Tolerance
click to flood-fill with the foreground color · constrained to the selection · Alt/Ctrl+Backspace fills`, 1); +var root_8$5 = from_html(`
Blur
Feather
lasso a region — it blurs and feather-blends on release`, 1); +var root_9$4 = from_html(`
Size
Hardness
Opacity
Flow
`, 1); +var root_11$3 = from_html(`
Opacity
Flow
`, 1); +var root_12$4 = from_html(`paint over a blemish — it heals from the surroundings on release`); +var root_13$4 = from_html(` `); +var root_14$4 = from_html(` `); +var root_10$4 = from_html(`
Size
Hardness
`, 1); +var root_16$3 = from_html(``); var root_17$2 = from_html(`
Rate
`); -var root_15$2 = from_html(`
Size
Density
Pressure
`, 1); +var root_15$3 = from_html(`
Size
Density
Pressure
`, 1); var root_19$2 = from_html(``); -var root_18$1 = from_html(`
Ratio
drag handles · drag inside to move · Shift locks ratio · Enter / double-click applies · Esc resets`, 1); +var root_18$2 = from_html(`
Ratio
drag handles · drag inside to move · Shift locks ratio · Enter / double-click applies · Esc resets`, 1); var root_21$1 = from_html(``); var root_20$1 = from_html(`
Feather
Opacity
Ctrl+J copy / Ctrl+Shift+J cut to a new layer · Ctrl+C/V · Esc deselects · Feather softens the edge`, 1); var root_23$1 = from_html(``); -var root_22$1 = from_html(` `, 1); +var root_22 = from_html(` `, 1); var root_24$1 = from_html(`
Sample
click the image to pick a color · hold to preview · Screen pick grabs from anywhere`, 1); var root_25$1 = from_html(``); var root_27$1 = from_html(``); @@ -5043,14 +5043,14 @@ var root_59$1 = from_html(` < var root_57$1 = from_html(` `); var root_55$1 = from_html(`
⠿
`); var root_53$1 = from_html(`
`, 1); -var root_61 = from_html(`
`); -var root_60 = from_html(`
`); +var root_61$1 = from_html(`
`); +var root_60$1 = from_html(`
`); var root_63$1 = from_html(``); var root_64$1 = from_html(``); var root_66$1 = from_html(``); var root_67$1 = from_html(` `, 1); -var root_68$1 = from_html(``); -var root_69$1 = from_html(``); +var root_68 = from_html(``); +var root_69 = from_html(``); var root_62$1 = from_html(`
`); var root_71$1 = from_html(` `, 1); var root_72$1 = from_html(` `, 1); @@ -5061,7 +5061,7 @@ var root_75$1 = from_html(`
var root_76$1 = from_html(`
Alt-click a clean area of the image to define a source point, then paint.
`); var root_77$1 = from_html(`
`); var root_78$1 = from_html(`✓ Edits saved`); -var root_1$3 = from_html(`
Editing:
Navigator
`); +var root_1$5 = from_html(`
Editing:
Navigator
`); function EditModal($$anchor, $$props) { push($$props, true); let srcUrlProp = prop($$props, "sourceUrl", 3, ""), widthProp = prop($$props, "width", 3, 0), heightProp = prop($$props, "height", 3, 0), filename = prop($$props, "filename", 3, ""), caps = prop($$props, "caps", 3, null), fetchApi = prop($$props, "fetchApi", 3, null); @@ -9628,7 +9628,7 @@ function EditModal($$anchor, $$props) { var node = first_child(fragment); { var consequent_60 = ($$anchor2) => { - var div = root_1$3(); + var div = root_1$5(); var div_1 = child(div); let classes; var div_2 = child(div_1); @@ -9638,7 +9638,7 @@ function EditModal($$anchor, $$props) { var node_1 = sibling(span_1); { var consequent = ($$anchor3) => { - var span_2 = root_2$3(); + var span_2 = root_2$5(); var text_1 = child(span_2); template_effect(() => set_text(text_1, `${get(width) ?? ""}×${get(height) ?? ""}`)); append($$anchor3, span_2); @@ -9652,11 +9652,11 @@ function EditModal($$anchor, $$props) { var node_2 = child(button); { var consequent_1 = ($$anchor3) => { - var svg_1 = root_3$3(); + var svg_1 = root_3$5(); append($$anchor3, svg_1); }; var alternate = ($$anchor3) => { - var svg_2 = root_4$3(); + var svg_2 = root_4$5(); append($$anchor3, svg_2); }; if_block(node_2, ($$render) => { @@ -9668,7 +9668,7 @@ function EditModal($$anchor, $$props) { var node_3 = sibling(div_2, 2); { var consequent_2 = ($$anchor3) => { - var div_4 = root_5$3(); + var div_4 = root_5$5(); var div_5 = child(div_4); var span_3 = sibling(child(div_5), 2); var text_2 = child(span_3); @@ -9684,7 +9684,7 @@ function EditModal($$anchor, $$props) { var node_4 = child(div_7); { var consequent_3 = ($$anchor3) => { - var fragment_1 = root_6$3(); + var fragment_1 = root_6$5(); var button_2 = sibling(first_child(fragment_1), 2); var button_3 = sibling(button_2, 2); var span_4 = sibling(button_3, 2); @@ -9701,7 +9701,7 @@ function EditModal($$anchor, $$props) { append($$anchor3, fragment_1); }; var consequent_4 = ($$anchor3) => { - var fragment_2 = root_7$3(); + var fragment_2 = root_7$5(); var div_8 = first_child(fragment_2); var node_5 = sibling(child(div_8), 2); SettingsSlider(node_5, { @@ -9725,7 +9725,7 @@ function EditModal($$anchor, $$props) { append($$anchor3, fragment_2); }; var consequent_5 = ($$anchor3) => { - var fragment_3 = root_8$3(); + var fragment_3 = root_8$5(); var div_9 = first_child(fragment_3); var node_6 = sibling(child(div_9), 2); SettingsSlider(node_6, { @@ -9757,7 +9757,7 @@ function EditModal($$anchor, $$props) { append($$anchor3, fragment_3); }; var consequent_6 = ($$anchor3) => { - var fragment_4 = root_9$2(); + var fragment_4 = root_9$4(); var div_11 = first_child(fragment_4); var node_8 = sibling(child(div_11), 2); SettingsSlider(node_8, { @@ -9823,7 +9823,7 @@ function EditModal($$anchor, $$props) { append($$anchor3, fragment_4); }; var consequent_10 = ($$anchor3) => { - var fragment_5 = root_10$2(); + var fragment_5 = root_10$4(); var div_15 = first_child(fragment_5); var node_12 = sibling(child(div_15), 2); SettingsSlider(node_12, { @@ -9855,7 +9855,7 @@ function EditModal($$anchor, $$props) { var node_14 = sibling(div_16, 2); { var consequent_7 = ($$anchor4) => { - var fragment_6 = root_11$1(); + var fragment_6 = root_11$3(); var div_17 = first_child(fragment_6); var node_15 = sibling(child(div_17), 2); SettingsSlider(node_15, { @@ -9893,17 +9893,17 @@ function EditModal($$anchor, $$props) { var node_17 = sibling(node_14, 2); { var consequent_8 = ($$anchor4) => { - var span_5 = root_12$2(); + var span_5 = root_12$4(); append($$anchor4, span_5); }; var consequent_9 = ($$anchor4) => { - var span_6 = root_13$2(); + var span_6 = root_13$4(); var text_4 = child(span_6); template_effect(() => set_text(text_4, get(healSource) ? "paint over the flaw — texture from the source, lighting from the spot · Alt-click to re-sample" : "Alt-click a clean area to set the sample source")); append($$anchor4, span_6); }; var alternate_1 = ($$anchor4) => { - var span_7 = root_14$2(); + var span_7 = root_14$4(); var text_5 = child(span_7); template_effect(() => set_text(text_5, get(healSource) ? "paint to clone source pixels exactly · Alt-click to re-sample" : "Alt-click the area to clone from, then paint")); append($$anchor4, span_7); @@ -9917,7 +9917,7 @@ function EditModal($$anchor, $$props) { append($$anchor3, fragment_5); }; var consequent_12 = ($$anchor3) => { - var fragment_7 = root_15$2(); + var fragment_7 = root_15$3(); var div_19 = first_child(fragment_7); each( div_19, @@ -9935,7 +9935,7 @@ function EditModal($$anchor, $$props) { var $$array_1 = user_derived(() => to_array($$item, 2)); let id = () => get($$array_1)[0]; let label = () => get($$array_1)[1]; - var button_8 = root_16$2(); + var button_8 = root_16$3(); let classes_3; var text_6 = child(button_8); template_effect(() => { @@ -10019,7 +10019,7 @@ function EditModal($$anchor, $$props) { append($$anchor3, fragment_7); }; var consequent_13 = ($$anchor3) => { - var fragment_8 = root_18$1(); + var fragment_8 = root_18$2(); var div_24 = first_child(fragment_8); var select = sibling(child(div_24), 2); each(select, 21, () => CROP_RATIOS, index, ($$anchor4, cr) => { @@ -10103,7 +10103,7 @@ function EditModal($$anchor, $$props) { append($$anchor3, fragment_9); }; var consequent_15 = ($$anchor3) => { - var fragment_10 = root_22$1(); + var fragment_10 = root_22(); var button_14 = first_child(fragment_10); var text_12 = child(button_14); var node_26 = sibling(button_14, 2); @@ -10810,9 +10810,9 @@ function EditModal($$anchor, $$props) { append($$anchor3, fragment_16); }; var alternate_4 = ($$anchor3) => { - var div_73 = root_60(); + var div_73 = root_60$1(); each(div_73, 21, () => get(history2), index, ($$anchor4, h, i) => { - var div_74 = root_61(); + var div_74 = root_61$1(); let classes_13; var text_23 = child(div_74); template_effect(() => { @@ -10905,7 +10905,7 @@ function EditModal($$anchor, $$props) { var node_63 = sibling(node_61, 2); { var consequent_48 = ($$anchor4) => { - var button_43 = root_68$1(); + var button_43 = root_68(); delegated("click", button_43, () => { const i = get(layerMenu).i; set(layerMenu, null); @@ -10924,7 +10924,7 @@ function EditModal($$anchor, $$props) { var node_64 = sibling(button_44, 2); { var consequent_49 = ($$anchor4) => { - var button_45 = root_69$1(); + var button_45 = root_69(); var text_25 = child(button_45); template_effect(() => set_text(text_25, get(selectedIds).length > 1 ? "Delete Layers" : "Delete Layer")); delegated("click", button_45, () => { @@ -11284,25 +11284,25 @@ delegate([ "input", "keydown" ]); -var root_3$2 = from_html(``); -var root_2$2 = from_html(`
`); -var root_4$2 = from_html(`preview`); -var root_6$2 = from_html(`
Before
After
`, 1); -var root_5$2 = from_html(`
`, 1); -var root_7$2 = from_html(`
`); -var root_8$2 = from_html(``); -var root_9$1 = from_html(`
`); -var root_10$1 = from_html(``); -var root_12$1 = from_html(`
`); -var root_13$1 = from_html(``); -var root_14$1 = from_html(``); -var root_15$1 = from_html(``); -var root_16$1 = from_html(`
Depth-locked: the poser outputs a depth map; output follows the pose frame.
`); +var root_3$4 = from_html(``); +var root_2$4 = from_html(`
`); +var root_4$4 = from_html(`preview`); +var root_6$4 = from_html(`
Before
After
`, 1); +var root_5$4 = from_html(`
`, 1); +var root_7$4 = from_html(`
`); +var root_8$4 = from_html(``); +var root_9$3 = from_html(`
`); +var root_10$3 = from_html(``); +var root_12$3 = from_html(`
`); +var root_13$3 = from_html(``); +var root_14$3 = from_html(``); +var root_15$2 = from_html(``); +var root_16$2 = from_html(`
Depth-locked: the poser outputs a depth map; output follows the pose frame.
`); var root_17$1 = from_html(``); -var root_18 = from_html(` `, 1); +var root_18$1 = from_html(` `, 1); var root_19$1 = from_html(` `, 1); var root_20 = from_html(` `, 1); -var root_1$2 = from_html(`
`); +var root_1$4 = from_html(`
`); function RePoseModal($$anchor, $$props) { push($$props, true); let open = prop($$props, "open", 3, false), sourceUrl = prop($$props, "sourceUrl", 3, ""), width = prop($$props, "width", 3, 0), height = prop($$props, "height", 3, 0), imageKey = prop( @@ -11730,7 +11730,7 @@ function RePoseModal($$anchor, $$props) { var node_1 = first_child(fragment); { var consequent_14 = ($$anchor2) => { - var div = root_1$2(); + var div = root_1$4(); set_style(div, "", {}, { "z-index": 10006 }); var div_1 = child(div); var div_2 = child(div_1); @@ -11748,11 +11748,11 @@ function RePoseModal($$anchor, $$props) { var node_2 = sibling(span_1, 2); { var consequent_1 = ($$anchor3) => { - var div_8 = root_2$2(); + var div_8 = root_2$4(); var node_3 = child(div_8); { var consequent = ($$anchor4) => { - var button_1 = root_3$2(); + var button_1 = root_3$4(); let classes; template_effect(() => classes = set_class(button_1, 1, "pcr-rp-fit-btn svelte-1elgmyq", null, classes, { on: get(compareSplit) })); delegated("click", button_1, toggleCompare); @@ -11775,18 +11775,18 @@ function RePoseModal($$anchor, $$props) { var node_4 = child(div_9); { var consequent_2 = ($$anchor3) => { - var img = root_4$2(); + var img = root_4$4(); template_effect(() => set_attribute(img, "src", get(liveTile))); append($$anchor3, img); }; var consequent_4 = ($$anchor3) => { - var fragment_1 = root_5$2(); + var fragment_1 = root_5$4(); var div_10 = first_child(fragment_1); var img_1 = child(div_10); var node_5 = sibling(div_10, 2); { var consequent_3 = ($$anchor4) => { - var fragment_2 = root_6$2(); + var fragment_2 = root_6$4(); var div_11 = first_child(fragment_2); var div_12 = child(div_11); var img_2 = child(div_12); @@ -11825,7 +11825,7 @@ function RePoseModal($$anchor, $$props) { var node_6 = sibling(div_9, 2); { var consequent_5 = ($$anchor3) => { - var div_14 = root_7$2(); + var div_14 = root_7$4(); var div_15 = child(div_14); var div_16 = child(div_15); let styles; @@ -11851,7 +11851,7 @@ function RePoseModal($$anchor, $$props) { var div_18 = child(div_17); var select = sibling(child(div_18), 2); each(select, 21, () => get(recipes), index, ($$anchor3, r) => { - var option = root_8$2(); + var option = root_8$4(); var text_4 = child(option); var option_value = {}; template_effect(() => { @@ -11868,7 +11868,7 @@ function RePoseModal($$anchor, $$props) { var node_7 = sibling(select, 2); { var consequent_6 = ($$anchor3) => { - var div_19 = root_9$1(); + var div_19 = root_9$3(); var text_5 = child(div_19); template_effect(() => set_text(text_5, get(recipe).blurb)); append($$anchor3, div_19); @@ -11883,7 +11883,7 @@ function RePoseModal($$anchor, $$props) { var _a; return ((_a = get(recipe)) == null ? void 0 : _a.models) || []; }, index, ($$anchor3, m) => { - var option_1 = root_10$1(); + var option_1 = root_10$3(); var text_6 = child(option_1); var option_1_value = {}; template_effect(() => { @@ -11901,12 +11901,12 @@ function RePoseModal($$anchor, $$props) { var node_9 = first_child(fragment_3); { var consequent_7 = ($$anchor4) => { - var div_22 = root_12$1(); + var div_22 = root_12$3(); action(div_22, ($$node) => promptEditor == null ? void 0 : promptEditor($$node)); append($$anchor4, div_22); }; var alternate = ($$anchor4) => { - var textarea = root_13$1(); + var textarea = root_13$3(); bind_value(textarea, () => get(promptDoc), ($$value) => set(promptDoc, $$value)); append($$anchor4, textarea); }; @@ -11929,7 +11929,7 @@ function RePoseModal($$anchor, $$props) { var node_10 = sibling(label_2, 2); { var consequent_8 = ($$anchor3) => { - var label_3 = root_14$1(); + var label_3 = root_14$3(); var input_3 = sibling(child(label_3)); bind_value(input_3, () => get(megapixels), ($$value) => set(megapixels, $$value)); append($$anchor3, label_3); @@ -11944,7 +11944,7 @@ function RePoseModal($$anchor, $$props) { var node_11 = sibling(label_4, 2); { var consequent_9 = ($$anchor3) => { - var label_5 = root_15$1(); + var label_5 = root_15$2(); var input_5 = sibling(child(label_5)); bind_value(input_5, () => get(seed), ($$value) => set(seed, $$value)); append($$anchor3, label_5); @@ -11956,7 +11956,7 @@ function RePoseModal($$anchor, $$props) { var node_12 = sibling(node_11, 2); { var consequent_10 = ($$anchor3) => { - var div_26 = root_16$1(); + var div_26 = root_16$2(); append($$anchor3, div_26); }; if_block(node_12, ($$render) => { @@ -11973,7 +11973,7 @@ function RePoseModal($$anchor, $$props) { append($$anchor3, button_3); }; var consequent_12 = ($$anchor3) => { - var fragment_4 = root_18(); + var fragment_4 = root_18$1(); var button_4 = first_child(fragment_4); var button_5 = sibling(button_4, 2); var button_6 = sibling(button_5, 2); @@ -12062,6 +12062,835 @@ delegate([ "contextmenu", "change" ]); +var root_2$3 = from_html(`
last frame continues from here →
`); +var root_3$3 = from_html(`
`); +var root_4$3 = from_html(``); +var root_6$3 = from_html(``); +var root_7$3 = from_html(``); +var root_5$3 = from_html(`
Length
Output FPS
Steps
CFG
Shift
`); +var root_9$2 = from_html(`
`); +var root_11$2 = from_html(``, 2); +var root_10$2 = from_html(`
✓ Continuation rendered. Adjust and Run Again, or Done to view it.
`, 1); +var root_13$2 = from_html(`preview`); +var root_12$2 = from_html(`
`, 1); +var root_8$3 = from_html(`
`); +var root_14$2 = from_html(``); +var root_15$1 = from_html(` `, 1); +var root_16$1 = from_html(` `, 1); +var root_1$3 = from_html(`
Extend Video
Speed
`); +function ExtendModal($$anchor, $$props) { + push($$props, true); + let open = prop($$props, "open", 3, false), lastFrameUrl = prop($$props, "lastFrameUrl", 3, ""), fps = prop($$props, "fps", 3, 16), frameCount = prop($$props, "frameCount", 3, 0), lightningAvailable = prop( + $$props, + "lightningAvailable", + 3, + true + // false = the LightX2V speed LoRAs aren't on disk + ), defaults = prop( + $$props, + "defaults", + 3, + null + // {width,height,length,fps} seeded from the source clip + ), recipeDefaults = prop( + $$props, + "recipeDefaults", + 3, + null + // {normal:{steps,cfg,shift,sampler,scheduler}, lightning:{…}} + ), mountPromptEditor = prop( + $$props, + "mountPromptEditor", + 3, + null + // (container, initialValue, onChange, getModelInfo) => EditorView + ), modelInfo = prop( + $$props, + "modelInfo", + 3, + null + // {hash, architecture} for the editor's @prompt autocomplete + ), progress = prop($$props, "progress", 3, null); + 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); + 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; + function seedSharedDefaults() { + if (!defaults()) return; + set(width, defaults().width, true); + set(height, defaults().height, true); + set(length, defaults().length, true); + set( + outFps, + 16 + // the continuation is always i2v-native 16fps; stitch matches the source automatically + ); + } + function seedRecipeDefaults(r) { + var _a; + const rd2 = (_a = recipeDefaults()) == null ? void 0 : _a[r]; + if (!rd2) return; + set(steps, rd2.steps, true); + set(cfg, rd2.cfg, true); + set(shift, rd2.shift, true); + set(sampler, rd2.sampler, true); + set(scheduler, rd2.scheduler, true); + } + let primed = false; + let lastRecipe = "normal"; + user_effect(() => { + if (open() && !primed) { + set(recipe, "normal"); + lastRecipe = "normal"; + set(stitch, true); + set(advancedOpen, false); + set(seed, 0); + set(seedFixed, false); + set(promptText, ""); + seedSharedDefaults(); + seedRecipeDefaults("normal"); + primed = true; + } + if (!open()) primed = false; + }); + user_effect(() => { + if (!lightningAvailable() && get(recipe) === "lightning") set(recipe, "normal"); + }); + user_effect(() => { + if (get(recipe) !== lastRecipe) { + lastRecipe = get(recipe); + seedRecipeDefaults(get(recipe)); + } + }); + let rd = user_derived(() => { + var _a; + return ((_a = recipeDefaults()) == null ? void 0 : _a[get(recipe)]) || {}; + }); + let phase = user_derived(() => { + var _a; + return ((_a = progress()) == null ? void 0 : _a.phase) || null; + }); + let running = user_derived(() => get(phase) === "building" || get(phase) === "queueing" || get(phase) === "running"); + let done = user_derived(() => get(phase) === "done"); + let pct = user_derived(() => { + var _a; + return ((_a = progress()) == null ? void 0 : _a.max) ? Math.round(progress().value / progress().max * 100) : 0; + }); + const editorModelInfo = () => modelInfo() || null; + function prefill() { + return ` +Negative Prompt: ${WAN_NEG}`; + } + function promptEditor(node) { + let disposed = false, view = null; + (async () => { + var _a; + if (!mountPromptEditor()) return; + const v = await mountPromptEditor()( + node, + prefill(), + (text) => { + set(promptText, text, true); + }, + editorModelInfo + ); + if (disposed) { + (_a = v == null ? void 0 : v.destroy) == null ? void 0 : _a.call(v); + return; + } + view = v; + promptEditorView = v; + })().catch((e) => console.error("[Extend] prompt editor mount failed", e)); + return { + destroy() { + var _a; + disposed = true; + (_a = view == null ? void 0 : view.destroy) == null ? void 0 : _a.call(view); + if (promptEditorView === view) promptEditorView = null; + } + }; + } + function currentPromptDoc() { + if (promptEditorView) return promptEditorView.state.doc.toString(); + const neg = `Negative Prompt: ${WAN_NEG}`; + return get(promptText).trim() ? `${get(promptText).trim()} + +${neg}` : neg; + } + function run() { + var _a; + if (get(running)) return; + (_a = $$props.onRun) == null ? void 0 : _a.call($$props, { + promptDoc: currentPromptDoc(), + stitch: get(stitch), + recipe: get(recipe), + width: get(width), + height: get(height), + length: get(length), + outFps: get(outFps), + steps: get(steps), + cfg: get(cfg), + shift: get(shift), + sampler: get(sampler), + scheduler: get(scheduler), + seed: get( + seedFixed + // omit unless pinned — keeps template noise behaviour + ) ? get(seed) : void 0 + }); + } + function close() { + var _a, _b; + if (get(done)) (_a = $$props.onDone) == null ? void 0 : _a.call($$props); + else (_b = $$props.onCancel) == null ? void 0 : _b.call($$props); + } + var fragment = comment(); + var node_1 = first_child(fragment); + { + var consequent_10 = ($$anchor2) => { + var div = root_1$3(); + var div_1 = child(div); + var div_2 = sibling(child(div_1), 2); + var text_1 = child(div_2); + var node_2 = sibling(div_2, 2); + { + var consequent = ($$anchor3) => { + var div_3 = root_2$3(); + var img = child(div_3); + template_effect(() => set_attribute(img, "src", lastFrameUrl())); + append($$anchor3, div_3); + }; + if_block(node_2, ($$render) => { + if (lastFrameUrl()) $$render(consequent); + }); + } + var div_4 = sibling(node_2, 4); + var button = child(div_4); + let classes; + var button_1 = sibling(button, 2); + let classes_1; + var span = sibling(child(button_1), 2); + var text_2 = child(span); + var node_3 = sibling(div_4, 4); + { + var consequent_1 = ($$anchor3) => { + var div_5 = root_3$3(); + action(div_5, ($$node) => promptEditor == null ? void 0 : promptEditor($$node)); + append($$anchor3, div_5); + }; + var alternate = ($$anchor3) => { + var textarea = root_4$3(); + template_effect(() => textarea.disabled = get(running)); + bind_value(textarea, () => get(promptText), ($$value) => set(promptText, $$value)); + append($$anchor3, textarea); + }; + if_block(node_3, ($$render) => { + if (mountPromptEditor()) $$render(consequent_1); + else $$render(alternate, -1); + }); + } + var label = sibling(node_3, 2); + var input = child(label); + var button_2 = sibling(label, 2); + var text_3 = child(button_2); + var node_4 = sibling(button_2, 2); + { + var consequent_2 = ($$anchor3) => { + var div_6 = root_5$3(); + var div_7 = child(div_6); + var span_1 = child(div_7); + var small = sibling(child(span_1)); + var text_4 = child(small); + var node_5 = sibling(span_1, 2); + { + let $0 = user_derived(() => { + var _a; + return (_a = defaults()) == null ? void 0 : _a.length; + }); + SettingsSlider(node_5, { + min: 5, + max: 161, + step: 4, + get value() { + return get(length); + }, + get savedValue() { + return get($0); + }, + onChange: (v) => set(length, v, true) + }); + } + var div_8 = sibling(div_7, 2); + var span_2 = child(div_8); + var small_1 = sibling(child(span_2)); + var text_5 = child(small_1); + var node_6 = sibling(span_2, 2); + SettingsSlider(node_6, { + min: 8, + max: 60, + step: 1, + get value() { + return get(outFps); + }, + savedValue: 16, + onChange: (v) => set(outFps, v, true) + }); + var div_9 = sibling(div_8, 2); + var span_3 = child(div_9); + var small_2 = sibling(child(span_3)); + var text_6 = child(small_2); + var node_7 = sibling(span_3, 2); + SettingsSlider(node_7, { + min: 1, + max: 40, + step: 1, + get value() { + return get(steps); + }, + get savedValue() { + return get(rd).steps; + }, + onChange: (v) => set(steps, v, true) + }); + var div_10 = sibling(div_9, 2); + var span_4 = child(div_10); + var small_3 = sibling(child(span_4)); + var text_7 = child(small_3); + var node_8 = sibling(span_4, 2); + SettingsSlider(node_8, { + min: 1, + max: 8, + step: 0.1, + get value() { + return get(cfg); + }, + get savedValue() { + return get(rd).cfg; + }, + onChange: (v) => set(cfg, v, true) + }); + var div_11 = sibling(div_10, 2); + var span_5 = child(div_11); + var small_4 = sibling(child(span_5)); + var text_8 = child(small_4); + var node_9 = sibling(span_5, 2); + SettingsSlider(node_9, { + min: 1, + max: 12, + step: 0.5, + get value() { + return get(shift); + }, + get savedValue() { + return get(rd).shift; + }, + onChange: (v) => set(shift, v, true) + }); + var div_12 = sibling(div_11, 2); + var label_1 = child(div_12); + var select = sibling(child(label_1), 2); + each(select, 21, () => SAMPLERS, index, ($$anchor4, s) => { + var option = root_6$3(); + var text_9 = child(option); + var option_value = {}; + template_effect(() => { + set_text(text_9, get(s)); + if (option_value !== (option_value = get(s))) { + option.value = (option.__value = get(s)) ?? ""; + } + }); + append($$anchor4, option); + }); + var label_2 = sibling(label_1, 2); + var select_1 = sibling(child(label_2), 2); + each(select_1, 21, () => SCHEDULERS, index, ($$anchor4, s) => { + var option_1 = root_7$3(); + var text_10 = child(option_1); + var option_1_value = {}; + template_effect(() => { + set_text(text_10, get(s)); + if (option_1_value !== (option_1_value = get(s))) { + option_1.value = (option_1.__value = get(s)) ?? ""; + } + }); + append($$anchor4, option_1); + }); + var div_13 = sibling(div_12, 2); + var label_3 = child(div_13); + var input_1 = sibling(child(label_3), 2); + var label_4 = sibling(label_3, 2); + var input_2 = child(label_4); + template_effect(() => { + set_text(text_4, `${get(length) ?? ""} frames`); + set_text(text_5, get(outFps)); + set_text(text_6, get(steps)); + set_text(text_7, get(cfg)); + set_text(text_8, get(shift)); + select.disabled = get(running); + select_1.disabled = get(running); + input_1.disabled = get(running) || !get(seedFixed); + input_2.disabled = get(running); + }); + bind_select_value(select, () => get(sampler), ($$value) => set(sampler, $$value)); + bind_select_value(select_1, () => get(scheduler), ($$value) => set(scheduler, $$value)); + bind_value(input_1, () => get(seed), ($$value) => set(seed, $$value)); + bind_checked(input_2, () => get(seedFixed), ($$value) => set(seedFixed, $$value)); + append($$anchor3, div_6); + }; + if_block(node_4, ($$render) => { + if (get(advancedOpen)) $$render(consequent_2); + }); + } + var node_10 = sibling(node_4, 2); + { + var consequent_7 = ($$anchor3) => { + var div_14 = root_8$3(); + var node_11 = child(div_14); + { + var consequent_3 = ($$anchor4) => { + var div_15 = root_9$2(); + var text_11 = child(div_15); + template_effect(() => set_text(text_11, `⚠ ${(progress().message || "render failed") ?? ""}`)); + append($$anchor4, div_15); + }; + var consequent_5 = ($$anchor4) => { + var fragment_1 = root_10$2(); + var node_12 = sibling(first_child(fragment_1), 2); + { + var consequent_4 = ($$anchor5) => { + var video = root_11$2(); + video.muted = true; + template_effect(() => set_attribute(video, "src", progress().resultUrl)); + append($$anchor5, video); + }; + if_block(node_12, ($$render) => { + if (progress().resultUrl) $$render(consequent_4); + }); + } + append($$anchor4, fragment_1); + }; + var alternate_1 = ($$anchor4) => { + var fragment_2 = root_12$2(); + var div_16 = first_child(fragment_2); + var div_17 = child(div_16); + var div_18 = sibling(div_16, 2); + var text_12 = child(div_18); + var node_13 = sibling(div_18, 2); + { + var consequent_6 = ($$anchor5) => { + var img_1 = root_13$2(); + template_effect(() => set_attribute(img_1, "src", progress().previewUrl)); + append($$anchor5, img_1); + }; + if_block(node_13, ($$render) => { + if (progress().previewUrl) $$render(consequent_6); + }); + } + template_effect(() => { + set_style(div_17, `width:${get(pct) ?? ""}%`); + set_text(text_12, `${get(phase) ?? ""}${progress().max ? ` · ${get(pct)}%` : "…"}`); + }); + append($$anchor4, fragment_2); + }; + if_block(node_11, ($$render) => { + if (get(phase) === "error") $$render(consequent_3); + else if (get(done)) $$render(consequent_5, 1); + else $$render(alternate_1, -1); + }); + } + append($$anchor3, div_14); + }; + if_block(node_10, ($$render) => { + if (progress()) $$render(consequent_7); + }); + } + var div_19 = sibling(node_10, 2); + var node_14 = child(div_19); + { + var consequent_8 = ($$anchor3) => { + var button_3 = root_14$2(); + delegated("click", button_3, () => { + var _a; + return (_a = $$props.onCancel) == null ? void 0 : _a.call($$props); + }); + append($$anchor3, button_3); + }; + var consequent_9 = ($$anchor3) => { + var fragment_3 = root_15$1(); + var button_4 = first_child(fragment_3); + var button_5 = sibling(button_4, 2); + delegated("click", button_4, () => { + var _a; + return (_a = $$props.onDone) == null ? void 0 : _a.call($$props); + }); + delegated("click", button_5, run); + append($$anchor3, fragment_3); + }; + var alternate_2 = ($$anchor3) => { + var fragment_4 = root_16$1(); + var button_6 = first_child(fragment_4); + var button_7 = sibling(button_6, 2); + delegated("click", button_6, () => { + var _a; + return (_a = $$props.onCancel) == null ? void 0 : _a.call($$props); + }); + delegated("click", button_7, run); + append($$anchor3, fragment_4); + }; + if_block(node_14, ($$render) => { + if (get(running)) $$render(consequent_8); + else if (get(done)) $$render(consequent_9, 1); + else $$render(alternate_2, -1); + }); + } + template_effect(() => { + set_text(text_1, `Renders the next clip continuing from this video's last frame${frameCount() ? ` · ${frameCount()} frames @ ${fps()}fps` : ""}. Saved as a lineage child.`); + classes = set_class(button, 1, "pcr-ext-recipe-btn svelte-5qj8hq", null, classes, { active: get(recipe) === "normal" }); + button.disabled = get(running); + classes_1 = set_class(button_1, 1, "pcr-ext-recipe-btn svelte-5qj8hq", null, classes_1, { + active: get(recipe) === "lightning", + unavail: !lightningAvailable() + }); + button_1.disabled = get(running) || !lightningAvailable(); + set_attribute(button_1, "title", lightningAvailable() ? "" : "Install the LightX2V speed LoRAs (~1.3 GB) to use Lightning"); + set_text(text_2, lightningAvailable() ? "4-step · fast draft" : "speed LoRAs not installed"); + input.disabled = get(running); + set_text(text_3, `${get(advancedOpen) ? "▾" : "▸"} Advanced`); + }); + delegated("click", div, (e) => { + if (e.target === e.currentTarget && !get(running)) close(); + }); + delegated("click", button, () => { + set(recipe, "normal"); + }); + delegated("click", button_1, () => { + if (lightningAvailable()) set(recipe, "lightning"); + }); + bind_checked(input, () => get(stitch), ($$value) => set(stitch, $$value)); + delegated("click", button_2, () => set(advancedOpen, !get(advancedOpen))); + append($$anchor2, div); + }; + if_block(node_1, ($$render) => { + if (open()) $$render(consequent_10); + }); + } + append($$anchor, fragment); + pop(); +} +delegate(["click"]); +var root_2$2 = from_html(``); +var root_4$2 = from_html(``); +var root_5$2 = from_html(``); +var root_3$2 = from_html(`
`); +var root_7$2 = from_html(`
`); +var root_9$1 = from_html(``, 2); +var root_8$2 = from_html(`
✓ Smoothed. Adjust and Run Again, or Done to view it.
`, 1); +var root_11$1 = from_html(`preview`); +var root_10$1 = from_html(`
`, 1); +var root_6$2 = from_html(`
`); +var root_12$1 = from_html(``); +var root_13$1 = from_html(` `, 1); +var root_14$1 = from_html(` `, 1); +var root_1$2 = from_html(`
Smooth Video
Interpolation
`); +function SmoothModal($$anchor, $$props) { + push($$props, true); + let open = prop($$props, "open", 3, false), fps = prop($$props, "fps", 3, 16), frameCount = prop($$props, "frameCount", 3, 0), progress = prop($$props, "progress", 3, null); + const CKPTS = ["rife49.pth", "rife426.pth", "rife417.pth", "rife47.pth"]; + const SCALES = [0.25, 0.5, 1, 2, 4]; + let mult = state(2); + let advancedOpen = state(false); + let ckptName = state("rife49.pth"); + let scaleFactor = state( + 1 + // <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; + user_effect(() => { + if (open() && !primed) { + set(mult, 2); + set(advancedOpen, false); + set(ckptName, "rife49.pth"); + set(scaleFactor, 1); + set(ensemble, true); + set(fastMode, true); + primed = true; + } + if (!open()) primed = false; + }); + let phase = user_derived(() => { + var _a; + return ((_a = progress()) == null ? void 0 : _a.phase) || null; + }); + let running = user_derived(() => get(phase) === "building" || get(phase) === "queueing" || get(phase) === "running"); + let done = user_derived(() => get(phase) === "done"); + let pct = user_derived(() => { + var _a; + return ((_a = progress()) == null ? void 0 : _a.max) ? Math.round(progress().value / progress().max * 100) : 0; + }); + let outFrames = user_derived(() => frameCount() ? (frameCount() - 1) * get(mult) + 1 : 0); + function run() { + var _a; + if (get(running)) return; + (_a = $$props.onRun) == null ? void 0 : _a.call($$props, { + multiplier: get(mult), + ckptName: get(ckptName), + scaleFactor: get(scaleFactor), + ensemble: get(ensemble), + fastMode: get(fastMode) + }); + } + function close() { + var _a, _b; + if (get(done)) (_a = $$props.onDone) == null ? void 0 : _a.call($$props); + else (_b = $$props.onCancel) == null ? void 0 : _b.call($$props); + } + var fragment = comment(); + var node = first_child(fragment); + { + var consequent_8 = ($$anchor2) => { + var div = root_1$2(); + var div_1 = child(div); + var div_2 = sibling(child(div_1), 2); + var text = child(div_2); + var div_3 = sibling(div_2, 4); + each(div_3, 20, () => [2, 3, 4], index, ($$anchor3, m) => { + var button = root_2$2(); + let classes; + var text_1 = child(button); + template_effect(() => { + classes = set_class(button, 1, "pcr-sm-mult svelte-1j60x3s", null, classes, { "pcr-sm-active": get(mult) === m }); + button.disabled = get(running); + set_text(text_1, `${m ?? ""}×`); + }); + delegated("click", button, () => { + set(mult, m, true); + }); + append($$anchor3, button); + }); + var button_1 = sibling(div_3, 2); + var text_2 = child(button_1); + var node_1 = sibling(button_1, 2); + { + var consequent = ($$anchor3) => { + var div_4 = root_3$2(); + var label = child(div_4); + var select = sibling(child(label), 2); + each(select, 21, () => CKPTS, index, ($$anchor4, c) => { + var option = root_4$2(); + var text_3 = child(option); + var option_value = {}; + template_effect( + ($0) => { + set_text(text_3, $0); + if (option_value !== (option_value = get(c))) { + option.value = (option.__value = get(c)) ?? ""; + } + }, + [() => get(c).replace(".pth", "")] + ); + append($$anchor4, option); + }); + var label_1 = sibling(label, 2); + var select_1 = sibling(child(label_1), 2); + each(select_1, 21, () => SCALES, index, ($$anchor4, s) => { + var option_1 = root_5$2(); + var text_4 = child(option_1); + var option_1_value = {}; + template_effect(() => { + set_text(text_4, `${get(s) ?? ""}×`); + if (option_1_value !== (option_1_value = get(s))) { + option_1.value = (option_1.__value = get(s)) ?? ""; + } + }); + append($$anchor4, option_1); + }); + var label_2 = sibling(label_1, 2); + var input = child(label_2); + var label_3 = sibling(label_2, 2); + var input_1 = child(label_3); + template_effect(() => { + select.disabled = get(running); + select_1.disabled = get(running); + input.disabled = get(running); + input_1.disabled = get(running); + }); + bind_select_value(select, () => get(ckptName), ($$value) => set(ckptName, $$value)); + bind_select_value(select_1, () => get(scaleFactor), ($$value) => set(scaleFactor, $$value)); + bind_checked(input, () => get(ensemble), ($$value) => set(ensemble, $$value)); + bind_checked(input_1, () => get(fastMode), ($$value) => set(fastMode, $$value)); + append($$anchor3, div_4); + }; + if_block(node_1, ($$render) => { + if (get(advancedOpen)) $$render(consequent); + }); + } + var node_2 = sibling(node_1, 2); + { + var consequent_5 = ($$anchor3) => { + var div_5 = root_6$2(); + var node_3 = child(div_5); + { + var consequent_1 = ($$anchor4) => { + var div_6 = root_7$2(); + var text_5 = child(div_6); + template_effect(() => set_text(text_5, `⚠ ${(progress().message || "render failed") ?? ""}`)); + append($$anchor4, div_6); + }; + var consequent_3 = ($$anchor4) => { + var fragment_1 = root_8$2(); + var node_4 = sibling(first_child(fragment_1), 2); + { + var consequent_2 = ($$anchor5) => { + var video = root_9$1(); + video.muted = true; + template_effect(() => set_attribute(video, "src", progress().resultUrl)); + append($$anchor5, video); + }; + if_block(node_4, ($$render) => { + if (progress().resultUrl) $$render(consequent_2); + }); + } + append($$anchor4, fragment_1); + }; + var alternate = ($$anchor4) => { + var fragment_2 = root_10$1(); + var div_7 = first_child(fragment_2); + var div_8 = child(div_7); + var div_9 = sibling(div_7, 2); + var text_6 = child(div_9); + var node_5 = sibling(div_9, 2); + { + var consequent_4 = ($$anchor5) => { + var img = root_11$1(); + template_effect(() => set_attribute(img, "src", progress().previewUrl)); + append($$anchor5, img); + }; + if_block(node_5, ($$render) => { + if (progress().previewUrl) $$render(consequent_4); + }); + } + template_effect(() => { + set_style(div_8, `width:${get(pct) ?? ""}%`); + set_text(text_6, `${get(phase) ?? ""}${progress().max ? ` · ${get(pct)}%` : "…"}`); + }); + append($$anchor4, fragment_2); + }; + if_block(node_3, ($$render) => { + if (get(phase) === "error") $$render(consequent_1); + else if (get(done)) $$render(consequent_3, 1); + else $$render(alternate, -1); + }); + } + append($$anchor3, div_5); + }; + if_block(node_2, ($$render) => { + if (progress()) $$render(consequent_5); + }); + } + var div_10 = sibling(node_2, 2); + var node_6 = child(div_10); + { + var consequent_6 = ($$anchor3) => { + var button_2 = root_12$1(); + delegated("click", button_2, () => { + var _a; + return (_a = $$props.onCancel) == null ? void 0 : _a.call($$props); + }); + append($$anchor3, button_2); + }; + var consequent_7 = ($$anchor3) => { + var fragment_3 = root_13$1(); + var button_3 = first_child(fragment_3); + var button_4 = sibling(button_3, 2); + delegated("click", button_3, () => { + var _a; + return (_a = $$props.onDone) == null ? void 0 : _a.call($$props); + }); + delegated("click", button_4, run); + append($$anchor3, fragment_3); + }; + var alternate_1 = ($$anchor3) => { + var fragment_4 = root_14$1(); + var button_5 = first_child(fragment_4); + var button_6 = sibling(button_5, 2); + delegated("click", button_5, () => { + var _a; + return (_a = $$props.onCancel) == null ? void 0 : _a.call($$props); + }); + delegated("click", button_6, run); + append($$anchor3, fragment_4); + }; + if_block(node_6, ($$render) => { + if (get(running)) $$render(consequent_6); + else if (get(done)) $$render(consequent_7, 1); + else $$render(alternate_1, -1); + }); + } + template_effect(() => { + set_text(text, `Adds in-between frames with RIFE for smoother motion. ${fps() ?? ""} → ${fps() * get(mult)} fps${frameCount() ? ` · ${frameCount()} → ${get(outFrames)} frames` : ""}. Saved as a lineage child.`); + set_text(text_2, `${get(advancedOpen) ? "▾" : "▸"} Advanced`); + }); + delegated("click", div, (e) => { + if (e.target === e.currentTarget && !get(running)) close(); + }); + delegated("click", button_1, () => set(advancedOpen, !get(advancedOpen))); + append($$anchor2, div); + }; + if_block(node, ($$render) => { + if (open()) $$render(consequent_8); + }); + } + append($$anchor, fragment); + pop(); +} +delegate(["click"]); var root_1$1 = from_html(``); var root_2$1 = from_html(``); var root_3$1 = from_html(`
`); @@ -12070,10 +12899,10 @@ var root_5$1 = from_html(`
`); var root_8$1 = from_html(``); var root_6$1 = from_html(` `, 1); -var root = from_html(`
`); +var root = from_html(`
`); function LineageContextMenu($$anchor, $$props) { push($$props, true); - let canExpand = prop($$props, "canExpand", 3, false), canFocus = prop($$props, "canFocus", 3, false), canEdit = prop($$props, "canEdit", 3, false), canDelete = prop($$props, "canDelete", 3, false), isLocal = prop($$props, "isLocal", 3, false), isWindows = prop($$props, "isWindows", 3, false), orphaned = prop($$props, "orphaned", 3, false); + let canExpand = prop($$props, "canExpand", 3, false), canFocus = prop($$props, "canFocus", 3, false), pinned = prop($$props, "pinned", 3, false), canEdit = prop($$props, "canEdit", 3, false), canDelete = prop($$props, "canDelete", 3, false), isLocal = prop($$props, "isLocal", 3, false), isWindows = prop($$props, "isWindows", 3, false), orphaned = prop($$props, "orphaned", 3, false); let menuEl; function act(action2) { var _a, _b; @@ -12127,22 +12956,24 @@ function LineageContextMenu($$anchor, $$props) { if (canFocus()) $$render(consequent_1); }); } - var node_2 = sibling(node_1, 2); + var button_2 = sibling(node_1, 2); + var text = child(button_2); + var node_2 = sibling(button_2, 2); { var consequent_2 = ($$anchor2) => { var div_1 = root_3$1(); append($$anchor2, div_1); }; if_block(node_2, ($$render) => { - if ((canExpand() || canFocus()) && (canEdit() || canDelete() || isLocal())) $$render(consequent_2); + if (canEdit() || canDelete() || isLocal()) $$render(consequent_2); }); } var node_3 = sibling(node_2, 2); { var consequent_3 = ($$anchor2) => { - var button_2 = root_4$1(); - delegated("click", button_2, () => act("edit")); - append($$anchor2, button_2); + var button_3 = root_4$1(); + delegated("click", button_3, () => act("edit")); + append($$anchor2, button_3); }; if_block(node_3, ($$render) => { if (canEdit()) $$render(consequent_3); @@ -12151,9 +12982,9 @@ function LineageContextMenu($$anchor, $$props) { var node_4 = sibling(node_3, 2); { var consequent_4 = ($$anchor2) => { - var button_3 = root_5$1(); - delegated("click", button_3, () => act("delete")); - append($$anchor2, button_3); + var button_4 = root_5$1(); + delegated("click", button_4, () => act("delete")); + append($$anchor2, button_4); }; if_block(node_4, ($$render) => { if (canDelete()) $$render(consequent_4); @@ -12173,29 +13004,29 @@ function LineageContextMenu($$anchor, $$props) { if (canEdit() || canDelete()) $$render(consequent_5); }); } - var button_4 = sibling(node_6, 2); - var button_5 = sibling(button_4, 2); + var button_5 = sibling(node_6, 2); var button_6 = sibling(button_5, 2); - var node_7 = sibling(button_6, 2); + var button_7 = sibling(button_6, 2); + var node_7 = sibling(button_7, 2); { var consequent_6 = ($$anchor3) => { - var button_7 = root_8$1(); - template_effect(() => button_7.disabled = orphaned()); - delegated("click", button_7, () => act("properties")); - append($$anchor3, button_7); + var button_8 = root_8$1(); + template_effect(() => button_8.disabled = orphaned()); + delegated("click", button_8, () => act("properties")); + append($$anchor3, button_8); }; if_block(node_7, ($$render) => { if (isWindows()) $$render(consequent_6); }); } template_effect(() => { - button_4.disabled = orphaned(); button_5.disabled = orphaned(); button_6.disabled = orphaned(); + button_7.disabled = orphaned(); }); - delegated("click", button_4, () => act("copy-path")); - delegated("click", button_5, () => act("open-file")); - delegated("click", button_6, () => act("open-folder")); + delegated("click", button_5, () => act("copy-path")); + delegated("click", button_6, () => act("open-file")); + delegated("click", button_7, () => act("open-folder")); append($$anchor2, fragment); }; if_block(node_5, ($$render) => { @@ -12207,7 +13038,11 @@ function LineageContextMenu($$anchor, $$props) { return (_a = portal) == null ? void 0 : _a($$node); }); bind_this(div, ($$value) => menuEl = $$value, () => menuEl); - template_effect(() => set_style(div, `left:${get(pos).left ?? ""}px;top:${get(pos).top ?? ""}px;`)); + template_effect(() => { + set_style(div, `left:${get(pos).left ?? ""}px;top:${get(pos).top ?? ""}px;`); + set_text(text, pinned() ? "Unpin" : "Pin"); + }); + delegated("click", button_2, () => act("pin")); append($$anchor, div); pop(); } @@ -12248,7 +13083,7 @@ const DEFAULT_REVEAL = CHILDREN_PER_ROW_CAP - 1; function buildRender(tree, anchorHash, brokenOut, opts = {}) { var _a; const { byHash, childrenOf } = tree; - const { displayedHash, revealed = /* @__PURE__ */ new Map(), rootHash: rootOverride } = opts; + const { displayedHash, revealed = /* @__PURE__ */ new Map(), rootHash: rootOverride, pinned = /* @__PURE__ */ new Set() } = opts; const a = ancestry(tree, anchorHash); const laneStart = rootOverride && byHash.has(rootOverride) ? rootOverride : a.root; const col0 = buildLane(tree, laneStart, a.set); @@ -12297,7 +13132,7 @@ function buildRender(tree, anchorHash, brokenOut, opts = {}) { let bundleKids = []; let reals = kids; if (kids.length > CHILDREN_PER_ROW_CAP) { - const optional = kids.filter((k) => !activePath.has(k.hash) && !hasShownSubtree(k.hash)); + const optional = kids.filter((k) => !activePath.has(k.hash) && !hasShownSubtree(k.hash) && !pinned.has(k.hash)); const budget = revealed.get(h) ?? DEFAULT_REVEAL; const forced = kids.length - optional.length; const keepOptional = Math.max(0, budget - forced); @@ -12449,92 +13284,102 @@ function buildFigureRegions({ workflow, regionsRaw, poseFilesOk, apiURL }) { return []; } } -var root_3 = from_html(`
✓
`); -var root_4 = from_html(`
`); -var root_5 = from_html(`
`); -var root_6 = from_html(` `, 1); -var root_7 = from_html(``); -var root_2 = from_html(`
`); -var root_9 = from_html(``); -var root_10 = from_html(``); -var root_11 = from_html(`
`); -var root_13 = from_svg(``); -var root_14 = from_svg(``); -var root_17 = from_html(``); -var root_16 = from_html(``); -var root_15 = from_html(`
`); -var root_12 = from_html(`
`); -var root_22 = from_html(`
✓
`); -var root_23 = from_html(`
`); -var root_21 = from_html(`
`); -var root_26 = from_html(``); -var root_25 = from_html(`
`, 1); -var root_27 = from_html(`▴`); -var root_24 = from_html(`
`); -var root_28 = from_html(`
`); -var root_19 = from_html(`
`, 1); -var root_8 = from_html(`
`); -var root_29 = from_html(`
`); -var root_31 = from_html(`
`); -var root_32 = from_html(`
`); -var root_33 = from_html(`
`); -var root_34 = from_html(`
`); -var root_30 = from_html(`
`); -var root_35 = from_html(`
`); -var root_36 = from_html(`
📋 Path copied to clipboard
`); -var root_38 = from_html(`
`); -var root_39 = from_html(`Compare`); -var root_42 = from_html(`
`); -var root_41 = from_html( +var root_3 = from_html(``); +var root_4 = from_html(`
✓
`); +var root_5 = from_html(`
★
`); +var root_6 = from_html(`
`); +var root_7 = from_html(`
`); +var root_8 = from_html(` `, 1); +var root_9 = from_html(``); +var root_2 = from_html(`
`); +var root_11 = from_html(``); +var root_12 = from_html(``); +var root_13 = from_html(`
`); +var root_15 = from_svg(``); +var root_16 = from_svg(``); +var root_19 = from_html(``); +var root_18 = from_html(``); +var root_17 = from_html(`
`); +var root_14 = from_html(`
`); +var root_24 = from_html(``); +var root_25 = from_html(`
✓
`); +var root_26 = from_html(`
★
`); +var root_27 = from_html(`
`); +var root_23 = from_html(`
`); +var root_30 = from_html(``); +var root_29 = from_html(`
`, 1); +var root_31 = from_html(`▴`); +var root_28 = from_html(`
`); +var root_32 = from_html(`
`); +var root_21 = from_html(`
`, 1); +var root_10 = from_html(`
`); +var root_33 = from_html(`
`); +var root_35 = from_html(`
`); +var root_36 = from_html(`
`); +var root_37 = from_html(`
`); +var root_38 = from_html(`
`); +var root_34 = from_html(`
`); +var root_39 = from_html(`
`); +var root_40 = from_html(`
📋 Path copied to clipboard
`); +var root_42 = from_html(`
`); +var root_43 = from_html(``, 2); +var root_44 = from_html(``); +var root_45 = from_html(`Compare`); +var root_48 = from_html(`
`); +var root_47 = from_html( `
This image's record points to a file that no longer exists. If the file was moved or renamed, locate it — it's verified against the image's content hash, so the wrong file can't be attached.
`, 1 ); -var root_40 = from_html(`
`); -var root_44 = from_html(`
`); -var root_45 = from_html(`
`); -var root_43 = from_html(`
`, 1); -var root_37 = from_html(` `, 1); -var root_46 = from_html(`
`); -var root_47 = from_html(`
`); -var root_49 = from_html(`
`); -var root_50 = from_svg(``); -var root_51 = from_svg(``); -var root_53 = from_html(`
`); -var root_52 = from_html(`
`); -var root_48 = from_html(`
`); -var root_55 = from_html(`
`); -var root_54 = from_html(`
`); -var root_58 = from_html(`
`); -var root_59 = from_html(`
Clear
`, 1); -var root_57 = from_html(`
`); -var root_56 = from_html(`
Compare
`); -var root_63 = from_html(` `); -var root_62 = from_html(`
`); -var root_64 = from_html(`
`); -var root_65 = from_html(`
Open Folder
`); -var root_67 = from_html(`
Dimensions
`); -var root_68 = from_html(`
Size
`); -var root_69 = from_html(`
Generated
`); -var root_70 = from_html(`
Filename
`); -var root_71 = from_html(`
Status Source deleted
`); -var root_74 = from_html(`

`); -var root_75 = from_html(`
Global:

`); -var root_73 = from_html(` `, 1); -var root_76 = from_html(`
Positive Prompt:

`); -var root_77 = from_html(`
Negative Prompt:

`); -var root_72 = from_html(`
Prompt
`, 1); -var root_79 = from_html(`
Model
`); -var root_80 = from_html(`
Seed
`); -var root_81 = from_html(`
Steps
`); -var root_82 = from_html(`
CFG
`); -var root_83 = from_html(`
Sampler
`); -var root_84 = from_html(`
Denoise
`); -var root_78 = from_html(`
Generation Settings
`, 1); -var root_66 = from_html(`
Image Info
`, 1); -var root_85 = from_html(`
Loading metadata...
`); -var root_1 = from_html(`
`, 1); +var root_46 = from_html(`
`); +var root_50 = from_html(`
`); +var root_51 = from_html(`
`); +var root_49 = from_html(`
`, 1); +var root_41 = from_html(` `, 1); +var root_52 = from_html(`
`); +var root_53 = from_html(`
`); +var root_56 = from_html(``); +var root_55 = from_html(`
`); +var root_57 = from_svg(``); +var root_58 = from_svg(``); +var root_61 = from_html(``); +var root_60 = from_html(`
`); +var root_59 = from_html(`
`); +var root_54 = from_html(`
`); +var root_63 = from_html(`
`); +var root_62 = from_html(`
`); +var root_66 = from_html(`
`); +var root_67 = from_html(`
Clear
`, 1); +var root_65 = from_html(`
`); +var root_64 = from_html(`
Compare
`); +var root_71 = from_html(` `); +var root_70 = from_html(`
`); +var root_72 = from_html(`
`); +var root_73 = from_html(`
`); +var root_74 = from_html(`
`); +var root_75 = from_html(`
Open Folder
`); +var root_77 = from_html(`
Dimensions
`); +var root_78 = from_html(`
Size
`); +var root_79 = from_html(`
Generated
`); +var root_80 = from_html(`
Filename
`); +var root_81 = from_html(`
Status Source deleted
`); +var root_84 = from_html(`

`); +var root_85 = from_html(`
Global:

`); +var root_83 = from_html(` `, 1); +var root_86 = from_html(`
Positive Prompt:

`); +var root_87 = from_html(`
Negative Prompt:

`); +var root_82 = from_html(`
Prompt
`, 1); +var root_89 = from_html(`
Model
`); +var root_90 = from_html(`
Seed
`); +var root_91 = from_html(`
Steps
`); +var root_92 = from_html(`
CFG
`); +var root_93 = from_html(`
Sampler
`); +var root_94 = from_html(`
Denoise
`); +var root_88 = from_html(`
Generation Settings
`, 1); +var root_76 = from_html(`
Image Info
`, 1); +var root_95 = from_html(`
Loading metadata...
`); +var root_1 = from_html(`
`, 1); function ImageViewer($$anchor, $$props) { var _a; push($$props, true); @@ -12557,6 +13402,42 @@ function ImageViewer($$anchor, $$props) { 3, null // (el, {width,height,outputMode}) => Promise — detached 3D poser + ), onExtendPrepare = prop( + $$props, + "onExtendPrepare", + 3, + null + // (hash) => Promise<{lastFrameFilename,sourceRef,fps,frameCount,templateId,...}> + ), onExtendRun = prop( + $$props, + "onExtendRun", + 3, + null + // (opts) => tracker — background video continuation render + ), onSmoothCheck = prop( + $$props, + "onSmoothCheck", + 3, + null + // () => bool — RIFE pack + core video nodes registered + ), onSmoothInstall = prop( + $$props, + "onSmoothInstall", + 3, + null + // () => Promise — offer to install the RIFE pack + ), onSmoothPrepare = prop( + $$props, + "onSmoothPrepare", + 3, + null + // (hash) => Promise<{sourceRef,fps,frameCount,...}> + ), onSmoothRun = prop( + $$props, + "onSmoothRun", + 3, + null + // (opts) => tracker — background RIFE interpolation ), autoEdit = prop($$props, "autoEdit", 3, false); let currentIndex = state(proxy(startIndex())); let displayedHash = state(proxy(((_a = images()[startIndex()]) == null ? void 0 : _a.hash) || null)); @@ -12833,12 +13714,20 @@ function ImageViewer($$anchor, $$props) { let lineageHiddenCount = user_derived(() => get(lineageList).length - get(lineageVisible).length); let brokenOut = state(proxy(/* @__PURE__ */ new Set())); let lineageRevealed = state(proxy(/* @__PURE__ */ new Map())); + let pinnedHashes = state(proxy(/* @__PURE__ */ new Set())); + function togglePin(hash) { + const next = new Set(get(pinnedHashes)); + if (next.has(hash)) next.delete(hash); + else next.add(hash); + set(pinnedHashes, next, true); + } let subwayAnchor = user_derived(() => get(lineageFocusHash) && get(lineageTree).byHash.has(get(lineageFocusHash)) ? get(lineageFocusHash) : get(displayedHash)); let focusRootHash = state(null); let laneRender = user_derived(() => buildRender(get(lineageTree), get(subwayAnchor), get(brokenOut), { displayedHash: get(displayedHash), revealed: get(lineageRevealed), - rootHash: get(focusRootHash) + rootHash: get(focusRootHash), + pinned: get(pinnedHashes) })); let lanesActive = user_derived(() => (get(brokenOut).size > 0 || !!get(focusRootHash)) && get(lineageList).length > 1); function isInSubtree(desc, rootHash) { @@ -12881,14 +13770,15 @@ function ImageViewer($$anchor, $$props) { const prefs = readLanePrefs()[root2]; set(brokenOut, new Set(((prefs == null ? void 0 : prefs.brokenOut) || []).filter((h) => get(lineageTree).byHash.has(h))), true); set(lineageRevealed, new Map(((prefs == null ? void 0 : prefs.revealed) || []).filter(([k]) => get(lineageTree).byHash.has(k))), true); + set(pinnedHashes, new Set(((prefs == null ? void 0 : prefs.pinned) || []).filter((h) => get(lineageTree).byHash.has(h))), true); }); user_effect(() => { const root2 = get(familyRootHash); - const bo = [...get(brokenOut)], rv = [...get(lineageRevealed)]; + const bo = [...get(brokenOut)], rv = [...get(lineageRevealed)], pn = [...get(pinnedHashes)]; if (!root2 || root2 !== hydratedRoot) return; try { const all = readLanePrefs(); - if (bo.length || rv.length) all[root2] = { brokenOut: bo, revealed: rv }; + if (bo.length || rv.length || pn.length) all[root2] = { brokenOut: bo, revealed: rv, pinned: pn }; else delete all[root2]; localStorage.setItem(LANE_PREFS_KEY, JSON.stringify(all)); } catch { @@ -12943,7 +13833,7 @@ function ImageViewer($$anchor, $$props) { } function handleNodeClick(e, item) { if (get(compareDropdownOpen)) { - if (item.hash !== get(displayedHash)) enterCompareWith(item.hash, item.filename || item.hash.slice(0, 8)); + if (item.hash !== get(displayedHash) && !isVideoItem(item)) enterCompareWith(item.hash, item.filename || item.hash.slice(0, 8)); return; } if (e.ctrlKey || e.metaKey || e.shiftKey) { @@ -13103,7 +13993,7 @@ function ImageViewer($$anchor, $$props) { get(laneRender).nodes; requestAnimationFrame(centerCurrentNode); }); - let compareTargets = user_derived(() => get(lineageList).filter((item) => (item == null ? void 0 : item.hash) !== get(displayedHash))); + let compareTargets = user_derived(() => get(lineageList).filter((item) => (item == null ? void 0 : item.hash) !== get(displayedHash) && !isVideoItem(item))); let hasCompareTargets = user_derived(() => get(compareTargets).length > 0); let compareImageUrl = user_derived(() => get(compareTargetHash) ? imageUrl(get(compareTargetHash)) : null); let compareTargetItem = user_derived(() => get(compareTargetHash) ? get(lineageList).find((item) => (item == null ? void 0 : item.hash) === get(compareTargetHash)) || null : null); @@ -13193,6 +14083,30 @@ function ImageViewer($$anchor, $$props) { if (img == null ? void 0 : img._directUrl) return img._directUrl; return apiURL()(`/promptchain/thumb/${hash}`); } + 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(); + } + const VIDEO_EXT_RE = /\.(mp4|webm|m4v|mkv|mov|avi)(\?|$)/i; + function isVideoItem(item) { + return VIDEO_EXT_RE.test((item == null ? void 0 : item.filename) || ""); + } + function isVideoHash(hash) { + var _a2; + if (!hash) return false; + const entry = get(imageByHash).get(hash); + if (entry == null ? void 0 : entry.filename) return isVideoItem(entry); + const fam = get(lineageList).find((i) => (i == null ? void 0 : i.hash) === hash); + if (fam == null ? void 0 : fam.filename) return isVideoItem(fam); + if (((_a2 = get(imageInfo)) == null ? void 0 : _a2.hash) === hash) return isVideoItem(get(imageInfo)); + return false; + } + let displayedIsVideo = user_derived(() => isVideoHash(get(displayedHash))); function jumpTo(idx) { if (idx >= 0 && idx < images().length && idx !== get(currentIndex)) { set(currentIndex, idx, true); @@ -13209,6 +14123,25 @@ function ImageViewer($$anchor, $$props) { set(panY, 0); } function navigateLineage(delta) { + var _a2; + if (get(lanesActive)) { + const renderedReal = (h) => get(laneRender).nodes.some((n) => n.kind === "node" && n.hash === h); + if (delta < 0) { + const p = (_a2 = get(lineageTree).byHash.get(get(displayedHash))) == null ? void 0 : _a2.parent_hash; + if (p && renderedReal(p)) { + lineageJump(p); + exitCompare(); + } + } else { + const kids = (get(lineageTree).childrenOf.get(get(displayedHash)) || []).map((k) => k.hash).filter(renderedReal); + if (kids.length) { + const a = ancestry(get(lineageTree), get(subwayAnchor)); + lineageJump(kids.find((h) => a.set.has(h)) || kids[0]); + exitCompare(); + } + } + return; + } const idx = get(lineageVisible).findIndex((item) => item.hash === get(displayedHash)); if (idx < 0) return; const next = idx + delta; @@ -13217,7 +14150,20 @@ function ImageViewer($$anchor, $$props) { exitCompare(); } } + function navigateTreeSibling(delta) { + const reals = get(laneRender).nodes.filter((n) => n.kind === "node"); + const cur = reals.find((n) => n.hash === get(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 === get(displayedHash)); + const next = row[i + delta]; + if (next) { + lineageJump(next.hash); + exitCompare(); + } + } function enterCompareWith(targetHash, label) { + if (isVideoHash(targetHash)) return; if (get(compareMode) && get(compareTargetHash) === targetHash) { exitCompare(); return; @@ -13240,6 +14186,9 @@ function ImageViewer($$anchor, $$props) { set(compareTargetLabel, ""); set(compareDropdownOpen, false); } + user_effect(() => { + if (get(displayedIsVideo) && (get(compareMode) || get(compareDropdownOpen))) exitCompare(); + }); function handleSliderDown(e) { e.preventDefault(); e.stopPropagation(); @@ -13496,6 +14445,182 @@ function ImageViewer($$anchor, $$props) { reposeUnsub = null; reposeTracker = null; } + 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; + let extendUnsub = null; + function landOnResult(entry) { + if (!(entry == null ? void 0 : entry.hash)) return; + const existingIdx = images().findIndex((i) => i.hash === entry.hash); + if (existingIdx >= 0) { + set(currentIndex, existingIdx, true); + } else { + const srcIdx = images().findIndex((i) => i.hash === get(displayedHash)); + const insertAt = srcIdx >= 0 ? srcIdx + 1 : images().length; + images([ + ...images().slice(0, insertAt), + entry, + ...images().slice(insertAt) + ]); + set(currentIndex, insertAt, true); + } + set(displayedHash, entry.hash, true); + } + async function openExtend() { + if (get(extendPreparing) || !onExtendPrepare() || !onExtendRun() || !get(displayedHash)) return; + set(extendPreparing, true); + try { + const prepared = await onExtendPrepare()(get(displayedHash)); + if (prepared == null ? void 0 : prepared.lastFrameFilename) { + set(extendPrepared, prepared, true); + set(extendProgress, null); + set(extendModalOpen, true); + } + } catch (e) { + console.error("[PromptChain] extend prepare failed", e); + } + set(extendPreparing, false); + } + function confirmExtend(opts) { + if (!onExtendRun() || !get(extendPrepared)) return; + const base = get(extendPrepared).templateId; + const templateId = opts.recipe === "lightning" ? `${base}-fast` : base; + const tracker = onExtendRun()({ + templateId, + lastFrameFilename: get(extendPrepared).lastFrameFilename, + parentFilename: get(extendPrepared).sourceRef, + length: Math.min(161, opts.length || get( + extendPrepared + // ceiling: one continuation clip, never a long source's full frame count + ).frameCount || 81), + width: opts.width || void 0, + height: opts.height || void 0, + outFps: opts.outFps || void 0, + 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: get(extendPrepared).sourceRef, + fps: get(extendPrepared).fps + }); + extendTracker = tracker; + extendUnsub == null ? void 0 : extendUnsub(); + extendUnsub = tracker.subscribe((state2) => { + var _a2; + if (state2.phase === "cancelled") { + set(extendProgress, null); + return; + } + set(extendProgress, state2, true); + if (state2.phase === "done" && ((_a2 = state2.entry) == null ? void 0 : _a2.hash)) set(extendLastEntry, state2.entry, true); + }); + } + function finishExtend() { + if (get(extendLastEntry)) { + landOnResult(get(extendLastEntry)); + showNewGenBanner(null); + } + closeExtend(); + } + function closeExtend() { + set(extendModalOpen, false); + set(extendPrepared, null); + set(extendProgress, null); + set(extendLastEntry, null); + extendUnsub == null ? void 0 : extendUnsub(); + extendUnsub = null; + extendTracker = null; + } + 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; + let smoothUnsub = null; + async function openSmooth() { + var _a2; + if (get(smoothPreparing) || !onSmoothPrepare() || !onSmoothRun() || !get(displayedHash)) return; + if (onSmoothCheck() && !onSmoothCheck()()) { + (_a2 = onSmoothInstall()) == null ? void 0 : _a2(); + return; + } + set(smoothPreparing, true); + try { + const prepared = await onSmoothPrepare()(get(displayedHash)); + if (prepared == null ? void 0 : prepared.sourceRef) { + set(smoothPrepared, prepared, true); + set(smoothProgress, null); + set(smoothModalOpen, true); + } + } catch (e) { + console.error("[PromptChain] smooth prepare failed", e); + } + set(smoothPreparing, false); + } + function confirmSmooth(opts) { + if (!onSmoothRun() || !get(smoothPrepared)) return; + const srcFps = get(smoothPrepared).fps || 16; + const maxMult = Math.max(2, Math.floor(120 / srcFps)); + const mult = Math.min(opts.multiplier || 2, maxMult); + const tracker = onSmoothRun()({ + sourceVideoFilename: get(smoothPrepared).sourceRef, + multiplier: mult, + outFps: Math.round(srcFps * mult), + ckptName: opts.ckptName, + scaleFactor: opts.scaleFactor, + ensemble: opts.ensemble, + fastMode: opts.fastMode, + parentFilename: get(smoothPrepared).sourceRef + }); + smoothTracker = tracker; + smoothUnsub == null ? void 0 : smoothUnsub(); + smoothUnsub = tracker.subscribe((state2) => { + var _a2; + if (state2.phase === "cancelled") { + set(smoothProgress, null); + return; + } + set(smoothProgress, state2, true); + if (state2.phase === "done" && ((_a2 = state2.entry) == null ? void 0 : _a2.hash)) set(smoothLastEntry, state2.entry, true); + }); + } + function finishSmooth() { + if (get(smoothLastEntry)) { + landOnResult(get(smoothLastEntry)); + showNewGenBanner(null); + } + closeSmooth(); + } + function closeSmooth() { + set(smoothModalOpen, false); + set(smoothPrepared, null); + set(smoothProgress, null); + set(smoothLastEntry, null); + smoothUnsub == null ? void 0 : smoothUnsub(); + smoothUnsub = null; + smoothTracker = null; + } let inpaintModalOpen = state(false); let inpaintPrepared = state(null); function inpaintSaved(entry) { @@ -13547,6 +14672,7 @@ function ImageViewer($$anchor, $$props) { let inpaintFromEdit = state(false); async function openEdit(targetHash = get(displayedHash)) { if (get(editPreparing) || !onEditPrepare() || !onEditSave()) return; + if (isVideoHash(targetHash)) return; const entry = images().find((i) => i.hash === targetHash) || { hash: targetHash }; set(editPreparing, true); try { @@ -13865,18 +14991,24 @@ ${negative}` : "")); requestDeleteBatch(); return; } - if ((e.key === "c" || e.key === "C") && !e.ctrlKey && !e.metaKey && get(hasCompareTargets)) { + if ((e.key === "c" || e.key === "C") && !e.ctrlKey && !e.metaKey && get(hasCompareTargets) && !get(displayedIsVideo)) { e.preventDefault(); if (get(compareMode)) exitCompare(); else set(compareDropdownOpen, !get(compareDropdownOpen)); return; } if (e.key === "ArrowLeft") { - navigate(-1); + if (get(lanesActive)) { + e.preventDefault(); + navigateTreeSibling(-1); + } else navigate(-1); return; } if (e.key === "ArrowRight") { - navigate(1); + if (get(lanesActive)) { + e.preventDefault(); + navigateTreeSibling(1); + } else navigate(1); return; } if (e.key === "ArrowUp") { @@ -14058,6 +15190,8 @@ ${negative}` : "")); expandBranches(item); } else if (action2 === "focus-branch") { focusBranch(hash); + } else if (action2 === "pin") { + togglePin(hash); } else if (action2 === "copy-path") { copyPath(hash); } else if (action2 === "edit") { @@ -14238,32 +15372,54 @@ ${negative}` : "")); var div_2 = root_3(); append($$anchor3, div_2); }; - var d_1 = user_derived(() => get(selectedHashes).has(item().hash)); + var d_1 = user_derived(() => isVideoItem(item())); if_block(node_1, ($$render) => { if (get(d_1)) $$render(consequent); }); } - var div_3 = sibling(node_1, 2); - let classes_1; - var node_2 = sibling(div_3, 2); + var node_2 = sibling(node_1, 2); { var consequent_1 = ($$anchor3) => { - var div_4 = root_4(); - append($$anchor3, div_4); + var div_3 = root_4(); + append($$anchor3, div_3); }; + var d_2 = user_derived(() => get(selectedHashes).has(item().hash)); if_block(node_2, ($$render) => { - if (get(editDocLayers)[item().hash]) $$render(consequent_1); + if (get(d_2)) $$render(consequent_1); }); } var node_3 = sibling(node_2, 2); { var consequent_2 = ($$anchor3) => { + var div_4 = root_5(); + append($$anchor3, div_4); + }; + var d_3 = user_derived(() => get(pinnedHashes).has(item().hash)); + if_block(node_3, ($$render) => { + if (get(d_3)) $$render(consequent_2); + }); + } + var div_5 = sibling(node_3, 2); + let classes_1; + var node_4 = sibling(div_5, 2); + { + var consequent_3 = ($$anchor3) => { + var div_6 = root_6(); + append($$anchor3, div_6); + }; + if_block(node_4, ($$render) => { + if (get(editDocLayers)[item().hash]) $$render(consequent_3); + }); + } + var node_5 = sibling(node_4, 2); + { + var consequent_4 = ($$anchor3) => { const op = user_derived(() => get(lineageOps).get(item().hash)); - var div_5 = root_5(); - var text_1 = child(div_5); + var div_7 = root_7(); + var text_1 = child(div_7); template_effect( ($0, $1) => { - set_attribute(div_5, "title", `Upscaled ${$0 ?? ""}from ${get(op).from.w ?? ""}×${get(op).from.h ?? ""}`); + set_attribute(div_7, "title", `Upscaled ${$0 ?? ""}from ${get(op).from.w ?? ""}×${get(op).from.h ?? ""}`); set_text(text_1, `▲${$1 ?? ""}`); }, [ @@ -14271,24 +15427,24 @@ ${negative}` : "")); () => get(op).uniform ? fmtFactor(get(op).factor) : "" ] ); - append($$anchor3, div_5); + append($$anchor3, div_7); }; - var d_2 = user_derived(() => { + var d_4 = user_derived(() => { var _a2; return ((_a2 = get(lineageOps).get(item().hash)) == null ? void 0 : _a2.kind) === "upscale"; }); - if_block(node_3, ($$render) => { - if (get(d_2)) $$render(consequent_2); + if_block(node_5, ($$render) => { + if (get(d_4)) $$render(consequent_4); }); } - var node_4 = sibling(node_3, 2); + var node_6 = sibling(node_5, 2); { - var consequent_3 = ($$anchor3) => { + var consequent_5 = ($$anchor3) => { const n = user_derived(() => get(laneRender).junctionsByNode.get(item().hash).hidden.length); - var fragment_1 = root_6(); - var div_6 = first_child(fragment_1); - var text_2 = child(div_6); - var button = sibling(div_6, 2); + var fragment_1 = root_8(); + var div_8 = first_child(fragment_1); + var text_2 = child(div_8); + var button = sibling(div_8, 2); var text_3 = child(button); template_effect(() => { set_text(text_2, `⋔${get(n) ?? ""}`); @@ -14301,27 +15457,27 @@ ${negative}` : "")); }); append($$anchor3, fragment_1); }; - var d_3 = user_derived(() => { + var d_5 = user_derived(() => { var _a2; return (_a2 = get(laneRender).junctionsByNode.get(item().hash)) == null ? void 0 : _a2.hidden.length; }); - if_block(node_4, ($$render) => { - if (get(d_3)) $$render(consequent_3); + if_block(node_6, ($$render) => { + if (get(d_5)) $$render(consequent_5); }); } - var node_5 = sibling(node_4, 2); + var node_7 = sibling(node_6, 2); { - var consequent_4 = ($$anchor3) => { - var button_1 = root_7(); + var consequent_6 = ($$anchor3) => { + var button_1 = root_9(); delegated("click", button_1, (e) => { e.stopPropagation(); foldLane(item().hash); }); append($$anchor3, button_1); }; - var d_4 = user_derived(() => get(brokenOut).has(item().hash)); - if_block(node_5, ($$render) => { - if (get(d_4)) $$render(consequent_4); + var d_6 = user_derived(() => get(brokenOut).has(item().hash)); + if_block(node_7, ($$render) => { + if (get(d_6)) $$render(consequent_6); }); } template_effect( @@ -14330,7 +15486,7 @@ ${negative}` : "")); classes = set_class(div_1, 1, "pcr-viewer-lineage-node svelte-5cciiw", null, classes, $0); set_attribute(div_1, "data-hash", item().hash); set_attribute(img_1, "src", $1); - classes_1 = set_class(div_3, 1, "pcr-viewer-lineage-dot svelte-5cciiw", null, classes_1, { + classes_1 = set_class(div_5, 1, "pcr-viewer-lineage-dot svelte-5cciiw", null, classes_1, { "dot-root": item().hash === get(laneRender).rootHash, "dot-current": item().hash === get(displayedHash), "dot-tip": ((_a2 = meta()) == null ? void 0 : _a2.isTip) && item().hash !== get(displayedHash) @@ -14340,8 +15496,9 @@ ${negative}` : "")); () => ({ current: item().hash === get(displayedHash), selected: get(selectedHashes).has(item().hash), + pinned: get(pinnedHashes).has(item().hash), orphaned: item().orphaned === 1, - "compare-candidate": get(compareDropdownOpen) && item().hash !== get(displayedHash), + "compare-candidate": get(compareDropdownOpen) && item().hash !== get(displayedHash) && !isVideoItem(item()), "compare-left": get(compareMode) && item().hash === get(displayedHash), "compare-right": get(compareMode) && item().hash === get(compareTargetHash) }), @@ -14355,20 +15512,21 @@ ${negative}` : "")); hideTip(); openLineageMenu(e, item()); }); + event("error", img_1, retryThumb); append($$anchor2, div_1); }; - var node_6 = child(div); + var node_8 = child(div); { - var consequent_15 = ($$anchor2) => { - var div_7 = root_8(); + var consequent_19 = ($$anchor2) => { + var div_9 = root_10(); let classes_2; - var div_8 = child(div_7); - var span = child(div_8); + var div_10 = child(div_9); + var span = child(div_10); var text_4 = child(span); - var node_7 = sibling(span, 2); + var node_9 = sibling(span, 2); { - var consequent_5 = ($$anchor3) => { - var button_2 = root_9(); + var consequent_7 = ($$anchor3) => { + var button_2 = root_11(); let classes_3; var text_5 = child(button_2); template_effect(() => { @@ -14379,70 +15537,71 @@ ${negative}` : "")); delegated("click", button_2, () => get(lanesActive) ? collapseAllLanes() : expandAll()); append($$anchor3, button_2); }; - if_block(node_7, ($$render) => { - if (get(hasBranches)) $$render(consequent_5); + if_block(node_9, ($$render) => { + if (get(hasBranches)) $$render(consequent_7); }); } - var node_8 = sibling(div_8, 2); + var node_10 = sibling(div_10, 2); { - var consequent_6 = ($$anchor3) => { - var button_3 = root_10(); + var consequent_8 = ($$anchor3) => { + var button_3 = root_12(); delegated("click", button_3, clearFocus); append($$anchor3, button_3); }; - if_block(node_8, ($$render) => { - if (get(focusRootHash)) $$render(consequent_6); + if_block(node_10, ($$render) => { + if (get(focusRootHash)) $$render(consequent_8); }); } - var node_9 = sibling(node_8, 2); + var node_11 = sibling(node_10, 2); { - var consequent_7 = ($$anchor3) => { - var div_9 = root_11(); - var text_6 = child(div_9); + var consequent_9 = ($$anchor3) => { + var div_11 = root_13(); + var text_6 = child(div_11); var button_4 = sibling(text_6); var button_5 = sibling(button_4, 2); template_effect(() => set_text(text_6, `${get(selectedHashes).size ?? ""} selected `)); delegated("click", button_4, requestDeleteBatch); delegated("click", button_5, clearSelection); - append($$anchor3, div_9); + append($$anchor3, div_11); }; - if_block(node_9, ($$render) => { - if (get(selectedHashes).size) $$render(consequent_7); + if_block(node_11, ($$render) => { + if (get(selectedHashes).size) $$render(consequent_9); }); } - var node_10 = sibling(node_9, 2); + var node_12 = sibling(node_11, 2); { - var consequent_9 = ($$anchor3) => { - var div_10 = root_12(); - var div_11 = child(div_10); - var svg = child(div_11); - var node_11 = child(svg); - each(node_11, 17, () => get(connectorsBase), (g) => g.key, ($$anchor4, g) => { - var path_1 = root_13(); + var consequent_11 = ($$anchor3) => { + var div_12 = root_14(); + var div_13 = child(div_12); + var svg = child(div_13); + var node_13 = child(svg); + each(node_13, 17, () => get(connectorsBase), (g) => g.key, ($$anchor4, g) => { + var path_1 = root_15(); template_effect(() => set_attribute(path_1, "d", get(g).d)); append($$anchor4, path_1); }); - var node_12 = sibling(node_11); - each(node_12, 17, () => get(connectorsActive), (g) => g.key, ($$anchor4, g) => { - var path_2 = root_14(); + var node_14 = sibling(node_13); + each(node_14, 17, () => get(connectorsActive), (g) => g.key, ($$anchor4, g) => { + var path_2 = root_16(); template_effect(() => set_attribute(path_2, "d", get(g).d)); append($$anchor4, path_2); }); - var node_13 = sibling(svg, 2); - each(node_13, 17, () => get(laneRender).nodes, (node) => node.hash, ($$anchor4, node) => { - var div_12 = root_15(); + var node_15 = sibling(svg, 2); + each(node_15, 17, () => get(laneRender).nodes, (node) => node.hash, ($$anchor4, node) => { + var div_14 = root_17(); let classes_4; - var node_14 = child(div_12); + var node_16 = child(div_14); { - var consequent_8 = ($$anchor5) => { - var button_6 = root_16(); - var div_13 = child(button_6); - each(div_13, 20, () => get(node).preview, (p) => p, ($$anchor6, p) => { - var img_2 = root_17(); + var consequent_10 = ($$anchor5) => { + var button_6 = root_18(); + var div_15 = child(button_6); + each(div_15, 20, () => get(node).preview, (p) => p, ($$anchor6, p) => { + var img_2 = root_19(); template_effect(($0) => set_attribute(img_2, "src", $0), [() => thumbUrl(p)]); + event("error", img_2, retryThumb); append($$anchor6, img_2); }); - var span_1 = sibling(div_13, 2); + var span_1 = sibling(div_15, 2); var text_7 = child(span_1); template_effect(() => { set_attribute(button_6, "data-hash", get(node).hash); @@ -14455,72 +15614,94 @@ ${negative}` : "")); var alternate = ($$anchor5) => { lineageNode($$anchor5, () => get(node).item, () => get(node)); }; - if_block(node_14, ($$render) => { - if (get(node).kind === "bundle") $$render(consequent_8); + if_block(node_16, ($$render) => { + if (get(node).kind === "bundle") $$render(consequent_10); else $$render(alternate, -1); }); } template_effect( ($0) => { - classes_4 = set_class(div_12, 1, "pcr-lin-treenode svelte-5cciiw", null, classes_4, $0); - set_style(div_12, `left: ${get(node).x * LANE_COL_W}px; top: ${get(node).depth * LANE_ROW_H}px`); + classes_4 = set_class(div_14, 1, "pcr-lin-treenode svelte-5cciiw", null, classes_4, $0); + set_style(div_14, `left: ${get(node).x * LANE_COL_W}px; top: ${get(node).depth * LANE_ROW_H}px`); }, [ () => ({ dimmed: !get(activePath).nodes.has(get(node).hash) }) ] ); - append($$anchor4, div_12); + append($$anchor4, div_14); }); - bind_this(div_11, ($$value) => set(lanesWrapEl, $$value), () => get(lanesWrapEl)); - bind_this(div_10, ($$value) => set(treeScrollEl, $$value), () => get(treeScrollEl)); + bind_this(div_13, ($$value) => set(lanesWrapEl, $$value), () => get(lanesWrapEl)); + bind_this(div_12, ($$value) => set(treeScrollEl, $$value), () => get(treeScrollEl)); template_effect(() => { - set_style(div_11, `width: ${get(laneRender).width * LANE_COL_W}px; height: ${get(laneRender).height * LANE_ROW_H}px`); + set_style(div_13, `width: ${get(laneRender).width * LANE_COL_W}px; height: ${get(laneRender).height * LANE_ROW_H}px`); set_attribute(svg, "width", get(connectorBox).w); set_attribute(svg, "height", get(connectorBox).h); }); - append($$anchor3, div_10); + append($$anchor3, div_12); }; var alternate_3 = ($$anchor3) => { - var fragment_3 = root_19(); - var div_14 = first_child(fragment_3); - each(div_14, 23, () => get(lineageDisplay), (d) => d.kind === "node" ? d.item.hash : `${d.kind}:${d.at}`, ($$anchor4, d, i) => { + var fragment_3 = root_21(); + var div_16 = first_child(fragment_3); + each(div_16, 23, () => get(lineageDisplay), (d) => d.kind === "node" ? d.item.hash : `${d.kind}:${d.at}`, ($$anchor4, d, i) => { var fragment_4 = comment(); - var node_15 = first_child(fragment_4); + var node_17 = first_child(fragment_4); { - var consequent_12 = ($$anchor5) => { + var consequent_16 = ($$anchor5) => { const item = user_derived(() => get(d).item); - var div_15 = root_21(); + var div_17 = root_23(); let classes_5; - var img_3 = child(div_15); - var node_16 = sibling(img_3, 2); + var img_3 = child(div_17); + var node_18 = sibling(img_3, 2); { - var consequent_10 = ($$anchor6) => { - var div_16 = root_22(); - append($$anchor6, div_16); - }; - var d_5 = user_derived(() => get(selectedHashes).has(get(item).hash)); - if_block(node_16, ($$render) => { - if (get(d_5)) $$render(consequent_10); - }); - } - var div_17 = sibling(node_16, 2); - let classes_6; - var node_17 = sibling(div_17, 2); - { - var consequent_11 = ($$anchor6) => { - var div_18 = root_23(); + var consequent_12 = ($$anchor6) => { + var div_18 = root_24(); append($$anchor6, div_18); }; - if_block(node_17, ($$render) => { - if (get(editDocLayers)[get(item).hash]) $$render(consequent_11); + var d_7 = user_derived(() => isVideoItem(get(item))); + if_block(node_18, ($$render) => { + if (get(d_7)) $$render(consequent_12); + }); + } + var node_19 = sibling(node_18, 2); + { + var consequent_13 = ($$anchor6) => { + var div_19 = root_25(); + append($$anchor6, div_19); + }; + var d_8 = user_derived(() => get(selectedHashes).has(get(item).hash)); + if_block(node_19, ($$render) => { + if (get(d_8)) $$render(consequent_13); + }); + } + var node_20 = sibling(node_19, 2); + { + var consequent_14 = ($$anchor6) => { + var div_20 = root_26(); + append($$anchor6, div_20); + }; + var d_9 = user_derived(() => get(pinnedHashes).has(get(item).hash)); + if_block(node_20, ($$render) => { + if (get(d_9)) $$render(consequent_14); + }); + } + var div_21 = sibling(node_20, 2); + let classes_6; + var node_21 = sibling(div_21, 2); + { + var consequent_15 = ($$anchor6) => { + var div_22 = root_27(); + append($$anchor6, div_22); + }; + if_block(node_21, ($$render) => { + if (get(editDocLayers)[get(item).hash]) $$render(consequent_15); }); } template_effect( ($0, $1) => { var _a2; - classes_5 = set_class(div_15, 1, "pcr-viewer-lineage-node svelte-5cciiw", null, classes_5, $0); + classes_5 = set_class(div_17, 1, "pcr-viewer-lineage-node svelte-5cciiw", null, classes_5, $0); set_attribute(img_3, "src", $1); - classes_6 = set_class(div_17, 1, "pcr-viewer-lineage-dot svelte-5cciiw", null, classes_6, { + classes_6 = set_class(div_21, 1, "pcr-viewer-lineage-dot svelte-5cciiw", null, classes_6, { "dot-root": get(item).hash === ((_a2 = get(lineageList)[0]) == null ? void 0 : _a2.hash), "dot-current": get(i) === get(lineageDisplayCurrentIdx), "dot-tip": get(i) === get(lineageDisplay).length - 1 && get(i) !== get(lineageDisplayCurrentIdx) @@ -14533,163 +15714,166 @@ ${negative}` : "")); descendant: get(i) > get(lineageDisplayCurrentIdx), branch: get(d).branch, selected: get(selectedHashes).has(get(item).hash), + pinned: get(pinnedHashes).has(get(item).hash), orphaned: get(item).orphaned === 1, - "compare-candidate": get(compareDropdownOpen) && get(item).hash !== get(displayedHash), + "compare-candidate": get(compareDropdownOpen) && get(item).hash !== get(displayedHash) && !isVideoItem(get(item)), "compare-left": get(compareMode) && get(item).hash === get(displayedHash), "compare-right": get(compareMode) && get(item).hash === get(compareTargetHash) }), () => thumbUrl(get(item).hash) ] ); - delegated("click", div_15, (e) => handleNodeClick(e, get(item))); - event("mouseenter", div_15, (e) => showTip(e, get(item))); - event("mouseleave", div_15, hideTip); - delegated("contextmenu", div_15, (e) => { + delegated("click", div_17, (e) => handleNodeClick(e, get(item))); + event("mouseenter", div_17, (e) => showTip(e, get(item))); + event("mouseleave", div_17, hideTip); + delegated("contextmenu", div_17, (e) => { hideTip(); openLineageMenu(e, get(item)); }); - append($$anchor5, div_15); + event("error", img_3, retryThumb); + append($$anchor5, div_17); }; var alternate_2 = ($$anchor5) => { - var div_19 = root_24(); + var div_23 = root_28(); let classes_7; - var node_18 = child(div_19); + var node_22 = child(div_23); { - var consequent_13 = ($$anchor6) => { - var fragment_5 = root_25(); - var div_20 = first_child(fragment_5); - each(div_20, 21, () => get(d).preview, (p) => p.hash, ($$anchor7, p) => { - var img_4 = root_26(); + var consequent_17 = ($$anchor6) => { + var fragment_5 = root_29(); + var div_24 = first_child(fragment_5); + each(div_24, 21, () => get(d).preview, (p) => p.hash, ($$anchor7, p) => { + var img_4 = root_30(); template_effect(($0) => set_attribute(img_4, "src", $0), [() => thumbUrl(get(p).hash)]); + event("error", img_4, retryThumb); append($$anchor7, img_4); }); - var span_2 = sibling(div_20, 2); + var span_2 = sibling(div_24, 2); var text_8 = child(span_2); template_effect(() => set_text(text_8, `+${get(d).count ?? ""}`)); append($$anchor6, fragment_5); }; var alternate_1 = ($$anchor6) => { - var span_3 = root_27(); + var span_3 = root_31(); append($$anchor6, span_3); }; - if_block(node_18, ($$render) => { - if (get(d).kind === "bundle") $$render(consequent_13); + if_block(node_22, ($$render) => { + if (get(d).kind === "bundle") $$render(consequent_17); else $$render(alternate_1, -1); }); } template_effect(() => { - classes_7 = set_class(div_19, 1, "pcr-viewer-lineage-bundle svelte-5cciiw", null, classes_7, { expanded: get(d).kind === "collapse" }); - set_attribute(div_19, "title", get(d).kind === "collapse" ? "Collapse this branch" : `${get(d).count} other generation${get(d).count > 1 ? "s" : ""} branch off here — click to expand`); + classes_7 = set_class(div_23, 1, "pcr-viewer-lineage-bundle svelte-5cciiw", null, classes_7, { expanded: get(d).kind === "collapse" }); + set_attribute(div_23, "title", get(d).kind === "collapse" ? "Collapse this branch" : `${get(d).count} other generation${get(d).count > 1 ? "s" : ""} branch off here — click to expand`); }); - delegated("click", div_19, () => toggleBundle(get(d).at)); - append($$anchor5, div_19); + delegated("click", div_23, () => toggleBundle(get(d).at)); + append($$anchor5, div_23); }; - if_block(node_15, ($$render) => { - if (get(d).kind === "node") $$render(consequent_12); + if_block(node_17, ($$render) => { + if (get(d).kind === "node") $$render(consequent_16); else $$render(alternate_2, -1); }); } append($$anchor4, fragment_4); }); - var node_19 = sibling(div_14, 2); + var node_23 = sibling(div_16, 2); { - var consequent_14 = ($$anchor4) => { - var div_21 = root_28(); - var text_9 = child(div_21); + var consequent_18 = ($$anchor4) => { + var div_25 = root_32(); + var text_9 = child(div_25); template_effect(() => set_text(text_9, `${get(lineageHiddenCount) ?? ""} hidden`)); - append($$anchor4, div_21); + append($$anchor4, div_25); }; - if_block(node_19, ($$render) => { - if (get(lineageHiddenCount) > 0) $$render(consequent_14); + if_block(node_23, ($$render) => { + if (get(lineageHiddenCount) > 0) $$render(consequent_18); }); } append($$anchor3, fragment_3); }; - if_block(node_10, ($$render) => { - if (get(lanesActive)) $$render(consequent_9); + if_block(node_12, ($$render) => { + if (get(lanesActive)) $$render(consequent_11); else $$render(alternate_3, -1); }); } template_effect(() => { - classes_2 = set_class(div_7, 1, "pcr-viewer-lineage pcr-viewer-lineage-visible svelte-5cciiw", null, classes_2, { lanes: get(lanesActive) }); - set_style(div_7, get(lanesActive) ? `width: ${get(laneRender).width * LANE_COL_W + 24}px` : ""); + classes_2 = set_class(div_9, 1, "pcr-viewer-lineage pcr-viewer-lineage-visible svelte-5cciiw", null, classes_2, { lanes: get(lanesActive) }); + set_style(div_9, get(lanesActive) ? `width: ${get(laneRender).width * LANE_COL_W + 24}px` : ""); set_text(text_4, `${get(lineageCurrentIdx) + 1} / ${get(lineageList).length ?? ""}`); }); - append($$anchor2, div_7); + append($$anchor2, div_9); }; var alternate_4 = ($$anchor2) => { - var div_22 = root_29(); - append($$anchor2, div_22); + var div_26 = root_33(); + append($$anchor2, div_26); }; - if_block(node_6, ($$render) => { - if (get(lineageList).length > 1) $$render(consequent_15); + if_block(node_8, ($$render) => { + if (get(lineageList).length > 1) $$render(consequent_19); else $$render(alternate_4, -1); }); } - var node_20 = sibling(node_6, 2); + var node_24 = sibling(node_8, 2); { - var consequent_20 = ($$anchor2) => { - var div_23 = root_30(); - var div_24 = child(div_23); - var text_10 = child(div_24); - var node_21 = sibling(div_24, 2); + var consequent_24 = ($$anchor2) => { + var div_27 = root_34(); + var div_28 = child(div_27); + var text_10 = child(div_28); + var node_25 = sibling(div_28, 2); { - var consequent_16 = ($$anchor3) => { - var div_25 = root_31(); - var text_11 = child(div_25); + var consequent_20 = ($$anchor3) => { + var div_29 = root_35(); + var text_11 = child(div_29); template_effect(($0) => set_text(text_11, $0), [() => timeAgo(get(hoverTip).item.created_at)]); - append($$anchor3, div_25); + append($$anchor3, div_29); }; - if_block(node_21, ($$render) => { - if (get(hoverTip).item.created_at) $$render(consequent_16); + if_block(node_25, ($$render) => { + if (get(hoverTip).item.created_at) $$render(consequent_20); }); } - var node_22 = sibling(node_21, 2); + var node_26 = sibling(node_25, 2); { - var consequent_17 = ($$anchor3) => { - var div_26 = root_32(); - var text_12 = child(div_26); + var consequent_21 = ($$anchor3) => { + var div_30 = root_36(); + var text_12 = child(div_30); template_effect(() => set_text(text_12, `${get(hoverTip).item.width ?? ""}×${get(hoverTip).item.height ?? ""}`)); - append($$anchor3, div_26); + append($$anchor3, div_30); }; - if_block(node_22, ($$render) => { - if (get(hoverTip).item.width && get(hoverTip).item.height) $$render(consequent_17); + if_block(node_26, ($$render) => { + if (get(hoverTip).item.width && get(hoverTip).item.height) $$render(consequent_21); }); } - var node_23 = sibling(node_22, 2); + var node_27 = sibling(node_26, 2); { - var consequent_18 = ($$anchor3) => { + var consequent_22 = ($$anchor3) => { const op = user_derived(() => get(lineageOps).get(get(hoverTip).item.hash)); - var div_27 = root_33(); - var text_13 = child(div_27); + var div_31 = root_37(); + var text_13 = child(div_31); template_effect(($0) => set_text(text_13, `▲ upscaled ${$0 ?? ""}from ${get(op).from.w ?? ""}×${get(op).from.h ?? ""}`), [ () => get(op).uniform ? fmtFactor(get(op).factor) + " " : "" ]); - append($$anchor3, div_27); + append($$anchor3, div_31); }; - var d_6 = user_derived(() => { + var d_10 = user_derived(() => { var _a2; return ((_a2 = get(lineageOps).get(get(hoverTip).item.hash)) == null ? void 0 : _a2.kind) === "upscale"; }); - if_block(node_23, ($$render) => { - if (get(d_6)) $$render(consequent_18); + if_block(node_27, ($$render) => { + if (get(d_10)) $$render(consequent_22); }); } - var node_24 = sibling(node_23, 2); + var node_28 = sibling(node_27, 2); { - var consequent_19 = ($$anchor3) => { - var div_28 = root_34(); - var text_14 = child(div_28); + var consequent_23 = ($$anchor3) => { + var div_32 = root_38(); + var text_14 = child(div_32); template_effect(() => set_text(text_14, `▦ ${get(editDocLayers)[get(hoverTip).item.hash] ?? ""} layers — opens layered in Edit`)); - append($$anchor3, div_28); + append($$anchor3, div_32); }; - if_block(node_24, ($$render) => { - if (get(editDocLayers)[get(hoverTip).item.hash]) $$render(consequent_19); + if_block(node_28, ($$render) => { + if (get(editDocLayers)[get(hoverTip).item.hash]) $$render(consequent_23); }); } template_effect( ($0) => { - set_style(div_23, `top:${get(hoverTip).top ?? ""}px; left:${get(hoverTip).left ?? ""}px;`); + set_style(div_27, `top:${get(hoverTip).top ?? ""}px; left:${get(hoverTip).left ?? ""}px;`); set_text(text_10, $0); }, [ @@ -14699,91 +15883,147 @@ ${negative}` : "")); } ] ); - append($$anchor2, div_23); + append($$anchor2, div_27); }; - if_block(node_20, ($$render) => { - if (get(hoverTip)) $$render(consequent_20); + if_block(node_24, ($$render) => { + if (get(hoverTip)) $$render(consequent_24); }); } - var div_29 = sibling(node_20, 2); - var node_25 = child(div_29); + var div_33 = sibling(node_24, 2); + var node_29 = child(div_33); { - var consequent_21 = ($$anchor2) => { - var div_30 = root_35(); - var text_15 = child(div_30); + var consequent_25 = ($$anchor2) => { + var div_34 = root_39(); + var text_15 = child(div_34); template_effect(() => set_text(text_15, `✨ ${get(newGenBanner) ?? ""}`)); - append($$anchor2, div_30); + append($$anchor2, div_34); }; - if_block(node_25, ($$render) => { - if (get(newGenBanner)) $$render(consequent_21); + if_block(node_29, ($$render) => { + if (get(newGenBanner)) $$render(consequent_25); }); } - var node_26 = sibling(node_25, 2); + var node_30 = sibling(node_29, 2); { - var consequent_22 = ($$anchor2) => { - var div_31 = root_36(); - append($$anchor2, div_31); + var consequent_26 = ($$anchor2) => { + var div_35 = root_40(); + append($$anchor2, div_35); }; - if_block(node_26, ($$render) => { - if (get(copyPathToast)) $$render(consequent_22); + if_block(node_30, ($$render) => { + if (get(copyPathToast)) $$render(consequent_26); }); } - var node_27 = sibling(node_26, 2); + var node_31 = sibling(node_30, 2); { - var consequent_31 = ($$anchor2) => { - var fragment_6 = root_37(); - var node_28 = first_child(fragment_6); + var consequent_36 = ($$anchor2) => { + var fragment_6 = root_41(); + var node_32 = first_child(fragment_6); { - var consequent_23 = ($$anchor3) => { - var div_32 = root_38(); - append($$anchor3, div_32); + var consequent_27 = ($$anchor3) => { + var div_36 = root_42(); + append($$anchor3, div_36); }; - if_block(node_28, ($$render) => { - if (!get(imageLoaded) && !get(imageError)) $$render(consequent_23); + if_block(node_32, ($$render) => { + if (!get(imageLoaded) && !get(imageError)) $$render(consequent_27); }); } - var img_5 = sibling(node_28, 2); - let classes_8; - bind_this(img_5, ($$value) => set(mainImageEl, $$value), () => get(mainImageEl)); - var node_29 = sibling(img_5, 2); + var node_33 = sibling(node_32, 2); { - var consequent_24 = ($$anchor3) => { - var img_6 = root_39(); + var consequent_28 = ($$anchor3) => { + var video = root_43(); + let classes_8; + video.muted = true; + bind_this(video, ($$value) => set(mainImageEl, $$value), () => get(mainImageEl)); + template_effect( + ($0, $1) => { + set_attribute(video, "src", $0); + set_attribute(video, "poster", $1); + classes_8 = set_class(video, 1, "pcr-viewer-image svelte-5cciiw", null, classes_8, { hidden: !get(imageLoaded) }); + set_style(video, `transform: translate(${get(panX) ?? ""}px, ${get(panY) ?? ""}px) scale(${get(zoom) ?? ""})`); + }, + [ + () => mainImageSrc(get(displayedHash)), + () => { + var _a2; + return ((_a2 = get(imageByHash).get(get(displayedHash))) == null ? void 0 : _a2._directUrl) ? null : thumbUrl(get(displayedHash)); + } + ] + ); + event("loadeddata", video, () => { + set(imageLoaded, true); + }); + event("error", video, () => { + set(imageError, true); + set(imageLoaded, false); + }); + delegated("pointerdown", video, (e) => e.stopPropagation()); + delegated("dblclick", video, (e) => e.stopPropagation()); + append($$anchor3, video); + }; + var alternate_5 = ($$anchor3) => { + var img_5 = root_44(); + let classes_9; + bind_this(img_5, ($$value) => set(mainImageEl, $$value), () => get(mainImageEl)); + template_effect( + ($0) => { + set_attribute(img_5, "src", $0); + classes_9 = set_class(img_5, 1, "pcr-viewer-image svelte-5cciiw", null, classes_9, { hidden: !get(imageLoaded) }); + set_style(img_5, `transform: translate(${get(panX) ?? ""}px, ${get(panY) ?? ""}px) scale(${get(zoom) ?? ""})`); + }, + [() => mainImageSrc(get(displayedHash))] + ); + event("load", img_5, () => { + set(imageLoaded, true); + }); + event("error", img_5, () => { + set(imageError, true); + set(imageLoaded, false); + }); + append($$anchor3, img_5); + }; + if_block(node_33, ($$render) => { + if (get(displayedIsVideo)) $$render(consequent_28); + else $$render(alternate_5, -1); + }); + } + var node_34 = sibling(node_33, 2); + { + var consequent_29 = ($$anchor3) => { + var img_6 = root_45(); template_effect(() => { set_attribute(img_6, "src", get(compareImageUrl)); set_style(img_6, `transform: translate(${get(panX) ?? ""}px, ${get(panY) ?? ""}px) scale(${get(zoom) ?? ""}); clip-path: inset(0 0 0 ${get(compareClipPercent) ?? ""}%);`); }); append($$anchor3, img_6); }; - if_block(node_29, ($$render) => { - if (get(compareMode) && get(compareImageUrl)) $$render(consequent_24); + if_block(node_34, ($$render) => { + if (get(compareMode) && get(compareImageUrl)) $$render(consequent_29); }); } - var node_30 = sibling(node_29, 2); + var node_35 = sibling(node_34, 2); { - var consequent_27 = ($$anchor3) => { - var div_33 = root_40(); - var div_34 = child(div_33); - var text_16 = child(div_34); - var node_31 = sibling(div_34, 2); + var consequent_32 = ($$anchor3) => { + var div_37 = root_46(); + var div_38 = child(div_37); + var text_16 = child(div_38); + var node_36 = sibling(div_38, 2); { - var consequent_26 = ($$anchor4) => { - var fragment_7 = root_41(); + var consequent_31 = ($$anchor4) => { + var fragment_7 = root_47(); var button_7 = sibling(first_child(fragment_7), 2); var text_17 = child(button_7); - var node_32 = sibling(button_7, 2); + var node_37 = sibling(button_7, 2); { - var consequent_25 = ($$anchor5) => { - var div_35 = root_42(); - var text_18 = child(div_35); + var consequent_30 = ($$anchor5) => { + var div_39 = root_48(); + var text_18 = child(div_39); template_effect(() => set_text(text_18, get(reattachMsg))); - append($$anchor5, div_35); + append($$anchor5, div_39); }; - if_block(node_32, ($$render) => { - if (get(reattachMsg)) $$render(consequent_25); + if_block(node_37, ($$render) => { + if (get(reattachMsg)) $$render(consequent_30); }); } - var input = sibling(node_32, 2); + var input = sibling(node_37, 2); bind_this(input, ($$value) => set(reattachInputEl, $$value), () => get(reattachInputEl)); template_effect(() => { button_7.disabled = get(reattaching); @@ -14796,304 +16036,313 @@ ${negative}` : "")); delegated("change", input, handleReattachPick); append($$anchor4, fragment_7); }; - if_block(node_31, ($$render) => { - if (get(canReattach)) $$render(consequent_26); + if_block(node_36, ($$render) => { + if (get(canReattach)) $$render(consequent_31); }); } template_effect(() => set_text(text_16, get(isOrphaned) ? "Source file deleted" : "Image not found")); - append($$anchor3, div_33); + append($$anchor3, div_37); }; - if_block(node_30, ($$render) => { - if (get(imageError)) $$render(consequent_27); + if_block(node_35, ($$render) => { + if (get(imageError)) $$render(consequent_32); }); } - var node_33 = sibling(node_30, 2); + var node_38 = sibling(node_35, 2); { - var consequent_30 = ($$anchor3) => { - var fragment_8 = root_43(); - var div_36 = first_child(fragment_8); - var div_37 = sibling(div_36, 2); - var text_19 = child(div_37); - var node_34 = sibling(text_19); + var consequent_35 = ($$anchor3) => { + var fragment_8 = root_49(); + var div_40 = first_child(fragment_8); + var div_41 = sibling(div_40, 2); + var text_19 = child(div_41); + var node_39 = sibling(text_19); { - var consequent_28 = ($$anchor4) => { - var div_38 = root_44(); - var text_20 = child(div_38); + var consequent_33 = ($$anchor4) => { + var div_42 = root_50(); + var text_20 = child(div_42); template_effect(($0) => set_text(text_20, $0), [() => timeAgo(get(imageInfo).created_at)]); - append($$anchor4, div_38); + append($$anchor4, div_42); }; - if_block(node_34, ($$render) => { + if_block(node_39, ($$render) => { var _a2; - if ((_a2 = get(imageInfo)) == null ? void 0 : _a2.created_at) $$render(consequent_28); + if ((_a2 = get(imageInfo)) == null ? void 0 : _a2.created_at) $$render(consequent_33); }); } - var div_39 = sibling(div_37, 2); - var text_21 = child(div_39); - var node_35 = sibling(text_21); + var div_43 = sibling(div_41, 2); + var text_21 = child(div_43); + var node_40 = sibling(text_21); { - var consequent_29 = ($$anchor4) => { - var div_40 = root_45(); - var text_22 = child(div_40); + var consequent_34 = ($$anchor4) => { + var div_44 = root_51(); + var text_22 = child(div_44); template_effect(($0) => set_text(text_22, $0), [() => timeAgo(get(compareTargetItem).created_at)]); - append($$anchor4, div_40); + append($$anchor4, div_44); }; - if_block(node_35, ($$render) => { + if_block(node_40, ($$render) => { var _a2; - if ((_a2 = get(compareTargetItem)) == null ? void 0 : _a2.created_at) $$render(consequent_29); + if ((_a2 = get(compareTargetItem)) == null ? void 0 : _a2.created_at) $$render(consequent_34); }); } template_effect(() => { var _a2; - set_style(div_36, `left: ${get(compareSliderPos) ?? ""}%;`); + set_style(div_40, `left: ${get(compareSliderPos) ?? ""}%;`); set_text(text_19, `${(((_a2 = get(imageInfo)) == null ? void 0 : _a2.filename) || "Current") ?? ""} `); set_text(text_21, `${(get(compareTargetLabel) || "Compare") ?? ""} `); }); - delegated("mousedown", div_36, handleSliderDown); + delegated("mousedown", div_40, handleSliderDown); append($$anchor3, fragment_8); }; - if_block(node_33, ($$render) => { - if (get(compareMode)) $$render(consequent_30); + if_block(node_38, ($$render) => { + if (get(compareMode)) $$render(consequent_35); }); } - template_effect( - ($0) => { - set_attribute(img_5, "src", $0); - classes_8 = set_class(img_5, 1, "pcr-viewer-image svelte-5cciiw", null, classes_8, { hidden: !get(imageLoaded) }); - set_style(img_5, `transform: translate(${get(panX) ?? ""}px, ${get(panY) ?? ""}px) scale(${get(zoom) ?? ""})`); - }, - [() => mainImageSrc(get(displayedHash))] - ); - event("load", img_5, () => { - set(imageLoaded, true); - }); - event("error", img_5, () => { - set(imageError, true); - set(imageLoaded, false); - }); append($$anchor2, fragment_6); }; - if_block(node_27, ($$render) => { - if (get(displayedHash)) $$render(consequent_31); + if_block(node_31, ($$render) => { + if (get(displayedHash)) $$render(consequent_36); }); } - var node_36 = sibling(node_27, 2); + var node_41 = sibling(node_31, 2); { - var consequent_32 = ($$anchor2) => { - var div_41 = root_46(); - delegated("pointerdown", div_41, (e) => e.stopPropagation()); - delegated("click", div_41, (e) => { + var consequent_37 = ($$anchor2) => { + var div_45 = root_52(); + delegated("pointerdown", div_45, (e) => e.stopPropagation()); + delegated("click", div_45, (e) => { e.stopPropagation(); navigate(-1); }); - append($$anchor2, div_41); + append($$anchor2, div_45); }; - if_block(node_36, ($$render) => { - if (get(currentIndex) > 0) $$render(consequent_32); + if_block(node_41, ($$render) => { + if (get(currentIndex) > 0) $$render(consequent_37); }); } - var node_37 = sibling(node_36, 2); + var node_42 = sibling(node_41, 2); { - var consequent_33 = ($$anchor2) => { - var div_42 = root_47(); - delegated("pointerdown", div_42, (e) => e.stopPropagation()); - delegated("click", div_42, (e) => { + var consequent_38 = ($$anchor2) => { + var div_46 = root_53(); + delegated("pointerdown", div_46, (e) => e.stopPropagation()); + delegated("click", div_46, (e) => { e.stopPropagation(); navigate(1); }); - append($$anchor2, div_42); + append($$anchor2, div_46); }; - if_block(node_37, ($$render) => { - if (get(currentIndex) < images().length - 1) $$render(consequent_33); + if_block(node_42, ($$render) => { + if (get(currentIndex) < images().length - 1) $$render(consequent_38); }); } - bind_this(div_29, ($$value) => containerEl = $$value, () => containerEl); - var div_43 = sibling(div_29, 2); - var div_44 = child(div_43); - var span_4 = child(div_44); + bind_this(div_33, ($$value) => containerEl = $$value, () => containerEl); + var div_47 = sibling(div_33, 2); + var div_48 = child(div_47); + var span_4 = child(div_48); var text_23 = child(span_4); - var div_45 = sibling(span_4, 2); - var node_38 = sibling(div_44, 2); + var div_49 = sibling(span_4, 2); + var node_43 = sibling(div_48, 2); { - var consequent_36 = ($$anchor2) => { - var div_46 = root_48(); - let classes_9; - var div_47 = child(div_46); - var div_48 = child(div_47); - var node_39 = child(div_48); - each(node_39, 17, () => get(visibleThumbs), (thumb) => thumb.hash, ($$anchor3, thumb) => { - var div_49 = root_49(); - let classes_10; - var img_7 = child(div_49); + var consequent_43 = ($$anchor2) => { + var div_50 = root_54(); + let classes_10; + var div_51 = child(div_50); + var div_52 = child(div_51); + var node_44 = child(div_52); + each(node_44, 17, () => get(visibleThumbs), (thumb) => thumb.hash, ($$anchor3, thumb) => { + var div_53 = root_55(); + let classes_11; + var img_7 = child(div_53); + var node_45 = sibling(img_7, 2); + { + var consequent_39 = ($$anchor4) => { + var div_54 = root_56(); + append($$anchor4, div_54); + }; + var d_11 = user_derived(() => isVideoItem(get(thumb))); + if_block(node_45, ($$render) => { + if (get(d_11)) $$render(consequent_39); + }); + } template_effect( ($0) => { - classes_10 = set_class(div_49, 1, "pcr-viewer-history-thumb svelte-5cciiw", null, classes_10, { current: get(thumb).globalIndex === get(currentIndex) }); + classes_11 = set_class(div_53, 1, "pcr-viewer-history-thumb svelte-5cciiw", null, classes_11, { current: get(thumb).globalIndex === get(currentIndex) }); set_attribute(img_7, "src", $0); }, [() => thumbUrl(get(thumb).hash)] ); - delegated("click", div_49, () => jumpTo(get(thumb).globalIndex)); - append($$anchor3, div_49); + delegated("click", div_53, () => jumpTo(get(thumb).globalIndex)); + event("error", img_7, retryThumb); + append($$anchor3, div_53); }); - var div_50 = sibling(div_48, 2); - var svg_1 = child(div_50); - var node_40 = child(svg_1); + var div_55 = sibling(div_52, 2); + var svg_1 = child(div_55); + var node_46 = child(svg_1); { - var consequent_34 = ($$anchor3) => { - var path_3 = root_50(); + var consequent_40 = ($$anchor3) => { + var path_3 = root_57(); append($$anchor3, path_3); }; - var alternate_5 = ($$anchor3) => { - var path_4 = root_51(); + var alternate_6 = ($$anchor3) => { + var path_4 = root_58(); append($$anchor3, path_4); }; - if_block(node_40, ($$render) => { - if (get(historyExpanded)) $$render(consequent_34); - else $$render(alternate_5, -1); + if_block(node_46, ($$render) => { + if (get(historyExpanded)) $$render(consequent_40); + else $$render(alternate_6, -1); }); } - var div_51 = sibling(div_47, 2); - var div_52 = child(div_51); - var div_53 = sibling(div_52, 2); - var node_41 = sibling(div_51, 2); + var div_56 = sibling(div_51, 2); + var div_57 = child(div_56); + var div_58 = sibling(div_57, 2); + var node_47 = sibling(div_56, 2); { - var consequent_35 = ($$anchor3) => { - var div_54 = root_52(); - each(div_54, 23, images, (img) => img.hash, ($$anchor4, img, idx) => { - var div_55 = root_53(); - let classes_11; - var img_8 = child(div_55); + var consequent_42 = ($$anchor3) => { + var div_59 = root_59(); + each(div_59, 23, images, (img) => img.hash, ($$anchor4, img, idx) => { + var div_60 = root_60(); + let classes_12; + var img_8 = child(div_60); + var node_48 = sibling(img_8, 2); + { + var consequent_41 = ($$anchor5) => { + var div_61 = root_61(); + append($$anchor5, div_61); + }; + var d_12 = user_derived(() => isVideoItem(get(img))); + if_block(node_48, ($$render) => { + if (get(d_12)) $$render(consequent_41); + }); + } template_effect( ($0) => { - classes_11 = set_class(div_55, 1, "pcr-viewer-grid-thumb svelte-5cciiw", null, classes_11, { current: get(idx) === get(currentIndex) }); + classes_12 = set_class(div_60, 1, "pcr-viewer-grid-thumb svelte-5cciiw", null, classes_12, { current: get(idx) === get(currentIndex) }); set_attribute(img_8, "src", $0); }, [() => thumbUrl(get(img).hash)] ); - delegated("click", div_55, () => jumpTo(get(idx))); - append($$anchor4, div_55); + delegated("click", div_60, () => jumpTo(get(idx))); + event("error", img_8, retryThumb); + append($$anchor4, div_60); }); - bind_this(div_54, ($$value) => set(historyGridEl, $$value), () => get(historyGridEl)); - append($$anchor3, div_54); + bind_this(div_59, ($$value) => set(historyGridEl, $$value), () => get(historyGridEl)); + append($$anchor3, div_59); }; - if_block(node_41, ($$render) => { - if (get(historyExpanded)) $$render(consequent_35); + if_block(node_47, ($$render) => { + if (get(historyExpanded)) $$render(consequent_42); }); } template_effect(() => { - classes_9 = set_class(div_46, 1, "pcr-viewer-history svelte-5cciiw", null, classes_9, { expanded: get(historyExpanded) }); - set_attribute(div_50, "title", get(historyExpanded) ? "Collapse" : "Expand"); - set_style(div_52, `width: ${get(scrubberPercent) ?? ""}%`); - set_style(div_53, `left: ${get(scrubberPercent) ?? ""}%`); + classes_10 = set_class(div_50, 1, "pcr-viewer-history svelte-5cciiw", null, classes_10, { expanded: get(historyExpanded) }); + set_attribute(div_55, "title", get(historyExpanded) ? "Collapse" : "Expand"); + set_style(div_57, `width: ${get(scrubberPercent) ?? ""}%`); + set_style(div_58, `left: ${get(scrubberPercent) ?? ""}%`); }); - delegated("click", div_50, toggleGrid); - delegated("mousedown", div_51, handleScrubberTrackDown); - delegated("mousedown", div_53, handleScrubberHandleDown); - append($$anchor2, div_46); + delegated("click", div_55, toggleGrid); + delegated("mousedown", div_56, handleScrubberTrackDown); + delegated("mousedown", div_58, handleScrubberHandleDown); + append($$anchor2, div_50); }; - if_block(node_38, ($$render) => { - if (images().length > 1) $$render(consequent_36); + if_block(node_43, ($$render) => { + if (images().length > 1) $$render(consequent_43); }); } - var div_56 = sibling(node_38, 2); - var div_57 = child(div_56); - var div_58 = child(div_57); - var div_59 = child(div_58); - var span_5 = sibling(child(div_59), 2); + var div_62 = sibling(node_43, 2); + var div_63 = child(div_62); + var div_64 = child(div_63); + var div_65 = child(div_64); + var span_5 = sibling(child(div_65), 2); var text_24 = child(span_5); - var node_42 = sibling(div_59, 2); + var node_49 = sibling(div_65, 2); { - var consequent_37 = ($$anchor2) => { - var div_60 = root_54(); - each(div_60, 21, () => ZOOM_PRESETS, index, ($$anchor3, level) => { - var div_61 = root_55(); - var text_25 = child(div_61); + var consequent_44 = ($$anchor2) => { + var div_66 = root_62(); + each(div_66, 21, () => ZOOM_PRESETS, index, ($$anchor3, level) => { + var div_67 = root_63(); + var text_25 = child(div_67); template_effect(() => set_text(text_25, `${get(level) ?? ""}%`)); - delegated("click", div_61, () => setZoomLevel(get(level))); - append($$anchor3, div_61); + delegated("click", div_67, () => setZoomLevel(get(level))); + append($$anchor3, div_67); }); - append($$anchor2, div_60); + append($$anchor2, div_66); }; - if_block(node_42, ($$render) => { - if (get(zoomDropdownOpen)) $$render(consequent_37); + if_block(node_49, ($$render) => { + if (get(zoomDropdownOpen)) $$render(consequent_44); }); } - var input_1 = sibling(div_58, 2); - var node_43 = sibling(div_57, 2); + var input_1 = sibling(div_64, 2); + var node_50 = sibling(div_63, 2); { - var consequent_40 = ($$anchor2) => { - var div_62 = root_56(); - var div_63 = child(div_62); - let classes_12; - var node_44 = sibling(div_63, 2); + var consequent_47 = ($$anchor2) => { + var div_68 = root_64(); + var div_69 = child(div_68); + let classes_13; + var node_51 = sibling(div_69, 2); { - var consequent_39 = ($$anchor3) => { - var div_64 = root_57(); - var node_45 = child(div_64); - each(node_45, 17, () => get(compareTargets), index, ($$anchor4, target) => { - var div_65 = root_58(); - let classes_13; - var text_26 = child(div_65); + var consequent_46 = ($$anchor3) => { + var div_70 = root_65(); + var node_52 = child(div_70); + each(node_52, 17, () => get(compareTargets), index, ($$anchor4, target) => { + var div_71 = root_66(); + let classes_14; + var text_26 = child(div_71); template_effect( ($0) => { - classes_13 = set_class(div_65, 1, "pcr-viewer-compare-dropdown-item svelte-5cciiw", null, classes_13, { active: get(compareTargetHash) === get(target).hash }); + classes_14 = set_class(div_71, 1, "pcr-viewer-compare-dropdown-item svelte-5cciiw", null, classes_14, { active: get(compareTargetHash) === get(target).hash }); set_text(text_26, $0); }, [ () => get(target).filename || get(target).hash.slice(0, 8) ] ); - delegated("click", div_65, () => enterCompareWith(get(target).hash, get(target).filename || get(target).hash.slice(0, 8))); - append($$anchor4, div_65); + delegated("click", div_71, () => enterCompareWith(get(target).hash, get(target).filename || get(target).hash.slice(0, 8))); + append($$anchor4, div_71); }); - var node_46 = sibling(node_45, 2); + var node_53 = sibling(node_52, 2); { - var consequent_38 = ($$anchor4) => { - var fragment_9 = root_59(); - var div_66 = sibling(first_child(fragment_9), 2); - delegated("click", div_66, exitCompare); + var consequent_45 = ($$anchor4) => { + var fragment_9 = root_67(); + var div_72 = sibling(first_child(fragment_9), 2); + delegated("click", div_72, exitCompare); append($$anchor4, fragment_9); }; - if_block(node_46, ($$render) => { - if (get(compareMode)) $$render(consequent_38); + if_block(node_53, ($$render) => { + if (get(compareMode)) $$render(consequent_45); }); } - append($$anchor3, div_64); + append($$anchor3, div_70); }; - if_block(node_44, ($$render) => { - if (get(compareDropdownOpen)) $$render(consequent_39); + if_block(node_51, ($$render) => { + if (get(compareDropdownOpen)) $$render(consequent_46); }); } - template_effect(() => classes_12 = set_class(div_63, 1, "pcr-viewer-toolbar-btn svelte-5cciiw", null, classes_12, { active: get(compareMode) || get(compareDropdownOpen) })); - delegated("click", div_63, () => { + template_effect(() => classes_13 = set_class(div_69, 1, "pcr-viewer-toolbar-btn svelte-5cciiw", null, classes_13, { active: get(compareMode) || get(compareDropdownOpen) })); + delegated("click", div_69, () => { set(compareDropdownOpen, !get(compareDropdownOpen)); }); - append($$anchor2, div_62); + append($$anchor2, div_68); }; - if_block(node_43, ($$render) => { - if (get(hasCompareTargets)) $$render(consequent_40); + if_block(node_50, ($$render) => { + if (get(hasCompareTargets) && !get(displayedIsVideo)) $$render(consequent_47); }); } - var node_47 = sibling(node_43, 2); + var node_54 = sibling(node_50, 2); { - if_block(node_47, ($$render) => { + if_block(node_54, ($$render) => { }); } - var node_48 = sibling(node_47, 2); + var node_55 = sibling(node_54, 2); { - if_block(node_48, ($$render) => { + if_block(node_55, ($$render) => { }); } - var node_49 = sibling(node_48, 2); + var node_56 = sibling(node_55, 2); { - var consequent_44 = ($$anchor2) => { - var div_71 = root_62(); - var div_72 = child(div_71); - var span_8 = sibling(child(div_72), 2); + var consequent_51 = ($$anchor2) => { + var div_77 = root_70(); + var div_78 = child(div_77); + var span_8 = sibling(child(div_78), 2); var text_29 = child(span_8); - var node_50 = sibling(span_8, 2); + var node_57 = sibling(span_8, 2); { - var consequent_43 = ($$anchor3) => { - var span_9 = root_63(); + var consequent_50 = ($$anchor3) => { + var span_9 = root_71(); var text_30 = sibling(child(span_9)); template_effect(() => { set_attribute(span_9, "title", `This image has ${get(editDocLayers)[get(displayedHash)] ?? ""} saved layers — Edit opens it layered, un-flattened`); @@ -15101,319 +16350,353 @@ ${negative}` : "")); }); append($$anchor3, span_9); }; - if_block(node_50, ($$render) => { - if (get(editDocLayers)[get(displayedHash)]) $$render(consequent_43); + if_block(node_57, ($$render) => { + if (get(editDocLayers)[get(displayedHash)]) $$render(consequent_50); }); } template_effect(() => set_text(text_29, get(editPreparing) ? "Reading..." : "Edit")); - delegated("click", div_72, () => { + delegated("click", div_78, () => { if (!get(editPreparing)) openEdit(); }); - append($$anchor2, div_71); + append($$anchor2, div_77); }; - if_block(node_49, ($$render) => { - if (onEditSave()) $$render(consequent_44); + if_block(node_56, ($$render) => { + if (onEditSave() && !get(displayedIsVideo)) $$render(consequent_51); }); } - var div_73 = sibling(node_49, 2); - var node_51 = child(div_73); + var node_58 = sibling(node_56, 2); { - var consequent_45 = ($$anchor2) => { - var div_74 = root_64(); - var span_10 = sibling(child(div_74), 2); + var consequent_52 = ($$anchor2) => { + var div_79 = root_72(); + var div_80 = child(div_79); + var span_10 = sibling(child(div_80), 2); var text_31 = child(span_10); - template_effect(() => set_text(text_31, get(deleting) ? "Deleting..." : "Delete")); - delegated("click", div_74, requestDelete); - append($$anchor2, div_74); + template_effect(() => set_text(text_31, get(extendPreparing) ? "Reading..." : "Extend")); + delegated("click", div_80, () => { + if (!get(extendPreparing)) openExtend(); + }); + append($$anchor2, div_79); }; - if_block(node_51, ($$render) => { - if (onDelete()) $$render(consequent_45); + if_block(node_58, ($$render) => { + if (onExtendRun() && get(displayedIsVideo)) $$render(consequent_52); }); } - var node_52 = sibling(node_51, 2); + var node_59 = sibling(node_58, 2); { - var consequent_46 = ($$anchor2) => { - var div_75 = root_65(); - delegated("click", div_75, openFolder); - append($$anchor2, div_75); + var consequent_53 = ($$anchor2) => { + var div_81 = root_73(); + var div_82 = child(div_81); + var span_11 = sibling(child(div_82), 2); + var text_32 = child(span_11); + template_effect(() => set_text(text_32, get(smoothPreparing) ? "Reading..." : "Smooth")); + delegated("click", div_82, () => { + if (!get(smoothPreparing)) openSmooth(); + }); + append($$anchor2, div_81); }; - if_block(node_52, ($$render) => { - if (isLocalClient) $$render(consequent_46); + if_block(node_59, ($$render) => { + if (onSmoothRun() && get(displayedIsVideo)) $$render(consequent_53); }); } - var div_76 = sibling(node_52, 2); - let classes_14; - var span_11 = sibling(child(div_76), 2); - var text_32 = child(span_11); - var div_77 = sibling(div_56, 2); - var node_53 = child(div_77); + var div_83 = sibling(node_59, 2); + var node_60 = child(div_83); { - var consequent_64 = ($$anchor2) => { - var fragment_10 = root_66(); - var div_78 = sibling(first_child(fragment_10), 2); - var div_79 = child(div_78); - var node_54 = child(div_79); - { - var consequent_47 = ($$anchor3) => { - var div_80 = root_67(); - var span_12 = sibling(child(div_80), 2); - var text_33 = child(span_12); - template_effect(() => set_text(text_33, `${get(imageInfo).width ?? ""} × ${get(imageInfo).height ?? ""}`)); - append($$anchor3, div_80); - }; - if_block(node_54, ($$render) => { - if (get(imageInfo).width && get(imageInfo).height) $$render(consequent_47); - }); - } - var node_55 = sibling(node_54, 2); - { - var consequent_48 = ($$anchor3) => { - var div_81 = root_68(); - var span_13 = sibling(child(div_81), 2); - var text_34 = child(span_13); - template_effect(($0) => set_text(text_34, $0), [ - () => get(imageInfo).file_size > 1048576 ? (get(imageInfo).file_size / 1048576).toFixed(1) + " MB" : Math.round(get(imageInfo).file_size / 1024) + " KB" - ]); - append($$anchor3, div_81); - }; - if_block(node_55, ($$render) => { - if (get(imageInfo).file_size) $$render(consequent_48); - }); - } - var node_56 = sibling(div_79, 2); - { - var consequent_49 = ($$anchor3) => { - var div_82 = root_69(); - var span_14 = sibling(child(div_82), 2); - var text_35 = child(span_14); - template_effect(($0) => set_text(text_35, $0), [ - () => new Date(get(imageInfo).created_at * 1e3).toLocaleDateString() - ]); - append($$anchor3, div_82); - }; - if_block(node_56, ($$render) => { - if (get(imageInfo).created_at) $$render(consequent_49); - }); - } - var node_57 = sibling(node_56, 2); - { - var consequent_50 = ($$anchor3) => { - var div_83 = root_70(); - var span_15 = sibling(child(div_83), 2); - var text_36 = child(span_15); - template_effect(() => set_text(text_36, get(imageInfo).filename)); - append($$anchor3, div_83); - }; - if_block(node_57, ($$render) => { - if (get(imageInfo).filename) $$render(consequent_50); - }); - } - var node_58 = sibling(node_57, 2); - { - var consequent_51 = ($$anchor3) => { - var div_84 = root_71(); - append($$anchor3, div_84); - }; - if_block(node_58, ($$render) => { - if (get(isOrphaned)) $$render(consequent_51); - }); - } - var node_59 = sibling(div_78, 2); + var consequent_54 = ($$anchor2) => { + var div_84 = root_74(); + var span_12 = sibling(child(div_84), 2); + var text_33 = child(span_12); + template_effect(() => set_text(text_33, get(deleting) ? "Deleting..." : "Delete")); + delegated("click", div_84, requestDelete); + append($$anchor2, div_84); + }; + if_block(node_60, ($$render) => { + if (onDelete()) $$render(consequent_54); + }); + } + var node_61 = sibling(node_60, 2); + { + var consequent_55 = ($$anchor2) => { + var div_85 = root_75(); + delegated("click", div_85, openFolder); + append($$anchor2, div_85); + }; + if_block(node_61, ($$render) => { + if (isLocalClient) $$render(consequent_55); + }); + } + var div_86 = sibling(node_61, 2); + let classes_15; + var span_13 = sibling(child(div_86), 2); + var text_34 = child(span_13); + var div_87 = sibling(div_62, 2); + var node_62 = child(div_87); + { + var consequent_73 = ($$anchor2) => { + var fragment_10 = root_76(); + var div_88 = sibling(first_child(fragment_10), 2); + var div_89 = child(div_88); + var node_63 = child(div_89); { var consequent_56 = ($$anchor3) => { + var div_90 = root_77(); + var span_14 = sibling(child(div_90), 2); + var text_35 = child(span_14); + template_effect(() => set_text(text_35, `${get(imageInfo).width ?? ""} × ${get(imageInfo).height ?? ""}`)); + append($$anchor3, div_90); + }; + if_block(node_63, ($$render) => { + if (get(imageInfo).width && get(imageInfo).height) $$render(consequent_56); + }); + } + var node_64 = sibling(node_63, 2); + { + var consequent_57 = ($$anchor3) => { + var div_91 = root_78(); + var span_15 = sibling(child(div_91), 2); + var text_36 = child(span_15); + template_effect(($0) => set_text(text_36, $0), [ + () => get(imageInfo).file_size > 1048576 ? (get(imageInfo).file_size / 1048576).toFixed(1) + " MB" : Math.round(get(imageInfo).file_size / 1024) + " KB" + ]); + append($$anchor3, div_91); + }; + if_block(node_64, ($$render) => { + if (get(imageInfo).file_size) $$render(consequent_57); + }); + } + var node_65 = sibling(div_89, 2); + { + var consequent_58 = ($$anchor3) => { + var div_92 = root_79(); + var span_16 = sibling(child(div_92), 2); + var text_37 = child(span_16); + template_effect(($0) => set_text(text_37, $0), [ + () => new Date(get(imageInfo).created_at * 1e3).toLocaleDateString() + ]); + append($$anchor3, div_92); + }; + if_block(node_65, ($$render) => { + if (get(imageInfo).created_at) $$render(consequent_58); + }); + } + var node_66 = sibling(node_65, 2); + { + var consequent_59 = ($$anchor3) => { + var div_93 = root_80(); + var span_17 = sibling(child(div_93), 2); + var text_38 = child(span_17); + template_effect(() => set_text(text_38, get(imageInfo).filename)); + append($$anchor3, div_93); + }; + if_block(node_66, ($$render) => { + if (get(imageInfo).filename) $$render(consequent_59); + }); + } + var node_67 = sibling(node_66, 2); + { + var consequent_60 = ($$anchor3) => { + var div_94 = root_81(); + append($$anchor3, div_94); + }; + if_block(node_67, ($$render) => { + if (get(isOrphaned)) $$render(consequent_60); + }); + } + var node_68 = sibling(div_88, 2); + { + var consequent_65 = ($$anchor3) => { const regionData = user_derived(() => parseRegions(get(imageInfo).regions)); - var fragment_11 = root_72(); - var div_85 = sibling(first_child(fragment_11), 2); - var node_60 = child(div_85); + var fragment_11 = root_82(); + var div_95 = sibling(first_child(fragment_11), 2); + var node_69 = child(div_95); { - var consequent_53 = ($$anchor4) => { - var fragment_12 = root_73(); - var node_61 = first_child(fragment_12); - each(node_61, 17, () => get(regionData).regions, (region) => region.id, ($$anchor5, region) => { - var div_86 = root_74(); - var span_16 = child(div_86); - var text_37 = child(span_16); - var p_1 = sibling(span_16, 2); - var text_38 = child(p_1); + var consequent_62 = ($$anchor4) => { + var fragment_12 = root_83(); + var node_70 = first_child(fragment_12); + each(node_70, 17, () => get(regionData).regions, (region) => region.id, ($$anchor5, region) => { + var div_96 = root_84(); + var span_18 = child(div_96); + var text_39 = child(span_18); + var p_1 = sibling(span_18, 2); + var text_40 = child(p_1); template_effect(() => { - set_text(text_37, `$${get(region).name ?? ""}`); - set_text(text_38, get(region).text); + set_text(text_39, `$${get(region).name ?? ""}`); + set_text(text_40, get(region).text); }); - append($$anchor5, div_86); + append($$anchor5, div_96); }); - var node_62 = sibling(node_61, 2); + var node_71 = sibling(node_70, 2); { - var consequent_52 = ($$anchor5) => { - var div_87 = root_75(); - var p_2 = sibling(child(div_87), 2); - var text_39 = child(p_2); - template_effect(() => set_text(text_39, get(regionData).global)); - append($$anchor5, div_87); + var consequent_61 = ($$anchor5) => { + var div_97 = root_85(); + var p_2 = sibling(child(div_97), 2); + var text_41 = child(p_2); + template_effect(() => set_text(text_41, get(regionData).global)); + append($$anchor5, div_97); }; - if_block(node_62, ($$render) => { - if (get(regionData).global) $$render(consequent_52); + if_block(node_71, ($$render) => { + if (get(regionData).global) $$render(consequent_61); }); } append($$anchor4, fragment_12); }; - var consequent_54 = ($$anchor4) => { - var div_88 = root_76(); - var p_3 = sibling(child(div_88), 2); - var text_40 = child(p_3); - template_effect(() => set_text(text_40, get(imageInfo).prompt)); - append($$anchor4, div_88); + var consequent_63 = ($$anchor4) => { + var div_98 = root_86(); + var p_3 = sibling(child(div_98), 2); + var text_42 = child(p_3); + template_effect(() => set_text(text_42, get(imageInfo).prompt)); + append($$anchor4, div_98); }; - if_block(node_60, ($$render) => { - if (get(regionData)) $$render(consequent_53); - else if (get(imageInfo).prompt) $$render(consequent_54, 1); + if_block(node_69, ($$render) => { + if (get(regionData)) $$render(consequent_62); + else if (get(imageInfo).prompt) $$render(consequent_63, 1); }); } - var node_63 = sibling(node_60, 2); + var node_72 = sibling(node_69, 2); { - var consequent_55 = ($$anchor4) => { - var div_89 = root_77(); - var p_4 = sibling(child(div_89), 2); - var text_41 = child(p_4); - template_effect(() => set_text(text_41, get(imageInfo).negative)); - append($$anchor4, div_89); + var consequent_64 = ($$anchor4) => { + var div_99 = root_87(); + var p_4 = sibling(child(div_99), 2); + var text_43 = child(p_4); + template_effect(() => set_text(text_43, get(imageInfo).negative)); + append($$anchor4, div_99); }; - if_block(node_63, ($$render) => { - if (get(imageInfo).negative) $$render(consequent_55); + if_block(node_72, ($$render) => { + if (get(imageInfo).negative) $$render(consequent_64); }); } append($$anchor3, fragment_11); }; - if_block(node_59, ($$render) => { - if (get(imageInfo).prompt || get(imageInfo).negative) $$render(consequent_56); + if_block(node_68, ($$render) => { + if (get(imageInfo).prompt || get(imageInfo).negative) $$render(consequent_65); }); } - var node_64 = sibling(node_59, 2); + var node_73 = sibling(node_68, 2); { - var consequent_63 = ($$anchor3) => { - var fragment_13 = root_78(); - var div_90 = sibling(first_child(fragment_13), 2); - var node_65 = child(div_90); + var consequent_72 = ($$anchor3) => { + var fragment_13 = root_88(); + var div_100 = sibling(first_child(fragment_13), 2); + var node_74 = child(div_100); { - var consequent_57 = ($$anchor4) => { - var div_91 = root_79(); - var span_17 = sibling(child(div_91), 2); - var text_42 = child(span_17); - template_effect(() => set_text(text_42, get(imageInfo).model)); - append($$anchor4, div_91); - }; - if_block(node_65, ($$render) => { - if (get(imageInfo).model) $$render(consequent_57); - }); - } - var node_66 = sibling(node_65, 2); - { - var consequent_58 = ($$anchor4) => { - var div_92 = root_80(); - var span_18 = sibling(child(div_92), 2); - var text_43 = child(span_18); - template_effect(($0) => set_text(text_43, $0), [() => formatSeed(get(imageInfo).seed)]); - delegated("click", div_92, copySeed); - append($$anchor4, div_92); - }; - if_block(node_66, ($$render) => { - if (get(imageInfo).seed != null) $$render(consequent_58); - }); - } - var div_93 = sibling(node_66, 2); - var node_67 = child(div_93); - { - var consequent_59 = ($$anchor4) => { - var div_94 = root_81(); - var span_19 = sibling(child(div_94), 2); + var consequent_66 = ($$anchor4) => { + var div_101 = root_89(); + var span_19 = sibling(child(div_101), 2); var text_44 = child(span_19); - template_effect(() => set_text(text_44, get(imageInfo).steps)); - append($$anchor4, div_94); + template_effect(() => set_text(text_44, get(imageInfo).model)); + append($$anchor4, div_101); }; - if_block(node_67, ($$render) => { - if (get(imageInfo).steps) $$render(consequent_59); + if_block(node_74, ($$render) => { + if (get(imageInfo).model) $$render(consequent_66); }); } - var node_68 = sibling(node_67, 2); + var node_75 = sibling(node_74, 2); { - var consequent_60 = ($$anchor4) => { - var div_95 = root_82(); - var span_20 = sibling(child(div_95), 2); + var consequent_67 = ($$anchor4) => { + var div_102 = root_90(); + var span_20 = sibling(child(div_102), 2); var text_45 = child(span_20); - template_effect(() => set_text(text_45, get(imageInfo).cfg)); - append($$anchor4, div_95); + template_effect(($0) => set_text(text_45, $0), [() => formatSeed(get(imageInfo).seed)]); + delegated("click", div_102, copySeed); + append($$anchor4, div_102); }; - if_block(node_68, ($$render) => { - if (get(imageInfo).cfg) $$render(consequent_60); + if_block(node_75, ($$render) => { + if (get(imageInfo).seed != null) $$render(consequent_67); }); } - var node_69 = sibling(node_68, 2); + var div_103 = sibling(node_75, 2); + var node_76 = child(div_103); { - var consequent_61 = ($$anchor4) => { - var div_96 = root_83(); - var span_21 = sibling(child(div_96), 2); + var consequent_68 = ($$anchor4) => { + var div_104 = root_91(); + var span_21 = sibling(child(div_104), 2); var text_46 = child(span_21); - template_effect(() => set_text(text_46, get(imageInfo).sampler)); - append($$anchor4, div_96); + template_effect(() => set_text(text_46, get(imageInfo).steps)); + append($$anchor4, div_104); }; - if_block(node_69, ($$render) => { - if (get(imageInfo).sampler) $$render(consequent_61); + if_block(node_76, ($$render) => { + if (get(imageInfo).steps) $$render(consequent_68); }); } - var node_70 = sibling(node_69, 2); + var node_77 = sibling(node_76, 2); { - var consequent_62 = ($$anchor4) => { - var div_97 = root_84(); - var span_22 = sibling(child(div_97), 2); + var consequent_69 = ($$anchor4) => { + var div_105 = root_92(); + var span_22 = sibling(child(div_105), 2); var text_47 = child(span_22); - template_effect(() => set_text(text_47, get(imageInfo).denoise)); - append($$anchor4, div_97); + template_effect(() => set_text(text_47, get(imageInfo).cfg)); + append($$anchor4, div_105); }; - if_block(node_70, ($$render) => { - if (get(imageInfo).denoise != null) $$render(consequent_62); + if_block(node_77, ($$render) => { + if (get(imageInfo).cfg) $$render(consequent_69); + }); + } + var node_78 = sibling(node_77, 2); + { + var consequent_70 = ($$anchor4) => { + var div_106 = root_93(); + var span_23 = sibling(child(div_106), 2); + var text_48 = child(span_23); + template_effect(() => set_text(text_48, get(imageInfo).sampler)); + append($$anchor4, div_106); + }; + if_block(node_78, ($$render) => { + if (get(imageInfo).sampler) $$render(consequent_70); + }); + } + var node_79 = sibling(node_78, 2); + { + var consequent_71 = ($$anchor4) => { + var div_107 = root_94(); + var span_24 = sibling(child(div_107), 2); + var text_49 = child(span_24); + template_effect(() => set_text(text_49, get(imageInfo).denoise)); + append($$anchor4, div_107); + }; + if_block(node_79, ($$render) => { + if (get(imageInfo).denoise != null) $$render(consequent_71); }); } append($$anchor3, fragment_13); }; - if_block(node_64, ($$render) => { - if (get(imageInfo).model || get(imageInfo).seed != null || get(imageInfo).steps) $$render(consequent_63); + if_block(node_73, ($$render) => { + if (get(imageInfo).model || get(imageInfo).seed != null || get(imageInfo).steps) $$render(consequent_72); }); } append($$anchor2, fragment_10); }; - var consequent_65 = ($$anchor2) => { - var div_98 = root_85(); - append($$anchor2, div_98); + var consequent_74 = ($$anchor2) => { + var div_108 = root_95(); + append($$anchor2, div_108); }; - if_block(node_53, ($$render) => { - if (get(imageInfo)) $$render(consequent_64); - else if (get(currentImage)) $$render(consequent_65, 1); + if_block(node_62, ($$render) => { + if (get(imageInfo)) $$render(consequent_73); + else if (get(currentImage)) $$render(consequent_74, 1); }); } bind_this(div, ($$value) => viewerEl = $$value, () => viewerEl); template_effect(() => { - set_style(div_29, `cursor: ${get(isPanning) ? "grabbing" : "grab"}`); + set_style(div_33, `cursor: ${get(isPanning) ? "grabbing" : "grab"}`); set_text(text_23, `${get(currentIndex) + 1} / ${images().length ?? ""}`); set_text(text_24, get(zoomDisplayText)); set_value(input_1, get(zoomSliderValue)); - classes_14 = set_class(div_76, 1, "pcr-viewer-toolbar-btn svelte-5cciiw", null, classes_14, { active: get(linkCopied) }); - set_text(text_32, get(linkCopied) ? "Copied!" : "Copy Link"); + classes_15 = set_class(div_86, 1, "pcr-viewer-toolbar-btn svelte-5cciiw", null, classes_15, { active: get(linkCopied) }); + set_text(text_34, get(linkCopied) ? "Copied!" : "Copy Link"); }); - event("wheel", div_29, handleWheel); - delegated("pointerdown", div_29, handlePointerDown); - delegated("pointermove", div_29, handlePointerMove); - delegated("pointerup", div_29, handlePointerUp); - delegated("dblclick", div_29, handleDblClick); - delegated("click", div_45, function(...$$args) { + event("wheel", div_33, handleWheel); + delegated("pointerdown", div_33, handlePointerDown); + delegated("pointermove", div_33, handlePointerMove); + delegated("pointerup", div_33, handlePointerUp); + delegated("dblclick", div_33, handleDblClick); + delegated("click", div_49, function(...$$args) { var _a2; (_a2 = onClose()) == null ? void 0 : _a2.apply(this, $$args); }); - delegated("click", div_59, () => { + delegated("click", div_65, () => { set(zoomDropdownOpen, !get(zoomDropdownOpen)); }); delegated("input", input_1, handleZoomSlider); - delegated("click", div_76, copyLink); + delegated("click", div_86, copyLink); } - var node_71 = sibling(div, 2); + var node_80 = sibling(div, 2); { let $0 = user_derived(() => get(displayedHash) || ""); let $1 = user_derived(() => { @@ -15441,7 +16724,7 @@ ${negative}` : "")); var _a2; return get(upscaleFromEdit) ? ((_a2 = get(upscaleEditCtx)) == null ? void 0 : _a2.docHeight) || 0 : 0; }); - UpscaleOptionsModal(node_71, { + UpscaleOptionsModal(node_80, { get open() { return get(upscaleModalOpen); }, @@ -15493,7 +16776,7 @@ ${negative}` : "")); } }); } - var node_72 = sibling(node_71, 2); + var node_81 = sibling(node_80, 2); { let $0 = user_derived(() => get(displayedHash) || ""); let $1 = user_derived(() => { @@ -15534,7 +16817,7 @@ ${get(imageInfo).negative}` : "") : ""); let $9 = user_derived(() => get(inpaintFromEdit) ? get(inpaintMovedContent) : false); let $10 = user_derived(() => get(inpaintFromEdit) ? get(inpaintEditMask) : null); let $11 = user_derived(() => get(inpaintFromEdit) ? handleInpaintToEdit : null); - InpaintModal(node_72, { + InpaintModal(node_81, { get open() { return get(inpaintModalOpen); }, @@ -15613,7 +16896,7 @@ ${get(imageInfo).negative}` : "") : ""); } }); } - var node_73 = sibling(node_72, 2); + var node_82 = sibling(node_81, 2); { let $0 = user_derived(() => { var _a2; @@ -15641,7 +16924,7 @@ ${get(imageInfo).negative}` : "") : ""); let $8 = user_derived(() => onReposeCaps() && onReposeRun() && onMountPoser() && onInpaintUploadReference() ? openReposeFromEdit : null); let $9 = user_derived(() => get(inpaintFromEdit) && get(inpaintModalOpen) || get(upscaleFromEdit) && get(upscaleModalOpen) || get(reposeModalOpen)); bind_this( - EditModal(node_73, { + EditModal(node_82, { get open() { return get(editModalOpen); }, @@ -15696,7 +16979,7 @@ ${get(imageInfo).negative}` : "") : ""); () => get(editModalRef) ); } - var node_74 = sibling(node_73, 2); + var node_83 = sibling(node_82, 2); { let $0 = user_derived(() => get(displayedHash) || ""); let $1 = user_derived(() => { @@ -15715,7 +16998,7 @@ ${get(imageInfo).negative}` : "") : ""); var _a2; return (_a2 = get(reposePrepared)) == null ? void 0 : _a2.caps; }); - RePoseModal(node_74, { + RePoseModal(node_83, { get open() { return get(reposeModalOpen); }, @@ -15758,7 +17041,102 @@ ${get(imageInfo).negative}` : "") : ""); } }); } - var node_75 = sibling(node_74, 2); + var node_84 = sibling(node_83, 2); + { + let $0 = user_derived(() => { + var _a2; + return ((_a2 = get(extendPrepared)) == null ? void 0 : _a2.lastFrameUrl) || ""; + }); + let $1 = user_derived(() => { + var _a2; + return ((_a2 = get(extendPrepared)) == null ? void 0 : _a2.fps) || 16; + }); + let $2 = user_derived(() => { + var _a2; + return ((_a2 = get(extendPrepared)) == null ? void 0 : _a2.frameCount) || 0; + }); + let $3 = user_derived(() => { + var _a2; + return ((_a2 = get(extendPrepared)) == null ? void 0 : _a2.lightningAvailable) ?? true; + }); + let $4 = user_derived(() => { + var _a2; + return ((_a2 = get(extendPrepared)) == null ? void 0 : _a2.defaults) || null; + }); + let $5 = user_derived(() => { + var _a2; + return ((_a2 = get(extendPrepared)) == null ? void 0 : _a2.recipeDefaults) || null; + }); + ExtendModal(node_84, { + get open() { + return get(extendModalOpen); + }, + get lastFrameUrl() { + return get($0); + }, + get fps() { + return get($1); + }, + get frameCount() { + return get($2); + }, + get lightningAvailable() { + return get($3); + }, + get defaults() { + return get($4); + }, + get recipeDefaults() { + return get($5); + }, + get mountPromptEditor() { + return onMountPromptEditor(); + }, + get progress() { + return get(extendProgress); + }, + onRun: confirmExtend, + onDone: finishExtend, + onCancel: () => { + var _a2; + if (get(extendProgress) && ["building", "queueing", "running"].includes(get(extendProgress).phase)) (_a2 = extendTracker == null ? void 0 : extendTracker.cancel) == null ? void 0 : _a2.call(extendTracker); + else closeExtend(); + } + }); + } + var node_85 = sibling(node_84, 2); + { + let $0 = user_derived(() => { + var _a2; + return ((_a2 = get(smoothPrepared)) == null ? void 0 : _a2.fps) || 16; + }); + let $1 = user_derived(() => { + var _a2; + return ((_a2 = get(smoothPrepared)) == null ? void 0 : _a2.frameCount) || 0; + }); + SmoothModal(node_85, { + get open() { + return get(smoothModalOpen); + }, + get fps() { + return get($0); + }, + get frameCount() { + return get($1); + }, + get progress() { + return get(smoothProgress); + }, + onRun: confirmSmooth, + onDone: finishSmooth, + onCancel: () => { + var _a2; + if (get(smoothProgress) && ["building", "queueing", "running"].includes(get(smoothProgress).phase)) (_a2 = smoothTracker == null ? void 0 : smoothTracker.cancel) == null ? void 0 : _a2.call(smoothTracker); + else closeSmooth(); + } + }); + } + var node_86 = sibling(node_85, 2); { let $0 = user_derived(() => { var _a2; @@ -15772,7 +17150,7 @@ ${get(imageInfo).negative}` : "") : ""); var _a2; return ((_a2 = get(deleteBatch)) == null ? void 0 : _a2.length) ? `Delete ${get(deleteBatch).length}` : "Delete"; }); - ConfirmModal(node_75, { + ConfirmModal(node_86, { get open() { return get(confirmDeleteOpen); }, @@ -15789,18 +17167,19 @@ ${get(imageInfo).negative}` : "") : ""); onCancel: cancelDelete }); } - var node_76 = sibling(node_75, 2); + var node_87 = sibling(node_86, 2); { - var consequent_66 = ($$anchor2) => { + var consequent_75 = ($$anchor2) => { { let $0 = user_derived(() => get(lineageMenu).item ? canExpand(get(lineageMenu).item) : false); let $1 = user_derived(() => { var _a2; return get(lineageMenu).item ? ((_a2 = get(lineageTree).childrenOf.get(get(lineageMenu).item.hash)) == null ? void 0 : _a2.length) > 0 && get(lineageMenu).item.hash !== get(focusRootHash) : false; }); - let $2 = user_derived(() => !!onEditSave()); - let $3 = user_derived(() => !!onDelete()); - let $4 = user_derived(() => { + let $2 = user_derived(() => get(lineageMenu).item ? get(pinnedHashes).has(get(lineageMenu).item.hash) : false); + let $3 = user_derived(() => !!onEditSave()); + let $4 = user_derived(() => !!onDelete()); + let $5 = user_derived(() => { var _a2; return ((_a2 = get(lineageMenu).item) == null ? void 0 : _a2.orphaned) === 1; }); @@ -15817,12 +17196,15 @@ ${get(imageInfo).negative}` : "") : ""); get canFocus() { return get($1); }, - get canEdit() { + get pinned() { return get($2); }, - get canDelete() { + get canEdit() { return get($3); }, + get canDelete() { + return get($4); + }, get isLocal() { return isLocalClient; }, @@ -15830,7 +17212,7 @@ ${get(imageInfo).negative}` : "") : ""); return isWindows; }, get orphaned() { - return get($4); + return get($5); }, onAction: (action2) => lineageMenuAction(action2, get(lineageMenu).item), onClose: () => { @@ -15839,8 +17221,8 @@ ${get(imageInfo).negative}` : "") : ""); }); } }; - if_block(node_76, ($$render) => { - if (get(lineageMenu)) $$render(consequent_66); + if_block(node_87, ($$render) => { + if (get(lineageMenu)) $$render(consequent_75); }); } template_effect(() => set_style(div, `transform: translateY(${get(viewerTransformY) ?? ""}px); opacity: ${get(viewerOpacity) ?? ""}`)); diff --git a/js/lib/viewer-bridge.js b/js/lib/viewer-bridge.js index d333bb8..22371f8 100644 --- a/js/lib/viewer-bridge.js +++ b/js/lib/viewer-bridge.js @@ -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), }); } diff --git a/js/main.js b/js/main.js index 567972b..ea0005e 100644 --- a/js/main.js +++ b/js/main.js @@ -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); } diff --git a/js/pose-studio.js b/js/pose-studio.js index 68007a9..3904388 100644 --- a/js/pose-studio.js +++ b/js/pose-studio.js @@ -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 "_" 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); }; diff --git a/js/region-box.js b/js/region-box.js new file mode 100644 index 0000000..6dc2d92 --- /dev/null +++ b/js/region-box.js @@ -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 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); + }, +}); diff --git a/nodes/ideogram_caption.py b/nodes/ideogram_caption.py index 68c5df3..2c20ba7 100644 --- a/nodes/ideogram_caption.py +++ b/nodes/ideogram_caption.py @@ -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)) diff --git a/nodes/region_box.py b/nodes/region_box.py new file mode 100644 index 0000000..ad9dd17 --- /dev/null +++ b/nodes/region_box.py @@ -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() diff --git a/nodes/regional_detailer.py b/nodes/regional_detailer.py index 1c6638c..9b572f7 100644 --- a/nodes/regional_detailer.py +++ b/nodes/regional_detailer.py @@ -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: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..46bbd91 --- /dev/null +++ b/pyproject.toml @@ -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 = "" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a5840f1 --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/scripts/natlang_decompose_probe.py b/scripts/natlang_decompose_probe.py index 17ca85e..491f44b 100644 --- a/scripts/natlang_decompose_probe.py +++ b/scripts/natlang_decompose_probe.py @@ -19,7 +19,7 @@ The output schema is intentionally tiny: ... Run: - cd C:/comfyui/comfyui/custom_nodes/ComfyUI-PromptChain + cd /custom_nodes/ComfyUI-PromptChain python scripts/natlang_decompose_probe.py python scripts/natlang_decompose_probe.py """ diff --git a/scripts/natlang_scene_composer.py b/scripts/natlang_scene_composer.py index 0e8b79d..e043c8f 100644 --- a/scripts/natlang_scene_composer.py +++ b/scripts/natlang_scene_composer.py @@ -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 /custom_nodes/ComfyUI-PromptChain python scripts/natlang_scene_composer.py """ from __future__ import annotations diff --git a/svelte-src/components/ExtendModal.svelte b/svelte-src/components/ExtendModal.svelte new file mode 100644 index 0000000..0a5261d --- /dev/null +++ b/svelte-src/components/ExtendModal.svelte @@ -0,0 +1,350 @@ + + +{#if open} + + +
{ if (e.target === e.currentTarget && !running) close(); }}> +
+
Extend Video
+
+ Renders the next clip continuing from this video's last frame{frameCount ? ` · ${frameCount} frames @ ${fps}fps` : ""}. Saved as a lineage child. +
+ + {#if lastFrameUrl} +
+ last frame + continues from here → +
+ {/if} + +
Speed
+
+ + +
+ + + {#if mountPromptEditor} +
+ {:else} + + {/if} + + + + + + {#if advancedOpen} +
+
+ Length {length} frames + length = v} /> +
+
+ Output FPS {outFps} + outFps = v} /> +
+
+ Steps {steps} + steps = v} /> +
+
+ CFG {cfg} + cfg = v} /> +
+
+ Shift {shift} + shift = v} /> +
+
+ + +
+
+ + +
+
+ {/if} + + {#if progress} +
+ {#if phase === "error"} +
⚠ {progress.message || "render failed"}
+ {:else if done} +
✓ Continuation rendered. Adjust and Run Again, or Done to view it.
+ {#if progress.resultUrl} + + + {/if} + {:else} +
+
{phase}{progress.max ? ` · ${pct}%` : "…"}
+ {#if progress.previewUrl} + preview + {/if} + {/if} +
+ {/if} + +
+ {#if running} + + {:else if done} + + + {:else} + + + {/if} +
+
+
+{/if} + + diff --git a/svelte-src/components/ImageViewer.svelte b/svelte-src/components/ImageViewer.svelte index e869968..389c166 100644 --- a/svelte-src/components/ImageViewer.svelte +++ b/svelte-src/components/ImageViewer.svelte @@ -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 — 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 + 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 can latch onto a 404 from a request that raced the + // server-side record (the thumb is written as part of recording); a broken + // 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/ + // 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