diff --git a/.gitattributes b/.gitattributes index 1354edc59a3ba3f6c320616d318eb6301678529e..59212135f650defe3bee7736aad612f566d843f7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,11 @@ +* text=auto + +# Track large model files with Git LFS +*.safetensors filter=lfs diff=lfs merge=lfs -text +*.ckpt filter=lfs diff=lfs merge=lfs -text +*.pt filter=lfs diff=lfs merge=lfs -text +*.pth filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text *.jpg filter=lfs diff=lfs merge=lfs -text *.jpeg filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000000000000000000000000000000000..bb4901bcd354a15a4db8f0556355ad776a639270 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,9 @@ +# Code owners for MagicNodes +# Docs: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners + +# Default owner for the whole repository +* @1dZb1 + +# (Optional) +# /docs/ @1dZb1 +# /mod/ @1dZb1 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000000000000000000000000000000000..0a7dd1b3e5e419f0aea39cf44c73eea61199879c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,5 @@ +github: 1dZb1 +buy_me_a_coffee: dzrobo +custom: + - https://buymeacoffee.com/dzrobo + - https://github.com/sponsors/1dZb1 diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000000000000000000000000000000000..9f0fa5ae9df70f7c939376db3be5b68a5e23bf47 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,67 @@ +name: "Bug report" +description: "Something is broken or not working as expected" +title: "[Bug] " +labels: [bug] +body: + - type: checkboxes + id: preflight + attributes: + label: Pre‑flight + description: Please confirm you checked these first + options: + - label: I searched existing issues and discussions + required: true + - label: I’m on the latest MagicNodes commit and preset files + required: false + - type: textarea + id: summary + attributes: + label: Summary + description: What happened? What did you expect instead? + placeholder: A clear and concise description of the issue + validations: + required: true + - type: textarea + id: repro + attributes: + label: Steps to reproduce + description: Minimal steps / workflow to reproduce the problem + placeholder: | + 1. Preset/step/config used + 2. Node settings (seed/steps/cfg/denoise/sampler) + 3. What you observed + validations: + required: true + - type: textarea + id: env + attributes: + label: Environment + description: OS/GPU/driver and versions + placeholder: | + OS: Windows 11 / Linux + GPU: RTX 4090 (driver 560.xx) + Python: 3.10.x | PyTorch: 2.8.0+cu129 + ComfyUI: + MagicNodes: + validations: + required: false + - type: textarea + id: logs + attributes: + label: Logs / Screenshots + description: Paste relevant logs, stack traces, or attach screenshots + render: shell + validations: + required: false + - type: dropdown + id: severity + attributes: + label: Impact + options: + - Crash/blocks generation + - Wrong output/quality regression + - UI/Docs glitch + - Minor inconvenience + validations: + required: false + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000000000000000000000000000000000..eabe1ece75ef5274291913dc58a7249624f2b134 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Q&A and ideas (Discussions) + url: https://github.com/1dZb1/MagicNodes/discussions + about: Ask questions and share ideas in Discussions + - name: Hugging Face page + url: https://huggingface.co/DD32/MagicNodes + about: Releases and mirrors on HF diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000000000000000000000000000000000000..f39799e2b942a4ea6266ee77ef0f9d0a95d16bfe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,39 @@ +name: "Feature request" +description: "Request an enhancement or new capability" +title: "[Feat] " +labels: [enhancement] +body: + - type: textarea + id: problem + attributes: + label: Problem / motivation + description: What use‑case does this solve? Why is it valuable? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: API/UI/UX draft, presets, examples + placeholder: Describe the change and how it would work + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Any workarounds or different approaches + validations: + required: false + - type: checkboxes + id: scope + attributes: + label: Scope + options: + - label: Easy nodes / presets + - label: Hard nodes / advanced params + - label: Docs / examples / workflows + - label: Performance / memory + validations: + required: false + diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 0000000000000000000000000000000000000000..9c4cac48fba8b3a3b96ef4392d493cbf92b8a6f3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,20 @@ +name: "Question / Help" +description: "Ask a question about usage, presets, or behavior" +title: "[Q] " +labels: [question] +body: + - type: textarea + id: question + attributes: + label: Your question + description: What do you want to understand or achieve? + validations: + required: true + - type: textarea + id: context + attributes: + label: Context + description: Share your preset, node settings, or screenshot if helpful + validations: + required: false + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000000000000000000000000000000000..42a511e4ee51f9fdf38cdae3f3daa14a6d0d76af --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,28 @@ + + +Title: + +What +- Briefly describe the change (user‑facing behavior, docs, presets, or internals). + +Why +- What problem does it solve? Link issues/discussions if applicable. + +How (high level) +- Outline the approach. Call out any trade‑offs. + +Test plan +- Steps or screenshots proving it works (minimal workflow, seed/steps/cfg if relevant). + +Checklist +- [ ] Builds/runs locally with default presets +- [ ] Docs/README updated if behavior changed +- [ ] No large binaries added (weights go to HF or Releases) +- [ ] Passes lint/format (if configured) + +Notes for reviewers +- Anything sensitive or risky to double‑check (paths, presets, defaults). + diff --git a/.github/workflows/hf-mirror.yml b/.github/workflows/hf-mirror.yml new file mode 100644 index 0000000000000000000000000000000000000000..1313aeafc8f11bf036cf0d354c178aa3090d4655 --- /dev/null +++ b/.github/workflows/hf-mirror.yml @@ -0,0 +1,41 @@ +name: Mirror to Hugging Face + +on: + push: + branches: [main] + workflow_dispatch: + +concurrency: + group: hf-mirror-${{ github.ref }} + cancel-in-progress: true + +jobs: + mirror: + runs-on: ubuntu-latest + steps: + - name: Checkout (full history) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + lfs: false + + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Push to HF mirror + env: + HF_USERNAME: ${{ secrets.HF_USERNAME }} + HF_TOKEN: ${{ secrets.HF_TOKEN }} + HF_REPO: MagicNodes + run: | + if [ -z "${HF_USERNAME}" ] || [ -z "${HF_TOKEN}" ]; then + echo "HF secrets are missing. Set HF_USERNAME and HF_TOKEN in repo secrets." >&2 + exit 1 + fi + git lfs install || true + git remote add hf "https://${HF_USERNAME}:${HF_TOKEN}@huggingface.co/${HF_USERNAME}/${HF_REPO}.git" 2>/dev/null || true + # Mirror current branch to HF main + git push --force --prune hf HEAD:main + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..34d1a72b06a748a527a587f488517929c5092baa --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Byte-compiled / cache +__pycache__/ +*.py[cod] +*.pyo + +# Virtual envs / tooling +.venv/ +venv/ +.env +.env.* +.idea/ +.vscode/ + +# OS junk +.DS_Store +Thumbs.db + +# Build/output/temp +dist/ +build/ +out/ +*.log +*.tmp +temp/ +temp_patch.diff + +# NOTE: We intentionally keep model weights in repo via Git LFS. +# If you prefer not to ship them, re-add ignores for models/** and weight extensions. + +# ComfyUI caches +**/web/tmp/ +**/web/dist/ + +# Node / front-end (if any submodules appear later) +node_modules/ diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000000000000000000000000000000000000..2d19f23112f14158c57e4ac5836e0858b85b071a --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,35 @@ +cff-version: 1.2.0 +message: If you use CADE 2.5 / MG_SuperSimple, please cite our preprints (ZeResFDG and QSilk). +title: "MagicNodes: CADE 2.5 (ZeResFDG and QSilk)" +authors: + - family-names: Rychkovskiy + given-names: Denis + alias: DZRobo +version: preprint +date-released: 2025-10-11 +repository-code: https://github.com/1dZb1/MagicNodes +url: https://huggingface.co/DD32/MagicNodes + +preferred-citation: + type: article + title: CADE 2.5: ZeResFDG - Frequency-Decoupled, Rescaled and Zero-Projected Guidance for SD/SDXL Latent Diffusion Models + authors: + - family-names: Rychkovskiy + given-names: Denis + year: 2025 + journal: arXiv + identifiers: + - type: url + value: https://arxiv.org/abs/2510.12954 + +references: + - type: article + title: QSilk: Micrograin Stabilization and Adaptive Quantile Clipping for Detail-Friendly Latent Diffusion + authors: + - family-names: Rychkovskiy + given-names: Denis + year: 2025 + journal: arXiv + identifiers: + - type: url + value: https://arxiv.org/abs/2510.15761 diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 0000000000000000000000000000000000000000..94de8ef286852aeb590b14eb427ec9572b519a45 --- /dev/null +++ b/CREDITS.md @@ -0,0 +1,14 @@ +# Credits and Attributions + +This project includes adapted code and ideas from: + +- KJ-Nodes — ComfyUI-KJNodes (GPL-3.0) by KJ (kijai) + Repository: https://github.com/kijai/ComfyUI-KJNodes + Usage: SageAttention integration and attention override approach in mod/mg_sagpu_attention.py. + +- ComfyUI (GPL-3.0+) + Source idea: early beta node "Mahiro is so cute that she deserves a better guidance function!! (。・ω・。)" + Repository: https://github.com/comfyanonymous/ComfyUI + Usage: inspiration for directional post‑mix ("Muse Blend"); implementation rewritten and expanded in MagicNodes. + +Licensing note: Under GPLv3 §13, the combined work is distributed under AGPL-3.0-or-later. See LICENSE. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..ae4c9c8a01a3ea3e4837781bf496d94076d4e105 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +MagicNodes +SPDX-License-Identifier: AGPL-3.0-or-later + +This project is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +The full text of the GNU Affero General Public License v3.0 is available at: +https://www.gnu.org/licenses/agpl-3.0.txt + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see https://www.gnu.org/licenses/. + +Copyright (C) 2025 MagicNodes contributors + +Third-party notices: Portions of this project are derived from third-party code. +See CREDITS.md for attribution and links to original repositories. + diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000000000000000000000000000000000000..d596ab8786e2c04de1a1f101e5be9015cc7243c5 --- /dev/null +++ b/NOTICE @@ -0,0 +1,6 @@ +Attribution (kind request) + +Includes CADE 2.5 (ZeResFDG) by Denis Rychkovskiy (“DZRobo”). + +If you use this work or parts of it, please consider preserving this notice +in your README/About or documentation. diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..74c80f6e66dfb4952830926234165b287e97bdad --- /dev/null +++ b/README.md @@ -0,0 +1,322 @@ +# MagicNodes — ComfyUI Render Pipeline (SD/SDXL) +Simple start. Expert-grade results. Reliable detail. +[![arXiv](https://img.shields.io/badge/arXiv-2510.12954-B31B1B.svg)](https://arxiv.org/abs/2510.12954) / [![arXiv](https://img.shields.io/badge/arXiv-2510.15761-B31B1B.svg)](https://arxiv.org/pdf/2510.15761) + + + + + +
+ MagicNodes + +TL;DR: MagicNodes, it's a plug-and-play multi-pass "render-machine" for SD/SDXL models. Simple one-node start, expert-grade results. Core is ZeResFDG (Frequency-Decoupled + Rescale + Zero-Projection) and the always-on QSilk Micrograin Stabilizer, complemented by practical stabilizers (NAG, local masks, EPS, Muse Blend, Polish). Ships with a four-pass preset for robust, clean, and highly detailed outputs. + +Our pipeline runs through several purposeful passes: early steps assemble global shapes, mid steps refine important regions, and late steps polish without overcooking the texture. We gently stabilize the amplitudes of the "image’s internal draft" (latent) and adapt the allowed value range per region: where the model is confident we give more freedom, and where it’s uncertain we act more conservatively. The result is clean gradients, crisp edges, and photographic detail even at very high resolutions and, as a side effect on SDXL models, text becomes noticeably more stable and legible. +
+ +Please note that the SDXL architecture itself has limitations and the result depends on the success of the seed, the purity of your prompt and the quality of your model+LoRA. + +Draw +
+ Anime full + Anime crop + +
+ +Photo Portrait +
+ Photo A + Photo B +
+
+ Photo C + Photo D +
+ +Photo Cup +
+ Photo A + Photo B +
+ +Photo Dog +
+ Photo A + Photo B +
+ +--- + +## Features +- ZeResFDG: LF/HF split, energy rescale, and zero-projection (stable early, sharp late) +- NAG (Normalized Attention Guidance): small attention variance normalization (positive branch) +- Local spatial gating: optional CLIPSeg masks for faces/hands/pose +- EPS scale: small early-step exposure bias +- QSilk Micrograin Stabilizer: gently smooths rare spikes and lets natural micro-texture (skin, fabric, tiny hairs) show through — without halos or grid patterns. Always on, zero knobs, near‑zero cost. +- Adaptive Quantile Clip (AQClip): softly adapts the allowed range per region. Confident areas keep more texture; uncertain ones get cleaner denoising. Tile‑based with seamless blending (no seams). Optional Attn mode uses attention confidence for an even smarter balance. +- MGHybrid scheduler: hybrid Karras/Beta sigma stack with smooth tail blending and tiny schedule jitter (ZeSmart-inspired) for more stable, detail-friendly denoising; used by CADE and SuperSimple by default +- Seed Latent (MG_SeedLatent): fast, deterministic latent initializer aligned to VAE stride; supports pure-noise starts or image-mixed starts (encode + noise) to gently bias content; batch-ready and resolution-agnostic, pairs well with SuperSimple recommended latent sizes for reproducible pipelines +- Muse Blend and Polish: directional post-mix and final low-frequency-preserving clean-up +- SmartSeed (CADE Easy and SuperSimple): set `seed = 0` to auto-pick a good seed from a tiny low-step probe. Uses a low-discrepancy sweep, avoids speckles/overexposure, and, if available, leverages CLIP-Vision (with `reference_image`) and CLIPSeg focus text to favor semantically aligned candidates. Logs `Smart_seed_random: Start/End`. +I highly recommend working with SmartSeed. +- CADE2.5 pipeline does not just upscale the image, it iterates and adds small details, doing it carefully, at every stage. + +## Hardware Requirements +- GPU VRAM: ~10-28 GB (free memory) for the default presets (start latent ~ 672x944 -> final ~ 3688x5192 across 4 steps). 15-25 GB is recommended; 32 GB is comfortable for large prompts/batches. +- System RAM: ~12-20 GB during generation (depends on start latent and whether Depth/ControlFusion are enabled). 16+ GB recommended. +- Notes + - Lowering the starting latent (e.g., 512x768) reduces both VRAM and RAM. + - Disabling hi-res depth/edges (ControlFusion) reduces peaks. (not recommended!) + - Depth weights add a bit of RAM on load; models live under `depth-anything/`. + + +## Install (ComfyUI 0.3.60, tested on this version) +Preparing: +I recomend update pytorch version: 2.8.0+cu129. +1. PyTorch install: `pip install torch==2.8.0 torchvision==0.23.0 torchaudio==2.8.0 --index-url https://download.pytorch.org/whl/cu129` +2. CUDA manual download and install: https://developer.nvidia.com/cuda-12-9-0-download-archive?target_os=Windows&target_arch=x86_64&target_version=11&target_type=exe_local +3. Install `SageAttention 2.2.0`, manualy `https://github.com/thu-ml/SageAttention` or use script `scripts/check_sageattention.bat`. The installation takes a few minutes, wait for the installation to finish. + +Next: +1. Clone or download this repo into `ComfyUI/custom_nodes/` +2. Install helpers: `pip install -r requirements.txt` +3. Take my negative LoRA `models/LoRA/mg_7lambda_negative.safetensors` and place the file in ComfyUI, to `ComfyUI/models/loras` +4. download model `depth_anything_v2_vitl.pth` https://huggingface.co/depth-anything/Depth-Anything-V2-Large/tree/main and place inside in to `depth-anything/` folder. +5. Workflows +Folder `workflows/` contains ready-to-use graphs: +- `mg_SuperSimple-Workflow.json` — one-node pipeline (2/3/4 steps) with presets +- `mg_Easy-Workflow.json` — the same logic built from individual Easy nodes +You can save this workflow to ComfyUI `ComfyUI\user\default\workflows` +6. Restart ComfyUI. Nodes appear under the "MagicNodes" categories. + +💥 I strongly recommend use `mg_Easy-Workflow` workflow + default settings + your model and my negative LoRA `mg_7lambda_negative.safetensors`, for best result. + + +## 🚀 "One-Node" Quickstart (MG_SuperSimple) +Start with `MG_SuperSimple` for the easiest path: +1. Drop `MG_SuperSimple` into the graph +2. Connect `model / positive / negative / vae / latent` and a `Load ControlNet Model` module +3. Choose `step_count` (2/3/4) and Run + +or load `mg_SuperSimple-Workflow` in panel ComfyUI + +Notes: +- When "Custom" is off, presets fully drive parameters +- When "Custom" is on, the visible CADE controls override the Step presets across all steps; Step 1 still enforces `denoise=1.0` +- CLIP Vision (if connected) is applied from Step 2 onward; if no reference image is provided, SuperSimple uses the previous step image as reference + +## ❗Tips +(!) There are almost always artifacts in the first step, don't pay attention to them, they will be removed in the next steps. Keep your prompt clean and logical, don't duplicate details and be careful with symbols. + +0) `MG_SuperSimple-Workflow` is a bit less flexible than `MG_Easy-Workflow`, but extremely simple to use. If you just want a stable, interesting result, start with SuperSimple. + +1) Recommended negative LoRA: `mg_7lambda_negative.safetensors` with `strength_model = -1.0`, `strength_clip = 0.2`. Place LoRA files under `ComfyUI/models/loras` so they appear in the LoRA selector. + +2) Download a CLIP Vision model and place it under `ComfyUI/models/clip_vision` (e.g., https://huggingface.co/openai/clip-vit-large-patch14; heavy alternative: https://huggingface.co/laion/CLIP-ViT-H-14-laion2B-s32B-b79K). SuperSimple/CADE will use it for reference-based polish. + +3) Samplers: i recomend use `ddim` for many cases (Draw and Realism style). Scheduler: use `MGHybrid` in this pipeline. + +4) Denoise: higher -> more expressive and vivid; you can go up to 1.0. The same applies to CFG: higher -> more expressive but may introduce artifacts. Suggested CFG range: ~4.5–8.5. + +5) If you see unwanted artifacts on the final (4th) step, slightly lower denoise to ~0.5–0.6 or simply change the seed. + +6) You can get interesting results by repeating steps (in Easy/Hard workflows), e.g., `1 -> 2 -> 3 -> 3`.  Just experiment with it! + +7) Recommended starting latent close to ~672x944 (other aspect ratios are fine). With that, step 4 produces ~3688x5192. Larger starting sizes are OK if the model and your hardware allow. + +8) Unlucky seeds happen — just try another. (We may later add stabilization to this process.) + +9) Rarely, step 3 can show a strange grid artifact (in both Easy and Hard workflows). If this happens, try changing CFG or seed. Root cause still under investigation. + +10) Results depend on checkpoint/LoRA quality. The pipeline “squeezes” everything SDXL and your model can deliver, so prefer high‑quality checkpoints and non‑overtrained LoRAs. + +11) Avoid using more than 3 LoRAs at once, and keep only one “lead” LoRA (one you trust is not overtrained). Too many/strong LoRAs can spoil results. + +12) Try connecting reference images in either workflow — you can get unusual and interesting outcomes. + +13) Very often, the image in `step 3 is of very good quality`, but it usually lacks sharpness. But if you have a `weak system`, you can `limit yourself to 3 steps`. + +14) SmartSeed (auto seed pick): set `seed = 0` in Easy or SuperSimple. The node will sample several candidate seeds and do a quick low‑step probe to choose a balanced one. You’ll see logs `Smart_seed_random: Start` and `Smart_seed_random: End. Seed is: `. Use any non‑zero seed for fully deterministic runs. + +15) The 4th step sometimes saves the image for a long time, just wait for the end of the process, it depends on the initial resolution you set. + + + +## Repository Layout +``` +MagicNodes/ +├─ README.md +├─ LICENSE # AGPL-3.0-or-later +├─ assets/ +├─ docs/ +│ ├─ EasyNodes.md +│ ├─ HardNodes.md +│ └─ hard/ +│ ├─ CADE25.md +│ ├─ ControlFusion.md +│ ├─ UpscaleModule.md +│ ├─ IDS.md +│ └─ ZeSmartSampler.md +│ +├─ mod/ +│ ├─ easy/ +│ │ ├─ mg_cade25_easy.py +│ │ ├─ mg_controlfusion_easy.py +│ │ └─ mg_supersimple_easy.py +│ │ └─ preset_loader.py +│ └─ hard/ +│ ├─ mg_cade25.py +│ ├─ mg_controlfusion.py +│ ├─ mg_tde2.py +│ ├─ mg_upscale_module.py +│ ├─ mg_ids.py +│ └─ mg_zesmart_sampler_v1_1.py +│ +├─ pressets/ +│ ├─ mg_cade25.cfg +│ └─ mg_controlfusion.cfg +│ +├─ scripts/ +│ ├─ check_sageattention.bat +│ └─ check_sageattention.ps1 +│ +├─ depth-anything/ # place Depth Anything v2 weights (.pth), e.g., depth_anything_v2_vitl.pth +│ └─depth_anything_v2_vitl.pth +│ +├─ vendor/ +│ └─ depth_anything_v2/ # vendored Depth Anything v2 code (Apache-2.0) +│ +├─ models/ +│ └─ LoRA/ +│ └─ mg_7lambda_negative.safetensors +│ +├─ workflows/ +│ ├─ mg_SuperSimple-Workflow.json +│ └─ mg_Easy-Workflow.json +| +└─ requirements.txt +``` + +Models folder +- The repo includes a sample negative LoRA at `models/LoRA/mg_7lambda_negative.safetensors`. +- To use it in ComfyUI, copy or move the file to `ComfyUI/models/loras` — it will then appear in LoRA selectors. +- Keeping a copy under `models/` here is fine as a backup. + +Depth models (Depth Anything v2) +- Place DA v2 weights (`.pth`) in `depth-anything/`. Recommended: `depth_anything_v2_vitl.pth` (ViT-L). Supported names include: + `depth_anything_v2_vits.pth`, `depth_anything_v2_vitb.pth`, `depth_anything_v2_vitl.pth`, `depth_anything_v2_vitg.pth`, + and the metric variants `depth_anything_v2_metric_vkitti_vitl.pth`, `depth_anything_v2_metric_hypersim_vitl.pth`. +- ControlFusion auto-detects the correct config from the filename and uses this path by default. You can override via the + `depth_model_path` parameter (preset) if needed. +- If no weights are found, ControlFusion falls back gracefully (luminance pseudo-depth), but results are better with DA v2. +- Where to get weights: see the official Depth Anything v2 repository (https://github.com/DepthAnything/Depth-Anything-V2) + and its Hugging Face models page (https://huggingface.co/Depth-Anything) for pre-trained `.pth` files. + + +## Documentation +- Easy nodes overview and `MG_SuperSimple`: `docs/EasyNodes.md` +- Hard nodes documentation index: `docs/HardNodes.md` + +## Control Fusion (mg_controlfusion.py, mg_controlfusion_easy.py,) +- Builds depth + edge masks with preserved aspect ratio; hires-friendly mask mode +- Key surface knobs: `edge_alpha`, `edge_smooth`, `edge_width`, `edge_single_line`/`edge_single_strength`, `edge_depth_gate`/`edge_depth_gamma` +- Preview can optionally reflect ControlNet strength via `preview_show_strength` and `preview_strength_branch` + +## CADE 2.5 (mg_cade25.py, mg_cade25_easy.py) +- Deterministic preflight: CLIPSeg pinned to CPU; preview mask reset; noise tied to `iter_seed` +- Encode/Decode: stride-aligned, with larger overlap for >2K to avoid artifacts +- Polish mode (final hi-res refinement): + - `polish_enable`, `polish_keep_low` (global form from reference), `polish_edge_lock`, `polish_sigma` + - Smooth start via `polish_start_after` and `polish_keep_low_ramp` +- `eps_scale` supported for gentle exposure shaping + +## Depth Anything v2 (vendor) +- Lives under `vendor/depth_anything_v2`; Apache-2.0 license + +## MG_ZeSmartSampler (Experimental) +- Custom sampler that builds hybrid sigma schedules (Karras/Beta blend) with tail smoothing +- Inputs/Outputs match KSampler: `MODEL/SEED/STEPS/CFG/base_sampler/schedule/CONDITIONING/LATENT` -> `LATENT` +- Key params: `hybrid_mix`, `jitter_sigma`, `tail_smooth`, optional PC2-like shaping (`smart_strength`, `target_error`, `curv_sensitivity`) + +## Seed Latent (mg_seed_latent.py) +- Purpose: quick LATENT initializer aligned to VAE stride (4xC, H/8, W/8). Can start from pure noise or mix an input image encoding with noise to gently bias content. +- Inputs + - `width`, `height`, `batch_size` + - `sigma` (noise amplitude) and `bias` (additive offset) + - Optional `vae` and `image` when `mix_image` is enabled +- Output: `LATENT` dict `{ "samples": tensor }` ready to feed into CADE/SuperSimple. +- Usage notes + - Keep dimensions multiples of 8; recommended starting sizes around ~672x944 (other aspect ratios work). With SuperSimple’s default scale, step 4 lands near ~3688x5192. + - `mix_image=True` encodes the provided image via VAE and adds noise: a soft way to keep global structure while allowing refinement downstream. + - For run-to-run comparability, hold your sampler seed fixed (in SuperSimple/CADE). SeedLatent itself does not expose a seed; variation is primarily controlled by the sampler seed. + - Batch friendly: `batch_size>1` produces independent latents of the chosen size. + +## Dependencies (Why These Packages) +- transformers — used by CADE for CLIPSeg (CIDAS/clipseg-rd64-refined) to build text‑driven masks (e.g., face/hands). If missing, CLIPSeg is disabled gracefully. + +- opencv-contrib-python — ControlFusion edge stack (Pyramid Canny, thinning via ximgproc), morphological ops, light smoothing. +- Pillow — image I/O and small conversions in preview/CLIPSeg pipelines. +- scipy — preferred Gaussian filtering path for IDS (quality). If not installed, IDS falls back to a PyTorch implementation. +- sageattention — accelerated attention kernels (auto-picks a kernel per GPU arch); CADE/attention patch falls back to stock attention if not present. + +Optional extras +- controlnet-aux — alternative loader for Depth Anything v2 if you don’t use the vendored implementation (not required by default). + + +## Preprint +- CADE 2.5 - ZeResFDG +- PDF: https://arxiv.org/pdf/2510.12954.pdf +- arXiv: https://arxiv.org/abs/2510.12954 + +- CADE 2.5 - QSilk +- PDF: https://arxiv.org/pdf/2510.15761 +- arXiv: https://arxiv.org/abs/2510.15761 + + +### How to Cite +``` +@misc{rychkovskiy2025cade25zeresfdg, + title={CADE 2.5 - ZeResFDG: Frequency-Decoupled, Rescaled and Zero-Projected Guidance for SD/SDXL Latent Diffusion Models}, + author={Denis Rychkovskiy}, + year={2025}, + eprint={2510.12954}, + archivePrefix={arXiv}, + primaryClass={cs.CV}, + url={https://arxiv.org/abs/2510.12954}, +} +``` +``` +@misc{rychkovskiy2025qsilkmicrograinstabilizationadaptive, + title={QSilk: Micrograin Stabilization and Adaptive Quantile Clipping for Detail-Friendly Latent Diffusion}, + author={Denis Rychkovskiy}, + year={2025}, + eprint={2510.15761}, + archivePrefix={arXiv}, + primaryClass={cs.CV}, + url={https://arxiv.org/abs/2510.15761}, +} +``` + +## Attribution (kind request) +If you use this work or parts of it, please consider adding the following credit in your README/About/credits: "Includes CADE 2.5 (ZeResFDG, QSilk) by Denis Rychkovskiy (“DZRobo”)" + + +## License and Credits +- License: AGPL-3.0-or-later (see `LICENSE`) + + +## Support +If this project saved you time, you can leave a tip: +- GitHub Sponsors: https://github.com/sponsors/1dZb1 +- Bymeacoffee: https://buymeacoffee.com/dzrobo + + + + + + + + + + + diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..639080236df0721ed68cb2c15e8e7ee3e056f119 --- /dev/null +++ b/__init__.py @@ -0,0 +1,97 @@ +import os, sys, importlib.util + +# Normalize package name so relative imports work even if loaded by absolute path +if __name__ != 'MagicNodes': + sys.modules['MagicNodes'] = sys.modules[__name__] + __package__ = 'MagicNodes' + # Precreate subpackage alias MagicNodes.mod + _mod_pkg_name = 'MagicNodes.mod' + _mod_pkg_dir = os.path.join(os.path.dirname(__file__), 'mod') + _mod_pkg_file = os.path.join(_mod_pkg_dir, '__init__.py') + if _mod_pkg_name not in sys.modules and os.path.isfile(_mod_pkg_file): + _spec = importlib.util.spec_from_file_location( + _mod_pkg_name, _mod_pkg_file, submodule_search_locations=[_mod_pkg_dir] + ) + _mod = importlib.util.module_from_spec(_spec) + sys.modules[_mod_pkg_name] = _mod + assert _spec.loader is not None + _spec.loader.exec_module(_mod) + +# Imports of active nodes +from .mod.mg_combinode import MagicNodesCombiNode +from .mod.hard.mg_upscale_module import MagicUpscaleModule +from .mod.hard.mg_adaptive import AdaptiveSamplerHelper +from .mod.hard.mg_cade25 import ComfyAdaptiveDetailEnhancer25 +from .mod.hard.mg_ids import IntelligentDetailStabilizer +from .mod.mg_seed_latent import MagicSeedLatent +from .mod.mg_sagpu_attention import PatchSageAttention +from .mod.hard.mg_controlfusion import MG_ControlFusion +from .mod.hard.mg_zesmart_sampler_v1_1 import MG_ZeSmartSampler +from .mod.easy.mg_cade25_easy import CADEEasyUI as ComfyAdaptiveDetailEnhancer25_Easy +from .mod.easy.mg_controlfusion_easy import MG_ControlFusionEasyUI as MG_ControlFusion_Easy +from .mod.easy.mg_supersimple_easy import MG_SuperSimple + +# Place Easy/Hard variants under dedicated UI categories +try: + ComfyAdaptiveDetailEnhancer25_Easy.CATEGORY = "MagicNodes/Easy" +except Exception: + pass +try: + MG_ControlFusion_Easy.CATEGORY = "MagicNodes/Easy" +except Exception: + pass +try: + MG_SuperSimple.CATEGORY = "MagicNodes/Easy" +except Exception: + pass +try: + ComfyAdaptiveDetailEnhancer25.CATEGORY = "MagicNodes/Hard" + IntelligentDetailStabilizer.CATEGORY = "MagicNodes/Hard" + MagicUpscaleModule.CATEGORY = "MagicNodes/Hard" + AdaptiveSamplerHelper.CATEGORY = "MagicNodes/Hard" + PatchSageAttention.CATEGORY = "MagicNodes" + MG_ControlFusion.CATEGORY = "MagicNodes/Hard" + MG_ZeSmartSampler.CATEGORY = "MagicNodes/Hard" +except Exception: + pass + +NODE_CLASS_MAPPINGS = { + "MagicNodesCombiNode": MagicNodesCombiNode, + "MagicSeedLatent": MagicSeedLatent, + "PatchSageAttention": PatchSageAttention, + "MagicUpscaleModule": MagicUpscaleModule, + "ComfyAdaptiveDetailEnhancer25": ComfyAdaptiveDetailEnhancer25, + "IntelligentDetailStabilizer": IntelligentDetailStabilizer, + "MG_ControlFusion": MG_ControlFusion, + "MG_ZeSmartSampler": MG_ZeSmartSampler, + # Easy variants (limited-surface controls) + "ComfyAdaptiveDetailEnhancer25_Easy": ComfyAdaptiveDetailEnhancer25_Easy, + "MG_ControlFusion_Easy": MG_ControlFusion_Easy, + "MG_SuperSimple": MG_SuperSimple, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "MagicNodesCombiNode": "MG_CombiNode", + "MagicSeedLatent": "MG_SeedLatent", + # TDE removed from this build + "PatchSageAttention": "MG_AccelAttention", + "ComfyAdaptiveDetailEnhancer25": "MG_CADE 2.5", + "MG_ControlFusion": "MG_ControlFusion", + "MG_ZeSmartSampler": "MG_ZeSmartSampler", + "IntelligentDetailStabilizer": "MG_IDS", + "MagicUpscaleModule": "MG_UpscaleModule", + # Easy variants (grouped under MagicNodes/Easy) + "ComfyAdaptiveDetailEnhancer25_Easy": "MG_CADE 2.5 (Easy)", + "MG_ControlFusion_Easy": "MG_ControlFusion (Easy)", + "MG_SuperSimple": "MG_SuperSimple", +} + +__all__ = [ + 'NODE_CLASS_MAPPINGS', + 'NODE_DISPLAY_NAME_MAPPINGS', +] + + + + + diff --git a/assets/Anime1.jpg b/assets/Anime1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a48947c1f0d49006b6b79e38a55ef098a38e680c --- /dev/null +++ b/assets/Anime1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d837bededf87a14359c770da2b5f34f037372725d0be48195049645c05437345 +size 680141 diff --git a/assets/Anime1_crop.jpg b/assets/Anime1_crop.jpg new file mode 100644 index 0000000000000000000000000000000000000000..af4a3d4d9196b12976c86090c9cc934c5e80a6ef --- /dev/null +++ b/assets/Anime1_crop.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94a52c1a6209bfef368c8d1b042bee69be7a7e7456dfbd843a5dc501e1cc1d6c +size 402596 diff --git a/assets/Dog1_crop_ours_CADE25_QSilk.jpg b/assets/Dog1_crop_ours_CADE25_QSilk.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7985ddb3371840671b9f426027ae06ab5a74fea5 --- /dev/null +++ b/assets/Dog1_crop_ours_CADE25_QSilk.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd12b232fcf55a7a3e85c25307e53510d4b2672a94919d81f82149a664a9bdb1 +size 294126 diff --git a/assets/Dog1_ours_CADE25_QSilk.jpg b/assets/Dog1_ours_CADE25_QSilk.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c47602cff7731fe1ac4f1f6ae54d05427cb7aea3 --- /dev/null +++ b/assets/Dog1_ours_CADE25_QSilk.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7352d5eaec36ddbd3f658711ffc8b31585b561ede97687f4e7bae61ae067863b +size 360204 diff --git a/assets/MagicNodes.png b/assets/MagicNodes.png new file mode 100644 index 0000000000000000000000000000000000000000..62e03ab167d2a4c7c63c7cb7a355531a77674626 --- /dev/null +++ b/assets/MagicNodes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd0efb24491b12d5dad7ca7961e5bcea1997e1d14258ecec34f1bcae660a88e2 +size 8114 diff --git a/assets/PhotoCup1.jpg b/assets/PhotoCup1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d3b2b6d57c0ba63c44947882de5326ef91ddda26 --- /dev/null +++ b/assets/PhotoCup1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1272b61d2c39cd4cc825077649768a4e65047c3a6dd9bb3cc4a539eb8284e455 +size 273595 diff --git a/assets/PhotoCup1_crop.jpg b/assets/PhotoCup1_crop.jpg new file mode 100644 index 0000000000000000000000000000000000000000..aebffdb080a1d578c8e70ae1804d6a3179c2ffe2 --- /dev/null +++ b/assets/PhotoCup1_crop.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1dc467287b69ab4e1b6e60dc674a81d9c1b8499bc684aaaa54eab6f89a8b603 +size 258435 diff --git a/assets/PhotoPortrait1.jpg b/assets/PhotoPortrait1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7d1f519e55eac5e93713fe2ff191a9133bf9d986 --- /dev/null +++ b/assets/PhotoPortrait1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04f60ab63d7ab6c392f02c30343ff78214dc950540490f6ec0b08d012e3b59d0 +size 316924 diff --git a/assets/PhotoPortrait1_crop1.jpg b/assets/PhotoPortrait1_crop1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..078de6bc5a3d7b54bf37ecb0ad091d626c0bf6fa --- /dev/null +++ b/assets/PhotoPortrait1_crop1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18afb84e894c236ed416a74c3fdd26c2203783c3f24abcc0453b1808300b35d8 +size 270811 diff --git a/assets/PhotoPortrait1_crop2.jpg b/assets/PhotoPortrait1_crop2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6d783544e574a449809e0a3a6ef334a2cf37f479 --- /dev/null +++ b/assets/PhotoPortrait1_crop2.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:620685c05fc9114ae42c03a5e89343e175fba0123976c8937d2b47d91b7d3851 +size 273939 diff --git a/assets/PhotoPortrait1_crop3.jpg b/assets/PhotoPortrait1_crop3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6033461fcb499591eb0339812a6f4a24807a04c4 --- /dev/null +++ b/assets/PhotoPortrait1_crop3.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5aa9059493a089d05165502dad20a9304b422b333541f8a2f37105f6e9042721 +size 233657 diff --git a/depth-anything/place depth model here b/depth-anything/place depth model here new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/docs/EasyNodes.md b/docs/EasyNodes.md new file mode 100644 index 0000000000000000000000000000000000000000..750114603d3b10b1a27e4f3d527f674db3048447 --- /dev/null +++ b/docs/EasyNodes.md @@ -0,0 +1,54 @@ +# Easy Nodes and MG_SuperSimple + +MagicNodes provides simplified “Easy” variants that expose only high‑value controls while relying on preset files for the rest. These are grouped under the UI category `MagicNodes/Easy`. + +- Presets live in `pressets/mg_cade25.cfg` and `pressets/mg_controlfusion.cfg` with INI‑like sections `Step 1..4` and simple `key: value` pairs. The token `$(ROOT)` is supported in paths and is substituted at load time. +- Loader: `mod/easy/preset_loader.py` caches by mtime and does light type parsing. +- The Step+Custom scheme keeps UI and presets in sync: choose a Step to load defaults, then optionally toggle Custom to override only the visible controls, leaving hidden parameters from the Step preset intact. + +## MG_SuperSimple (Easy) + +Single node that reproduces the 2/3/4‑step CADE+ControlFusion pipeline with minimal surface. + +Category: `MagicNodes/Easy` + +Inputs +- `model` (MODEL) +- `positive` (CONDITIONING), `negative` (CONDITIONING) +- `vae` (VAE) +- `latent` (LATENT) +- `control_net` (CONTROL_NET) — required by ControlFusion +- `reference_image` (IMAGE, optional) — forwarded to CADE +- `clip_vision` (CLIP_VISION, optional) — forwarded to CADE + +Controls +- `step_count` int (1..4): how many steps to run +- `custom` toggle: when On, the visible CADE controls below override the Step presets across all steps; when Off, all CADE values come from presets +- `seed` int with `control_after_generate` +- `steps` int (default 25) — applies to steps 2..4 +- `cfg` float (default 4.5) +- `denoise` float (default 0.65, clamped 0.45..0.9) — applies to steps 2..4 +- `sampler_name` (default `ddim`) +- `scheduler` (default `MGHybrid`) +- `clipseg_text` string (default `hand, feet, face`) + +Behavior +- Step 1 runs CADE with `Step 1` preset and forces `denoise=1.0` (single exception to the override rule). All other visible fields follow the Step+Custom logic described above. +- For steps 2..N: ControlFusion (with `Step N` preset) updates `positive/negative` based on the current image, then CADE (with `Step N` preset) refines the latent/image. +- Initial `positive/negative` come from the node inputs; subsequent steps use the latest CF outputs. `latent` is always taken from the previous CADE. +- When `custom` is Off, UI values are ignored entirely; presets define all CADE parameters. +- ControlFusion inside this node always relies on presets (no additional CF UI here) to keep the surface minimal. + +Outputs +- `(LATENT, IMAGE)` from the final executed step (e.g., step 2 if `step_count=2`). No preview outputs. + +Quickstart +1) Drop `MG_SuperSimple` into your graph under `MagicNodes/Easy`. +2) Connect `model/positive/negative/vae/latent`, and a `control_net` module; optionally connect `reference_image` and `clip_vision`. +3) Choose `step_count` (2/3/4). Leave `custom` Off to use pure presets, or enable it to apply your `seed/steps/cfg/denoise/sampler/scheduler/clipseg_text` across all steps (with Step 1 `denoise=1.0`). +4) Run. The node returns the final `(LATENT, IMAGE)` for the chosen depth. + +Notes +- Presets are read from `pressets/mg_cade25.cfg` and `pressets/mg_controlfusion.cfg`. Keep them in UTF‑8 and prefer `$(ROOT)` over absolute paths. +- `seed` is shared across all steps for determinism; if per‑step offsets are desired later, this can be added as an option without breaking current behavior. + diff --git a/docs/HardNodes.md b/docs/HardNodes.md new file mode 100644 index 0000000000000000000000000000000000000000..bf1fe4a04cb3368207acb54cc997d949dafda9a2 --- /dev/null +++ b/docs/HardNodes.md @@ -0,0 +1,11 @@ +# Hard Nodes (Overview) + +This folder documents the advanced (Hard) variants in MagicNodes. These nodes expose the full surface of controls and are intended for expert tuning and experimentation. Easy variants cover most use‑cases with presets; Hard variants reveal the rest. + +Available docs +- CADE 2.5: see `docs/hard/CADE25.md` +- ControlFusion: see `docs/hard/ControlFusion.md` +- Upscale Module: see `docs/hard/UpscaleModule.md` +- Intelligent Detail Stabilizer (IDS): see `docs/hard/IDS.md` +- ZeSmart Sampler: see `docs/hard/ZeSmartSampler.md` + diff --git a/docs/hard/CADE25.md b/docs/hard/CADE25.md new file mode 100644 index 0000000000000000000000000000000000000000..77aa3299f6eda77f4cb4098e420868a24d2e8f34 --- /dev/null +++ b/docs/hard/CADE25.md @@ -0,0 +1,72 @@ +# CADE 2.5 (ComfyAdaptiveDetailEnhancer25) + +CADE 2.5 is a refined adaptive enhancer with a single clean iteration loop, optional reference‑driven polishing, and flexible sampler scheduling. It can run standalone or as part of multi‑step pipelines (e.g., with ControlFusion masks in between passes). + +This document describes the Hard variant — the full‑surface node that exposes advanced controls. For a minimal, preset‑driven experience, use the Easy variant or the `MG_SuperSimple` orchestrator. + +## Overview +- Iterative latent refinement with configurable steps/CFG/denoise +- Optional guidance override (Rescale/CFGZero‑style, FDG/NAG ideas, epsilon scaling) +- Hybrid schedule path (`MGHybrid`) that builds ZeSmart‑style sigma stacks +- Local spatial guidance via CLIPSeg prompts +- Reference polishing with CLIP‑Vision (preserves low‑frequency structure) +- Optional upscaling mid‑run, detail stabilization, and gentle sharpening +- Determinism helpers: CLIPSeg pinned to CPU, mask state cleared per run + +## Inputs +- `model` (MODEL) +- `positive` (CONDITIONING), `negative` (CONDITIONING) +- `vae` (VAE) +- `latent` (LATENT) +- `reference_image` (IMAGE, optional) +- `clip_vision` (CLIP_VISION, optional) + +## Outputs +- `LATENT`: refined latent +- `IMAGE`: decoded image after the last internal iteration +- `mask_preview` (IMAGE): last fused mask preview (RGB 0..1) +- Internal values like effective `steps/cfg/denoise` are tracked across the loop (the Easy wrapper surfaces them if needed). + +## Core Controls (essentials) +- `seed` (with control_after_generate) +- `steps`, `cfg`, `denoise` +- `sampler_name` (e.g., `ddim`) +- `scheduler` (`MGHybrid` recommended for smooth tails) + +Typical starting points +- General: steps≈25, cfg≈7.0, denoise≈0.7, sampler=`euler_ancestral`, scheduler=`MGHybrid` +- As the first pass of a multi‑step pipeline: denoise=1.0 (full rewrite pass) + +## MGHybrid schedule +When `scheduler = MGHybrid`, CADE builds a hybrid sigma schedule compatible with the internal KSampler path. It follows ZeSmart principles (hybrid mix and smooth tail), then calls a custom sampler entry — falling back to `nodes.common_ksampler` if anything goes wrong. The behavior remains deterministic under fixed `seed/steps/cfg/denoise`. + +## Local guidance (CLIPSeg) +- CLIPSeg prompts (comma‑separated) produce a soft mask that can attenuate denoise/CFG. +- CLIPSeg inference is pinned to CPU by default for reproducibility. + +## Reference polish (CLIP‑Vision) +Provide `reference_image` and `clip_vision` to preserve global form while refining details. CADE encodes the current and reference images and reduces denoise/CFG when they diverge; in polish mode it also mixes low frequencies from the reference using a blur‑based split. + +## Advanced features (high‑level) +- Guidance override wrapper (rescale curves, momentum, perpendicular dampers) +- FDG/ZeRes‑inspired options with adaptive thresholds +- Mid‑run upscale support via `MagicUpscaleModule` with post‑adjusted CFG/denoise +- Post passes: `IntelligentDetailStabilizer`, optional mild sharpen + +## Related +- QSilk (micrograin stabilizer + AQClip): a lightweight latent‑space regularizer that suppresses rare activation tails while preserving micro‑texture. Works plug‑and‑play inside CADE 2.5 and synergizes with ZeResFDG by allowing slightly higher effective CFG without speckle. See preprint draft in `Arxiv_QSilk/` (source: [Arxiv_QSilk/main_qsilk.tex](../../Arxiv_QSilk/main_qsilk.tex)). Replace with arXiv link when available. + +## Tips +- Keep `vae` consistent across passes; CADE re‑encodes when scale changes. +- For multi‑step flows (e.g., with ControlFusion), feed the current decoded `IMAGE` into CF, update `positive/negative`, then run CADE again with the latest `LATENT`. +- If you rely on presets, consider the Easy wrapper or `MG_SuperSimple` to avoid UI/preset drift. + +## Quickstart (Hard) +1) Connect `MODEL / VAE / CONDITIONING / LATENT`. +2) Set `seed`, `steps≈25`, `cfg≈7.0`, `denoise≈0.7`, `sampler=euler_ancestral`, `scheduler=MGHybrid`. +3) (Optional) Add `reference_image` and `clip_vision`, and a CLIPSeg prompt. +4) Run and fine‑tune denoise/CFG first; only then adjust sampler/schedule. + +Notes +- The node clears internal masks and patches at the end of a run even on errors. +- Some experimental toggles are intentionally conservative in default configs to avoid destabilizing results. diff --git a/docs/hard/ControlFusion.md b/docs/hard/ControlFusion.md new file mode 100644 index 0000000000000000000000000000000000000000..18abce3d558d94b815dc268195779fae6716be91 --- /dev/null +++ b/docs/hard/ControlFusion.md @@ -0,0 +1,70 @@ +# ControlFusion (Hard) + +Builds a fused control mask from Depth and Pyramid Canny Edges, then injects it into ControlNet for both positive and negative conditionings. Designed to be resolution‑aware (keeps aspect), with optional split application (Depth then Edges) and a rich set of edge post‑processing knobs. + +For minimal usage, see the Easy wrapper documented in `docs/EasyNodes.md`. + +## Overview +- Depth: Depth Anything v2 if available (vendored/local/aux fallbacks), otherwise pseudo‑depth from luminance + blur. +- Edges: multi‑scale Pyramid Canny with optional thinning, width adjust, smoothing, single‑line collapse, and depth‑based gating. +- Blending: `normal` (weighted mix), `max`, or `edge_over_depth` prior to ControlNet. +- Application: single fused hint or `split_apply` (Depth first, then Edges) with independent strengths and schedules. +- Preview: aspect‑kept visualization with optional strength reflection (display‑only). + +## Inputs +- `image` (IMAGE, BHWC 0..1) +- `positive` (CONDITIONING), `negative` (CONDITIONING) +- `control_net` (CONTROL_NET) +- `vae` (VAE) + +## Outputs +- `positive` (CONDITIONING), `negative` (CONDITIONING) — updated with ControlNet hint +- `Mask_Preview` (IMAGE) — fused mask preview (RGB 0..1) + +## Core Controls +Depth +- `enable_depth` (bool) +- `depth_model_path` (pth for Depth Anything v2) +- `depth_resolution` (min‑side target; hires mode keeps aspect) + +Edges (PyraCanny) +- `enable_pyra` (bool), `pyra_low`, `pyra_high`, `pyra_resolution` +- `edge_thin_iter` (thinning passes, auto‑tuned in smart mode) +- `edge_alpha` (pre‑blend opacity), `edge_boost` (micro‑contrast), `smart_tune`, `smart_boost` + +Blend and Strength +- `blend_mode`: `normal` | `max` | `edge_over_depth` +- `blend_factor` (for `normal`) +- `strength_pos`, `strength_neg` (global) +- `start_percent`, `end_percent` (schedule window 0..1) + +Preview and Quality +- `preview_res` (min‑side), `mask_brightness` +- `preview_show_strength` with `preview_strength_branch` = `positive` | `negative` | `max` | `avg` +- `hires_mask_auto` (keep aspect and higher caps) + +Application Options +- `apply_to_uncond` (mirror ControlNet hint to uncond) +- `stack_prev_control` (stack with previous ControlNet in the cond dict) +- `split_apply` (Depth first, Edges second) +- Separate schedules and multipliers when split: + - Depth: `depth_start_percent`, `depth_end_percent`, `depth_strength_mul` + - Edges: `edge_start_percent`, `edge_end_percent`, `edge_strength_mul` + +Extra Edge Controls +- `edge_width` (thin/thicken), `edge_smooth` (reduce pixelation) +- `edge_single_line`, `edge_single_strength` (collapse double outlines) +- `edge_depth_gate`, `edge_depth_gamma` (weigh edges by depth) + +## Behavior Notes +- Depth min‑side is capped (default 1024) and aspect is preserved to avoid distortions. +- In `split_apply`, the order is deterministic: Depth → Edges. +- Preview image reflects strength only if `preview_show_strength` is enabled; it does not affect the hint itself. +- When both Depth and Edges are disabled, the node passes inputs through and returns a zero preview. + +## Quickstart +1) Connect `image/positive/negative/control_net/vae`. +2) Enable Depth and/or PyraCanny. Start with `edge_alpha≈1.0`, `blend_mode=normal`, `blend_factor≈0.02`. +3) Schedule the apply window (`start_percent/end_percent`) and tune `strength_pos/neg`. +4) Use `split_apply` if you want Depth to anchor structure and Edges to refine contours separately. + diff --git a/docs/hard/IDS.md b/docs/hard/IDS.md new file mode 100644 index 0000000000000000000000000000000000000000..86c802e66eeb1a4f0a72577bf59a9a1207d452d3 --- /dev/null +++ b/docs/hard/IDS.md @@ -0,0 +1,20 @@ +# IntelligentDetailStabilizer (IDS) + +Gentle, fast post‑pass for stabilizing micro‑detail and suppressing noise while preserving sharpness. + +## Overview +- Two‑stage blur/sharpen split with strength‑controlled recombination. +- Uses SciPy Gaussian if available; otherwise a portable PyTorch separable blur. +- Operates on images (BHWC, 0..1) and returns a single stabilized `IMAGE`. + +## Inputs +- `image` (IMAGE) +- `ids_strength` (float, default 0.5, range −1.0..1.0) + +## Outputs +- `IMAGE` — stabilized image + +## Tips +- Start around `ids_strength≈0.5` for gentle cleanup. +- Negative values bias toward more smoothing; positive increases sharpening of denoised base. + diff --git a/docs/hard/UpscaleModule.md b/docs/hard/UpscaleModule.md new file mode 100644 index 0000000000000000000000000000000000000000..7235508b544194eee3bddb306d59c1e4440eef82 --- /dev/null +++ b/docs/hard/UpscaleModule.md @@ -0,0 +1,23 @@ +# MagicUpscaleModule + +Lightweight latent‑space upscaler that keeps shapes aligned to the VAE stride to avoid border artifacts. + +## Overview +- Decodes latent to image, resamples with selected filter, and re‑encodes. +- Aligns target size up to the VAE spatial compression stride to keep shapes consistent. +- Clears GPU/RAM caches to minimize fragmentation before heavy resizes. + +## Inputs +- `samples` (LATENT) +- `vae` (VAE) +- `upscale_method` in `nearest-exact | bilinear | area | bicubic | lanczos` +- `scale_by` (float) + +## Outputs +- `LATENT` — upscaled latent +- `Upscaled Image` — convenience decoded image + +## Tips +- Use modest `scale_by` first (e.g., 1.2–1.5) and chain passes if needed. +- Keep the same `vae` before and after upscale in a larger pipeline. + diff --git a/docs/hard/ZeSmartSampler.md b/docs/hard/ZeSmartSampler.md new file mode 100644 index 0000000000000000000000000000000000000000..9c69aab0db62fb3eb11f3a8c2be703045637f939 --- /dev/null +++ b/docs/hard/ZeSmartSampler.md @@ -0,0 +1,22 @@ +# MG_ZeSmartSampler (v1.1) + +Custom sampler that builds hybrid sigma schedules (Karras/Beta blend), adds tiny schedule jitter, and optionally applies a PC2‑like predictor‑corrector shaping. + +## Overview +- Inputs/Outputs match a standard KSampler: `MODEL / SEED / STEPS / CFG / base_sampler / schedule / CONDITIONING / LATENT` → `LATENT`. +- `hybrid_mix` blends the tail toward Beta; `tail_smooth` softens tail jumps adaptively. +- `jitter_sigma` introduces a tiny monotonic noise to schedules for de‑ringing; remains deterministic with fixed seed. +- PC2‑style shaping is available via `smart_strength/target_error/curv_sensitivity` (kept conservative by default). + +## Controls (high‑level) +- `base_sampler` and `schedule` (karras/beta/hybrid) +- `hybrid_mix` ∈ [0..1] +- `jitter_sigma` ∈ [0..0.1] +- `tail_smooth` ∈ [0..1] +- `smart_strength`, `target_error`, `curv_sensitivity` + +## Tips +- Start hybrid at `hybrid_mix≈0.3` for 2D work; 0.5–0.7 for photo‑like. +- Keep `jitter_sigma` very small (≈0.005–0.01) to avoid destabilizing steps. +- If using inside CADE (`scheduler=MGHybrid`), CADE will construct the schedule and run the custom path automatically. + diff --git a/init b/init deleted file mode 100644 index 755058448d86569c36023bbe8374e55bc8de6a1f..0000000000000000000000000000000000000000 --- a/init +++ /dev/null @@ -1 +0,0 @@ -Init: MagicNodes (CADE 2.5, QSilk) diff --git a/mod/__init__.py b/mod/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e2d40292590c72e38d4a78d56fc4d3cc89880f37 --- /dev/null +++ b/mod/__init__.py @@ -0,0 +1,8 @@ +"""MagicNodes.mod package + +Holds the primary node implementations after repo cleanup. Keeping this as a +package ensures stable relative imports from the project root. +""" + +# No runtime side effects; modules are imported from MagicNodes.__init__. + diff --git a/mod/easy/__init__.py b/mod/easy/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ae39c35cdd67a5e3556873cf877d76034a5a5c9e --- /dev/null +++ b/mod/easy/__init__.py @@ -0,0 +1,8 @@ +"""MagicNodes Easy variants + +Holds simplified, user‑friendly node variants that expose only +high‑level parameters. Registered under category "MagicNodes/Easy". +""" + +# Modules are imported from MagicNodes.__init__ to control registration. + diff --git a/mod/easy/mg_cade25_easy.py b/mod/easy/mg_cade25_easy.py new file mode 100644 index 0000000000000000000000000000000000000000..6c7899df7c8efa373393227252e384eccb8e8f38 --- /dev/null +++ b/mod/easy/mg_cade25_easy.py @@ -0,0 +1,2968 @@ +"""CADE 2.5: refined adaptive enhancer with reference clean and accumulation override. +""" + +from __future__ import annotations # moved/renamed module: mg_cade25 + +import torch +import os +import numpy as np +import torch.nn.functional as F + +import nodes +import comfy.model_management as model_management + +from ..hard.mg_adaptive import AdaptiveSamplerHelper +from ..hard.mg_zesmart_sampler_v1_1 import _build_hybrid_sigmas +import comfy.sample as _sample +import comfy.samplers as _samplers +import comfy.utils as _utils +from ..hard.mg_upscale_module import MagicUpscaleModule, clear_gpu_and_ram_cache +from ..hard.mg_controlfusion import _build_depth_map as _cf_build_depth_map +from ..hard.mg_ids import IntelligentDetailStabilizer +from .. import mg_sagpu_attention as sa_patch +from .preset_loader import get as load_preset +from ..hard.mg_controlfusion import _pyracanny as _cf_pyracanny, _build_depth_map as _cf_build_depth +# FDG/NAG experimental paths removed for now; keeping code lean + +_ONNX_RT = None +_ONNX_SESS = {} # name -> onnxruntime.InferenceSession +_ONNX_WARNED = False +_ONNX_DEBUG = False +_ONNX_FORCE_CPU = True # pin ONNX to CPU for deterministic behavior +_ONNX_COUNT_DEBUG = True # print detected counts (faces/hands/persons) when True (temporarily forced ON) + +# Lazy CLIPSeg cache +_CLIPSEG_MODEL = None +_CLIPSEG_PROC = None +_CLIPSEG_DEV = "cpu" +_CLIPSEG_FORCE_CPU = True # pin CLIPSeg to CPU to avoid device drift + +# ONNX keypoints (wholebody/pose) parsing toggles (set by UI at runtime) +_ONNX_KPTS_ENABLE = False +_ONNX_KPTS_SIGMA = 2.5 +_ONNX_KPTS_GAIN = 1.5 +_ONNX_KPTS_CONF = 0.20 + +# Per-iteration spatial guidance mask (B,1,H,W) in [0,1]; used by cfg_func when enabled +CURRENT_ONNX_MASK_BCHW = None + + +# --- AQClip-Lite: adaptive soft quantile clipping in latent space (tile overlap) --- +@torch.no_grad() +def _aqclip_lite(latent_bchw: torch.Tensor, + tile: int = 32, + stride: int = 16, + alpha: float = 2.0, + ema_state: dict | None = None, + ema_beta: float = 0.8) -> tuple[torch.Tensor, dict]: + try: + z = latent_bchw + B, C, H, W = z.shape + dev, dt = z.device, z.dtype + ksize = max(8, min(int(tile), min(H, W))) + kstride = max(1, min(int(stride), ksize)) + + # Confidence proxy: gradient magnitude on channel-mean latent + zm = z.mean(dim=1, keepdim=True) + kx = torch.tensor([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], device=dev, dtype=dt).view(1, 1, 3, 3) + ky = torch.tensor([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], device=dev, dtype=dt).view(1, 1, 3, 3) + gx = F.conv2d(zm, kx, padding=1) + gy = F.conv2d(zm, ky, padding=1) + gmag = torch.sqrt(gx * gx + gy * gy) + gpool = F.avg_pool2d(gmag, kernel_size=ksize, stride=kstride) + gmax = gpool.amax(dim=(2, 3), keepdim=True).clamp_min(1e-6) + Hn = (gpool / gmax).squeeze(1) # B,h',w' + L = Hn.shape[1] * Hn.shape[2] + Hn = Hn.reshape(B, L) + + # Map confidence -> quantiles + ql = 0.5 * (Hn ** 2) + qh = 1.0 - 0.5 * ((1.0 - Hn) ** 2) + + # Per-tile mean/std + unf = F.unfold(z, kernel_size=ksize, stride=kstride) # B, C*ksize*ksize, L + M = unf.shape[1] + mu = unf.mean(dim=1).to(torch.float32) # B,L + var = (unf.to(torch.float32) - mu.unsqueeze(1)).pow(2).mean(dim=1) + sigma = (var + 1e-12).sqrt() + + # Normal inverse approximation: ndtri(q) = sqrt(2)*erfinv(2q-1) + def _ndtri(q: torch.Tensor) -> torch.Tensor: + return (2.0 ** 0.5) * torch.special.erfinv(q.mul(2.0).sub(1.0).clamp(-0.999999, 0.999999)) + k_neg = _ndtri(ql).abs() + k_pos = _ndtri(qh).abs() + lo = mu - k_neg * sigma + hi = mu + k_pos * sigma + + # EMA smooth + if ema_state is None: + ema_state = {} + b = float(max(0.0, min(0.999, ema_beta))) + if 'lo' in ema_state and 'hi' in ema_state and ema_state['lo'].shape == lo.shape: + lo = b * ema_state['lo'] + (1.0 - b) * lo + hi = b * ema_state['hi'] + (1.0 - b) * hi + ema_state['lo'] = lo.detach() + ema_state['hi'] = hi.detach() + + # Soft tanh clip (vectorized in unfold domain) + mid = (lo + hi) * 0.5 + half = (hi - lo) * 0.5 + half = half.clamp_min(1e-6) + y = (unf.to(torch.float32) - mid.unsqueeze(1)) / half.unsqueeze(1) + y = torch.tanh(float(alpha) * y) + unf_clipped = mid.unsqueeze(1) + half.unsqueeze(1) * y + unf_clipped = unf_clipped.to(dt) + + out = F.fold(unf_clipped, output_size=(H, W), kernel_size=ksize, stride=kstride) + ones = torch.ones((B, M, L), device=dev, dtype=dt) + w = F.fold(ones, output_size=(H, W), kernel_size=ksize, stride=kstride).clamp_min(1e-6) + out = out / w + return out, ema_state + except Exception: + return latent_bchw, (ema_state or {}) + + +def _try_init_onnx(models_dir: str): + """Initialize onnxruntime and load all .onnx models in models_dir. + We prefer GPU providers when available, but gracefully fall back to CPU. + """ + global _ONNX_RT, _ONNX_SESS, _ONNX_WARNED + import os + if _ONNX_RT is None: + try: + import onnxruntime as ort + _ONNX_RT = ort + except Exception: + if not _ONNX_WARNED: + print("[CADE2.5][ONNX] onnxruntime not available, skipping ONNX detectors.") + _ONNX_WARNED = True + return False + + # Build provider preference list + try: + avail = set(_ONNX_RT.get_available_providers()) + except Exception: + avail = set() + pref = [] + if _ONNX_FORCE_CPU: + pref = ["CPUExecutionProvider"] + else: + for p in ("CUDAExecutionProvider", "DmlExecutionProvider", "CPUExecutionProvider"): + if p in avail or p == "CPUExecutionProvider": + pref.append(p) + if _ONNX_DEBUG: + try: + print(f"[CADE2.5][ONNX] Available providers: {sorted(list(avail))}") + print(f"[CADE2.5][ONNX] Provider preference: {pref}") + except Exception: + pass + + # Load any .onnx in models_dir + try: + for fname in os.listdir(models_dir): + if not fname.lower().endswith('.onnx'): + continue + if fname in _ONNX_SESS: + continue + full = os.path.join(models_dir, fname) + try: + _ONNX_SESS[fname] = _ONNX_RT.InferenceSession(full, providers=pref) + if _ONNX_DEBUG: + try: + print(f"[CADE2.5][ONNX] Loaded model: {fname}") + except Exception: + pass + except Exception as e: + if not _ONNX_WARNED: + print(f"[CADE2.5][ONNX] failed to load {fname}: {e}") + except Exception as e: + if not _ONNX_WARNED: + print(f"[CADE2.5][ONNX] cannot list models in {models_dir}: {e}") + if not _ONNX_SESS and not _ONNX_WARNED: + print("[CADE2.5][ONNX] No ONNX models found in", models_dir) + _ONNX_WARNED = True + return len(_ONNX_SESS) > 0 + + +def _try_init_clipseg(): + """Lazy-load CLIPSeg processor + model and choose device. + Returns True on success. + """ + global _CLIPSEG_MODEL, _CLIPSEG_PROC, _CLIPSEG_DEV + if (_CLIPSEG_MODEL is not None) and (_CLIPSEG_PROC is not None): + return True + try: + from transformers import CLIPSegProcessor, CLIPSegForImageSegmentation # type: ignore + except Exception: + if not globals().get("_CLIPSEG_WARNED", False): + print("[CADE2.5][CLIPSeg] transformers not available; CLIPSeg disabled.") + globals()["_CLIPSEG_WARNED"] = True + return False + try: + _CLIPSEG_PROC = CLIPSegProcessor.from_pretrained("CIDAS/clipseg-rd64-refined") + _CLIPSEG_MODEL = CLIPSegForImageSegmentation.from_pretrained("CIDAS/clipseg-rd64-refined") + if _CLIPSEG_FORCE_CPU: + _CLIPSEG_DEV = "cpu" + else: + _CLIPSEG_DEV = "cuda" if torch.cuda.is_available() else "cpu" + _CLIPSEG_MODEL = _CLIPSEG_MODEL.to(_CLIPSEG_DEV) + _CLIPSEG_MODEL.eval() + return True + except Exception as e: + print(f"[CADE2.5][CLIPSeg] failed to load model: {e}") + return False + + +def _clipseg_build_mask(image_bhwc: torch.Tensor, + text: str, + preview: int = 224, + threshold: float = 0.4, + blur: float = 7.0, + dilate: int = 4, + gain: float = 1.0, + ref_embed: torch.Tensor | None = None, + clip_vision=None, + ref_threshold: float = 0.03) -> torch.Tensor | None: + """Return BHWC single-channel mask [0,1] from CLIPSeg. + - Uses cached CLIPSeg model; gracefully returns None on failure. + - Applies optional threshold/blur/dilate and scaling gain. + - If clip_vision + ref_embed provided, gates mask by CLIP-Vision distance. + """ + if not text or not isinstance(text, str): + return None + if not _try_init_clipseg(): + return None + try: + # Prepare preview image (CPU PIL) + target = int(max(16, min(1024, preview))) + img = image_bhwc.detach().to('cpu') + B, H, W, C = img.shape + x = img[0].movedim(-1, 0).unsqueeze(0) # 1,C,H,W + x = F.interpolate(x, size=(target, target), mode='bilinear', align_corners=False) + x = x.clamp(0, 1) + arr = (x[0].movedim(0, -1).numpy() * 255.0).astype('uint8') + from PIL import Image # lazy import + pil_img = Image.fromarray(arr) + + # Run CLIPSeg + import re + prompts = [t.strip() for t in re.split(r"[\|,;\n]+", text) if t.strip()] + if not prompts: + prompts = [text.strip()] + prompts = prompts[:8] + inputs = _CLIPSEG_PROC(text=prompts, images=[pil_img] * len(prompts), return_tensors="pt") + inputs = {k: v.to(_CLIPSEG_DEV) for k, v in inputs.items()} + with torch.inference_mode(): + outputs = _CLIPSEG_MODEL(**inputs) # type: ignore + # logits: [N, H', W'] for N prompts + logits = outputs.logits # [N,h,w] + if logits.ndim == 2: + logits = logits.unsqueeze(0) + prob = torch.sigmoid(logits) # [N,h,w] + # Soft-OR fuse across prompts + prob = 1.0 - torch.prod(1.0 - prob.clamp(0, 1), dim=0, keepdim=True) # [1,h,w] + prob = prob.unsqueeze(1) # [1,1,h,w] + # Resize to original image size + prob = F.interpolate(prob, size=(H, W), mode='bilinear', align_corners=False) + m = prob[0, 0].to(dtype=image_bhwc.dtype, device=image_bhwc.device) + # Threshold + blur (approx) + if threshold > 0.0: + m = torch.where(m > float(threshold), m, torch.zeros_like(m)) + # Gaussian blur via our depthwise helper + if blur > 0.0: + rad = int(max(1, min(7, round(blur)))) + m = _gaussian_blur_nchw(m.unsqueeze(0).unsqueeze(0), sigma=float(max(0.5, blur)), radius=rad)[0, 0] + # Dilation via max-pool + if int(dilate) > 0: + k = int(dilate) * 2 + 1 + p = int(dilate) + m = F.max_pool2d(m.unsqueeze(0).unsqueeze(0), kernel_size=k, stride=1, padding=p)[0, 0] + # Optional CLIP-Vision gating by reference distance + if (clip_vision is not None) and (ref_embed is not None): + try: + cur = _encode_clip_image(image_bhwc, clip_vision, target_res=224) + dist = _clip_cosine_distance(cur, ref_embed) + if dist > float(ref_threshold): + # up to +50% gain if сильно уехали + gate = 1.0 + min(0.5, (dist - float(ref_threshold)) * 4.0) + m = m * gate + except Exception: + pass + m = (m * float(max(0.0, gain))).clamp(0, 1) + return m.unsqueeze(0).unsqueeze(-1) # BHWC with B=1,C=1 + except Exception as e: + if not globals().get("_CLIPSEG_WARNED", False): + print(f"[CADE2.5][CLIPSeg] mask failed: {e}") + globals()["_CLIPSEG_WARNED"] = True + return None + + +def _np_to_mask_tensor(np_map: np.ndarray, out_h: int, out_w: int, device, dtype): + """Convert numpy heatmap [H,W] or [1,H,W] or [H,W,1] to BHWC torch mask with B=1 and resize to out_h,out_w.""" + if np_map.ndim == 3: + np_map = np_map.reshape(np_map.shape[-2], np_map.shape[-1]) if (np_map.shape[0] == 1) else np_map.squeeze() + if np_map.ndim != 2: + return None + t = torch.from_numpy(np_map.astype(np.float32)) + t = t.clamp_min(0.0) + t = t.unsqueeze(0).unsqueeze(0) # B=1,C=1,H,W + t = F.interpolate(t, size=(out_h, out_w), mode="bilinear", align_corners=False) + t = t.permute(0, 2, 3, 1).to(device=device, dtype=dtype) # B,H,W,C + return t.clamp(0, 1) + + +# --- Firefly/Hot-pixel remover (image space, BHWC in 0..1) --- +def _median_pool3x3_bhwc(img_bhwc: torch.Tensor) -> torch.Tensor: + B, H, W, C = img_bhwc.shape + x = img_bhwc.permute(0, 3, 1, 2) # B,C,H,W + unfold = F.unfold(x, kernel_size=3, padding=1) # B, 9*C, H*W + unfold = unfold.view(B, x.shape[1], 9, H, W) # B,C,9,H,W + med, _ = torch.median(unfold, dim=2) # B,C,H,W + return med.permute(0, 2, 3, 1) # B,H,W,C + + +def _despeckle_fireflies(img_bhwc: torch.Tensor, + thr: float = 0.985, + max_iso: float | None = None, + grad_gate: float = 0.25) -> torch.Tensor: + try: + dev, dt = img_bhwc.device, img_bhwc.dtype + B, H, W, C = img_bhwc.shape + # Scale-aware window + s = max(H, W) / 1024.0 + k = 3 if s <= 1.1 else (5 if s <= 2.0 else 7) + pad = k // 2 + # Value/Saturation from RGB (fast, no colorspace conv required) + R, G, Bc = img_bhwc[..., 0], img_bhwc[..., 1], img_bhwc[..., 2] + V = torch.maximum(R, torch.maximum(G, Bc)) + m = torch.minimum(R, torch.minimum(G, Bc)) + S = 1.0 - (m / (V + 1e-6)) + # Dynamic bright threshold from top tail; allow manual override for very high thr + try: + q = float(torch.quantile(V.reshape(-1), 0.9995).item()) + thr_eff = float(thr) if float(thr) >= 0.99 else max(float(thr), min(0.997, q)) + except Exception: + thr_eff = float(thr) + v_thr = max(0.985, thr_eff) + s_thr = 0.06 + cand = (V > v_thr) & (S < s_thr) + # gradient gate to protect real edges/highlights + lum = (0.2126 * R + 0.7152 * G + 0.0722 * Bc) + kx = torch.tensor([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], device=dev, dtype=dt).view(1, 1, 3, 3) + ky = torch.tensor([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], device=dev, dtype=dt).view(1, 1, 3, 3) + gx = F.conv2d(lum.unsqueeze(1), kx, padding=1) + gy = F.conv2d(lum.unsqueeze(1), ky, padding=1) + grad = torch.sqrt(gx * gx + gy * gy).squeeze(1) + safe_gate = float(grad_gate) * (k / 3.0) ** 0.5 + cand = cand & (grad < safe_gate) + if not cand.any(): + return img_bhwc + # Prefer connected components (OpenCV) to drop small bright specks + try: + import cv2 + masks = [] + for b in range(cand.shape[0]): + msk = cand[b].detach().to('cpu').numpy().astype('uint8') * 255 + num, labels, stats, _ = cv2.connectedComponentsWithStats(msk, connectivity=8) + rem = np.zeros_like(msk, dtype='uint8') + # Size threshold grows with k + area_max = int(max(3, round((k * k) * 0.8))) + for lbl in range(1, num): + area = stats[lbl, cv2.CC_STAT_AREA] + if area <= area_max: + rem[labels == lbl] = 255 + masks.append(torch.from_numpy(rem > 0)) + rm = torch.stack(masks, dim=0).to(device=dev) # B,H,W (bool) + rm = rm.unsqueeze(-1) # B,H,W,1 + if not rm.any(): + return img_bhwc + med = _median_pool3x3_bhwc(img_bhwc) + return torch.where(rm, med, img_bhwc) + except Exception: + # Fallback: isolation via local density + dens = F.avg_pool2d(cand.float().unsqueeze(1), k, 1, pad).squeeze(1) + max_iso_eff = (2.0 / (k * k)) if (max_iso is None) else float(max_iso) + iso = cand & (dens < max_iso_eff) + if not iso.any(): + return img_bhwc + med = _median_pool3x3_bhwc(img_bhwc) + return torch.where(iso.unsqueeze(-1), med, img_bhwc) + except Exception: + return img_bhwc + + +def _try_heatmap_from_outputs(outputs: list, preview_hw: tuple[int, int]): + """Return [H,W] heatmap from model outputs if possible. + Supports: + - Segmentation logits/probabilities (NCHW / NHWC) + - Keypoints arrays -> gaussian disks on points + - Bounding boxes -> soft rectangles + """ + if not outputs: + return None + + Ht, Wt = int(preview_hw[0]), int(preview_hw[1]) + + def to_float(arr): + if arr.dtype not in (np.float32, np.float64): + try: + arr = arr.astype(np.float32) + except Exception: + return None + return arr + + def sigmoid(x): + return 1.0 / (1.0 + np.exp(-x)) + + # 1) Prefer any spatial heatmap first + for out in outputs: + try: + arr = np.asarray(out) + except Exception: + continue + arr = to_float(arr) + if arr is None: + continue + if arr.ndim == 4: + n, a, b, c = arr.shape + if c <= 4 and a >= 8 and b >= 8: + if c == 1: + hm = sigmoid(arr[0, :, :, 0]) if np.max(np.abs(arr)) > 1.5 else arr[0, :, :, 0] + else: + ex = np.exp(arr[0] - np.max(arr[0], axis=-1, keepdims=True)) + prob = ex / np.clip(ex.sum(axis=-1, keepdims=True), 1e-6, None) + hm = 1.0 - prob[..., 0] if prob.shape[-1] > 1 else prob[..., 0] + return hm.astype(np.float32) + else: + if a == 1: + ch = arr[0, 0] + hm = sigmoid(ch) if np.max(np.abs(ch)) > 1.5 else ch + return hm.astype(np.float32) + else: + x = arr[0] + x = x - np.max(x, axis=0, keepdims=True) + ex = np.exp(x) + prob = ex / np.clip(np.sum(ex, axis=0, keepdims=True), 1e-6, None) + bg = prob[0] if prob.shape[0] > 1 else prob[0] + hm = 1.0 - bg + return hm.astype(np.float32) + if arr.ndim == 3: + if arr.shape[0] == 1 and arr.shape[1] >= 8 and arr.shape[2] >= 8: + return arr[0].astype(np.float32) + if arr.ndim == 2 and arr.shape[0] >= 8 and arr.shape[1] >= 8: + return arr.astype(np.float32) + + # 2) Try keypoints and boxes + heat = np.zeros((Ht, Wt), dtype=np.float32) + + def draw_gaussian(hm, cx, cy, sigma=2.5, amp=1.0): + r = max(1, int(3 * sigma)) + xs = np.arange(-r, r + 1, dtype=np.float32) + ys = np.arange(-r, r + 1, dtype=np.float32) + gx = np.exp(-(xs**2) / (2 * sigma * sigma)) + gy = np.exp(-(ys**2) / (2 * sigma * sigma)) + g = np.outer(gy, gx) * float(amp) + x0 = int(round(cx)) - r + y0 = int(round(cy)) - r + x1 = x0 + g.shape[1] + y1 = y0 + g.shape[0] + if x1 < 0 or y1 < 0 or x0 >= Wt or y0 >= Ht: + return + xs0 = max(0, x0) + ys0 = max(0, y0) + xs1 = min(Wt, x1) + ys1 = min(Ht, y1) + gx0 = xs0 - x0 + gy0 = ys0 - y0 + gx1 = gx0 + (xs1 - xs0) + gy1 = gy0 + (ys1 - ys0) + hm[ys0:ys1, xs0:xs1] = np.maximum(hm[ys0:ys1, xs0:xs1], g[gy0:gy1, gx0:gx1]) + + def draw_soft_rect(hm, x0, y0, x1, y1, edge=3.0): + x0, y0, x1, y1 = int(x0), int(y0), int(x1), int(y1) + if x1 <= 0 or y1 <= 0 or x0 >= Wt or y0 >= Ht: + return + xs0 = max(0, min(x0, x1)) + ys0 = max(0, min(y0, y1)) + xs1 = min(Wt, max(x0, x1)) + ys1 = min(Ht, max(y0, y1)) + if xs1 - xs0 <= 0 or ys1 - ys0 <= 0: + return + hm[ys0:ys1, xs0:xs1] = np.maximum(hm[ys0:ys1, xs0:xs1], 1.0) + # feather edges with simple blur-like falloff + if edge > 0: + rad = int(edge) + if rad > 0: + # quick separable triangle filter + line = np.linspace(0, 1, rad + 1, dtype=np.float32)[1:] + for d in range(1, rad + 1): + w = line[d - 1] + if ys0 - d >= 0: + hm[ys0 - d:ys0, xs0:xs1] = np.maximum(hm[ys0 - d:ys0, xs0:xs1], w) + if ys1 + d <= Ht: + hm[ys1:ys1 + d, xs0:xs1] = np.maximum(hm[ys1:ys1 + d, xs0:xs1], w) + if xs0 - d >= 0: + hm[max(0, ys0 - d):min(Ht, ys1 + d), xs0 - d:xs0] = np.maximum( + hm[max(0, ys0 - d):min(Ht, ys1 + d), xs0 - d:xs0], w) + if xs1 + d <= Wt: + hm[max(0, ys0 - d):min(Ht, ys1 + d), xs1:xs1 + d] = np.maximum( + hm[max(0, ys0 - d):min(Ht, ys1 + d), xs1:xs1 + d], w) + + # Inspect outputs to find plausible keypoints/boxes + for out in outputs: + try: + arr = np.asarray(out) + except Exception: + continue + arr = to_float(arr) + if arr is None: + continue + a = arr + # Squeeze batch dims like [1,N,4] -> [N,4] + while a.ndim > 2 and a.shape[0] == 1: + a = np.squeeze(a, axis=0) + # Keypoints: [N,2] or [N,3] or [K, N, 2/3] (relax N limit; subsample if huge) + if a.ndim == 2 and a.shape[-1] in (2, 3): + pts = a + elif a.ndim == 3 and a.shape[-1] in (2, 3): + pts = a.reshape(-1, a.shape[-1]) + else: + pts = None + if pts is not None: + # Coordinates range guess: if max>1.2 -> absolute; else normalized + maxv = float(np.nanmax(np.abs(pts[:, :2]))) if pts.size else 0.0 + for px, py, *rest in pts: + if np.isnan(px) or np.isnan(py): + continue + if maxv <= 1.2: + cx = float(px) * (Wt - 1) + cy = float(py) * (Ht - 1) + else: + cx = float(px) + cy = float(py) + base_sig = max(1.5, min(Ht, Wt) / 128.0) + if _ONNX_KPTS_ENABLE: + draw_gaussian(heat, cx, cy, sigma=base_sig * float(_ONNX_KPTS_SIGMA), amp=float(_ONNX_KPTS_GAIN)) + else: + draw_gaussian(heat, cx, cy, sigma=base_sig) + continue + + # Wholebody-style packed keypoints: [N, K*3] with triples (x,y,conf) + if _ONNX_KPTS_ENABLE and a.ndim == 2 and a.shape[-1] >= 6 and (a.shape[-1] % 3) == 0: + K = a.shape[-1] // 3 + if K >= 5 and K <= 256: + # Guess coordinate range once + with np.errstate(invalid='ignore'): + maxv = float(np.nanmax(np.abs(a[:, :2]))) if a.size else 0.0 + for i in range(a.shape[0]): + row = a[i] + kp = row.reshape(K, 3) + for (px, py, pc) in kp: + if np.isnan(px) or np.isnan(py): + continue + if np.isfinite(pc) and pc < float(_ONNX_KPTS_CONF): + continue + if maxv <= 1.2: + cx = float(px) * (Wt - 1) + cy = float(py) * (Ht - 1) + else: + cx = float(px) + cy = float(py) + base_sig = max(1.0, min(Ht, Wt) / 128.0) + draw_gaussian(heat, cx, cy, sigma=base_sig * float(_ONNX_KPTS_SIGMA), amp=float(_ONNX_KPTS_GAIN)) + continue + # 1D edge-case: single detection row (post-processed model output) + if _ONNX_KPTS_ENABLE and a.ndim == 1 and a.shape[0] >= 6: + D = a.shape[0] + parsed = False + # try [xyxy, conf, cls, kpts] or [xyxy, conf, kpts] or [xyxy, kpts] or [kpts] + for offset in (6, 5, 4, 0): + t = D - offset + if t >= 6 and (t % 3) == 0: + k = t // 3 + kp = a[offset:offset + 3 * k].reshape(k, 3) + parsed = True + break + if parsed: + with np.errstate(invalid='ignore'): + maxv = float(np.nanmax(np.abs(kp[:, :2]))) if kp.size else 0.0 + for (px, py, pc) in kp: + if np.isnan(px) or np.isnan(py): + continue + if np.isfinite(pc) and pc < float(_ONNX_KPTS_CONF): + continue + if maxv <= 1.2: + cx = float(px) * (Wt - 1) + cy = float(py) * (Ht - 1) + else: + cx = float(px) + cy = float(py) + base_sig = max(1.0, min(Ht, Wt) / 128.0) + draw_gaussian(heat, cx, cy, sigma=base_sig * float(_ONNX_KPTS_SIGMA), amp=float(_ONNX_KPTS_GAIN)) + continue + # Boxes: [N,4+] (x0,y0,x1,y1) or [N, (x,y,w,h, [conf, ...])]; relax N limit (handle YOLO-style outputs) + if a.ndim == 2 and a.shape[-1] >= 4: + boxes = a + elif a.ndim == 3 and a.shape[-1] >= 4: + # choose the smallest first two dims as N + if a.shape[0] == 1: + boxes = a.reshape(-1, a.shape[-1]) + else: + boxes = a.reshape(-1, a.shape[-1]) + else: + boxes = None + if boxes is not None: + # Optional score gating (try to find a confidence column) + score = None + if boxes.shape[-1] >= 5: + score = boxes[:, 4] + # If trailing columns look like probabilities in [0,1], mix the best one; if they look like class ids, ignore + if boxes.shape[-1] > 5: + try: + tail = boxes[:, 5:] + tmax = np.max(tail, axis=-1) + # Heuristic: treat as probs if within [0,1] and not integer-like; else assume class ids + maybe_prob = np.all((tmax >= 0.0) & (tmax <= 1.0)) + frac = np.abs(tmax - np.round(tmax)) + maybe_classid = (np.mean(frac < 1e-6) > 0.9) and (np.max(tmax) >= 1.0) + if maybe_prob and not maybe_classid: + score = score * tmax + except Exception: + pass + # Keep top-K by score if available + if score is not None: + try: + order = np.argsort(-score) + keep = order[: min(12, order.shape[0])] + boxes = boxes[keep] + score = score[keep] + except Exception: + score = None + + xy = boxes[:, :4] + maxv = float(np.nanmax(np.abs(xy))) if xy.size else 0.0 + if maxv <= 1.2: + x0 = xy[:, 0] * (Wt - 1) + y0 = xy[:, 1] * (Ht - 1) + x1 = xy[:, 2] * (Wt - 1) + y1 = xy[:, 3] * (Ht - 1) + else: + x0, y0, x1, y1 = xy[:, 0], xy[:, 1], xy[:, 2], xy[:, 3] + # Heuristic: if many boxes are inverted, treat as [x,y,w,h] + invalid = np.sum((x1 <= x0) | (y1 <= y0)) + if invalid > 0.5 * x0.shape[0]: + x, y, w, h = x0, y0, x1, y1 + x0 = x - w * 0.5 + y0 = y - h * 0.5 + x1 = x + w * 0.5 + y1 = y + h * 0.5 + for i in range(x0.shape[0]): + if score is not None and np.isfinite(score[i]) and score[i] < 0.05: + continue + draw_soft_rect(heat, x0[i], y0[i], x1[i], y1[i], edge=3.0) + + # Embedded keypoints in YOLO-style rows: try to parse trailing triples (x,y,conf) + if _ONNX_KPTS_ENABLE and boxes.shape[-1] > 6: + D = boxes.shape[-1] + for i in range(boxes.shape[0]): + row = boxes[i] + parsed = False + # try [xyxy, conf, cls, kpts] or [xyxy, conf, kpts] or [xyxy, kpts] + for offset in (6, 5, 4): + t = D - offset + if t >= 6 and t % 3 == 0: + k = t // 3 + kp = row[offset:offset + 3 * k].reshape(k, 3) + parsed = True + break + if not parsed: + continue + for (px, py, pc) in kp: + if np.isnan(px) or np.isnan(py): + continue + if pc < float(_ONNX_KPTS_CONF): + continue + if maxv <= 1.2: + cx = float(px) * (Wt - 1) + cy = float(py) * (Ht - 1) + else: + cx = float(px) + cy = float(py) + base_sig = max(1.0, min(Ht, Wt) / 128.0) + draw_gaussian(heat, cx, cy, sigma=base_sig * float(_ONNX_KPTS_SIGMA), amp=float(_ONNX_KPTS_GAIN)) + + if heat.max() > 0: + heat = np.clip(heat, 0.0, 1.0) + return heat + return None + + +def _onnx_build_mask(image_bhwc: torch.Tensor, preview: int, sensitivity: float, models_dir: str, anomaly_gain: float = 1.0) -> torch.Tensor: + """Return BHWC single-channel mask [0,1] fused from all auto-detected ONNX models. + - Auto-loads any .onnx in models_dir. + - Heuristically extracts spatial heatmaps; non-spatial outputs are ignored. + - Uses soft-OR fusion across models. Models whose filename contains 'anomaly' are scaled by anomaly_gain. + """ + if not _try_init_onnx(models_dir): + # Explicit hint when debugging counts + try: + if globals().get("_ONNX_COUNT_DEBUG", False): + print("[CADE2.5][ONNX] inactive: onnxruntime not available or init failed") + except Exception: + pass + return torch.zeros((image_bhwc.shape[0], image_bhwc.shape[1], image_bhwc.shape[2], 1), device=image_bhwc.device, dtype=image_bhwc.dtype) + + if not _ONNX_SESS: + # Explicit hint when debugging counts + try: + if globals().get("_ONNX_COUNT_DEBUG", False): + print(f"[CADE2.5][ONNX] inactive: no .onnx models loaded from {models_dir}") + except Exception: + pass + return torch.zeros((image_bhwc.shape[0], image_bhwc.shape[1], image_bhwc.shape[2], 1), device=image_bhwc.device, dtype=image_bhwc.dtype) + + # One-time session summary when counting is enabled + if globals().get("_ONNX_COUNT_DEBUG", False): + try: + names = list(_ONNX_SESS.keys()) + short = names[:3] + more = "..." if len(names) > 3 else "" + print(f"[CADE2.5][ONNX] sessions={len(names)} models={short}{more}") + except Exception: + pass + + B, H, W, C = image_bhwc.shape + device = image_bhwc.device + dtype = image_bhwc.dtype + + # Process per-batch image + masks = [] + img_cpu = image_bhwc.detach().to('cpu') + for b in range(B): + masks_b = [] + counts_b: dict[str, int] = {} + if globals().get("_ONNX_COUNT_DEBUG", False): + try: + print(f"[CADE2.5][ONNX] build mask image[{b}] preview={int(max(16, min(1024, preview)))}") + except Exception: + pass + # Prepare base BCHW tensor and default preview size; per-model resize comes later + target = int(max(16, min(1024, preview))) + xb = img_cpu[b].movedim(-1, 0).unsqueeze(0) # 1,C,H,W + if _ONNX_DEBUG: + try: + print(f"[CADE2.5][ONNX] Build mask for image[{b}] -> preview {target}x{target}") + except Exception: + pass + + for name, sess in list(_ONNX_SESS.items()): + try: + inputs = sess.get_inputs() + if not inputs: + continue + in_name = inputs[0].name + in_shape = inputs[0].shape if hasattr(inputs[0], 'shape') else None + # Choose layout automatically based on the presence of channel dim=3 + if isinstance(in_shape, (list, tuple)) and len(in_shape) == 4: + dim_vals = [] + for d in in_shape: + try: + dim_vals.append(int(d)) + except Exception: + dim_vals.append(-1) + if dim_vals[-1] == 3: + layout = "NHWC" + else: + layout = "NCHW" + else: + layout = "NCHW?" + if _ONNX_DEBUG: + try: + print(f"[CADE2.5][ONNX] Model '{name}' in_shape={in_shape} layout={layout}") + except Exception: + pass + # Build per-model sized variants (respect fixed input shapes when provided) + th, tw = target, target + try: + if isinstance(in_shape, (list, tuple)) and len(in_shape) == 4: + dd = [] + for d in in_shape: + try: + dd.append(int(d)) + except Exception: + dd.append(-1) + if layout == "NCHW" and dd[2] > 8 and dd[3] > 8: + th, tw = int(dd[2]), int(dd[3]) + if layout.startswith("NHWC") and dd[1] > 8 and dd[2] > 8: + th, tw = int(dd[1]), int(dd[2]) + except Exception: + th, tw = target, target + + x_stretch_m = F.interpolate(xb, size=(th, tw), mode='bilinear', align_corners=False).clamp(0, 1) + if th == tw: + x_letter_m = _letterbox_nchw(xb, th).clamp(0, 1) + else: + sq = max(th, tw) + x_letter_sq = _letterbox_nchw(xb, sq).clamp(0, 1) + x_letter_m = F.interpolate(x_letter_sq, size=(th, tw), mode='bilinear', align_corners=False).clamp(0, 1) + + variants = [ + ("stretch-RGB", x_stretch_m), + ("letterbox-RGB", x_letter_m), + ("stretch-BGR", x_stretch_m[:, [2, 1, 0], :, :]), + ("letterbox-BGR", x_letter_m[:, [2, 1, 0], :, :]), + ] + + # Try multiple input variants and scales + hm = None + chosen = None + for vname, vx in variants: + if layout.startswith("NHWC"): + xin = vx.permute(0, 2, 3, 1) + else: + xin = vx + for scale in (1.0, 255.0): + inp = (xin * float(scale)).numpy().astype(np.float32) + feed = {in_name: inp} + outs = sess.run(None, feed) + if _ONNX_DEBUG: + try: + shapes = [] + for o in outs: + try: + shapes.append(tuple(np.asarray(o).shape)) + except Exception: + shapes.append("?") + print(f"[CADE2.5][ONNX] '{name}' {vname} scale={scale} -> outs shapes {shapes}") + except Exception: + pass + hm = _try_heatmap_from_outputs(outs, (target, target)) + if _ONNX_DEBUG: + try: + if hm is None: + print(f"[CADE2.5][ONNX] '{name}' {vname} scale={scale}: no spatial heatmap detected") + else: + print(f"[CADE2.5][ONNX] '{name}' {vname} scale={scale}: heat stats min={np.min(hm):.4f} max={np.max(hm):.4f} mean={np.mean(hm):.4f}") + except Exception: + pass + if hm is not None and np.max(hm) > 0: + chosen = (vname, scale) + break + if hm is not None and np.max(hm) > 0: + break + if hm is None: + continue + # Scale by sensitivity and optional anomaly gain + gain = float(max(0.0, sensitivity)) + if 'anomaly' in name.lower(): + gain *= float(max(0.0, anomaly_gain)) + hm = np.clip(hm * gain, 0.0, 1.0) + # Heuristic rejection of stripe artifacts (horizontal/vertical banding) + try: + rm = np.mean(hm, axis=1) # row means (H) + cm = np.mean(hm, axis=0) # col means (W) + rstd = float(np.std(rm)) + cstd = float(np.std(cm)) + zig_r = float(np.mean(np.abs(np.diff(rm)))) + zig_c = float(np.mean(np.abs(np.diff(cm)))) + horiz_bands = (rstd > 10.0 * max(cstd, 1e-6)) and (zig_r > 0.02) + vert_bands = (cstd > 10.0 * max(rstd, 1e-6)) and (zig_c > 0.02) + if horiz_bands or vert_bands: + if _ONNX_DEBUG: + print(f"[CADE2.5][ONNX] '{name}' rejected as stripe artifact (rstd={rstd:.4f} cstd={cstd:.4f} zig_r={zig_r:.4f} zig_c={zig_c:.4f})") + hm = None + except Exception: + pass + tmask = _np_to_mask_tensor(hm, H, W, device, dtype) + if tmask is not None: + masks_b.append(tmask) + # Optional counting for debugging: derive counts from raw outputs if possible + try: + if globals().get("_ONNX_COUNT_DEBUG", False): + lower = name.lower() + is_face = ("face" in lower) + is_hand = ("hand" in lower) + is_pose = any(w in lower for w in ["wholebody", "pose", "person", "body"]) and not (is_face or is_hand) + + boxes_cnt = 0 + persons_cnt = 0 + wrists_cnt = 0 + + try: + for outx in outs: + arr0 = np.asarray(outx) + if arr0 is None: + continue + a = arr0.astype(np.float32, copy=False) + while a.ndim > 2 and a.shape[0] == 1: + try: + a = np.squeeze(a, axis=0) + except Exception: + break + + # Face/Hand: count only plausible (N,D) detection tables, not spatial maps/grids + if (is_face or is_hand) and a.ndim == 2: + N, D = a.shape + if D >= 4 and D <= 64 and N <= 512 and not (N >= 32 and D >= 32): + n = N + # Try to locate a confidence column + conf = None + if D >= 5: + c = a[:, 4] + if np.all(np.isfinite(c)) and 0.0 <= float(np.nanmin(c)) and float(np.nanmax(c)) <= 1.0: + conf = c + if conf is None: + c = a[:, -1] + if np.all(np.isfinite(c)) and 0.0 <= float(np.nanmin(c)) and float(np.nanmax(c)) <= 1.0: + conf = c + if conf is not None: + n = int(np.sum(conf >= 0.25)) + boxes_cnt = max(boxes_cnt, int(n)) + + # Pose: prefer keypoints formats only + if is_pose: + # Packed per row [N, K*3] + if a.ndim == 2 and a.shape[-1] >= 6 and (a.shape[-1] % 3) == 0: + persons_cnt = max(persons_cnt, int(a.shape[0])) + try: + K = a.shape[-1] // 3 + if K >= 11: + lw = a[:, 9*3 + 2] + rw = a[:, 10*3 + 2] + wrists_cnt = max(wrists_cnt, int(np.sum(lw >= 0.2) + np.sum(rw >= 0.2))) + except Exception: + pass + # [N,K,2/3] + if a.ndim == 3 and a.shape[-1] in (2, 3): + persons_cnt = max(persons_cnt, int(a.shape[0])) + try: + if a.shape[1] >= 11 and a.shape[-1] == 3: + lw = a[:, 9, 2] + rw = a[:, 10, 2] + wrists_cnt = max(wrists_cnt, int(np.sum(lw >= 0.2) + np.sum(rw >= 0.2))) + except Exception: + pass + except Exception: + pass + + # Map to categories by model name using derived counts + if is_face: + if boxes_cnt > 0: + counts_b["faces"] = counts_b.get("faces", 0) + boxes_cnt + elif is_hand: + if boxes_cnt > 0: + counts_b["hands"] = counts_b.get("hands", 0) + boxes_cnt + elif is_pose: + if persons_cnt > 0: + counts_b["persons"] = counts_b.get("persons", 0) + persons_cnt + # Fallback hands from wrists or 2 per person + if wrists_cnt > 0: + counts_b["hands"] = counts_b.get("hands", 0) + wrists_cnt + elif persons_cnt > 0: + counts_b["hands"] = counts_b.get("hands", 0) + (2 * persons_cnt) + except Exception: + pass + if _ONNX_DEBUG: + try: + area = float(tmask.movedim(-1,1).mean().item()) + if chosen is not None: + vname, scale = chosen + print(f"[CADE2.5][ONNX] '{name}' via {vname} x{scale} area={area:.4f}") + else: + print(f"[CADE2.5][ONNX] '{name}' contribution area={area:.4f}") + except Exception: + pass + except Exception: + # Ignore failing models + continue + if not masks_b: + masks.append(torch.zeros((1, H, W, 1), device=device, dtype=dtype)) + if _ONNX_DEBUG or globals().get("_ONNX_COUNT_DEBUG", False): + try: + print(f"[CADE2.5][ONNX] Detected (image[{b}]): none (no contributing models)") + except Exception: + pass + else: + # Soft-OR fusion: 1 - prod(1 - m) + stack = torch.stack([masks_b[i] for i in range(len(masks_b))], dim=0) # M,1,H,W,1? actually B dims kept as 1 + fused = 1.0 - torch.prod(1.0 - stack.clamp(0, 1), dim=0) + # Light smoothing via bilinear down/up (anti alias) + ch = fused.permute(0, 3, 1, 2) # B=1,C=1,H,W + dd = F.interpolate(ch, scale_factor=0.5, mode='bilinear', align_corners=False, recompute_scale_factor=False) + uu = F.interpolate(dd, size=(H, W), mode='bilinear', align_corners=False) + fused = uu.permute(0, 2, 3, 1).clamp(0, 1) + if _ONNX_DEBUG or globals().get("_ONNX_COUNT_DEBUG", False): + try: + area = float(fused.movedim(-1,1).mean().item()) + if _ONNX_DEBUG: + print(f"[CADE2.5][ONNX] Fused area (image[{b}])={area:.4f}") + # Print per-image counts if requested + if globals().get("_ONNX_COUNT_DEBUG", False): + if counts_b: + faces = counts_b.get("faces", 0) + hands = counts_b.get("hands", 0) + persons = counts_b.get("persons", 0) + print(f"[CADE2.5][ONNX] Detected (image[{b}]): faces={faces} hands={hands} persons={persons}") + else: + print(f"[CADE2.5][ONNX] Detected (image[{b}]): counts unavailable (no categories or cv2 missing), area={area:.4f}") + except Exception: + pass + masks.append(fused) + + return torch.cat(masks, dim=0) + +def _sampler_names(): + try: + import comfy.samplers + return comfy.samplers.KSampler.SAMPLERS + except Exception: + return ["euler"] + + +def _scheduler_names(): + try: + import comfy.samplers + scheds = list(comfy.samplers.KSampler.SCHEDULERS) + if "MGHybrid" not in scheds: + scheds.append("MGHybrid") + return scheds + except Exception: + return ["normal", "MGHybrid"] + + +def safe_decode(vae, lat, tile=512, ovlp=64): + h, w = lat["samples"].shape[-2:] + if min(h, w) > 1024: + # Increase overlap for ultra-hires to reduce seam artifacts + ov = 128 if max(h, w) > 2048 else ovlp + return vae.decode_tiled(lat["samples"], tile_x=tile, tile_y=tile, overlap=ov) + return vae.decode(lat["samples"]) + + +def safe_encode(vae, img, tile=512, ovlp=64): + import math, torch.nn.functional as F + h, w = img.shape[1:3] + try: + stride = int(vae.spacial_compression_decode()) + except Exception: + stride = 8 + if stride <= 0: + stride = 8 + def _align_up(x, s): + return int(((x + s - 1) // s) * s) + Ht = _align_up(h, stride) + Wt = _align_up(w, stride) + x = img + if (Ht != h) or (Wt != w): + # pad on bottom/right using replicate to avoid black borders + pad_h = Ht - h + pad_w = Wt - w + x_nchw = img.movedim(-1, 1) + x_nchw = F.pad(x_nchw, (0, pad_w, 0, pad_h), mode='replicate') + x = x_nchw.movedim(1, -1) + if min(Ht, Wt) > 1024: + ov = 128 if max(Ht, Wt) > 2048 else ovlp + return vae.encode_tiled(x[:, :, :, :3], tile_x=tile, tile_y=tile, overlap=ov) + return vae.encode(x[:, :, :, :3]) + + + +def _gaussian_kernel(kernel_size: int, sigma: float, device=None): + x, y = torch.meshgrid( + torch.linspace(-1, 1, kernel_size, device=device), + torch.linspace(-1, 1, kernel_size, device=device), + indexing="ij", + ) + d = torch.sqrt(x * x + y * y) + g = torch.exp(-(d * d) / (2.0 * sigma * sigma)) + return g / g.sum() + + +def _sharpen_image(image: torch.Tensor, sharpen_radius: int, sigma: float, alpha: float): + if sharpen_radius == 0: + return (image,) + + image = image.to(model_management.get_torch_device()) + batch_size, height, width, channels = image.shape + + kernel_size = sharpen_radius * 2 + 1 + kernel = _gaussian_kernel(kernel_size, sigma, device=image.device) * -(alpha * 10) + kernel = kernel.to(dtype=image.dtype) + center = kernel_size // 2 + kernel[center, center] = kernel[center, center] - kernel.sum() + 1.0 + kernel = kernel.repeat(channels, 1, 1).unsqueeze(1) + + tensor_image = image.permute(0, 3, 1, 2) + tensor_image = F.pad(tensor_image, (sharpen_radius, sharpen_radius, sharpen_radius, sharpen_radius), 'reflect') + sharpened = F.conv2d(tensor_image, kernel, padding=center, groups=channels)[:, :, sharpen_radius:-sharpen_radius, sharpen_radius:-sharpen_radius] + sharpened = sharpened.permute(0, 2, 3, 1) + + result = torch.clamp(sharpened, 0, 1) + return (result.to(model_management.intermediate_device()),) + + +def _encode_clip_image(image: torch.Tensor, clip_vision, target_res: int) -> torch.Tensor: + # image: BHWC in [0,1] + img = image.movedim(-1, 1) # BCHW + img = F.interpolate(img, size=(target_res, target_res), mode="bilinear", align_corners=False) + img = (img * 2.0) - 1.0 + embeds = clip_vision.encode_image(img)["image_embeds"] + embeds = F.normalize(embeds, dim=-1) + return embeds + + +def _clip_cosine_distance(a: torch.Tensor, b: torch.Tensor) -> float: + if a.shape != b.shape: + m = min(a.shape[0], b.shape[0]) + a = a[:m] + b = b[:m] + sim = (a * b).sum(dim=-1).mean().clamp(-1.0, 1.0).item() + return 1.0 - sim + + +def _soft_symmetry_blend(image_bhwc: torch.Tensor, + mask_bhwc: torch.Tensor, + alpha: float = 0.03, + lp_sigma: float = 1.5) -> torch.Tensor: + """Gently mix a mirrored low-frequency component inside mask. + - image_bhwc: [B,H,W,C] in [0,1] + - mask_bhwc: [B,H,W,1] in [0,1] + """ + try: + if image_bhwc is None or mask_bhwc is None: + return image_bhwc + if image_bhwc.ndim != 4 or mask_bhwc.ndim != 4: + return image_bhwc + B, H, W, C = image_bhwc.shape + if C < 3: + return image_bhwc + # Mirror along width + mirror = torch.flip(image_bhwc, dims=[2]) + # Low-pass both + x = image_bhwc.movedim(-1, 1) + y = mirror.movedim(-1, 1) + rad = max(1, int(round(lp_sigma))) + x_lp = _gaussian_blur_nchw(x, sigma=float(lp_sigma), radius=rad) + y_lp = _gaussian_blur_nchw(y, sigma=float(lp_sigma), radius=rad) + # High-pass from original + hp = x - x_lp + # Blend LPs inside mask + m = mask_bhwc.movedim(-1, 1).clamp(0, 1) + a = float(max(0.0, min(0.2, alpha))) + base = (1.0 - a * m) * x_lp + (a * m) * y_lp + res = (base + hp).movedim(1, -1).clamp(0, 1) + return res.to(image_bhwc.dtype) + except Exception: + return image_bhwc + + +def _gaussian_blur_nchw(x: torch.Tensor, sigma: float = 1.0, radius: int = 1) -> torch.Tensor: + """Lightweight depthwise Gaussian blur for NCHW tensors. + Uses reflect padding and a normalized kernel built by _gaussian_kernel. + """ + if radius <= 0: + return x + ksz = radius * 2 + 1 + kernel = _gaussian_kernel(ksz, sigma, device=x.device).to(dtype=x.dtype) + kernel = kernel.repeat(x.shape[1], 1, 1).unsqueeze(1) # [C,1,K,K] + x_pad = F.pad(x, (radius, radius, radius, radius), mode='reflect') + y = F.conv2d(x_pad, kernel, padding=0, groups=x.shape[1]) + return y + + +def _letterbox_nchw(x: torch.Tensor, target: int, pad_val: float = 114.0 / 255.0) -> torch.Tensor: + """Letterbox a BCHW tensor to target x target with constant padding (YOLO-style). + Preserves aspect ratio, centers content, pads with pad_val. + """ + if x.ndim != 4: + return F.interpolate(x, size=(target, target), mode='bilinear', align_corners=False) + b, c, h, w = x.shape + if h == 0 or w == 0: + return F.interpolate(x, size=(target, target), mode='bilinear', align_corners=False) + r = float(min(target / max(1, h), target / max(1, w))) + nh = max(1, int(round(h * r))) + nw = max(1, int(round(w * r))) + y = F.interpolate(x, size=(nh, nw), mode='bilinear', align_corners=False) + pt = (target - nh) // 2 + pb = target - nh - pt + pl = (target - nw) // 2 + pr = target - nw - pl + if pt < 0 or pb < 0 or pl < 0 or pr < 0: + # Fallback stretch if rounding went weird + return F.interpolate(x, size=(target, target), mode='bilinear', align_corners=False) + return F.pad(y, (pl, pr, pt, pb), mode='constant', value=float(pad_val)) + + +def _fdg_filter(delta: torch.Tensor, low_gain: float, high_gain: float, sigma: float = 1.0, radius: int = 1) -> torch.Tensor: + """Frequency-Decoupled Guidance: split delta into low/high bands and reweight. + delta: [B,C,H,W] + """ + low = _gaussian_blur_nchw(delta, sigma=sigma, radius=radius) + high = delta - low + return low * float(low_gain) + high * float(high_gain) + + +def _fdg_energy_fraction(delta: torch.Tensor, sigma: float = 1.0, radius: int = 1) -> torch.Tensor: + """Return fraction of high-frequency energy: E_high / (E_low + E_high).""" + low = _gaussian_blur_nchw(delta, sigma=sigma, radius=radius) + high = delta - low + e_low = (low * low).mean(dim=(1, 2, 3), keepdim=True) + e_high = (high * high).mean(dim=(1, 2, 3), keepdim=True) + frac = e_high / (e_low + e_high + 1e-8) + return frac + + +def _fdg_split_three(delta: torch.Tensor, + sigma_lo: float = 0.8, + sigma_hi: float = 2.0, + radius: int = 1) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + sig_lo = float(max(0.05, sigma_lo)) + sig_hi = float(max(sig_lo + 1e-3, sigma_hi)) + blur_lo = _gaussian_blur_nchw(delta, sigma=sig_lo, radius=radius) + blur_hi = _gaussian_blur_nchw(delta, sigma=sig_hi, radius=radius) + low = blur_hi + mid = blur_lo - blur_hi + high = delta - blur_lo + return low, mid, high + + +def _wrap_model_with_guidance(model, guidance_mode: str, rescale_multiplier: float, momentum_beta: float, cfg_curve: float, perp_damp: float, use_zero_init: bool=False, zero_init_steps: int=0, fdg_low: float = 0.6, fdg_high: float = 1.3, fdg_sigma: float = 1.0, ze_zero_steps: int = 0, ze_adaptive: bool = False, ze_r_switch_hi: float = 0.6, ze_r_switch_lo: float = 0.45, fdg_low_adaptive: bool = False, fdg_low_min: float = 0.45, fdg_low_max: float = 0.7, fdg_ema_beta: float = 0.8, use_local_mask: bool = False, mask_inside: float = 1.0, mask_outside: float = 1.0, + midfreq_enable: bool = False, midfreq_gain: float = 0.0, midfreq_sigma_lo: float = 0.8, midfreq_sigma_hi: float = 2.0, + mahiro_plus_enable: bool = False, mahiro_plus_strength: float = 0.5, + eps_scale_enable: bool = False, eps_scale: float = 0.0): + + """Clone model and attach a cfg mixing function implementing RescaleCFG/FDG, CFGZero*/FD, or hybrid ZeResFDG. + guidance_mode: 'default' | 'RescaleCFG' | 'RescaleFDG' | 'CFGZero*' | 'CFGZeroFD' | 'ZeResFDG' + """ + if guidance_mode == "default": + return model + m = model.clone() + + # State for momentum and sigma normalization across steps + prev_delta = {"t": None} + sigma_seen = {"max": None, "min": None} + # Spectral switching/adaptive low state + spec_state = {"ema": None, "mode": "CFGZeroFD"} + + def cfg_func(args): + cond = args["cond"] + uncond = args["uncond"] + cond_scale = args["cond_scale"] + sigma = args.get("sigma", None) + x_orig = args.get("input", None) + + # Local spatial gain from CURRENT_ONNX_MASK_BCHW, resized to cond spatial size + def _local_gain_for(hw): + if not bool(use_local_mask): + return None + m = globals().get("CURRENT_ONNX_MASK_BCHW", None) + if m is None: + return None + try: + Ht, Wt = int(hw[0]), int(hw[1]) + g = m.to(device=cond.device, dtype=cond.dtype) + if g.shape[-2] != Ht or g.shape[-1] != Wt: + g = F.interpolate(g, size=(Ht, Wt), mode='bilinear', align_corners=False) + gi = float(mask_inside) + go = float(mask_outside) + gain = g * gi + (1.0 - g) * go # [B,1,H,W] + return gain + except Exception: + return None + + # Allow hybrid switch per-step + mode = guidance_mode + if guidance_mode == "ZeResFDG": + if bool(ze_adaptive): + try: + delta_raw = args["cond"] - args["uncond"] + frac_b = _fdg_energy_fraction(delta_raw, sigma=float(fdg_sigma), radius=1) # [B,1,1,1] + frac = float(frac_b.mean().clamp(0.0, 1.0).item()) + except Exception: + frac = 0.0 + if spec_state["ema"] is None: + spec_state["ema"] = frac + else: + beta = float(max(0.0, min(0.99, fdg_ema_beta))) + spec_state["ema"] = beta * float(spec_state["ema"]) + (1.0 - beta) * frac + r = float(spec_state["ema"]) + # Hysteresis: switch up/down with two thresholds + if spec_state["mode"] == "CFGZeroFD" and r >= float(ze_r_switch_hi): + spec_state["mode"] = "RescaleFDG" + elif spec_state["mode"] == "RescaleFDG" and r <= float(ze_r_switch_lo): + spec_state["mode"] = "CFGZeroFD" + mode = spec_state["mode"] + else: + try: + sigmas = args["model_options"]["transformer_options"]["sample_sigmas"] + matched_idx = (sigmas == args["timestep"][0]).nonzero() + if len(matched_idx) > 0: + current_idx = matched_idx.item() + else: + current_idx = 0 + except Exception: + current_idx = 0 + mode = "CFGZeroFD" if current_idx <= int(ze_zero_steps) else "RescaleFDG" + + if mode in ("CFGZero*", "CFGZeroFD"): + # Optional zero-init for the first N steps + if use_zero_init and "model_options" in args and args.get("timestep") is not None: + try: + sigmas = args["model_options"]["transformer_options"]["sample_sigmas"] + matched_idx = (sigmas == args["timestep"][0]).nonzero() + if len(matched_idx) > 0: + current_idx = matched_idx.item() + else: + # fallback lookup + current_idx = 0 + if current_idx <= int(zero_init_steps): + return cond * 0.0 + except Exception: + pass + # Project cond onto uncond subspace (batch-wise alpha) + bsz = cond.shape[0] + pos_flat = cond.view(bsz, -1) + neg_flat = uncond.view(bsz, -1) + dot = torch.sum(pos_flat * neg_flat, dim=1, keepdim=True) + denom = torch.sum(neg_flat * neg_flat, dim=1, keepdim=True).clamp_min(1e-8) + alpha = (dot / denom).view(bsz, *([1] * (cond.dim() - 1))) + resid = cond - uncond * alpha + # Adaptive low gain if enabled + low_gain_eff = float(fdg_low) + if bool(fdg_low_adaptive) and spec_state["ema"] is not None: + s = float(spec_state["ema"]) # 0..1 fraction of high-frequency energy + lmin = float(fdg_low_min) + lmax = float(fdg_low_max) + low_gain_eff = max(0.0, min(2.0, lmin + (lmax - lmin) * s)) + if mode == "CFGZeroFD": + resid = _fdg_filter(resid, low_gain=low_gain_eff, high_gain=fdg_high, sigma=float(fdg_sigma), radius=1) + # Apply local spatial gain to residual guidance + lg = _local_gain_for((cond.shape[-2], cond.shape[-1])) + if lg is not None: + resid = resid * lg.expand(-1, resid.shape[1], -1, -1) + noise_pred = uncond * alpha + cond_scale * resid + return noise_pred + + # RescaleCFG/FDG path (with optional momentum/perp damping and S-curve shaping) + delta = cond - uncond + pd = float(max(0.0, min(1.0, perp_damp))) + if pd > 0.0 and (prev_delta["t"] is not None) and (prev_delta["t"].shape == delta.shape): + prev = prev_delta["t"] + denom = (prev * prev).sum(dim=(1,2,3), keepdim=True).clamp_min(1e-6) + coeff = ((delta * prev).sum(dim=(1,2,3), keepdim=True) / denom) + parallel = coeff * prev + delta = delta - pd * parallel + beta = float(max(0.0, min(0.95, momentum_beta))) + if beta > 0.0: + if prev_delta["t"] is None or prev_delta["t"].shape != delta.shape: + prev_delta["t"] = delta.detach() + delta = (1.0 - beta) * delta + beta * prev_delta["t"] + prev_delta["t"] = delta.detach() + cond = uncond + delta + else: + prev_delta["t"] = delta.detach() + # After momentum: optionally apply FDG and rebuild cond + if mode == "RescaleFDG": + # Adaptive low gain if enabled + low_gain_eff = float(fdg_low) + if bool(fdg_low_adaptive) and spec_state["ema"] is not None: + s = float(spec_state["ema"]) # 0..1 + lmin = float(fdg_low_min) + lmax = float(fdg_low_max) + low_gain_eff = max(0.0, min(2.0, lmin + (lmax - lmin) * s)) + delta_fdg = _fdg_filter(delta, low_gain=low_gain_eff, high_gain=fdg_high, sigma=float(fdg_sigma), radius=1) + # Optional mid-frequency emphasis blended on top + if bool(midfreq_enable) and abs(float(midfreq_gain)) > 1e-6: + lo_b, mid_b, hi_b = _fdg_split_three(delta, sigma_lo=float(midfreq_sigma_lo), sigma_hi=float(midfreq_sigma_hi), radius=1) + lg = _local_gain_for((cond.shape[-2], cond.shape[-1])) + if lg is not None: + mid_b = mid_b * lg.expand(-1, mid_b.shape[1], -1, -1) + delta_fdg = delta_fdg + float(midfreq_gain) * mid_b + lg = _local_gain_for((cond.shape[-2], cond.shape[-1])) + if lg is not None: + delta_fdg = delta_fdg * lg.expand(-1, delta_fdg.shape[1], -1, -1) + cond = uncond + delta_fdg + else: + lg = _local_gain_for((cond.shape[-2], cond.shape[-1])) + if lg is not None: + delta = delta * lg.expand(-1, delta.shape[1], -1, -1) + cond = uncond + delta + + cond_scale_eff = cond_scale + if cfg_curve > 0.0 and (sigma is not None): + s = sigma + if s.ndim > 1: + s = s.flatten() + s_max = float(torch.max(s).item()) + s_min = float(torch.min(s).item()) + if sigma_seen["max"] is None: + sigma_seen["max"] = s_max + sigma_seen["min"] = s_min + else: + sigma_seen["max"] = max(sigma_seen["max"], s_max) + sigma_seen["min"] = min(sigma_seen["min"], s_min) + lo = max(1e-6, sigma_seen["min"]) + hi = max(lo * (1.0 + 1e-6), sigma_seen["max"]) + t = (torch.log(s + 1e-6) - torch.log(torch.tensor(lo, device=sigma.device))) / (torch.log(torch.tensor(hi, device=sigma.device)) - torch.log(torch.tensor(lo, device=sigma.device)) + 1e-6) + t = t.clamp(0.0, 1.0) + k = 6.0 * float(cfg_curve) + s_curve = torch.tanh((t - 0.5) * k) + gain = 1.0 + 0.15 * float(cfg_curve) * s_curve + if gain.ndim > 0: + gain = gain.mean().item() + cond_scale_eff = cond_scale * float(gain) + + # Epsilon scaling (exposure bias correction): early steps get multiplier closer to (1 + eps_scale) + eps_mult = 1.0 + if bool(eps_scale_enable) and (sigma is not None): + try: + s = sigma + if s.ndim > 1: + s = s.flatten() + s_max = float(torch.max(s).item()) + s_min = float(torch.min(s).item()) + if sigma_seen["max"] is None: + sigma_seen["max"] = s_max + sigma_seen["min"] = s_min + else: + sigma_seen["max"] = max(sigma_seen["max"], s_max) + sigma_seen["min"] = min(sigma_seen["min"], s_min) + lo = max(1e-6, sigma_seen["min"]) + hi = max(lo * (1.0 + 1e-6), sigma_seen["max"]) + t_lin = (torch.log(s + 1e-6) - torch.log(torch.tensor(lo, device=sigma.device))) / (torch.log(torch.tensor(hi, device=sigma.device)) - torch.log(torch.tensor(lo, device=sigma.device)) + 1e-6) + t_lin = t_lin.clamp(0.0, 1.0) + w_early = (1.0 - t_lin).mean().item() + eps_mult = float(1.0 + eps_scale * w_early) + except Exception: + eps_mult = float(1.0 + eps_scale) + + if sigma is None or x_orig is None: + return uncond + cond_scale * (cond - uncond) + sigma_ = sigma.view(sigma.shape[:1] + (1,) * (cond.ndim - 1)) + x = x_orig / (sigma_ * sigma_ + 1.0) + v_cond = ((x - (x_orig - cond)) * (sigma_ ** 2 + 1.0) ** 0.5) / (sigma_) + v_uncond = ((x - (x_orig - uncond)) * (sigma_ ** 2 + 1.0) ** 0.5) / (sigma_) + v_cfg = v_uncond + cond_scale_eff * (v_cond - v_uncond) + ro_pos = torch.std(v_cond, dim=(1, 2, 3), keepdim=True) + ro_cfg = torch.std(v_cfg, dim=(1, 2, 3), keepdim=True).clamp_min(1e-6) + v_rescaled = v_cfg * (ro_pos / ro_cfg) + v_final = float(rescale_multiplier) * v_rescaled + (1.0 - float(rescale_multiplier)) * v_cfg + eps = x_orig - (x - (v_final * eps_mult) * sigma_ / (sigma_ * sigma_ + 1.0) ** 0.5) + return eps + + m.set_model_sampler_cfg_function(cfg_func, disable_cfg1_optimization=True) + + # Optional directional post-mix (Muse Blend), global, no ONNX + if bool(mahiro_plus_enable): + s_clamp = float(max(0.0, min(1.0, mahiro_plus_strength))) + mb_state = {"ema": None} + + def _sqrt_sign(x: torch.Tensor) -> torch.Tensor: + return x.sign() * torch.sqrt(x.abs().clamp_min(1e-12)) + + def _hp_split(x: torch.Tensor, radius: int = 1, sigma: float = 1.0): + low = _gaussian_blur_nchw(x, sigma=sigma, radius=radius) + high = x - low + return low, high + + def _sched_gain(args) -> float: + # Gentle mid-steps boost: triangle peak at the middle of schedule + try: + sigmas = args["model_options"]["transformer_options"]["sample_sigmas"] + idx_t = args.get("timestep", None) + if idx_t is None: + return 1.0 + matched = (sigmas == idx_t[0]).nonzero() + if len(matched) == 0: + return 1.0 + i = float(matched.item()) + n = float(sigmas.shape[0]) + if n <= 1: + return 1.0 + phase = i / (n - 1.0) + tri = 1.0 - abs(2.0 * phase - 1.0) + return float(0.6 + 0.4 * tri) # 0.6 at edges -> 1.0 mid + except Exception: + return 1.0 + + def mahiro_plus_post(args): + try: + scale = args.get('cond_scale', 1.0) + cond_p = args['cond_denoised'] + uncond_p = args['uncond_denoised'] + cfg = args['denoised'] + + # Orthogonalize positive to negative direction (batch-wise) + bsz = cond_p.shape[0] + pos_flat = cond_p.view(bsz, -1) + neg_flat = uncond_p.view(bsz, -1) + dot = torch.sum(pos_flat * neg_flat, dim=1, keepdim=True) + denom = torch.sum(neg_flat * neg_flat, dim=1, keepdim=True).clamp_min(1e-8) + alpha = (dot / denom).view(bsz, *([1] * (cond_p.dim() - 1))) + c_orth = cond_p - uncond_p * alpha + + leap_raw = float(scale) * c_orth + # Light high-pass emphasis for detail, protect low-frequency tone + low, high = _hp_split(leap_raw, radius=1, sigma=1.0) + leap = 0.35 * low + 1.00 * high + + # Directional agreement (global cosine over flattened dims) + u_leap = float(scale) * uncond_p + merge = 0.5 * (leap + cfg) + nu = _sqrt_sign(u_leap).flatten(1) + nm = _sqrt_sign(merge).flatten(1) + sim = F.cosine_similarity(nu, nm, dim=1).mean() + a = torch.clamp((sim + 1.0) * 0.5, 0.0, 1.0) + # Small EMA for temporal smoothness + if mb_state["ema"] is None: + mb_state["ema"] = float(a) + else: + mb_state["ema"] = 0.8 * float(mb_state["ema"]) + 0.2 * float(a) + a_eff = float(mb_state["ema"]) + w = a_eff * cfg + (1.0 - a_eff) * leap + + # Gentle energy match to CFG + dims = tuple(range(1, w.dim())) + ro_w = torch.std(w, dim=dims, keepdim=True).clamp_min(1e-6) + ro_cfg = torch.std(cfg, dim=dims, keepdim=True).clamp_min(1e-6) + w_res = w * (ro_cfg / ro_w) + + # Schedule gain over steps (mid stronger) + s_eff = s_clamp * _sched_gain(args) + out = (1.0 - s_eff) * cfg + s_eff * w_res + return out + except Exception: + return args['denoised'] + + try: + m.set_model_sampler_post_cfg_function(mahiro_plus_post) + except Exception: + pass + + # Quantile clamp stabilizer (per-sample): soft range limit for denoised tensor + # Always on, under the hood. Helps prevent rare exploding values. + def _qclamp_post(args): + try: + x = args.get("denoised", None) + if x is None: + return args["denoised"] + dt = x.dtype + xf = x.to(dtype=torch.float32) + B = xf.shape[0] + lo_q, hi_q = 0.001, 0.999 + out = [] + for i in range(B): + t = xf[i].reshape(-1) + try: + lo = torch.quantile(t, lo_q) + hi = torch.quantile(t, hi_q) + except Exception: + n = t.numel() + k_lo = max(1, int(n * lo_q)) + k_hi = max(1, int(n * hi_q)) + lo = torch.kthvalue(t, k_lo).values + hi = torch.kthvalue(t, k_hi).values + out.append(xf[i].clamp(min=lo, max=hi)) + y = torch.stack(out, dim=0).to(dtype=dt) + return y + except Exception: + return args["denoised"] + + try: + m.set_model_sampler_post_cfg_function(_qclamp_post) + except Exception: + pass + + return m + + +def _edge_mask(image_bhwc: torch.Tensor, + threshold: float = 0.20, + blur: float = 1.0) -> torch.Tensor: + """Return a simple edge mask BHWC [0,1] using Sobel magnitude on luminance. + It is resolution-agnostic and intentionally lightweight. + """ + try: + img = image_bhwc + lum = (0.2126 * img[..., 0] + 0.7152 * img[..., 1] + 0.0722 * img[..., 2]) + kx = torch.tensor([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], device=img.device, dtype=img.dtype).view(1, 1, 3, 3) + ky = torch.tensor([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], device=img.device, dtype=img.dtype).view(1, 1, 3, 3) + g = torch.sqrt((F.conv2d(lum.unsqueeze(1), kx, padding=1) ** 2) + (F.conv2d(lum.unsqueeze(1), ky, padding=1) ** 2)) + g = g.squeeze(1) + # Robust normalization via 98th percentile + try: + q = torch.quantile(g.flatten(), 0.98).clamp_min(1e-6) + except Exception: + q = torch.topk(g.flatten(), max(1, int(g.numel() * 0.02))).values.min().clamp_min(1e-6) + m = (g / q).clamp(0, 1) + if threshold > 0.0: + m = (m > float(threshold)).to(img.dtype) + if blur > 0.0: + rad = int(max(1, min(5, round(float(blur))))) + m = _gaussian_blur_nchw(m.unsqueeze(1), sigma=float(max(0.5, blur)), radius=rad).squeeze(1) + return m.unsqueeze(-1) + except Exception: + return torch.zeros((image_bhwc.shape[0], image_bhwc.shape[1], image_bhwc.shape[2], 1), device=image_bhwc.device, dtype=image_bhwc.dtype) + + + +def _cf_edges_post(acc_t: torch.Tensor, + edge_width: float, + edge_smooth: float, + edge_single_line: bool, + edge_single_strength: float) -> torch.Tensor: + try: + import cv2, numpy as _np + img = (acc_t.clamp(0,1).detach().to('cpu').numpy()*255.0).astype(_np.uint8) + # Thickness adjust + if float(edge_width) != 0.0: + s = float(edge_width) + k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3)) + op = cv2.dilate if s > 0 else cv2.erode + it = int(max(0, min(6, round(abs(s) * 4.0)))) + frac = abs(s) * 4.0 - it + for _ in range(max(0, it)): + img = op(img, k, iterations=1) + if frac > 1e-6: + y2 = op(img, k, iterations=1) + img = ((1.0-frac)*img.astype(_np.float32) + frac*y2.astype(_np.float32)).astype(_np.uint8) + # Collapse double lines to single centerline + if bool(edge_single_line) and float(edge_single_strength) > 1e-6: + try: + s = float(edge_single_strength) + close = cv2.morphologyEx(img, cv2.MORPH_CLOSE, cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(3,3)), iterations=1) + if hasattr(cv2, 'ximgproc') and hasattr(cv2.ximgproc, 'thinning'): + sk = cv2.ximgproc.thinning(close) + else: + iters = max(1, int(round(2 + 6*s))) + sk = _np.zeros_like(close) + src = close.copy() + elem = cv2.getStructuringElement(cv2.MORPH_CROSS, (3,3)) + for _ in range(iters): + er = cv2.erode(src, elem, iterations=1) + opn = cv2.morphologyEx(er, cv2.MORPH_OPEN, elem) + tmp = cv2.subtract(er, opn) + sk = cv2.bitwise_or(sk, tmp) + src = er + if not _np.any(src): + break + img = ((1.0 - s) * img.astype(_np.float32) + s * sk.astype(_np.float32)).astype(_np.uint8) + except Exception: + pass + # Smooth + if float(edge_smooth) > 1e-6: + sigma = max(0.1, min(2.0, float(edge_smooth) * 1.2)) + img = cv2.GaussianBlur(img, (0,0), sigmaX=sigma) + out = torch.from_numpy((img.astype(_np.float32)/255.0)).to(device=acc_t.device, dtype=acc_t.dtype) + return out.clamp(0,1) + except Exception: + # Torch fallback: light blur-only and basic thicken/thin + y = acc_t + if float(edge_width) > 1e-6: + k = max(1, int(round(float(edge_width) * 2))) + p = k + y = F.max_pool2d(y.unsqueeze(0).unsqueeze(0), kernel_size=2*k+1, stride=1, padding=p)[0,0] + if float(edge_width) < -1e-6: + k = max(1, int(round(abs(float(edge_width)) * 2))) + p = k + maxed = F.max_pool2d((1.0 - y).unsqueeze(0).unsqueeze(0), kernel_size=2*k+1, stride=1, padding=p)[0,0] + y = 1.0 - maxed + if float(edge_smooth) > 1e-6: + s = max(1, int(round(float(edge_smooth)*2))) + y = F.avg_pool2d(y.unsqueeze(0).unsqueeze(0), kernel_size=2*s+1, stride=1, padding=s)[0,0] + return y.clamp(0,1) + + +def _build_cf_edge_mask_from_step(image_bhwc: torch.Tensor, preset_step: str) -> torch.Tensor | None: + try: + p = load_preset("mg_controlfusion", preset_step) + # Safe converters (preset values may be blank strings) + def _safe_int(val, default): + try: + iv = int(val) + return iv if iv > 0 else default + except Exception: + return default + def _safe_float(val, default): + try: + return float(val) + except Exception: + return default + + # Read CF params with safe defaults + enable_depth = bool(p.get('enable_depth', True)) + depth_model_path = str(p.get('depth_model_path', '')) + depth_resolution = _safe_int(p.get('depth_resolution', 768), 768) + hires_mask_auto = bool(p.get('hires_mask_auto', True)) + pyra_low = _safe_int(p.get('pyra_low', 109), 109) + pyra_high = _safe_int(p.get('pyra_high', 147), 147) + pyra_resolution = _safe_int(p.get('pyra_resolution', 1024), 1024) + edge_thin_iter = int(p.get('edge_thin_iter', 0)) + edge_boost = _safe_float(p.get('edge_boost', 0.0), 0.0) + smart_tune = bool(p.get('smart_tune', False)) + smart_boost = _safe_float(p.get('smart_boost', 0.2), 0.2) + edge_width = _safe_float(p.get('edge_width', 0.0), 0.0) + edge_smooth = _safe_float(p.get('edge_smooth', 0.0), 0.0) + edge_single_line = bool(p.get('edge_single_line', False)) + edge_single_strength = _safe_float(p.get('edge_single_strength', 0.0), 0.0) + edge_depth_gate = bool(p.get('edge_depth_gate', False)) + edge_depth_gamma = _safe_float(p.get('edge_depth_gamma', 1.5), 1.5) + edge_alpha = _safe_float(p.get('edge_alpha', 1.0), 1.0) + # Treat blend_factor as extra gain for edges (depth is not mixed here) + blend_factor = _safe_float(p.get('blend_factor', 0.02), 0.02) + # ControlNet multipliers section — use edge_strength_mul as an additional gain for edge mask + edge_strength_mul = _safe_float(p.get('edge_strength_mul', 1.0), 1.0) + + # Build edges with CF PyraCanny + ed = _cf_pyracanny(image_bhwc, pyra_low, pyra_high, pyra_resolution, + edge_thin_iter, edge_boost, smart_tune, smart_boost, + preserve_aspect=bool(hires_mask_auto)) + ed = _cf_edges_post(ed, edge_width, edge_smooth, edge_single_line, edge_single_strength) + # Depth-gate edges if enabled + if edge_depth_gate and enable_depth: + try: + depth = _cf_build_depth(image_bhwc, int(depth_resolution), str(depth_model_path), bool(hires_mask_auto)) + g = depth.clamp(0,1) ** float(edge_depth_gamma) + ed = (ed * g).clamp(0,1) + except Exception: + pass + # Apply opacity + edge strength + blend factor (as extra gain for edges only) + total_gain = max(0.0, float(edge_alpha)) * max(0.0, float(edge_strength_mul)) * max(0.0, float(blend_factor)) + # Keep at least alpha if blend_factor is set to 0 in presets + if total_gain == 0.0: + total_gain = max(0.0, float(edge_alpha)) + ed = (ed * total_gain).clamp(0,1) + # Return BHWC single-channel + return ed.unsqueeze(0).unsqueeze(-1) + except Exception: + return None +def _mask_dilate(mask_bhw1: torch.Tensor, k: int = 3) -> torch.Tensor: + if k <= 1: + return mask_bhw1 + m = mask_bhw1.movedim(-1, 1) + m = F.max_pool2d(m, kernel_size=k, stride=1, padding=k // 2) + return m.movedim(1, -1) + + +def _mask_erode(mask_bhw1: torch.Tensor, k: int = 3) -> torch.Tensor: + if k <= 1: + return mask_bhw1 + m = mask_bhw1.movedim(-1, 1) + e = 1.0 - F.max_pool2d(1.0 - m, kernel_size=k, stride=1, padding=k // 2) + return e.movedim(1, -1) + + +class ComfyAdaptiveDetailEnhancer25: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "preset_step": (["Step 1", "Step 2", "Step 3", "Step 4"], {"default": "Step 1", "tooltip": "Choose the Step preset. Toggle Custom below to apply UI values; otherwise Step preset values are used."}), + "custom": ("BOOLEAN", {"default": False, "tooltip": "Custom override: when enabled, your UI values override the selected Step for visible controls; hidden parameters still come from the Step preset."}), "model": ("MODEL", {}), + "positive": ("CONDITIONING", {}), + "negative": ("CONDITIONING", {}), + "vae": ("VAE", {}), + "latent": ("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, "step": 0.1}), + "denoise": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.0001}), + "sampler_name": (_sampler_names(), {"default": _sampler_names()[0]}), + "scheduler": (_scheduler_names(), {"default": _scheduler_names()[0]}), + "iterations": ("INT", {"default": 1, "min": 1, "max": 1000}), + "steps_delta": ("FLOAT", {"default": 0.0, "min": -1000.0, "max": 1000.0, "step": 0.01}), + "cfg_delta": ("FLOAT", {"default": 0.0, "min": -100.0, "max": 100.0, "step": 0.01}), + "denoise_delta": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.0001}), + "apply_sharpen": ("BOOLEAN", {"default": False}), + "apply_upscale": ("BOOLEAN", {"default": False}), + "apply_ids": ("BOOLEAN", {"default": False}), + "clip_clean": ("BOOLEAN", {"default": False}), + "ids_strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), + "upscale_method": (MagicUpscaleModule.upscale_methods, {"default": "lanczos"}), + "scale_by": ("FLOAT", {"default": 1.2, "min": 1.0, "max": 8.0, "step": 0.01}), + "scale_delta": ("FLOAT", {"default": 0.0, "min": -8.0, "max": 8.0, "step": 0.01}), + "noise_offset": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 0.5, "step": 0.01}), + "threshold": ("FLOAT", {"default": 0.03, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "RMS latent drift threshold (smaller = more damping)."}), + }, + "optional": { + "Sharpnes_strenght": ("FLOAT", {"default": 0.300, "min": 0.0, "max": 1.0, "step": 0.001}), + "latent_compare": ("BOOLEAN", {"default": False, "tooltip": "Use latent drift to gently damp params (safer than overwriting latents)."}), + "accumulation": (["default", "fp32+fp16", "fp32+fp32"], {"default": "default", "tooltip": "Override SageAttention PV accumulation mode for this node run."}), + "reference_clean": ("BOOLEAN", {"default": False, "tooltip": "Use CLIP-Vision similarity to a reference image to stabilize output."}), + "reference_image": ("IMAGE", {}), + "clip_vision": ("CLIP_VISION", {}), + "ref_preview": ("INT", {"default": 224, "min": 64, "max": 512, "step": 16}), + "ref_threshold": ("FLOAT", {"default": 0.03, "min": 0.0, "max": 0.2, "step": 0.001}), + "ref_cooldown": ("INT", {"default": 1, "min": 1, "max": 8}), + + # ONNX detectors (beta) unified toggle for Hands/Face/Pose + "onnx_detectors": ("BOOLEAN", {"default": False, "tooltip": "Use auto ONNX detectors (any .onnx in models) to refine artifact mask."}), + "onnx_preview": ("INT", {"default": 224, "min": 64, "max": 512, "step": 16, "tooltip": "Square preview size fed to ONNX models."}), + "onnx_sensitivity": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 2.0, "step": 0.01, "tooltip": "Global gain for fused ONNX mask."}), + "onnx_anomaly_gain": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 3.0, "step": 0.01, "tooltip": "Extra gain for 'anomaly' models (e.g., anomaly_det.onnx)."}), + + # Guidance controls + "guidance_mode": (["default", "RescaleCFG", "RescaleFDG", "CFGZero*", "CFGZeroFD", "ZeResFDG"], {"default": "RescaleCFG", "tooltip": "Rescale (stable), RescaleFDG (spectral), CFGZero*, CFGZeroFD, or hybrid ZeResFDG."}), + "rescale_multiplier": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Blend between rescaled and plain CFG (like comfy RescaleCFG)."}), + "momentum_beta": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 0.95, "step": 0.01, "tooltip": "EMA momentum in eps-space for (cond-uncond), 0 to disable."}), + "cfg_curve": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "S-curve shaping of cond_scale across steps (0=flat)."}), + "perp_damp": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Remove a small portion of the component parallel to previous delta (0-1)."}), + + # NAG (Normalized Attention Guidance) toggles + "use_nag": ("BOOLEAN", {"default": False, "tooltip": "Apply NAG inside CrossAttention (positive branch) during this node."}), + "nag_scale": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 50.0, "step": 0.1}), + "nag_tau": ("FLOAT", {"default": 2.5, "min": 0.0, "max": 10.0, "step": 0.01}), + "nag_alpha": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01}), + + # CFGZero* extras + "use_zero_init": ("BOOLEAN", {"default": False, "tooltip": "For CFGZero*, zero out first few steps."}), + "zero_init_steps": ("INT", {"default": 0, "min": 0, "max": 20, "step": 1}), + + # FDG controls (placed last to avoid reordering existing fields) + "fdg_low": ("FLOAT", {"default": 0.6, "min": 0.0, "max": 2.0, "step": 0.01, "tooltip": "Low-frequency gain (<1 to restrain masses)."}), + "fdg_high": ("FLOAT", {"default": 1.3, "min": 0.5, "max": 2.5, "step": 0.01, "tooltip": "High-frequency gain (>1 to boost details)."}), + "fdg_sigma": ("FLOAT", {"default": 1.0, "min": 0.5, "max": 2.5, "step": 0.05, "tooltip": "Gaussian sigma for FDG low-pass split."}), + "ze_res_zero_steps": ("INT", {"default": 2, "min": 0, "max": 20, "step": 1, "tooltip": "Hybrid: number of initial steps to use CFGZeroFD before switching to RescaleFDG."}), + + # Adaptive spectral switch (ZeRes) and adaptive low gain + "ze_adaptive": ("BOOLEAN", {"default": False, "tooltip": "Enable spectral switch: CFGZeroFD, RescaleFDG by HF/LF ratio (EMA)."}), + "ze_r_switch_hi": ("FLOAT", {"default": 0.60, "min": 0.10, "max": 0.95, "step": 0.01, "tooltip": "Switch to RescaleFDG when EMA fraction of high-frequency."}), + "ze_r_switch_lo": ("FLOAT", {"default": 0.45, "min": 0.05, "max": 0.90, "step": 0.01, "tooltip": "Switch back to CFGZeroFD when EMA fraction (hysteresis)."}), + "fdg_low_adaptive": ("BOOLEAN", {"default": False, "tooltip": "Adapt fdg_low by HF fraction (EMA)."}), + "fdg_low_min": ("FLOAT", {"default": 0.45, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Lower bound for adaptive fdg_low."}), + "fdg_low_max": ("FLOAT", {"default": 0.70, "min": 0.0, "max": 2.0, "step": 0.01, "tooltip": "Upper bound for adaptive fdg_low."}), + "fdg_ema_beta": ("FLOAT", {"default": 0.80, "min": 0.0, "max": 0.99, "step": 0.01, "tooltip": "EMA smoothing for spectral ratio (higher = smoother)."}), + + # ONNX local guidance (placed last to avoid reordering) + "onnx_local_guidance": ("BOOLEAN", {"default": False, "tooltip": "Modulate guidance spatially by ONNX mask."}), + "onnx_mask_inside": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Multiplier for guidance inside mask (protects)."}), + "onnx_mask_outside": ("FLOAT", {"default": 1.0, "min": 0.5, "max": 2.0, "step": 0.01, "tooltip": "Multiplier for guidance outside mask."}), + "onnx_debug": ("BOOLEAN", {"default": False, "tooltip": "Print ONNX mask area per iteration."}), + + # ONNX wholebody keypoints local heatmap (placed last) + "onnx_kpts_enable": ("BOOLEAN", {"default": False, "tooltip": "Parse YOLO wholebody keypoints and add local heatmap."}), + "onnx_kpts_sigma": ("FLOAT", {"default": 2.5, "min": 0.5, "max": 8.0, "step": 0.1, "tooltip": "Keypoint Gaussian sigma multiplier."}), + "onnx_kpts_gain": ("FLOAT", {"default": 1.5, "min": 0.1, "max": 5.0, "step": 0.1, "tooltip": "Keypoint heat amplitude multiplier."}), + "onnx_kpts_conf": ("FLOAT", {"default": 0.20, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Keypoint confidence threshold."}), + + # Muse Blend global directional post-mix + "muse_blend": ("BOOLEAN", {"default": False, "tooltip": "Enable Muse Blend: gentle directional positive blend (global)."}), + "muse_blend_strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Overall influence of Muse Blend over baseline CFG (0..1)."}), + # Exposure Bias Correction (epsilon scaling) + "eps_scale_enable": ("BOOLEAN", {"default": False, "tooltip": "Exposure Bias Correction: scale predicted noise early in schedule."}), + "eps_scale": ("FLOAT", {"default": 0.005, "min": -1.0, "max": 1.0, "step": 0.0005, "tooltip": "Signed scaling near early steps (recommended ~0.0045; use with care)."}), + "clipseg_enable": ("BOOLEAN", {"default": False, "tooltip": "Use CLIPSeg to build a text-driven mask (e.g., 'eyes | hands | face')."}), + "clipseg_text": ("STRING", {"default": "", "multiline": False}), + "clipseg_preview": ("INT", {"default": 224, "min": 64, "max": 512, "step": 16}), + "clipseg_threshold": ("FLOAT", {"default": 0.40, "min": 0.0, "max": 1.0, "step": 0.05}), + "clipseg_blur": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 15.0, "step": 0.1}), + "clipseg_dilate": ("INT", {"default": 4, "min": 0, "max": 10, "step": 1}), + "clipseg_gain": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 3.0, "step": 0.01}), + "clipseg_blend": (["fuse", "replace", "intersect"], {"default": "fuse", "tooltip": "How to combine CLIPSeg with ONNX mask."}), + "clipseg_ref_gate": ("BOOLEAN", {"default": False, "tooltip": "If reference provided, boost mask when far from reference (CLIP-Vision)."}), + "clipseg_ref_threshold": ("FLOAT", {"default": 0.03, "min": 0.0, "max": 0.2, "step": 0.001}), + + # Polish mode (final hi-res refinement) + "polish_enable": ("BOOLEAN", {"default": False, "tooltip": "Polish: keep low-frequency shape from reference while allowing high-frequency details to refine."}), + "polish_keep_low": ("FLOAT", {"default": 0.4, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "How much low-frequency (global form, lighting) to take from reference image (0=use current, 1=use reference)."}), + "polish_edge_lock": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Edge lock strength: protects edges from sideways drift (0=off, 1=strong)."}), + "polish_sigma": ("FLOAT", {"default": 1.0, "min": 0.3, "max": 3.0, "step": 0.1, "tooltip": "Radius for low/high split: larger keeps bigger shapes as 'low' (global form)."}), + "polish_start_after": ("INT", {"default": 1, "min": 0, "max": 3, "step": 1, "tooltip": "Enable polish after N iterations (0=immediately)."}), + "polish_keep_low_ramp": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Starting share of low-frequency mix; ramps to polish_keep_low over remaining iterations."}), + + }, + } + + RETURN_TYPES = ("LATENT", "IMAGE", "INT", "FLOAT", "FLOAT", "IMAGE") + RETURN_NAMES = ("LATENT", "IMAGE", "steps", "cfg", "denoise", "mask_preview") + FUNCTION = "apply_cade2" + CATEGORY = "MagicNodes" + + def apply_cade2(self, model, vae, positive, negative, latent, seed, steps, cfg, denoise, + sampler_name, scheduler, noise_offset, iterations=1, steps_delta=0.0, + cfg_delta=0.0, denoise_delta=0.0, apply_sharpen=False, + apply_upscale=False, apply_ids=False, clip_clean=False, + ids_strength=0.5, upscale_method="lanczos", scale_by=1.2, scale_delta=0.0, + Sharpnes_strenght=0.300, threshold=0.03, latent_compare=False, accumulation="default", + reference_clean=False, reference_image=None, clip_vision=None, ref_preview=224, ref_threshold=0.03, ref_cooldown=1, + onnx_detectors=False, onnx_preview=224, onnx_sensitivity=0.5, onnx_anomaly_gain=1.0, + guidance_mode="RescaleCFG", rescale_multiplier=0.7, momentum_beta=0.0, cfg_curve=0.0, perp_damp=0.0, + use_nag=False, nag_scale=4.0, nag_tau=2.5, nag_alpha=0.25, + use_zero_init=False, zero_init_steps=0, + fdg_low=0.6, fdg_high=1.3, fdg_sigma=1.0, ze_res_zero_steps=2, + ze_adaptive=False, ze_r_switch_hi=0.60, ze_r_switch_lo=0.45, + fdg_low_adaptive=False, fdg_low_min=0.45, fdg_low_max=0.70, fdg_ema_beta=0.80, + onnx_local_guidance=False, onnx_mask_inside=1.0, onnx_mask_outside=1.0, onnx_debug=False, + onnx_kpts_enable=False, onnx_kpts_sigma=2.5, onnx_kpts_gain=1.5, onnx_kpts_conf=0.20, + muse_blend=False, muse_blend_strength=0.5, + eps_scale_enable=False, eps_scale=0.005, + clipseg_enable=False, clipseg_text="", clipseg_preview=224, + clipseg_threshold=0.40, clipseg_blur=7.0, clipseg_dilate=4, + clipseg_gain=1.0, clipseg_blend="fuse", clipseg_ref_gate=False, clipseg_ref_threshold=0.03, + polish_enable=False, polish_keep_low=0.4, polish_edge_lock=0.2, polish_sigma=1.0, + polish_start_after=1, polish_keep_low_ramp=0.2, + preset_step="Step 1", custom_override=False): + # Load base preset for the selected Step. When custom_override is True, + # visible UI controls (top-level) are kept from UI; hidden ones still come from preset. + try: + p = load_preset("mg_cade25", preset_step) if isinstance(preset_step, str) else {} + except Exception: + p = {} + def pv(name, cur, top=False): + return cur if (top and bool(custom_override)) else p.get(name, cur) + seed = int(pv("seed", seed, top=True)) + steps = int(pv("steps", steps, top=True)) + cfg = float(pv("cfg", cfg, top=True)) + denoise = float(pv("denoise", denoise, top=True)) + sampler_name = str(pv("sampler_name", sampler_name, top=True)) + scheduler = str(pv("scheduler", scheduler, top=True)) + iterations = int(pv("iterations", iterations)) + steps_delta = float(pv("steps_delta", steps_delta)) + cfg_delta = float(pv("cfg_delta", cfg_delta)) + denoise_delta = float(pv("denoise_delta", denoise_delta)) + apply_sharpen = bool(pv("apply_sharpen", apply_sharpen)) + apply_upscale = bool(pv("apply_upscale", apply_upscale)) + apply_ids = bool(pv("apply_ids", apply_ids)) + clip_clean = bool(pv("clip_clean", clip_clean)) + ids_strength = float(pv("ids_strength", ids_strength)) + upscale_method = str(pv("upscale_method", upscale_method)) + scale_by = float(pv("scale_by", scale_by)) + scale_delta = float(pv("scale_delta", scale_delta)) + noise_offset = float(pv("noise_offset", noise_offset)) + threshold = float(pv("threshold", threshold)) + Sharpnes_strenght = float(pv("Sharpnes_strenght", Sharpnes_strenght)) + latent_compare = bool(pv("latent_compare", latent_compare)) + accumulation = str(pv("accumulation", accumulation)) + reference_clean = bool(pv("reference_clean", reference_clean)) + ref_preview = int(pv("ref_preview", ref_preview)) + ref_threshold = float(pv("ref_threshold", ref_threshold)) + ref_cooldown = int(pv("ref_cooldown", ref_cooldown)) + onnx_detectors = bool(pv("onnx_detectors", onnx_detectors)) + onnx_preview = int(pv("onnx_preview", onnx_preview)) + onnx_sensitivity = float(pv("onnx_sensitivity", onnx_sensitivity)) + onnx_anomaly_gain = float(pv("onnx_anomaly_gain", onnx_anomaly_gain)) + guidance_mode = str(pv("guidance_mode", guidance_mode)) + rescale_multiplier = float(pv("rescale_multiplier", rescale_multiplier)) + momentum_beta = float(pv("momentum_beta", momentum_beta)) + cfg_curve = float(pv("cfg_curve", cfg_curve)) + perp_damp = float(pv("perp_damp", perp_damp)) + use_nag = bool(pv("use_nag", use_nag)) + nag_scale = float(pv("nag_scale", nag_scale)) + nag_tau = float(pv("nag_tau", nag_tau)) + nag_alpha = float(pv("nag_alpha", nag_alpha)) + use_zero_init = bool(pv("use_zero_init", use_zero_init)) + zero_init_steps = int(pv("zero_init_steps", zero_init_steps)) + fdg_low = float(pv("fdg_low", fdg_low)) + fdg_high = float(pv("fdg_high", fdg_high)) + fdg_sigma = float(pv("fdg_sigma", fdg_sigma)) + ze_res_zero_steps = int(pv("ze_res_zero_steps", ze_res_zero_steps)) + ze_adaptive = bool(pv("ze_adaptive", ze_adaptive)) + ze_r_switch_hi = float(pv("ze_r_switch_hi", ze_r_switch_hi)) + ze_r_switch_lo = float(pv("ze_r_switch_lo", ze_r_switch_lo)) + fdg_low_adaptive = bool(pv("fdg_low_adaptive", fdg_low_adaptive)) + fdg_low_min = float(pv("fdg_low_min", fdg_low_min)) + fdg_low_max = float(pv("fdg_low_max", fdg_low_max)) + fdg_ema_beta = float(pv("fdg_ema_beta", fdg_ema_beta)) + # AQClip-Lite (hidden in Easy UI, controllable via presets) + aqclip_enable = bool(pv("aqclip_enable", False)) + aq_tile = int(pv("aq_tile", 32)) + aq_stride = int(pv("aq_stride", 16)) + aq_alpha = float(pv("aq_alpha", 2.0)) + aq_ema_beta = float(pv("aq_ema_beta", 0.8)) + midfreq_enable = bool(pv("midfreq_enable", False)) + midfreq_gain = float(pv("midfreq_gain", 0.0)) + midfreq_sigma_lo = float(pv("midfreq_sigma_lo", 0.8)) + midfreq_sigma_hi = float(pv("midfreq_sigma_hi", 2.0)) + onnx_local_guidance = bool(pv("onnx_local_guidance", onnx_local_guidance)) + onnx_mask_inside = float(pv("onnx_mask_inside", onnx_mask_inside)) + onnx_mask_outside = float(pv("onnx_mask_outside", onnx_mask_outside)) + onnx_debug = bool(pv("onnx_debug", onnx_debug)) + onnx_kpts_enable = bool(pv("onnx_kpts_enable", onnx_kpts_enable)) + onnx_kpts_sigma = float(pv("onnx_kpts_sigma", onnx_kpts_sigma)) + onnx_kpts_gain = float(pv("onnx_kpts_gain", onnx_kpts_gain)) + onnx_kpts_conf = float(pv("onnx_kpts_conf", onnx_kpts_conf)) + muse_blend = bool(pv("muse_blend", muse_blend)) + muse_blend_strength = float(pv("muse_blend_strength", muse_blend_strength)) + eps_scale_enable = bool(pv("eps_scale_enable", eps_scale_enable)) + eps_scale = float(pv("eps_scale", eps_scale)) + clipseg_enable = bool(pv("clipseg_enable", clipseg_enable)) + clipseg_text = str(pv("clipseg_text", clipseg_text, top=True)) + clipseg_preview = int(pv("clipseg_preview", clipseg_preview)) + clipseg_threshold = float(pv("clipseg_threshold", clipseg_threshold)) + clipseg_blur = float(pv("clipseg_blur", clipseg_blur)) + clipseg_dilate = int(pv("clipseg_dilate", clipseg_dilate)) + clipseg_gain = float(pv("clipseg_gain", clipseg_gain)) + clipseg_blend = str(pv("clipseg_blend", clipseg_blend)) + clipseg_ref_gate = bool(pv("clipseg_ref_gate", clipseg_ref_gate)) + clipseg_ref_threshold = float(pv("clipseg_ref_threshold", clipseg_ref_threshold)) + polish_enable = bool(pv("polish_enable", polish_enable)) + polish_keep_low = float(pv("polish_keep_low", polish_keep_low)) + polish_edge_lock = float(pv("polish_edge_lock", polish_edge_lock)) + polish_sigma = float(pv("polish_sigma", polish_sigma)) + polish_start_after = int(pv("polish_start_after", polish_start_after)) + polish_keep_low_ramp = float(pv("polish_keep_low_ramp", polish_keep_low_ramp)) + # CADE Seg: per-step toggle to include CF edges into Seg mask + seg_use_cf_edges = bool(pv("seg_use_cf_edges", True)) + # Hard reset of any sticky globals from prior runs + try: + global CURRENT_ONNX_MASK_BCHW, _ONNX_KPTS_ENABLE, _ONNX_KPTS_SIGMA, _ONNX_KPTS_GAIN, _ONNX_KPTS_CONF + CURRENT_ONNX_MASK_BCHW = None + # Reset KPTS toggles to sane defaults; they will be set again if enabled below + _ONNX_KPTS_ENABLE = False + _ONNX_KPTS_SIGMA = 2.5 + _ONNX_KPTS_GAIN = 1.5 + _ONNX_KPTS_CONF = 0.20 + except Exception: + pass + + image = safe_decode(vae, latent) + + tuned_steps, tuned_cfg, tuned_denoise = AdaptiveSamplerHelper().tune( + image, steps, cfg, denoise) + + current_steps = tuned_steps + current_cfg = tuned_cfg + current_denoise = tuned_denoise + # Work on a detached copy to avoid mutating input latent across runs + try: + current_latent = {"samples": latent["samples"].clone()} + except Exception: + current_latent = {"samples": latent["samples"]} + current_scale = scale_by + + # Derive a user-friendly step tag for logs + try: + _ps = str(preset_step) + _num = ''.join(ch for ch in _ps if ch.isdigit()) + step_tag = f"Step:{_num}" if _num else _ps + except Exception: + step_tag = str(preset_step) + + # Smart seed selection (Sobol + light probing) when effective seed==0 and not in custom override mode + try: + if int(seed) == 0 and not bool(custom_override): + seed = _smart_seed_select( + model, vae, positive, negative, current_latent, + str(sampler_name), str(scheduler), float(current_cfg), float(current_denoise), + base_seed=0, step_tag=step_tag, + clip_vision=clip_vision, reference_image=reference_image, clipseg_text=str(clipseg_text)) + except Exception: + pass + + # Visual separation and start marker after seed is finalized + try: + print("") + except Exception: + pass + try: + print(f"\x1b[32m==== {step_tag}, Starting main job ====\x1b[0m") + except Exception: + pass + + ref_embed = None + if reference_clean and (clip_vision is not None) and (reference_image is not None): + try: + ref_embed = _encode_clip_image(reference_image, clip_vision, ref_preview) + except Exception: + ref_embed = None + + # Pre-disable any lingering NAG patch from previous runs and set PV accumulation for this node + try: + sa_patch.enable_crossattention_nag_patch(False) + except Exception: + pass + prev_accum = getattr(sa_patch, "CURRENT_PV_ACCUM", None) + sa_patch.CURRENT_PV_ACCUM = None if accumulation == "default" else accumulation + # Enable NAG patch if requested + try: + sa_patch.enable_crossattention_nag_patch(bool(use_nag), float(nag_scale), float(nag_tau), float(nag_alpha)) + except Exception: + pass + + # Enable attention-entropy probe for AQClip Attn-mode (read from preset) + try: + aq_attn = bool(p.get("aq_attn", False)) if isinstance(p, dict) else False + if hasattr(sa_patch, "enable_attention_entropy_capture"): + sa_patch.enable_attention_entropy_capture(aq_attn, max_tokens=1024, max_heads=4) + except Exception: + pass + + # Enable KV pruning for self-attention (read from preset) + try: + kv_enable = bool(p.get("kv_prune_enable", False)) if isinstance(p, dict) else False + kv_keep = float(p.get("kv_keep", 0.85)) if isinstance(p, dict) else 0.85 + kv_min_tokens = int(p.get("kv_min_tokens", 128)) if isinstance(p, dict) else 128 + if hasattr(sa_patch, "set_kv_prune"): + sa_patch.set_kv_prune(kv_enable, kv_keep, kv_min_tokens) + except Exception: + pass + + onnx_mask_last = None + try: + with torch.inference_mode(): + __cade_noop = 0 # ensure non-empty with-block + + # Preflight: reset sticky state and build external masks once (CPU-pinned) + try: + CURRENT_ONNX_MASK_BCHW = None + except Exception: + pass + pre_mask = None + pre_area = 0.0 + # ONNX detectors disabled in Easy: prefer CLIPSeg + edge fusion + onnx_detectors = False + # Build CLIPSeg mask once + if bool(clipseg_enable) and isinstance(clipseg_text, str) and clipseg_text.strip() != "": + try: + cmask = _clipseg_build_mask(image, clipseg_text, int(clipseg_preview), float(clipseg_threshold), float(clipseg_blur), int(clipseg_dilate), float(clipseg_gain), None, None, float(clipseg_ref_threshold)) + if cmask is not None: + if pre_mask is None: + pre_mask = cmask + else: + if clipseg_blend == "replace": + pre_mask = cmask + elif clipseg_blend == "intersect": + pre_mask = (pre_mask * cmask).clamp(0, 1) + else: + pre_mask = (1.0 - (1.0 - pre_mask) * (1.0 - cmask)).clamp(0, 1) + except Exception: + pass + # Edge mask from ControlFusion Step (with depth gating) when enabled; fallback to Sobel + if bool(seg_use_cf_edges): + try: + emask = _build_cf_edge_mask_from_step(image, str(preset_step)) + except Exception: + emask = None + if emask is None: + try: + emask = _edge_mask(image, threshold=0.20, blur=1.0) + except Exception: + emask = None + if emask is not None: + pre_mask = emask if pre_mask is None else (1.0 - (1.0 - pre_mask) * (1.0 - emask)).clamp(0, 1) + if pre_mask is not None: + onnx_mask_last = pre_mask + om = pre_mask.movedim(-1, 1) + pre_area = float(om.mean().item()) + if bool(onnx_local_guidance): + try: + if 0.02 <= pre_area <= 0.35: + CURRENT_ONNX_MASK_BCHW = om.clamp(0, 1).to(model_management.get_torch_device()) + else: + CURRENT_ONNX_MASK_BCHW = None + except Exception: + CURRENT_ONNX_MASK_BCHW = None + # One-time damping from area (disabled by default) + if False: + try: + if pre_area > 0.005: + damp = 1.0 - min(0.04, 0.008 + pre_area * 0.02) + current_denoise = max(0.10, current_denoise * damp) + current_cfg = max(1.0, current_cfg * (1.0 - 0.003)) + except Exception: + pass + # Preflight symmetry disabled (kept for experiments only) + if False: + try: + img0 = image + sym_mask = _clipseg_build_mask(img0, "face | head | torso | shoulders", preview=int(clipseg_preview), threshold=0.45, blur=5.0, dilate=2, gain=1.0) + if sym_mask is not None: + img_sym = _soft_symmetry_blend(img0, sym_mask, alpha=0.012, lp_sigma=1.75) + current_latent = {"samples": safe_encode(vae, img_sym)} + image = img_sym + except Exception: + pass + # Compact status + try: + provs = [] + if _ONNX_RT is not None: + provs = list(_ONNX_RT.get_available_providers()) + clipseg_status = "on" if bool(clipseg_enable) and isinstance(clipseg_text, str) and clipseg_text.strip() != "" else "off" + kpts = f"kpts={'on' if bool(onnx_kpts_enable) else 'off'} sigma={float(onnx_kpts_sigma):.2f} gain={float(onnx_kpts_gain):.2f} conf={float(onnx_kpts_conf):.2f}" + # print preflight info only in debug sessions (muted by default) + if False: + print(f"[CADE2.5][preflight] onnx_sessions={len(_ONNX_SESS)} providers={provs if provs else ['CPU']} clipseg={clipseg_status} device={'cpu' if _CLIPSEG_FORCE_CPU else _CLIPSEG_DEV} mask_area={pre_area:.4f} {kpts}") + except Exception: + pass + # Freeze per-iteration external mask rebuild + onnx_detectors = False + clipseg_enable = False + # Depth gate cache for micro-detail injection (reuse per resolution) + depth_gate_cache = {"size": None, "mask": None} + for i in range(iterations): + if i % 2 == 0: + clear_gpu_and_ram_cache() + + prev_samples = current_latent["samples"].clone().detach() + + iter_seed = seed + i * 7777 + if noise_offset > 0.0: + # Deterministic noise offset tied to iter_seed + fade = 1.0 - (i / max(1, iterations)) + try: + gen = torch.Generator(device='cpu') + except Exception: + gen = torch.Generator() + gen.manual_seed(int(iter_seed) & 0xFFFFFFFF) + eps = torch.randn( + size=current_latent["samples"].shape, + dtype=current_latent["samples"].dtype, + device='cpu', + generator=gen, + ).to(current_latent["samples"].device) + current_latent["samples"] += (noise_offset * fade) * eps + + # Pre-sampling ONNX detectors: handled once below (kept compact) + + # Pre-sampling ONNX detectors (build mask and optionally adjust params for this iteration) + if onnx_detectors and (i % max(1, 1) == 0): + try: + import os + models_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "models") + img_preview = safe_decode(vae, current_latent) + # Set toggles for this iteration + globals()["_ONNX_DEBUG"] = bool(onnx_debug) + globals()["_ONNX_COUNT_DEBUG"] = True # force counts ON for debugging session + globals()["_ONNX_KPTS_ENABLE"] = bool(onnx_kpts_enable) + globals()["_ONNX_KPTS_SIGMA"] = float(onnx_kpts_sigma) + globals()["_ONNX_KPTS_GAIN"] = float(onnx_kpts_gain) + globals()["_ONNX_KPTS_CONF"] = float(onnx_kpts_conf) + onnx_mask = _onnx_build_mask(img_preview, int(onnx_preview), float(onnx_sensitivity), models_dir, float(onnx_anomaly_gain)) + if onnx_mask is not None: + onnx_mask_last = onnx_mask + om = onnx_mask.movedim(-1, 1) + area = float(om.mean().item()) + if bool(onnx_debug): + print(f"[CADE2.5][ONNX] iter={i} mask_area={area:.4f}") + if area > 0.005: + damp = 1.0 - min(0.25, 0.06 + onnx_sensitivity * 0.05 + area * 0.25) + current_denoise = max(0.10, current_denoise * damp) + current_cfg = max(1.0, current_cfg * (1.0 - 0.015 * onnx_sensitivity)) + # Prepare spatial mask for cfg_func if requested + if bool(onnx_local_guidance): + # store BCHW mask in global for this iteration (only for reasonable areas) + if 0.02 <= area <= 0.35: + CURRENT_ONNX_MASK_BCHW = om.clamp(0, 1).to(model_management.get_torch_device()) + else: + CURRENT_ONNX_MASK_BCHW = None + else: + CURRENT_ONNX_MASK_BCHW = None + except Exception: + CURRENT_ONNX_MASK_BCHW = None + + # CF edge mask (from current image) and fusion (only when enabled) + if bool(seg_use_cf_edges): + try: + img_prev2 = safe_decode(vae, current_latent) + em2 = _build_cf_edge_mask_from_step(img_prev2, str(preset_step)) + if em2 is not None: + if onnx_mask_last is None: + onnx_mask_last = em2 + else: + onnx_mask_last = (1.0 - (1.0 - onnx_mask_last) * (1.0 - em2)).clamp(0, 1) + om = onnx_mask_last.movedim(-1, 1) + area = float(om.mean().item()) + if bool(onnx_local_guidance): + if 0.02 <= area <= 0.35: + CURRENT_ONNX_MASK_BCHW = om.clamp(0, 1).to(model_management.get_torch_device()) + else: + CURRENT_ONNX_MASK_BCHW = None + except Exception: + pass + + # CLIPSeg mask (optional) and fusion with ONNX + try: + if bool(clipseg_enable) and isinstance(clipseg_text, str) and clipseg_text.strip() != "": + cmask = _clipseg_build_mask(img_prev2, clipseg_text, int(clipseg_preview), float(clipseg_threshold), float(clipseg_blur), int(clipseg_dilate), float(clipseg_gain), ref_embed if bool(clipseg_ref_gate) else None, clip_vision if bool(clipseg_ref_gate) else None, float(clipseg_ref_threshold)) + if cmask is not None: + if onnx_mask_last is None: + fused = cmask + else: + if clipseg_blend == "replace": + fused = cmask + elif clipseg_blend == "intersect": + fused = (onnx_mask_last * cmask).clamp(0, 1) + else: + fused = (1.0 - (1.0 - onnx_mask_last) * (1.0 - cmask)).clamp(0, 1) + onnx_mask_last = fused + om = fused.movedim(-1, 1) + area = float(om.mean().item()) + if bool(onnx_debug): + print(f"[CADE2.5][MASK] iter={i} mask_area={area:.4f}") + if area > 0.005: + damp = 1.0 - min(0.10, 0.02 + float(onnx_sensitivity) * 0.02 + area * 0.08) + current_denoise = max(0.10, current_denoise * damp) + current_cfg = max(1.0, current_cfg * (1.0 - 0.008 * float(onnx_sensitivity))) + if bool(onnx_local_guidance): + if 0.02 <= area <= 0.35: + CURRENT_ONNX_MASK_BCHW = om.clamp(0, 1).to(model_management.get_torch_device()) + else: + CURRENT_ONNX_MASK_BCHW = None + except Exception: + pass + + # Guidance override via cfg_func when requested + sampler_model = _wrap_model_with_guidance( + model, guidance_mode, rescale_multiplier, momentum_beta, cfg_curve, perp_damp, + use_zero_init=bool(use_zero_init), zero_init_steps=int(zero_init_steps), + fdg_low=float(fdg_low), fdg_high=float(fdg_high), fdg_sigma=float(fdg_sigma), + midfreq_enable=bool(midfreq_enable), midfreq_gain=float(midfreq_gain), midfreq_sigma_lo=float(midfreq_sigma_lo), midfreq_sigma_hi=float(midfreq_sigma_hi), + ze_zero_steps=int(ze_res_zero_steps), + ze_adaptive=bool(ze_adaptive), ze_r_switch_hi=float(ze_r_switch_hi), ze_r_switch_lo=float(ze_r_switch_lo), + fdg_low_adaptive=bool(fdg_low_adaptive), fdg_low_min=float(fdg_low_min), fdg_low_max=float(fdg_low_max), fdg_ema_beta=float(fdg_ema_beta), + use_local_mask=bool(onnx_local_guidance), mask_inside=float(onnx_mask_inside), mask_outside=float(onnx_mask_outside), + mahiro_plus_enable=bool(muse_blend), mahiro_plus_strength=float(muse_blend_strength), + eps_scale_enable=bool(eps_scale_enable), eps_scale=float(eps_scale) + ) + + # Local best-of-2 in ROI (hands/face), only on early iteration to limit overhead + try: + do_local_refine = False # disable local best-of-2 by default + if do_local_refine: + img_roi = safe_decode(vae, current_latent) + roi = _clipseg_build_mask(img_roi, "hand | hands | face", preview=max(192, int(clipseg_preview//2)), threshold=0.40, blur=5.0, dilate=2, gain=1.0) + if roi is None and onnx_mask_last is not None: + roi = torch.clamp(onnx_mask_last, 0.0, 1.0) + if roi is not None: + # Area gating + try: + ra = float(roi.mean().item()) + except Exception: + ra = 0.0 + if not (0.02 <= ra <= 0.15): + raise Exception("ROI area out of range; skip local_refine") + # Light erosion to avoid halo influence + try: + m = roi[..., 0].unsqueeze(1) + # disable erosion effect (kernel=1) + ero = 1.0 - F.max_pool2d(1.0 - m, kernel_size=1, stride=1, padding=0) + roi = ero.clamp(0, 1).movedim(1, -1) + except Exception: + pass + # micro sampling params + micro_steps = int(max(2, min(4, round(max(1, current_steps) * 0.05)))) + micro_denoise = float(min(0.22, max(0.10, current_denoise * 0.30))) + s1 = int((iter_seed ^ 0x9E3779B1) & 0xFFFFFFFFFFFFFFFF) + s2 = int((iter_seed ^ 0x85EBCA77) & 0xFFFFFFFFFFFFFFFF) + # Candidate A + lat_in_a = {"samples": current_latent["samples"].clone()} + lat_a, = nodes.common_ksampler( + sampler_model, s1, micro_steps, current_cfg, sampler_name, scheduler, + positive, negative, lat_in_a, denoise=micro_denoise) + img_a = safe_decode(vae, lat_a) + # Candidate B + lat_in_b = {"samples": current_latent["samples"].clone()} + lat_b, = nodes.common_ksampler( + sampler_model, s2, micro_steps, current_cfg, sampler_name, scheduler, + positive, negative, lat_in_b, denoise=micro_denoise) + img_b = safe_decode(vae, lat_b) + + # Score inside ROI + def _roi_stats(img, roi_mask): + try: + m = roi_mask[..., 0].clamp(0, 1) + R, G, Bc = img[..., 0], img[..., 1], img[..., 2] + lum = (0.2126 * R + 0.7152 * G + 0.0722 * Bc) + # edges + kx = torch.tensor([[-1,0,1],[-2,0,2],[-1,0,1]], device=img.device, dtype=img.dtype).view(1,1,3,3) + ky = torch.tensor([[-1,-2,-1],[0,0,0],[1,2,1]], device=img.device, dtype=img.dtype).view(1,1,3,3) + gx = F.conv2d(lum.unsqueeze(1), kx, padding=1) + gy = F.conv2d(lum.unsqueeze(1), ky, padding=1) + g = torch.sqrt(gx*gx + gy*gy).squeeze(1) + wmean = (g*m).mean() / (m.mean()+1e-6) + # speckles + V = torch.maximum(R, torch.maximum(G, Bc)) + mi = torch.minimum(R, torch.minimum(G, Bc)) + S = 1.0 - (mi / (V + 1e-6)) + cand = (V > 0.98) & (S < 0.12) + speck = (cand.float()*m).mean() / (m.mean()+1e-6) + lmean = (lum*m).mean() / (m.mean()+1e-6) + return float(wmean.item()), float(speck.item()), float(lmean.item()) + except Exception: + return 0.0, 0.5, 0.5 + + ed_a, sp_a, lm_a = _roi_stats(img_a, roi) + ed_b, sp_b, lm_b = _roi_stats(img_b, roi) + edge_target = 0.08 + score_a = -abs(ed_a - edge_target) - 0.8*sp_a - 0.10*abs(lm_a - 0.5) + score_b = -abs(ed_b - edge_target) - 0.8*sp_b - 0.10*abs(lm_b - 0.5) + + # Optional CLIP-Vision ref boost + if ref_embed is not None and clip_vision is not None: + try: + emb_a = _encode_clip_image(img_a, clip_vision, target_res=224) + emb_b = _encode_clip_image(img_b, clip_vision, target_res=224) + sim_a = float((emb_a * ref_embed).sum(dim=-1).mean().clamp(-1.0, 1.0).item()) + sim_b = float((emb_b * ref_embed).sum(dim=-1).mean().clamp(-1.0, 1.0).item()) + score_a += 0.25 * (0.5*(sim_a+1.0)) + score_b += 0.25 * (0.5*(sim_b+1.0)) + except Exception: + pass + + if score_b > score_a: + current_latent = lat_b + else: + current_latent = lat_a + except Exception: + pass + + if str(scheduler) == "MGHybrid": + try: + # Build ZeSmart hybrid sigmas with safe defaults + sigmas = _build_hybrid_sigmas( + sampler_model, int(current_steps), str(sampler_name), "hybrid", + mix=0.5, denoise=float(current_denoise), jitter=0.01, seed=int(iter_seed), + _debug=False, tail_smooth=0.15, auto_hybrid_tail=True, auto_tail_strength=0.4, + ) + # Prepare latent + noise like in MG_ZeSmartSampler + lat_img = current_latent["samples"] + lat_img = _sample.fix_empty_latent_channels(sampler_model, lat_img) + batch_inds = current_latent.get("batch_index", None) + noise = _sample.prepare_noise(lat_img, int(iter_seed), batch_inds) + noise_mask = current_latent.get("noise_mask", None) + callback = nodes.latent_preview.prepare_callback(sampler_model, int(current_steps)) + disable_pbar = not _utils.PROGRESS_BAR_ENABLED + sampler_obj = _samplers.sampler_object(str(sampler_name)) + samples = _sample.sample_custom( + sampler_model, noise, float(current_cfg), sampler_obj, sigmas, + positive, negative, lat_img, + noise_mask=noise_mask, callback=callback, + disable_pbar=disable_pbar, seed=int(iter_seed) + ) + current_latent = {**current_latent} + current_latent["samples"] = samples + except Exception as e: + # Fallback to original path if anything goes wrong + print(f"[CADE2.5][MGHybrid] fallback to common_ksampler due to: {e}") + current_latent, = nodes.common_ksampler( + sampler_model, iter_seed, int(current_steps), current_cfg, sampler_name, _scheduler_names()[0], + positive, negative, current_latent, denoise=current_denoise) + else: + current_latent, = nodes.common_ksampler( + sampler_model, iter_seed, int(current_steps), current_cfg, sampler_name, scheduler, + positive, negative, current_latent, denoise=current_denoise) + + if bool(latent_compare): + latent_diff = current_latent["samples"] - prev_samples + rms = torch.sqrt(torch.mean(latent_diff * latent_diff)) + drift = float(rms.item()) + if drift > float(threshold): + overshoot = max(0.0, drift - float(threshold)) + damp = 1.0 - min(0.15, overshoot * 2.0) + current_denoise = max(0.20, current_denoise * damp) + cfg_damp = 0.997 if damp > 0.9 else 0.99 + current_cfg = max(1.0, current_cfg * cfg_damp) + + # AQClip-Lite: adaptive soft clipping in latent space (before decode) + try: + if bool(aqclip_enable): + if 'aq_state' not in locals(): + aq_state = None + H_override = None + try: + if bool(aq_attn) and hasattr(sa_patch, "get_attention_entropy_map"): + Hm = sa_patch.get_attention_entropy_map(clear=False) + if Hm is not None: + H_override = F.interpolate(Hm, size=(current_latent["samples"].shape[-2], current_latent["samples"].shape[-1]), mode="bilinear", align_corners=False) + except Exception: + H_override = None + z_new, aq_state = _aqclip_lite( + current_latent["samples"], + tile=int(aq_tile), stride=int(aq_stride), + alpha=float(aq_alpha), ema_state=aq_state, ema_beta=float(aq_ema_beta), + H_override=H_override, + ) + current_latent["samples"] = z_new + except Exception: + pass + + image = safe_decode(vae, current_latent) + + # Polish mode: keep global form (low frequencies) from reference while letting details refine + if bool(polish_enable) and (i >= int(polish_start_after)): + try: + # Prepare tensors + img = image + ref = reference_image if (reference_image is not None) else img + if ref.shape[1] != img.shape[1] or ref.shape[2] != img.shape[2]: + # resize reference to match current image + ref_n = ref.movedim(-1, 1) + ref_n = F.interpolate(ref_n, size=(img.shape[1], img.shape[2]), mode='bilinear', align_corners=False) + ref = ref_n.movedim(1, -1) + x = img.movedim(-1, 1) + r = ref.movedim(-1, 1) + # Low/high split via Gaussian blur + rad = max(1, int(round(float(polish_sigma) * 2))) + low_x = _gaussian_blur_nchw(x, sigma=float(polish_sigma), radius=rad) + low_r = _gaussian_blur_nchw(r, sigma=float(polish_sigma), radius=rad) + high_x = x - low_x + # Mix low from reference and current with ramp + # a starts from polish_keep_low_ramp and linearly ramps to polish_keep_low over remaining iterations + try: + denom = max(1, int(iterations) - int(polish_start_after)) + t = max(0.0, min(1.0, (i - int(polish_start_after)) / denom)) + except Exception: + t = 1.0 + a0 = float(polish_keep_low_ramp) + at = float(polish_keep_low) + a = a0 + (at - a0) * t + low_mix = low_r * a + low_x * (1.0 - a) + new = low_mix + high_x + # Micro-detail injection on tail: very light HF boost gated by edges+depth + try: + phase = (i + 1) / max(1, int(iterations)) + ramp = max(0.0, min(1.0, (phase - 0.70) / 0.30)) + if ramp > 0.0: + micro = x - _gaussian_blur_nchw(x, sigma=0.6, radius=1) + gray = x.mean(dim=1, keepdim=True) + sobel_x = torch.tensor([[[-1,0,1],[-2,0,2],[-1,0,1]]], dtype=gray.dtype, device=gray.device).unsqueeze(1) + sobel_y = torch.tensor([[[-1,-2,-1],[0,0,0],[1,2,1]]], dtype=gray.dtype, device=gray.device).unsqueeze(1) + gx = F.conv2d(gray, sobel_x, padding=1) + gy = F.conv2d(gray, sobel_y, padding=1) + mag = torch.sqrt(gx*gx + gy*gy) + m_edge = (mag - mag.amin()) / (mag.amax() - mag.amin() + 1e-8) + g_edge = (1.0 - m_edge).clamp(0.0, 1.0).pow(0.65) + try: + sz = (int(img.shape[1]), int(img.shape[2])) + if depth_gate_cache.get("size") != sz or depth_gate_cache.get("mask") is None: + model_path = os.path.join(os.path.dirname(__file__), '..', 'depth-anything', 'depth_anything_v2_vitl.pth') + dm = _cf_build_depth_map(img, res=512, model_path=model_path, hires_mode=True) + depth_gate_cache = {"size": sz, "mask": dm} + dm = depth_gate_cache.get("mask") + if dm is not None: + g_depth = (dm.movedim(-1, 1).clamp(0,1)) ** 1.35 + else: + g_depth = torch.ones_like(g_edge) + except Exception: + g_depth = torch.ones_like(g_edge) + g = (g_edge * g_depth).clamp(0.0, 1.0) + micro_boost = 0.018 * ramp + new = new + micro_boost * (micro * g) + except Exception: + pass + # Edge-lock: protect edges from drift by biasing toward low_mix along edges + el = float(polish_edge_lock) + if el > 1e-6: + # Sobel edge magnitude on grayscale + gray = x.mean(dim=1, keepdim=True) + sobel_x = torch.tensor([[[-1,0,1],[-2,0,2],[-1,0,1]]], dtype=gray.dtype, device=gray.device).unsqueeze(1) + sobel_y = torch.tensor([[[-1,-2,-1],[0,0,0],[1,2,1]]], dtype=gray.dtype, device=gray.device).unsqueeze(1) + gx = F.conv2d(gray, sobel_x, padding=1) + gy = F.conv2d(gray, sobel_y, padding=1) + mag = torch.sqrt(gx*gx + gy*gy) + m = (mag - mag.amin()) / (mag.amax() - mag.amin() + 1e-8) + # Blend toward low_mix near edges + new = new * (1.0 - el*m) + (low_mix) * (el*m) + img2 = new.movedim(1, -1).clamp(0,1) + # Feed back to latent for next steps + current_latent = {"samples": safe_encode(vae, img2)} + image = img2 + except Exception: + pass + + # ONNX detectors (beta): fuse hands/face/pose mask if available (post-sampling; skip if already set) + if onnx_detectors and (i % max(1, 1) == 0) and (onnx_mask_last is None): + try: + import os + models_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "models") + globals()["_ONNX_DEBUG"] = False + globals()["_ONNX_KPTS_ENABLE"] = bool(onnx_kpts_enable) + globals()["_ONNX_KPTS_SIGMA"] = float(onnx_kpts_sigma) + globals()["_ONNX_KPTS_GAIN"] = float(onnx_kpts_gain) + globals()["_ONNX_KPTS_CONF"] = float(onnx_kpts_conf) + onnx_mask = _onnx_build_mask(image, int(onnx_preview), float(onnx_sensitivity), models_dir, float(onnx_anomaly_gain)) + if onnx_mask is not None: + onnx_mask_last = onnx_mask + om = onnx_mask.movedim(-1,1) + area = float(om.mean().item()) + # verbose post-mask log removed; keep single compact log above + if area > 0.005: + damp = 1.0 - min(0.25, 0.06 + onnx_sensitivity*0.05 + area*0.25) + current_denoise = max(0.10, current_denoise * damp) + current_cfg = max(1.0, current_cfg * (1.0 - 0.015*onnx_sensitivity)) + except Exception: + pass + + if reference_clean and (ref_embed is not None) and (i % max(1, ref_cooldown) == 0): + try: + cur_embed = _encode_clip_image(image, clip_vision, ref_preview) + dist = _clip_cosine_distance(cur_embed, ref_embed) + if dist > ref_threshold: + current_denoise = max(0.10, current_denoise * 0.9) + current_cfg = max(1.0, current_cfg * 0.99) + except Exception: + pass + + if apply_upscale and current_scale != 1.0: + current_latent, image = MagicUpscaleModule().process_upscale( + current_latent, vae, upscale_method, current_scale) + # After upscale at large sizes, add a tiny HF sprinkle gated by edges+depth + try: + H, W = int(image.shape[1]), int(image.shape[2]) + if max(H, W) > 1536: + # Simple BHWC blur + def _gb_bhwc(im: torch.Tensor, radius: float, sigma: float) -> torch.Tensor: + if radius <= 0.0: + return im + pad = int(max(1, round(radius))) + ksz = pad * 2 + 1 + k = _gaussian_kernel(ksz, sigma, device=im.device).to(dtype=im.dtype) + k = k.unsqueeze(0).unsqueeze(0) + b, h, w, c = im.shape + xch = im.permute(0, 3, 1, 2) + y = F.conv2d(F.pad(xch, (pad, pad, pad, pad), mode='reflect'), k.repeat(c, 1, 1, 1), groups=c) + return y.permute(0, 2, 3, 1) + blur = _gb_bhwc(image, radius=1.0, sigma=0.8) + hf = (image - blur).clamp(-1, 1) + lum = (0.2126 * image[..., 0] + 0.7152 * image[..., 1] + 0.0722 * image[..., 2]) + kx = torch.tensor([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], device=lum.device, dtype=lum.dtype).view(1, 1, 3, 3) + ky = torch.tensor([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], device=lum.device, dtype=lum.dtype).view(1, 1, 3, 3) + g = torch.sqrt(F.conv2d(lum.unsqueeze(1), kx, padding=1)**2 + F.conv2d(lum.unsqueeze(1), ky, padding=1)**2).squeeze(1) + m = (g - g.amin()) / (g.amax() - g.amin() + 1e-8) + g_edge = (1.0 - m).clamp(0,1).pow(0.5).unsqueeze(-1) + try: + sz = (H, W) + if depth_gate_cache.get("size") != sz or depth_gate_cache.get("mask") is None: + model_path = os.path.join(os.path.dirname(__file__), '..', 'depth-anything', 'depth_anything_v2_vitl.pth') + dm = _cf_build_depth_map(image, res=512, model_path=model_path, hires_mode=True) + depth_gate_cache = {"size": sz, "mask": dm} + dm = depth_gate_cache.get("mask") + if dm is not None: + g_depth = dm.clamp(0,1) ** 1.35 + else: + g_depth = torch.ones_like(g_edge) + except Exception: + g_depth = torch.ones_like(g_edge) + g_tot = (g_edge * g_depth).clamp(0,1) + image = (image + 0.045 * hf * g_tot).clamp(0,1) + except Exception: + pass + current_cfg = max(4.0, current_cfg * (1.0 / current_scale)) + current_denoise = max(0.15, current_denoise * (1.0 / current_scale)) + + current_steps = max(1, current_steps - steps_delta) + current_cfg = max(0.0, current_cfg - cfg_delta) + current_denoise = max(0.0, current_denoise - denoise_delta) + current_scale = max(1.0, current_scale - scale_delta) + + if apply_upscale and current_scale != 1.0 and max(image.shape[1:3]) > 1024: + current_latent = {"samples": safe_encode(vae, image)} + + finally: + # Always disable NAG patch and clear local mask, even on errors + try: + sa_patch.enable_crossattention_nag_patch(False) + except Exception: + pass + # Disable KV pruning as well (avoid leaking state) + try: + if hasattr(sa_patch, "set_kv_prune"): + sa_patch.set_kv_prune(False, 1.0, 128) + except Exception: + pass + try: + sa_patch.CURRENT_PV_ACCUM = prev_accum + except Exception: + pass + try: + CURRENT_ONNX_MASK_BCHW = None + except Exception: + pass + + if apply_ids: + image, = IntelligentDetailStabilizer().stabilize(image, ids_strength) + + if apply_sharpen: + image, = _sharpen_image(image, 2, 1.0, Sharpnes_strenght) + + # ONNX mask preview as IMAGE (RGB) + if onnx_mask_last is None: + onnx_mask_last = torch.zeros((image.shape[0], image.shape[1], image.shape[2], 1), device=image.device, dtype=image.dtype) + onnx_mask_img = onnx_mask_last.repeat(1, 1, 1, 3).clamp(0, 1) + + # Final pass: remove isolated hot whites ("fireflies") without touching real edges/highlights 6.0/9.0, 0.05 + try: + image = _despeckle_fireflies(image, thr=0.998, max_iso=4.0/9.0, grad_gate=0.15) + except Exception: + pass + + return current_latent, image, int(current_steps), float(current_cfg), float(current_denoise), onnx_mask_img + + +# === Easy UI wrapper: show only top-level controls === +class CADEEasyUI(ComfyAdaptiveDetailEnhancer25): + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "preset_step": (["Step 1", "Step 2", "Step 3", "Step 4"], {"default": "Step 1", "tooltip": "Choose the Step preset. Toggle Custom below to apply UI values; otherwise Step preset values are used."}), + "custom": ("BOOLEAN", {"default": False, "tooltip": "Custom override: when enabled, your UI values override the selected Step for visible controls; hidden parameters still come from the Step preset."}), + "model": ("MODEL", {}), + "positive": ("CONDITIONING", {}), + "negative": ("CONDITIONING", {}), + "vae": ("VAE", {}), + "latent": ("LATENT", {}), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF, "control_after_generate": True, "tooltip": "Seed 0 = SmartSeed (Sobol + light probe). Non?zero = fixed seed (deterministic)."}), + "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), + "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step": 0.1}), + "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.0001}), + "sampler_name": (_sampler_names(), {"default": _sampler_names()[0]}), + "scheduler": (_scheduler_names(), {"default": "MGHybrid"}), + }, + "optional": { + # Reference inputs must remain available in Easy + "reference_image": ("IMAGE", {}), + "clip_vision": ("CLIP_VISION", {}), + # CLIPSeg prompt + "clipseg_text": ("STRING", {"default": "", "multiline": False, "tooltip": "This field tells what the step should focus on (e.g., hand, feet, face). Separate with commas."}), + } + } + + # Easy outputs (hide steps/cfg/denoise) + RETURN_TYPES = ("LATENT", "IMAGE", "IMAGE") + RETURN_NAMES = ("LATENT", "IMAGE", "mask_preview") + FUNCTION = "apply_easy" + + def apply_easy(self, + preset_step, + model, positive, negative, vae, latent, + seed, steps, cfg, denoise, sampler_name, scheduler, + clipseg_text="", reference_image=None, clip_vision=None, custom=False): + lat, img, _s, _c, _d, mask = super().apply_cade2( + model, vae, positive, negative, latent, + int(seed), int(steps), float(cfg), float(denoise), + str(sampler_name), str(scheduler), 0.0, + preset_step=str(preset_step), custom_override=bool(custom), clipseg_text=str(clipseg_text), + reference_image=reference_image, + clip_vision=clip_vision, + ) + return lat, img, mask + + # Show simpler outputs in Easy variant + RETURN_TYPES = ("LATENT", "IMAGE", "IMAGE") + RETURN_NAMES = ("LATENT", "IMAGE", "mask_preview") + FUNCTION = "apply_easy" + + def apply_easy(self, + preset_step, + model, positive, negative, vae, latent, + seed, steps, cfg, denoise, sampler_name, scheduler, + clipseg_text=""): + lat, img, _s, _c, _d, mask = super().apply_cade2( + model, vae, positive, negative, latent, + int(seed), int(steps), float(cfg), float(denoise), + str(sampler_name), str(scheduler), 0.0, + preset_step=str(preset_step), custom_override=bool(custom), clipseg_text=str(clipseg_text), + ) + return lat, img, mask + + + + + +# === Smart seed helpers (Sobol/Halton + light probing) === +def _splitmix64(x: int) -> int: + x = (x + 0x9E3779B97F4A7C15) & 0xFFFFFFFFFFFFFFFF + z = x + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9 & 0xFFFFFFFFFFFFFFFF + z = (z ^ (z >> 27)) * 0x94D049BB133111EB & 0xFFFFFFFFFFFFFFFF + z = z ^ (z >> 31) + return z & 0xFFFFFFFFFFFFFFFF + +def _halton_single(index: int, base: int) -> float: + f = 1.0 + r = 0.0 + i = index + while i > 0: + f = f / base + r = r + f * (i % base) + i //= base + return r + +def _sobol_like_2d(n: int, anchor: int) -> tuple[float, float]: + # lightweight 2D low-discrepancy via Halton(2,3) scrambled by anchor + i = n + 1 + (anchor % 9973) + return (_halton_single(i, 2), _halton_single(i, 3)) + +def _edge_density(img_bhwc: torch.Tensor) -> float: + lum = (0.2126 * img_bhwc[..., 0] + 0.7152 * img_bhwc[..., 1] + 0.0722 * img_bhwc[..., 2]) + kx = torch.tensor([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], device=img_bhwc.device, dtype=img_bhwc.dtype).view(1,1,3,3) + ky = torch.tensor([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], device=img_bhwc.device, dtype=img_bhwc.dtype).view(1,1,3,3) + gx = F.conv2d(lum.unsqueeze(1), kx, padding=1) + gy = F.conv2d(lum.unsqueeze(1), ky, padding=1) + g = torch.sqrt(gx*gx + gy*gy) + return float(g.mean().item()) + +def _speckle_fraction(img_bhwc: torch.Tensor) -> float: + # reuse S/V-based candidate mask from despeckle logic (no replacement) to estimate fraction + R, G, Bc = img_bhwc[..., 0], img_bhwc[..., 1], img_bhwc[..., 2] + V = torch.maximum(R, torch.maximum(G, Bc)) + mi = torch.minimum(R, torch.minimum(G, Bc)) + S = 1.0 - (mi / (V + 1e-6)) + v_thr = 0.98 + s_thr = 0.12 + cand = (V > v_thr) & (S < s_thr) + return float(cand.float().mean().item()) + +def _smart_seed_state_path() -> str: + import os + base = os.path.join(os.path.dirname(__file__), "..", "state") + os.makedirs(base, exist_ok=True) + return os.path.join(base, "smart_seed.json") + +def _smart_seed_counter(anchor: int) -> int: + import os, json + path = _smart_seed_state_path() + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + except Exception: + data = {} + key = hex(anchor & 0xFFFFFFFFFFFFFFFF) + n = int(data.get(key, 0)) + data[key] = n + 1 + try: + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + except Exception: + pass + return n + +def _smart_seed_select(model, + vae, + positive, + negative, + latent, + sampler_name: str, + scheduler: str, + cfg: float, + denoise: float, + base_seed: int | None = None, + k: int = 6, + probe_steps: int = 6, + clip_vision=None, + reference_image=None, + clipseg_text: str = "", + step_tag: str | None = None) -> int: + # Log start of SmartSeed selection + try: + try: + # Visual separation before SmartSeed block + print("") + print("") + if step_tag: + print(f"\x1b[34m==== {step_tag}, Smart_seed_random: Start ====\x1b[0m") + else: + print("\x1b[34m==== Smart_seed_random: Start ====\x1b[0m") + except Exception: + pass + + # Optional: precompute CLIP-Vision embedding of reference image + ref_embed = None + if (clip_vision is not None) and (reference_image is not None): + try: + ref_embed = _encode_clip_image(reference_image, clip_vision, target_res=224) + except Exception: + ref_embed = None + + # Anchor from latent shape + sampler/scheduler (+ cfg/denoise) + sh = latent["samples"].shape if isinstance(latent, dict) and "samples" in latent else None + anchor = 1469598103934665603 # FNV offset basis + for v in (sh[2] if sh else 0, sh[3] if sh else 0, len(str(sampler_name)), len(str(scheduler)), int(cfg * 1000), int(denoise * 1000)): + anchor = _splitmix64(anchor ^ int(v)) + # Advance a persistent counter per anchor to vary indices between runs + offset = _smart_seed_counter(anchor) * 7 + + # Build K candidate seeds from Halton(2,3) + cands: list[int] = [] + for i in range(k): + u, v = _sobol_like_2d(offset + i, anchor) + lo = int(u * (1 << 32)) & 0xFFFFFFFF + hi = int(v * (1 << 32)) & 0xFFFFFFFF + seed64 = _splitmix64((hi << 32) ^ lo ^ anchor) + cands.append(seed64 & 0xFFFFFFFFFFFFFFFF) + + best_seed = cands[0] + best_score = -1e9 + for sd in cands: + try: + # quick KSampler preview at low steps + lat_in = {"samples": latent["samples"].clone()} if isinstance(latent, dict) else latent + lat_out, = nodes.common_ksampler( + model, int(sd), int(probe_steps), float(cfg), str(sampler_name), str(scheduler), + positive, negative, lat_in, denoise=float(min(denoise, 0.65)) + ) + img = safe_decode(vae, lat_out) + # Base score: edge density toward a target + low speckle + balanced exposure + ed = _edge_density(img) + speck = _speckle_fraction(img) + lum = float(img.mean().item()) + edge_target = 0.10 + score = -abs(ed - edge_target) - 2.0 * speck - 0.5 * abs(lum - 0.5) + + # Perceptual metrics: luminance std and Laplacian variance (downscaled) + try: + lum_t = (0.2126 * img[..., 0] + 0.7152 * img[..., 1] + 0.0722 * img[..., 2]) + lstd = float(lum_t.std().item()) + lch = lum_t.unsqueeze(1) + lch_small = F.interpolate(lch, size=(128, 128), mode='bilinear', align_corners=False) + lap_k = torch.tensor([[0.0, 1.0, 0.0], [1.0, -4.0, 1.0], [0.0, 1.0, 0.0]], device=lch_small.device, dtype=lch_small.dtype).view(1, 1, 3, 3) + lap = F.conv2d(lch_small, lap_k, padding=1) + lap_var = float(lap.var().item()) + score += 0.15 * lstd + 0.10 * lap_var + except Exception: + pass + + # Semantic alignment via CLIP-Vision when available + if ref_embed is not None and clip_vision is not None: + try: + cand_embed = _encode_clip_image(img, clip_vision, target_res=224) + sim = float((cand_embed * ref_embed).sum(dim=-1).mean().clamp(-1.0, 1.0).item()) + sim01 = 0.5 * (sim + 1.0) + score += 0.75 * sim01 + except Exception: + pass + + # Focus coverage via CLIPSeg when text provided + if isinstance(clipseg_text, str) and clipseg_text.strip() != "": + try: + cmask = _clipseg_build_mask(img, clipseg_text, preview=192, threshold=0.40, blur=5.0, dilate=2, gain=1.0) + if cmask is not None: + area = float(cmask.mean().item()) + cov_target = 0.06 + cov_score = 1.0 - min(1.0, abs(area - cov_target) / max(cov_target, 1e-3)) + score += 0.30 * cov_score + except Exception: + pass + if score > best_score: + best_score = score + best_seed = sd + except Exception: + continue + + # Log end with selected seed + try: + if step_tag: + print(f"\x1b[34m==== {step_tag}, Smart_seed_random: End. Seed is: {int(best_seed & 0xFFFFFFFFFFFFFFFF)} ====\x1b[0m") + else: + print(f"\x1b[34m==== Smart_seed_random: End. Seed is: {int(best_seed & 0xFFFFFFFFFFFFFFFF)} ====\x1b[0m") + except Exception: + pass + return int(best_seed & 0xFFFFFFFFFFFFFFFF) + except Exception: + # Fallback to time-based random + try: + import time + fallback_seed = int(_splitmix64(int(time.time_ns()))) + except Exception: + fallback_seed = int(base_seed or 0) + try: + if step_tag: + print(f"\x1b[34m==== {step_tag}, Smart_seed_random: End. Seed is: {fallback_seed} ====\x1b[0m") + else: + print(f"\x1b[34m==== Smart_seed_random: End. Seed is: {fallback_seed} ====\x1b[0m") + except Exception: + pass + return fallback_seed + + diff --git a/mod/easy/mg_controlfusion_easy.py b/mod/easy/mg_controlfusion_easy.py new file mode 100644 index 0000000000000000000000000000000000000000..ad64d4e64249cc3ec97b1f601efa66995ff0bbba --- /dev/null +++ b/mod/easy/mg_controlfusion_easy.py @@ -0,0 +1,611 @@ +import os +import sys +import math +import torch +import torch.nn.functional as F +import numpy as np + +import comfy.model_management as model_management +from .preset_loader import get as load_preset + + +_DEPTH_INIT = False +_DEPTH_MODEL = None +_DEPTH_PROC = None + + +def _insert_aux_path(): + try: + base = os.path.dirname(os.path.dirname(__file__)) # .../custom_nodes + aux_root = os.path.join(base, 'comfyui_controlnet_aux') + aux_src = os.path.join(aux_root, 'src') + for p in (aux_src, aux_root): + if os.path.isdir(p) and p not in sys.path: + sys.path.insert(0, p) + except Exception: + pass + + +def _try_init_depth_anything(model_path: str): + global _DEPTH_INIT, _DEPTH_MODEL, _DEPTH_PROC + if _DEPTH_INIT: + return _DEPTH_MODEL is not None + _DEPTH_INIT = True + # Prefer our vendored implementation first + try: + from ...vendor.depth_anything_v2.dpt import DepthAnythingV2 # type: ignore + # Guess config from filename + fname = os.path.basename(model_path or '') + cfgs = { + 'depth_anything_v2_vits.pth': dict(encoder='vits', features=64, out_channels=[48,96,192,384]), + 'depth_anything_v2_vitb.pth': dict(encoder='vitb', features=128, out_channels=[96,192,384,768]), + 'depth_anything_v2_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]), + 'depth_anything_v2_vitg.pth': dict(encoder='vitg', features=384, out_channels=[1536,1536,1536,1536]), + 'depth_anything_v2_metric_vkitti_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]), + 'depth_anything_v2_metric_hypersim_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]), + } + # fallback to vitl if unknown + cfg = cfgs.get(fname, cfgs['depth_anything_v2_vitl.pth']) + device = 'cuda' if torch.cuda.is_available() else 'cpu' + m = DepthAnythingV2(**cfg) + sd = torch.load(model_path, map_location='cpu') + m.load_state_dict(sd) + _DEPTH_MODEL = m.to(device).eval() + _DEPTH_PROC = True + return True + except Exception: + # Try local checkout of comfyui_controlnet_aux (if present) + _insert_aux_path() + try: + from custom_controlnet_aux.depth_anything_v2.dpt import DepthAnythingV2 # type: ignore + fname = os.path.basename(model_path or '') + cfgs = { + 'depth_anything_v2_vits.pth': dict(encoder='vits', features=64, out_channels=[48,96,192,384]), + 'depth_anything_v2_vitb.pth': dict(encoder='vitb', features=128, out_channels=[96,192,384,768]), + 'depth_anything_v2_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]), + 'depth_anything_v2_vitg.pth': dict(encoder='vitg', features=384, out_channels=[1536,1536,1536,1536]), + 'depth_anything_v2_metric_vkitti_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]), + 'depth_anything_v2_metric_hypersim_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]), + } + cfg = cfgs.get(fname, cfgs['depth_anything_v2_vitl.pth']) + device = 'cuda' if torch.cuda.is_available() else 'cpu' + m = DepthAnythingV2(**cfg) + sd = torch.load(model_path, map_location='cpu') + m.load_state_dict(sd) + _DEPTH_MODEL = m.to(device).eval() + _DEPTH_PROC = True + return True + except Exception: + # Fallback: packaged auxiliary API + try: + from controlnet_aux.depth_anything import DepthAnythingDetector, DepthAnythingV2 # type: ignore + device = 'cuda' if torch.cuda.is_available() else 'cpu' + _DEPTH_MODEL = DepthAnythingV2(model_path=model_path, device=device) + _DEPTH_PROC = True + return True + except Exception: + _DEPTH_MODEL = None + _DEPTH_PROC = False + return False + + +def _build_depth_map(image_bhwc: torch.Tensor, res: int, model_path: str, hires_mode: bool = True) -> torch.Tensor: + B, H, W, C = image_bhwc.shape + dev = image_bhwc.device + dtype = image_bhwc.dtype + # Choose target min-side for processing. In hires mode we allow higher caps and keep aspect. + # DepthAnything v2 can be memory-hungry on large inputs; cap min-side at 1024 + cap = 1024 + target = int(max(16, min(cap, res))) + if _try_init_depth_anything(model_path): + try: + # to CPU uint8 + img = image_bhwc.detach().to('cpu') + x = img[0].movedim(-1, 0).unsqueeze(0) + # keep aspect ratio: scale so that min(H,W) == target + _, Cc, Ht, Wt = x.shape + min_side = max(1, min(Ht, Wt)) + scale = float(target) / float(min_side) + out_h = max(1, int(round(Ht * scale))) + out_w = max(1, int(round(Wt * scale))) + x = F.interpolate(x, size=(out_h, out_w), mode='bilinear', align_corners=False) + # make channels-last and ensure contiguous layout for OpenCV + arr = (x[0].movedim(0, -1).contiguous().numpy() * 255.0).astype('uint8') + # Prefer direct DepthAnythingV2 inference if model has infer_image + if hasattr(_DEPTH_MODEL, 'infer_image'): + import cv2 + # Drive input_size from desired depth resolution (min side), let DA keep aspect + input_sz = int(max(224, min(cap, res))) + depth = _DEPTH_MODEL.infer_image(cv2.cvtColor(arr, cv2.COLOR_RGB2BGR), input_size=input_sz, max_depth=20.0) + d = np.asarray(depth, dtype=np.float32) + # Normalize DepthAnythingV2 output (0..max_depth) to 0..1 + d = d / 20.0 + else: + depth = _DEPTH_MODEL(arr) + d = np.asarray(depth, dtype=np.float32) + if d.max() > 1.0: + d = d / 255.0 + d = torch.from_numpy(d)[None, None] # 1,1,h,w + d = F.interpolate(d, size=(H, W), mode='bilinear', align_corners=False) + d = d[0, 0].to(device=dev, dtype=dtype) + d = d.clamp(0, 1) + return d + except Exception: + pass + # Fallback pseudo-depth: luminance + gentle blur + lum = (0.2126 * image_bhwc[..., 0] + 0.7152 * image_bhwc[..., 1] + 0.0722 * image_bhwc[..., 2]).to(dtype=dtype) + x = lum.movedim(-1, 0).unsqueeze(0) if lum.ndim == 3 else lum.unsqueeze(0).unsqueeze(0) + x = F.interpolate(x, size=(H, W), mode='bilinear', align_corners=False) + x = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1) + return x[0, 0].clamp(0, 1) + + +def _pyracanny(image_bhwc: torch.Tensor, + low: int, + high: int, + res: int, + thin_iter: int = 0, + edge_boost: float = 0.0, + smart_tune: bool = False, + smart_boost: float = 0.2, + preserve_aspect: bool = True) -> torch.Tensor: + try: + import cv2 + except Exception: + # Fallback: simple Sobel magnitude + x = image_bhwc.movedim(-1, 1) + xg = x.mean(dim=1, keepdim=True) + gx = F.conv2d(xg, torch.tensor([[[-1, 0, 1],[-2,0,2],[-1,0,1]]], dtype=x.dtype, device=x.device).unsqueeze(1), padding=1) + gy = F.conv2d(xg, torch.tensor([[[-1,-2,-1],[0,0,0],[1,2,1]]], dtype=x.dtype, device=x.device).unsqueeze(1), padding=1) + mag = torch.sqrt(gx*gx + gy*gy) + mag = (mag - mag.amin())/(mag.amax()-mag.amin()+1e-6) + return mag[0,0].clamp(0,1) + B,H,W,C = image_bhwc.shape + img = (image_bhwc.detach().to('cpu')[0].contiguous().numpy()*255.0).astype('uint8') + cap = 4096 + target = int(max(64, min(cap, res))) + if preserve_aspect: + scale = float(target) / float(max(1, min(H, W))) + out_h = max(8, int(round(H * scale))) + out_w = max(8, int(round(W * scale))) + img_res = cv2.resize(img, (out_w, out_h), interpolation=cv2.INTER_LINEAR) + else: + img_res = cv2.resize(img, (target, target), interpolation=cv2.INTER_LINEAR) + gray = cv2.cvtColor(img_res, cv2.COLOR_RGB2GRAY) + pyr_scales = [1.0, 0.5, 0.25] + acc = None + for s in pyr_scales: + if preserve_aspect: + sz = (max(8, int(round(img_res.shape[1]*s))), max(8, int(round(img_res.shape[0]*s)))) + else: + sz = (max(8, int(target*s)), max(8, int(target*s))) + g = cv2.resize(gray, sz, interpolation=cv2.INTER_AREA) + g = cv2.GaussianBlur(g, (5,5), 0) + e = cv2.Canny(g, threshold1=int(low*s), threshold2=int(high*s)) + e = cv2.resize(e, (W, H), interpolation=cv2.INTER_LINEAR) + e = (e.astype(np.float32)/255.0) + acc = e if acc is None else np.maximum(acc, e) + # Estimate density and sharpness for smart tuning + edensity_pre = None + try: + edensity_pre = float(np.mean(acc)) if acc is not None else None + except Exception: + edensity_pre = None + lap_var = None + try: + g32 = gray.astype(np.float32) / 255.0 + lap = cv2.Laplacian(g32, cv2.CV_32F) + lap_var = float(lap.var()) + except Exception: + lap_var = None + + # optional thinning + try: + thin_iter_eff = int(thin_iter) + if smart_tune: + # simple heuristic: more thinning on high res and dense edges + auto = 0 + if target >= 1024: + auto += 1 + if target >= 1400: + auto += 1 + if edensity_pre is not None and edensity_pre > 0.12: + auto += 1 + if edensity_pre is not None and edensity_pre < 0.05: + auto = max(0, auto - 1) + thin_iter_eff = max(thin_iter_eff, min(3, auto)) + if thin_iter_eff > 0: + import cv2 + if hasattr(cv2, 'ximgproc') and hasattr(cv2.ximgproc, 'thinning'): + th = acc.copy() + th = (th*255).astype('uint8') + th = cv2.ximgproc.thinning(th) + acc = th.astype(np.float32)/255.0 + else: + # simple erosion-based thinning approximation + kernel = np.ones((3,3), np.uint8) + t = (acc*255).astype('uint8') + for _ in range(int(thin_iter_eff)): + t = cv2.erode(t, kernel, iterations=1) + acc = t.astype(np.float32)/255.0 + except Exception: + pass + # optional edge boost (unsharp on edge map) + # We fix a gentle boost for micro‑contrast; smart_tune may nudge it slightly + boost_eff = 0.10 + if smart_tune: + try: + lv = 0.0 if lap_var is None else max(0.0, min(1.0, lap_var / 2.0)) + dens = 0.0 if edensity_pre is None else float(max(0.0, min(1.0, edensity_pre))) + boost_eff = max(0.05, min(0.20, boost_eff + (1.0 - dens) * 0.05 + (1.0 - lv) * 0.02)) + except Exception: + pass + if boost_eff and boost_eff != 0.0: + try: + import cv2 + blur = cv2.GaussianBlur(acc, (0,0), sigmaX=1.0) + acc = np.clip(acc + float(boost_eff)*(acc - blur), 0.0, 1.0) + except Exception: + pass + ed = torch.from_numpy(acc).to(device=image_bhwc.device, dtype=image_bhwc.dtype) + return ed.clamp(0,1) + + +def _blend(depth: torch.Tensor, edges: torch.Tensor, mode: str, factor: float) -> torch.Tensor: + depth = depth.clamp(0,1) + edges = edges.clamp(0,1) + if mode == 'max': + return torch.maximum(depth, edges) + if mode == 'edge_over_depth': + # edges override depth (edge=1) while preserving depth elsewhere + return (depth * (1.0 - edges) + edges).clamp(0,1) + # normal + f = float(max(0.0, min(1.0, factor))) + return (depth*(1.0-f) + edges*f).clamp(0,1) + + +def _apply_controlnet_separate(positive, negative, control_net, image_bhwc: torch.Tensor, + strength_pos: float, strength_neg: float, + start_percent: float, end_percent: float, vae=None, + apply_to_uncond: bool = False, + stack_prev_control: bool = False): + control_hint = image_bhwc.movedim(-1,1) + out_pos = [] + out_neg = [] + # POS + for t in positive: + d = t[1].copy() + prev = d.get('control', None) if stack_prev_control else None + c_net = control_net.copy().set_cond_hint(control_hint, float(strength_pos), (start_percent, end_percent), vae=vae, extra_concat=[]) + c_net.set_previous_controlnet(prev) + d['control'] = c_net + d['control_apply_to_uncond'] = bool(apply_to_uncond) + out_pos.append([t[0], d]) + # NEG + for t in negative: + d = t[1].copy() + prev = d.get('control', None) if stack_prev_control else None + c_net = control_net.copy().set_cond_hint(control_hint, float(strength_neg), (start_percent, end_percent), vae=vae, extra_concat=[]) + c_net.set_previous_controlnet(prev) + d['control'] = c_net + d['control_apply_to_uncond'] = bool(apply_to_uncond) + out_neg.append([t[0], d]) + return out_pos, out_neg + + +class MG_ControlFusion: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "preset_step": (["Custom", "Step 2", "Step 3", "Step 4"], {"default": "Custom", "tooltip": "Apply preset values from pressets/mg_controlfusion.cfg. UI values override."}), + "image": ("IMAGE", {"tooltip": "Input RGB image (B,H,W,3) in 0..1."}), + "positive": ("CONDITIONING", {"tooltip": "Positive conditioning to apply ControlNet to."}), + "negative": ("CONDITIONING", {"tooltip": "Negative conditioning to apply ControlNet to."}), + "control_net": ("CONTROL_NET", {"tooltip": "ControlNet module receiving the fused mask as hint."}), + "vae": ("VAE", {"tooltip": "VAE used by ControlNet when encoding the hint."}), + }, + "optional": { + "enable_depth": ("BOOLEAN", {"default": True, "tooltip": "Enable depth map fusion (Depth Anything v2 if available)."}), + "depth_model_path": ("STRING", {"default": os.path.join(os.path.dirname(os.path.dirname(__file__)), 'MagicNodes','depth-anything','depth_anything_v2_vitl.pth') if False else os.path.join(os.path.dirname(__file__), '..','depth-anything','depth_anything_v2_vitl.pth'), "tooltip": "Path to Depth Anything v2 .pth weights (vits/vitb/vitl/vitg)."}), + "depth_resolution": ("INT", {"default": 768, "min": 64, "max": 1024, "step": 64, "tooltip": "Depth min-side resolution (cap 1024). In Hi‑Res mode drives DepthAnything input_size."}), + "enable_pyra": ("BOOLEAN", {"default": True, "tooltip": "Enable PyraCanny edge detector."}), + "pyra_low": ("INT", {"default": 109, "min": 0, "max": 255, "tooltip": "Canny low threshold (0..255)."}), + "pyra_high": ("INT", {"default": 147, "min": 0, "max": 255, "tooltip": "Canny high threshold (0..255)."}), + "pyra_resolution": ("INT", {"default": 1024, "min": 64, "max": 4096, "step": 64, "tooltip": "Working resolution for edges (min side, keeps aspect)."}), + "edge_thin_iter": ("INT", {"default": 0, "min": 0, "max": 10, "step": 1, "tooltip": "Thinning iterations for edges (skeletonize). 0 = off."}), + "edge_alpha": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Opacity for edges before blending (0..1)."}), + "edge_boost": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Deprecated: internal boost fixed (~0.10); use edge_alpha instead."}), + "smart_tune": ("BOOLEAN", {"default": False, "tooltip": "Auto-adjust thinning/boost from image edge density and sharpness."}), + "smart_boost": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Scale for auto edge boost when Smart Tune is on."}), + "blend_mode": (["normal","max","edge_over_depth"], {"default": "normal", "tooltip": "Depth+edges merge: normal (mix), max (strongest), edge_over_depth (edges overlay)."}), + "blend_factor": ("FLOAT", {"default": 0.02, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Blend strength for edges into depth (depends on mode)."}), + "strength_pos": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01, "tooltip": "ControlNet strength for positive branch."}), + "strength_neg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01, "tooltip": "ControlNet strength for negative branch."}), + "start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Start percentage along the sampling schedule."}), + "end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "End percentage along the sampling schedule."}), + "preview_res": ("INT", {"default": 1024, "min": 256, "max": 2048, "step": 64, "tooltip": "Preview minimum side (keeps aspect ratio)."}), + "mask_brightness": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Preview brightness multiplier (visualization only)."}), + "preview_show_strength": ("BOOLEAN", {"default": True, "tooltip": "Multiply preview by ControlNet strength for visualization."}), + "preview_strength_branch": (["positive","negative","max","avg"], {"default": "max", "tooltip": "Which strength to reflect in preview (display only)."}), + "hires_mask_auto": ("BOOLEAN", {"default": True, "tooltip": "High‑res mask: keep aspect ratio, scale by minimal side for depth/edges, and drive DepthAnything with your depth_resolution (no 2K cap)."}), + "apply_to_uncond": ("BOOLEAN", {"default": False, "tooltip": "Apply ControlNet hint to the unconditional branch as well (stronger global hold on very large images)."}), + "stack_prev_control": ("BOOLEAN", {"default": False, "tooltip": "Chain with any previously attached ControlNet in the conditioning (advanced). Off = replace to avoid memory bloat."}), + # Split apply: chain Depth and Edges with separate schedules/strengths (fixed order: depth -> edges) + "split_apply": ("BOOLEAN", {"default": False, "tooltip": "Apply Depth and Edges as two chained ControlNets (fixed order: depth then edges)."}), + "edge_start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Edges start percent (when split is enabled)."}), + "edge_end_percent": ("FLOAT", {"default": 0.6, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Edges end percent (when split is enabled)."}), + "depth_start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Depth start percent (when split is enabled)."}), + "depth_end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Depth end percent (when split is enabled)."}), + "edge_strength_mul": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 3.0, "step": 0.01, "tooltip": "Multiply global strength for Edges when split is enabled."}), + "depth_strength_mul": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 3.0, "step": 0.01, "tooltip": "Multiply global strength for Depth when split is enabled."}), + # Extra edge controls (bottom) + "edge_width": ("FLOAT", {"default": 0.0, "min": -0.5, "max": 1.5, "step": 0.05, "tooltip": "Edge thickness adjust: negative thins, positive thickens."}), + "edge_smooth": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.05, "tooltip": "Small smooth on edges to reduce pixelation (0..1)."}), + "edge_single_line": ("BOOLEAN", {"default": False, "tooltip": "Try to collapse double outlines into a single centerline."}), + "edge_single_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Strength of single-line collapse (0..1). 0 = off, 1 = strong."}), + "edge_depth_gate": ("BOOLEAN", {"default": False, "tooltip": "Weigh edges by depth so distant lines are fainter."}), + "edge_depth_gamma": ("FLOAT", {"default": 1.5, "min": 0.2, "max": 4.0, "step": 0.1, "tooltip": "Gamma for depth gating: edges *= (1−depth)^gamma."}), + } + } + + RETURN_TYPES = ("CONDITIONING","CONDITIONING","IMAGE") + RETURN_NAMES = ("positive","negative","Mask_Preview") + FUNCTION = "apply" + CATEGORY = "MagicNodes" + + def apply(self, image, positive, negative, control_net, vae, + enable_depth=True, depth_model_path="", depth_resolution=1024, + enable_pyra=True, pyra_low=109, pyra_high=147, pyra_resolution=1024, + edge_thin_iter=0, edge_alpha=1.0, edge_boost=0.0, + smart_tune=False, smart_boost=0.2, + blend_mode="normal", blend_factor=0.02, + strength_pos=1.0, strength_neg=1.0, start_percent=0.0, end_percent=1.0, + preview_res=1024, mask_brightness=1.0, + preview_show_strength=True, preview_strength_branch="max", + hires_mask_auto=True, apply_to_uncond=False, stack_prev_control=False, + edge_width=0.0, edge_smooth=0.0, edge_single_line=False, edge_single_strength=0.0, + edge_depth_gate=False, edge_depth_gamma=1.5, + split_apply=False, edge_start_percent=0.0, edge_end_percent=0.6, + depth_start_percent=0.0, depth_end_percent=1.0, + edge_strength_mul=1.0, depth_strength_mul=1.0, + preset_step="Step 2", custom_override=False): + + # Merge preset values (if selected) with UI values; UI overrides preset + try: + if isinstance(preset_step, str) and preset_step.lower() != "custom": + p = load_preset("mg_controlfusion", preset_step) + else: + p = {} + except Exception: + p = {} + def pv(name, cur): + return p.get(name, cur) + enable_depth = bool(pv('enable_depth', enable_depth)) + depth_model_path = str(pv('depth_model_path', depth_model_path)) + depth_resolution = int(pv('depth_resolution', depth_resolution)) + enable_pyra = bool(pv('enable_pyra', enable_pyra)) + pyra_low = int(pv('pyra_low', pyra_low)) + pyra_high = int(pv('pyra_high', pyra_high)) + pyra_resolution = int(pv('pyra_resolution', pyra_resolution)) + edge_thin_iter = int(pv('edge_thin_iter', edge_thin_iter)) + edge_alpha = float(pv('edge_alpha', edge_alpha)) + edge_boost = float(pv('edge_boost', edge_boost)) + smart_tune = bool(pv('smart_tune', smart_tune)) + smart_boost = float(pv('smart_boost', smart_boost)) + blend_mode = str(pv('blend_mode', blend_mode)) + blend_factor = float(pv('blend_factor', blend_factor)) + strength_pos = float(pv('strength_pos', strength_pos)) + strength_neg = float(pv('strength_neg', strength_neg)) + start_percent = float(pv('start_percent', start_percent)) + end_percent = float(pv('end_percent', end_percent)) + preview_res = int(pv('preview_res', preview_res)) + mask_brightness = float(pv('mask_brightness', mask_brightness)) + preview_show_strength = bool(pv('preview_show_strength', preview_show_strength)) + preview_strength_branch = str(pv('preview_strength_branch', preview_strength_branch)) + hires_mask_auto = bool(pv('hires_mask_auto', hires_mask_auto)) + apply_to_uncond = bool(pv('apply_to_uncond', apply_to_uncond)) + stack_prev_control = bool(pv('stack_prev_control', stack_prev_control)) + split_apply = bool(pv('split_apply', split_apply)) + edge_start_percent = float(pv('edge_start_percent', edge_start_percent)) + edge_end_percent = float(pv('edge_end_percent', edge_end_percent)) + depth_start_percent = float(pv('depth_start_percent', depth_start_percent)) + depth_end_percent = float(pv('depth_end_percent', depth_end_percent)) + edge_strength_mul = float(pv('edge_strength_mul', edge_strength_mul)) + depth_strength_mul = float(pv('depth_strength_mul', depth_strength_mul)) + edge_width = float(pv('edge_width', edge_width)) + edge_smooth = float(pv('edge_smooth', edge_smooth)) + edge_single_line = bool(pv('edge_single_line', edge_single_line)) + edge_single_strength = float(pv('edge_single_strength', edge_single_strength)) + edge_depth_gate = bool(pv('edge_depth_gate', edge_depth_gate)) + edge_depth_gamma = float(pv('edge_depth_gamma', edge_depth_gamma)) + + dev = image.device + dtype = image.dtype + B,H,W,C = image.shape + # Build depth/edges + depth = None + edges = None + if enable_depth: + model_path = depth_model_path or os.path.join(os.path.dirname(__file__), '..','depth-anything','depth_anything_v2_vitl.pth') + depth = _build_depth_map(image, int(depth_resolution), model_path, bool(hires_mask_auto)) + if enable_pyra: + edges = _pyracanny(image, + int(pyra_low), int(pyra_high), int(pyra_resolution), + int(edge_thin_iter), float(edge_boost), + bool(smart_tune), float(smart_boost), bool(hires_mask_auto)) + if depth is None and edges is None: + # Nothing to do: return inputs and zero preview + prev = torch.zeros((B, max(H,1), max(W,1), 3), device=dev, dtype=dtype) + return positive, negative, prev + + if depth is None: + depth = torch.zeros_like(edges) + if edges is None: + edges = torch.zeros_like(depth) + + # Edge post-process: width/single-line/smooth + def _edges_post(acc_t: torch.Tensor) -> torch.Tensor: + try: + import cv2, numpy as _np + acc = acc_t.detach().to('cpu').numpy() + img = (acc*255.0).astype(_np.uint8) + k = _np.ones((3,3), _np.uint8) + # Adjust thickness + w = float(edge_width) + if abs(w) > 1e-6: + it = int(abs(w)) + frac = abs(w) - it + op = cv2.dilate if w > 0 else cv2.erode + y = img.copy() + for _ in range(max(0, it)): + y = op(y, k, iterations=1) + if frac > 1e-6: + y2 = op(y, k, iterations=1) + y = ((1.0-frac)*y.astype(_np.float32) + frac*y2.astype(_np.float32)).astype(_np.uint8) + img = y + # Collapse double lines to single centerline + if bool(edge_single_line) and float(edge_single_strength) > 1e-6: + try: + s = float(edge_single_strength) + close = cv2.morphologyEx(img, cv2.MORPH_CLOSE, k, iterations=1) + if hasattr(cv2, 'ximgproc') and hasattr(cv2.ximgproc, 'thinning'): + sk = cv2.ximgproc.thinning(close) + else: + # limited-iteration morphological skeletonization + iters = max(1, int(round(2 + 6*s))) + sk = _np.zeros_like(close) + src = close.copy() + elem = cv2.getStructuringElement(cv2.MORPH_CROSS, (3,3)) + for _ in range(iters): + er = cv2.erode(src, elem, iterations=1) + op = cv2.morphologyEx(er, cv2.MORPH_OPEN, elem) + tmp = cv2.subtract(er, op) + sk = cv2.bitwise_or(sk, tmp) + src = er + if not _np.any(src): + break + # Blend skeleton back with original according to strength + img = ((_np.float32(1.0 - s) * img.astype(_np.float32)) + (_np.float32(s) * sk.astype(_np.float32))).astype(_np.uint8) + except Exception: + pass + # Smooth + if float(edge_smooth) > 1e-6: + sigma = max(0.1, min(2.0, float(edge_smooth) * 1.2)) + img = cv2.GaussianBlur(img, (0,0), sigmaX=sigma) + out = torch.from_numpy((img.astype(_np.float32)/255.0)).to(device=acc_t.device, dtype=acc_t.dtype) + return out.clamp(0,1) + except Exception: + # Torch fallback: light blur-only + if float(edge_smooth) > 1e-6: + s = max(1, int(round(float(edge_smooth)*2))) + return F.avg_pool2d(acc_t.unsqueeze(0).unsqueeze(0), kernel_size=2*s+1, stride=1, padding=s)[0,0].clamp(0,1) + return acc_t + + edges = _edges_post(edges) + + # Depth gating of edges + if bool(edge_depth_gate): + # Inverted gating per feedback: use depth^gamma (nearer = stronger if depth is larger) + g = (depth.clamp(0,1)) ** float(edge_depth_gamma) + edges = (edges * g).clamp(0,1) + + # Apply edge alpha before blending + edges = (edges * float(edge_alpha)).clamp(0,1) + + fused = _blend(depth, edges, str(blend_mode), float(blend_factor)) + # Apply as split (Edges then Depth) or single fused hint + if bool(split_apply): + # Fixed order for determinism: Depth first, then Edges + hint_edges = edges.unsqueeze(-1).repeat(1,1,1,3) + hint_depth = depth.unsqueeze(-1).repeat(1,1,1,3) + # Depth first + pos_mid, neg_mid = _apply_controlnet_separate( + positive, negative, control_net, hint_depth, + float(strength_pos) * float(depth_strength_mul), + float(strength_neg) * float(depth_strength_mul), + float(depth_start_percent), float(depth_end_percent), vae, + bool(apply_to_uncond), True + ) + # Then edges + pos_out, neg_out = _apply_controlnet_separate( + pos_mid, neg_mid, control_net, hint_edges, + float(strength_pos) * float(edge_strength_mul), + float(strength_neg) * float(edge_strength_mul), + float(edge_start_percent), float(edge_end_percent), vae, + bool(apply_to_uncond), True + ) + else: + hint = fused.unsqueeze(-1).repeat(1,1,1,3) + pos_out, neg_out = _apply_controlnet_separate( + positive, negative, control_net, hint, + float(strength_pos), float(strength_neg), + float(start_percent), float(end_percent), vae, + bool(apply_to_uncond), bool(stack_prev_control) + ) + # Build preview: keep aspect ratio, set minimal side + prev_res = int(max(256, min(2048, preview_res))) + scale = prev_res / float(min(H, W)) + out_h = max(1, int(round(H * scale))) + out_w = max(1, int(round(W * scale))) + prev = F.interpolate(fused.unsqueeze(0).unsqueeze(0), size=(out_h, out_w), mode='bilinear', align_corners=False)[0,0] + # Optionally reflect ControlNet strength in preview (display only) + if bool(preview_show_strength): + br = str(preview_strength_branch) + sp = float(strength_pos) + sn = float(strength_neg) + if br == 'negative': + s_vis = sn + elif br == 'max': + s_vis = max(sp, sn) + elif br == 'avg': + s_vis = 0.5 * (sp + sn) + else: + s_vis = sp + # clamp for display range + s_vis = max(0.0, min(1.0, s_vis)) + prev = prev * s_vis + # Apply visualization brightness only for preview + prev = (prev * float(mask_brightness)).clamp(0.0, 1.0) + prev = prev.unsqueeze(-1).repeat(1,1,3).to(device=dev, dtype=dtype).unsqueeze(0) + return (pos_out, neg_out, prev) + + +# === Easy UI wrapper: simplified controls + Step/Custom preset logic === +class MG_ControlFusionEasyUI(MG_ControlFusion): + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + # Step preset first for emphasis + "preset_step": (["Step 2", "Step 3", "Step 4"], {"default": "Step 2", "tooltip": "Choose the Step preset. Toggle Custom below to apply UI values; otherwise Step preset values are used."}), + # Custom toggle: when enabled, UI values override the Step for visible controls + "custom": ("BOOLEAN", {"default": False, "tooltip": "Custom override: when enabled, your UI values override the selected Step for visible controls; hidden parameters still come from the Step preset."}), + # Connectors + "image": ("IMAGE", {"tooltip": "Input RGB image (B,H,W,3) in 0..1."}), + "positive": ("CONDITIONING", {"tooltip": "Positive conditioning to apply ControlNet to."}), + "negative": ("CONDITIONING", {"tooltip": "Negative conditioning to apply ControlNet to."}), + "control_net": ("CONTROL_NET", {"tooltip": "ControlNet module receiving the fused mask as hint."}), + "vae": ("VAE", {"tooltip": "VAE used by ControlNet when encoding the hint."}), + # Minimal surface controls + "enable_depth": ("BOOLEAN", {"default": True, "tooltip": "Enable depth map fusion (Depth Anything v2 if available)."}), + "enable_pyra": ("BOOLEAN", {"default": True, "tooltip": "Enable PyraCanny edge detector."}), + "edge_alpha": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Opacity for edges before blending (0..1)."}), + "blend_factor": ("FLOAT", {"default": 0.02, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Blend strength for edges into depth (depends on mode)."}), + }, + "optional": {} + } + + RETURN_TYPES = ("CONDITIONING","CONDITIONING","IMAGE") + RETURN_NAMES = ("positive","negative","Mask_Preview") + FUNCTION = "apply_easy" + + def apply_easy(self, preset_step, custom, image, positive, negative, control_net, vae, + enable_depth=True, enable_pyra=True, edge_alpha=1.0, blend_factor=0.02): + # Use Step preset; if custom is True, allow visible UI values to override inside base impl via custom_override + return super().apply( + image=image, positive=positive, negative=negative, control_net=control_net, vae=vae, + enable_depth=bool(enable_depth), enable_pyra=bool(enable_pyra), edge_alpha=float(edge_alpha), blend_factor=float(blend_factor), + preset_step=str(preset_step) if not bool(custom) else "Custom", + custom_override=bool(custom), + ) diff --git a/mod/easy/mg_supersimple_easy.py b/mod/easy/mg_supersimple_easy.py new file mode 100644 index 0000000000000000000000000000000000000000..8d106584f07659e82a144fad72a4f75952eab442 --- /dev/null +++ b/mod/easy/mg_supersimple_easy.py @@ -0,0 +1,148 @@ +from __future__ import annotations + +"""MG_SuperSimple: Orchestrates a 1–4 step pipeline over CF→CADE pairs. + +- Step 1: CADE with Step 1 preset. Exception: forces denoise=1.0. +- Steps 2..N: ControlFusion (CF) with Step N preset → CADE with Step N preset. +- When custom is True: visible CADE controls (seed/steps/cfg/denoise/sampler/scheduler/clipseg_text) + override corresponding Step presets across all steps (except step 1 denoise is always 1.0). +- When custom is False: all CADE values come from Step presets; node UI values are ignored. +- CF always uses its Step presets (no extra UI here) to keep the node minimal. + +Inputs +- model/vae/latent/positive/negative: standard Comfy connectors +- control_net: ControlNet module for CF (required) +- reference_image/clip_vision: forwarded into CADE (optional) + +Outputs +- (LATENT, IMAGE) from the final executed step +""" + +import torch + +from .mg_cade25_easy import ComfyAdaptiveDetailEnhancer25 as _CADE +from .mg_controlfusion_easy import MG_ControlFusion as _CF +from .mg_cade25_easy import _sampler_names as _sampler_names +from .mg_cade25_easy import _scheduler_names as _scheduler_names + + +class MG_SuperSimple: + CATEGORY = "MagicNodes/Easy" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + # High-level pipeline control + "step_count": ("INT", {"default": 4, "min": 1, "max": 4, "tooltip": "Number of steps to run (1..4)."}), + "custom": ("BOOLEAN", {"default": False, "tooltip": "When enabled, CADE UI values below override Step presets across all steps (denoise on Step 1 is still forced to 1.0)."}), + + # Connectors + "model": ("MODEL", {}), + "positive": ("CONDITIONING", {}), + "negative": ("CONDITIONING", {}), + "vae": ("VAE", {}), + "latent": ("LATENT", {}), + "control_net": ("CONTROL_NET", {"tooltip": "ControlNet module used by ControlFusion."}), + + # Shared CADE surface controls + "seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF, "control_after_generate": True, "tooltip": "Seed 0 = SmartSeed (Sobol + light probe). Non-zero = fixed seed (deterministic)."}), + "steps": ("INT", {"default": 25, "min": 1, "max": 10000, "tooltip": "KSampler steps for CADE (applies to all steps)."}), + "cfg": ("FLOAT", {"default": 4.5, "min": 0.0, "max": 100.0, "step": 0.1}), + # Denoise is clamped; Step 1 uses 1.0 regardless + "denoise": ("FLOAT", {"default": 0.65, "min": 0.35, "max": 0.9, "step": 0.0001}), + "sampler_name": (_sampler_names(), {"default": _sampler_names()[0]}), + "scheduler": (_scheduler_names(), {"default": "MGHybrid"}), + "clipseg_text": ("STRING", {"default": "hand, feet, face", "multiline": False, "tooltip": "Focus terms for CLIPSeg (comma-separated)."}), + }, + "optional": { + "reference_image": ("IMAGE", {}), + "clip_vision": ("CLIP_VISION", {}), + }, + } + + RETURN_TYPES = ("LATENT", "IMAGE") + RETURN_NAMES = ("LATENT", "IMAGE") + FUNCTION = "run" + + def _cade(self, + preset_step: str, + custom_override: bool, + model, vae, positive, negative, latent, + seed: int, steps: int, cfg: float, denoise: float, + sampler_name: str, scheduler: str, + clipseg_text: str, + reference_image=None, clip_vision=None): + # CADE core call mirrors CADEEasyUI -> apply_cade2 + lat, img, _s, _c, _d, _mask = _CADE().apply_cade2( + model, vae, positive, negative, latent, + int(seed), int(steps), float(cfg), float(denoise), + str(sampler_name), str(scheduler), 0.0, + preset_step=str(preset_step), custom_override=bool(custom_override), + clipseg_text=str(clipseg_text), + reference_image=reference_image, clip_vision=clip_vision, + ) + return lat, img + + def _cf(self, + preset_step: str, + image, positive, negative, control_net, vae): + # Keep CF strictly on presets for SuperSimple (no extra UI), + # so pass custom_override=False intentionally. + pos, neg, _prev = _CF().apply( + image=image, positive=positive, negative=negative, + control_net=control_net, vae=vae, + preset_step=str(preset_step), custom_override=False, + ) + return pos, neg + + def run(self, + step_count, custom, + model, positive, negative, vae, latent, control_net, + seed, steps, cfg, denoise, sampler_name, scheduler, clipseg_text, + reference_image=None, clip_vision=None): + # Clamp step_count to 1..4 + n = int(max(1, min(4, step_count))) + + cur_latent = latent + cur_image = None + cur_pos = positive + cur_neg = negative + + # Step 1: CADE with Step 1 preset, denoise forced to 1.0 + denoise_step1 = 1.0 + lat1, img1 = self._cade( + preset_step="Step 1", + custom_override=bool(custom), + model=model, vae=vae, positive=cur_pos, negative=cur_neg, latent=cur_latent, + seed=seed, steps=steps, cfg=cfg, denoise=denoise_step1, + sampler_name=sampler_name, scheduler=scheduler, + clipseg_text=clipseg_text, + reference_image=reference_image, clip_vision=clip_vision, + ) + cur_latent, cur_image = lat1, img1 + + # Steps 2..n: CF -> CADE per step + for i in range(2, n + 1): + # ControlFusion on current image/conds + cur_pos, cur_neg = self._cf( + preset_step=f"Step {i}", + image=cur_image, positive=cur_pos, negative=cur_neg, + control_net=control_net, vae=vae, + ) + # CADE with shared controls + # If no external reference_image is provided, use the previous step image + # so that reference_clean / CLIP-Vision gating can take effect. + ref_img = reference_image if (reference_image is not None) else cur_image + lat_i, img_i = self._cade( + preset_step=f"Step {i}", + custom_override=bool(custom), + model=model, vae=vae, positive=cur_pos, negative=cur_neg, latent=cur_latent, + seed=seed, steps=steps, cfg=cfg, denoise=denoise, + sampler_name=sampler_name, scheduler=scheduler, + clipseg_text=clipseg_text, + reference_image=ref_img, clip_vision=clip_vision, + ) + cur_latent, cur_image = lat_i, img_i + + return (cur_latent, cur_image) diff --git a/mod/easy/preset_loader.py b/mod/easy/preset_loader.py new file mode 100644 index 0000000000000000000000000000000000000000..83f4f02682f5a452acc7c9b1d8ea47a9310854a6 --- /dev/null +++ b/mod/easy/preset_loader.py @@ -0,0 +1,115 @@ +import os +from typing import Dict, Tuple + +_CACHE: Dict[str, Tuple[float, Dict[str, Dict[str, object]]]] = {} + +_MSG_PREFIX = "[MagicNodes][Presets]" + +def _root_dir() -> str: + # .../MagicNodes/mod/easy -> .../MagicNodes + return os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + +def _pressets_dir() -> str: + return os.path.join(_root_dir(), "pressets") + +def _cfg_path(kind: str) -> str: + # kind examples: "mg_cade25", "mg_controlfusion" + return os.path.join(_pressets_dir(), f"{kind}.cfg") + +def _parse_value(raw: str): + s = raw.strip() + if not s: + return "" + low = s.lower() + if low in ("true", "false"): + return low == "true" + try: + if "." in s or "e" in low: + return float(s) + return int(s) + except Exception: + pass + # variable substitution + s = s.replace("$(ROOT)", _root_dir()) + if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")): + s = s[1:-1] + return s + +def _load_kind(kind: str) -> Dict[str, Dict[str, object]]: + path = _cfg_path(kind) + if not os.path.isfile(path): + print(f"{_MSG_PREFIX} No configuration file for '{kind}' found; loaded defaults — results may be unpredictable!") + return {} + try: + mtime = os.path.getmtime(path) + cached = _CACHE.get(path) + if cached and cached[0] == mtime: + return cached[1] + + data: Dict[str, Dict[str, object]] = {} + cur_section = None + with open(path, "r", encoding="utf-8") as f: + for ln, line in enumerate(f, start=1): + line = line.strip() + if not line or line.startswith("#") or line.startswith(";"): + continue + if line.startswith("[") and line.endswith("]"): + cur_section = line[1:-1].strip().lower() + data.setdefault(cur_section, {}) + continue + if ":" in line: + if cur_section is None: + print(f"{_MSG_PREFIX} Parse warning at line {ln}: key outside of any [section]; ignored") + continue + k, v = line.split(":", 1) + key = k.strip() + try: + val = _parse_value(v) + except Exception: + print(f"{_MSG_PREFIX} Missing or invalid parameter '{key}'; this may affect results!") + continue + data[cur_section][key] = val + else: + print(f"{_MSG_PREFIX} Unknown line at {ln}: '{line}'; ignored") + + _CACHE[path] = (mtime, data) + return data + except Exception as e: + print(f"{_MSG_PREFIX} Failed to read '{path}': {e}. Loaded defaults — results may be unpredictable!") + return {} + +def get(kind: str, step: str) -> Dict[str, object]: + """Return dict of parameters for a given kind and step. + step accepts 'Step 1', '1', 'step1', case-insensitive. + """ + data = _load_kind(kind) + if not data: + return {} + label = step.strip().lower().replace(" ", "") + if label.startswith("step"): + key = label + elif label.isdigit(): + key = f"step{label}" + else: + key = f"step{label}" + + if key not in data: + # Special case: CF is intentionally not applied on Step 1 in this pipeline. + # Suppress noisy log for missing 'Step 1' in mg_controlfusion. + if kind == "mg_controlfusion" and key in ("step1", "1"): + return {} + print(f"{_MSG_PREFIX} Preset step '{step}' not found for '{kind}'; using defaults") + return {} + res = dict(data[key]) + # Side-effect: when CADE presets are loaded, optionally enable KV pruning in attention + try: + if kind == "mg_cade25": + from .. import mg_sagpu_attention as sa_patch # local import to avoid cycles + kv_enable = bool(res.get("kv_prune_enable", False)) + kv_keep = float(res.get("kv_keep", 0.85)) + kv_min = int(res.get("kv_min_tokens", 128)) if "kv_min_tokens" in res else 128 + if hasattr(sa_patch, "set_kv_prune"): + sa_patch.set_kv_prune(kv_enable, kv_keep, kv_min) + except Exception: + pass + return res diff --git a/mod/hard/__init__.py b/mod/hard/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f25e5afa69910c96e9874b2c3efedc1bb93b25d8 --- /dev/null +++ b/mod/hard/__init__.py @@ -0,0 +1,9 @@ +"""MagicNodes Hard variants + +Complex, full‑control node implementations. Imported and registered +from the package root to expose them under the UI category +"MagicNodes/Hard". +""" + +# No side effects on import. + diff --git a/mod/hard/mg_adaptive.py b/mod/hard/mg_adaptive.py new file mode 100644 index 0000000000000000000000000000000000000000..ec4c3c500256ef7b08f8892d3ba93e2d7d70ba18 --- /dev/null +++ b/mod/hard/mg_adaptive.py @@ -0,0 +1,39 @@ +"""Adaptive sampler helper node (moved to mod/). + +Keeps class/key name AdaptiveSamplerHelper for backward compatibility. +""" + +import numpy as np +from scipy.ndimage import laplace + + +class AdaptiveSamplerHelper: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE", {}), + "steps": ("INT", {"default": 20, "min": 1, "max": 200}), + "cfg": ("FLOAT", {"default": 7.0, "min": 0.1, "max": 20.0, "step": 0.1}), + "denoise": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), + } + } + + RETURN_TYPES = ("INT", "FLOAT", "FLOAT") + RETURN_NAMES = ("steps", "cfg", "denoise") + FUNCTION = "tune" + CATEGORY = "MagicNodes" + + def tune(self, image, steps, cfg, denoise): + img = image[0].cpu().numpy() + gray = img.mean(axis=2) + brightness = float(gray.mean()) + contrast = float(gray.std()) + sharpness = float(np.var(laplace(gray))) + + tuned_steps = int(max(1, round(steps + sharpness * 10))) + tuned_cfg = float(cfg + contrast * 2.0) + tuned_denoise = float(np.clip(denoise + (0.5 - brightness), 0.0, 1.0)) + + return (tuned_steps, tuned_cfg, tuned_denoise) + diff --git a/mod/hard/mg_cade25.py b/mod/hard/mg_cade25.py new file mode 100644 index 0000000000000000000000000000000000000000..a295701ddd7fbe5bae75a7dd131ab6cf7cf33fe3 --- /dev/null +++ b/mod/hard/mg_cade25.py @@ -0,0 +1,1864 @@ +"""CADE 2.5: refined adaptive enhancer with reference clean and accumulation override. + +Builds on the CADE2 Beta: single clean iteration loop, optional latent-based +parameter damping, CLIP-based reference clean, and per-run SageAttention +accumulation override. +""" + +from __future__ import annotations # moved/renamed module: mg_cade25 + +import torch +import os +import numpy as np +import torch.nn.functional as F + +import nodes +import comfy.model_management as model_management + +from .mg_adaptive import AdaptiveSamplerHelper +from .mg_zesmart_sampler_v1_1 import _build_hybrid_sigmas +import comfy.sample as _sample +import comfy.samplers as _samplers +import comfy.utils as _utils +from .mg_upscale_module import MagicUpscaleModule, clear_gpu_and_ram_cache +from .mg_controlfusion import _build_depth_map as _cf_build_depth_map +from .mg_ids import IntelligentDetailStabilizer +from .. import mg_sagpu_attention as sa_patch +# FDG/NAG experimental paths removed for now; keeping code lean + + +# Lazy CLIPSeg cache +_CLIPSEG_MODEL = None +_CLIPSEG_PROC = None +_CLIPSEG_DEV = "cpu" +_CLIPSEG_FORCE_CPU = True # pin CLIPSeg to CPU to avoid device drift + +# Per-iteration spatial guidance mask (B,1,H,W) in [0,1]; used by cfg_func when enabled +# Kept for potential future use with non-ONNX masks (e.g., CLIPSeg/ControlFusion), +# but not set by this node since ONNX paths are removed. +CURRENT_ONNX_MASK_BCHW = None + + +# ONNX runtime initialization removed + + +def _try_init_clipseg(): + """Lazy-load CLIPSeg processor + model and choose device. + Returns True on success. + """ + global _CLIPSEG_MODEL, _CLIPSEG_PROC, _CLIPSEG_DEV + if (_CLIPSEG_MODEL is not None) and (_CLIPSEG_PROC is not None): + return True + try: + from transformers import CLIPSegProcessor, CLIPSegForImageSegmentation # type: ignore + except Exception: + if not globals().get("_CLIPSEG_WARNED", False): + print("[CADE2.5][CLIPSeg] transformers not available; CLIPSeg disabled.") + globals()["_CLIPSEG_WARNED"] = True + return False + try: + _CLIPSEG_PROC = CLIPSegProcessor.from_pretrained("CIDAS/clipseg-rd64-refined") + _CLIPSEG_MODEL = CLIPSegForImageSegmentation.from_pretrained("CIDAS/clipseg-rd64-refined") + if _CLIPSEG_FORCE_CPU: + _CLIPSEG_DEV = "cpu" + else: + _CLIPSEG_DEV = "cuda" if torch.cuda.is_available() else "cpu" + _CLIPSEG_MODEL = _CLIPSEG_MODEL.to(_CLIPSEG_DEV) + _CLIPSEG_MODEL.eval() + return True + except Exception as e: + print(f"[CADE2.5][CLIPSeg] failed to load model: {e}") + return False + + +def _clipseg_build_mask(image_bhwc: torch.Tensor, + text: str, + preview: int = 224, + threshold: float = 0.4, + blur: float = 7.0, + dilate: int = 4, + gain: float = 1.0, + ref_embed: torch.Tensor | None = None, + clip_vision=None, + ref_threshold: float = 0.03) -> torch.Tensor | None: + """Return BHWC single-channel mask [0,1] from CLIPSeg. + - Uses cached CLIPSeg model; gracefully returns None on failure. + - Applies optional threshold/blur/dilate and scaling gain. + - If clip_vision + ref_embed provided, gates mask by CLIP-Vision distance. + """ + if not text or not isinstance(text, str): + return None + if not _try_init_clipseg(): + return None + try: + # Prepare preview image (CPU PIL) + target = int(max(16, min(1024, preview))) + img = image_bhwc.detach().to('cpu') + B, H, W, C = img.shape + x = img[0].movedim(-1, 0).unsqueeze(0) # 1,C,H,W + x = F.interpolate(x, size=(target, target), mode='bilinear', align_corners=False) + x = x.clamp(0, 1) + arr = (x[0].movedim(0, -1).numpy() * 255.0).astype('uint8') + from PIL import Image # lazy import + pil_img = Image.fromarray(arr) + + # Run CLIPSeg + import re + prompts = [t.strip() for t in re.split(r"[\|,;\n]+", text) if t.strip()] + if not prompts: + prompts = [text.strip()] + prompts = prompts[:8] + inputs = _CLIPSEG_PROC(text=prompts, images=[pil_img] * len(prompts), return_tensors="pt") + inputs = {k: v.to(_CLIPSEG_DEV) for k, v in inputs.items()} + with torch.inference_mode(): + outputs = _CLIPSEG_MODEL(**inputs) # type: ignore + # logits: [N, H', W'] for N prompts + logits = outputs.logits # [N,h,w] + if logits.ndim == 2: + logits = logits.unsqueeze(0) + prob = torch.sigmoid(logits) # [N,h,w] + # Soft-OR fuse across prompts + prob = 1.0 - torch.prod(1.0 - prob.clamp(0, 1), dim=0, keepdim=True) # [1,h,w] + prob = prob.unsqueeze(1) # [1,1,h,w] + # Resize to original image size + prob = F.interpolate(prob, size=(H, W), mode='bilinear', align_corners=False) + m = prob[0, 0].to(dtype=image_bhwc.dtype, device=image_bhwc.device) + # Threshold + blur (approx) + if threshold > 0.0: + m = torch.where(m > float(threshold), m, torch.zeros_like(m)) + # Gaussian blur via our depthwise helper + if blur > 0.0: + rad = int(max(1, min(7, round(blur)))) + m = _gaussian_blur_nchw(m.unsqueeze(0).unsqueeze(0), sigma=float(max(0.5, blur)), radius=rad)[0, 0] + # Dilation via max-pool + if int(dilate) > 0: + k = int(dilate) * 2 + 1 + p = int(dilate) + m = F.max_pool2d(m.unsqueeze(0).unsqueeze(0), kernel_size=k, stride=1, padding=p)[0, 0] + # Optional CLIP-Vision gating by reference distance + if (clip_vision is not None) and (ref_embed is not None): + try: + cur = _encode_clip_image(image_bhwc, clip_vision, target_res=224) + dist = _clip_cosine_distance(cur, ref_embed) + if dist > float(ref_threshold): + # up to +50% gain if сильно уехали + gate = 1.0 + min(0.5, (dist - float(ref_threshold)) * 4.0) + m = m * gate + except Exception: + pass + m = (m * float(max(0.0, gain))).clamp(0, 1) + return m.unsqueeze(0).unsqueeze(-1) # BHWC with B=1,C=1 + except Exception as e: + if not globals().get("_CLIPSEG_WARNED", False): + print(f"[CADE2.5][CLIPSeg] mask failed: {e}") + globals()["_CLIPSEG_WARNED"] = True + return None + + +def _np_to_mask_tensor(np_map: np.ndarray, out_h: int, out_w: int, device, dtype): + """Convert numpy heatmap [H,W] or [1,H,W] or [H,W,1] to BHWC torch mask with B=1 and resize to out_h,out_w.""" + if np_map.ndim == 3: + np_map = np_map.reshape(np_map.shape[-2], np_map.shape[-1]) if (np_map.shape[0] == 1) else np_map.squeeze() + if np_map.ndim != 2: + return None + t = torch.from_numpy(np_map.astype(np.float32)) + t = t.clamp_min(0.0) + t = t.unsqueeze(0).unsqueeze(0) # B=1,C=1,H,W + t = F.interpolate(t, size=(out_h, out_w), mode="bilinear", align_corners=False) + t = t.permute(0, 2, 3, 1).to(device=device, dtype=dtype) # B,H,W,C + return t.clamp(0, 1) + + +# --- Firefly/Hot-pixel remover (image space, BHWC in 0..1) --- +def _median_pool3x3_bhwc(img_bhwc: torch.Tensor) -> torch.Tensor: + B, H, W, C = img_bhwc.shape + x = img_bhwc.permute(0, 3, 1, 2) # B,C,H,W + unfold = F.unfold(x, kernel_size=3, padding=1) # B, 9*C, H*W + unfold = unfold.view(B, x.shape[1], 9, H, W) # B,C,9,H,W + med, _ = torch.median(unfold, dim=2) # B,C,H,W + return med.permute(0, 2, 3, 1) # B,H,W,C + + +def _despeckle_fireflies(img_bhwc: torch.Tensor, + thr: float = 0.985, + max_iso: float | None = None, + grad_gate: float = 0.25) -> torch.Tensor: + try: + dev, dt = img_bhwc.device, img_bhwc.dtype + B, H, W, C = img_bhwc.shape + s = max(H, W) / 1024.0 + k = 3 if s <= 1.1 else (5 if s <= 2.0 else 7) + pad = k // 2 + lum = (0.2126 * img_bhwc[..., 0] + 0.7152 * img_bhwc[..., 1] + 0.0722 * img_bhwc[..., 2]).to(device=dev, dtype=dt) + try: + q = float(torch.quantile(lum.reshape(-1), 0.9995).item()) + thr_eff = max(float(thr), min(0.997, q)) + except Exception: + thr_eff = float(thr) + # S/V based candidate: white, low saturation + R, G, Bc = img_bhwc[..., 0], img_bhwc[..., 1], img_bhwc[..., 2] + V = torch.maximum(R, torch.maximum(G, Bc)) + mi = torch.minimum(R, torch.minimum(G, Bc)) + S = 1.0 - (mi / (V + 1e-6)) + v_thr = max(0.985, thr_eff) + s_thr = 0.06 + cand = (V > v_thr) & (S < s_thr) + # gradient gate + kx = torch.tensor([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], device=dev, dtype=dt).view(1, 1, 3, 3) + ky = torch.tensor([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], device=dev, dtype=dt).view(1, 1, 3, 3) + gx = F.conv2d(lum.unsqueeze(1), kx, padding=1) + gy = F.conv2d(lum.unsqueeze(1), ky, padding=1) + grad = torch.sqrt(gx * gx + gy * gy).squeeze(1) + safe_gate = float(grad_gate) * (k / 3.0) ** 0.5 + cand = cand & (grad < safe_gate) + if cand.any(): + try: + import cv2, numpy as _np + masks = [] + for b in range(cand.shape[0]): + msk = cand[b].detach().to('cpu').numpy().astype('uint8') * 255 + num, labels, stats, _ = cv2.connectedComponentsWithStats(msk, connectivity=8) + rem = _np.zeros_like(msk, dtype='uint8') + area_max = int(max(3, round((k * k) * 0.6))) + for lbl in range(1, num): + area = stats[lbl, cv2.CC_STAT_AREA] + if area <= area_max: + rem[labels == lbl] = 255 + masks.append(torch.from_numpy(rem > 0)) + rm = torch.stack(masks, dim=0).to(device=dev) + rm = rm.unsqueeze(-1) + if rm.any(): + med = _median_pool3x3_bhwc(img_bhwc) + return torch.where(rm, med, img_bhwc) + except Exception: + pass + # Fallback: density isolation + bright = (img_bhwc.min(dim=-1).values > v_thr) + dens = F.avg_pool2d(bright.float().unsqueeze(1), k, 1, pad).squeeze(1) + max_iso_eff = (2.0 / (k * k)) if (max_iso is None) else float(max_iso) + iso = bright & (dens < max_iso_eff) & (grad < safe_gate) + if not iso.any(): + return img_bhwc + med = _median_pool3x3_bhwc(img_bhwc) + return torch.where(iso.unsqueeze(-1), med, img_bhwc) + except Exception: + return img_bhwc + + +def _try_heatmap_from_outputs(outputs: list, preview_hw: tuple[int, int]): + """Return [H,W] heatmap from model outputs if possible. + Supports: + - Segmentation logits/probabilities (NCHW / NHWC) + - Keypoints arrays -> gaussian disks on points + - Bounding boxes -> soft rectangles + """ + if not outputs: + return None + + Ht, Wt = int(preview_hw[0]), int(preview_hw[1]) + + def to_float(arr): + if arr.dtype not in (np.float32, np.float64): + try: + arr = arr.astype(np.float32) + except Exception: + return None + return arr + + def sigmoid(x): + return 1.0 / (1.0 + np.exp(-x)) + + # 1) Prefer any spatial heatmap first + for out in outputs: + try: + arr = np.asarray(out) + except Exception: + continue + arr = to_float(arr) + if arr is None: + continue + if arr.ndim == 4: + n, a, b, c = arr.shape + if c <= 4 and a >= 8 and b >= 8: + if c == 1: + hm = sigmoid(arr[0, :, :, 0]) if np.max(np.abs(arr)) > 1.5 else arr[0, :, :, 0] + else: + ex = np.exp(arr[0] - np.max(arr[0], axis=-1, keepdims=True)) + prob = ex / np.clip(ex.sum(axis=-1, keepdims=True), 1e-6, None) + hm = 1.0 - prob[..., 0] if prob.shape[-1] > 1 else prob[..., 0] + return hm.astype(np.float32) + else: + if a == 1: + ch = arr[0, 0] + hm = sigmoid(ch) if np.max(np.abs(ch)) > 1.5 else ch + return hm.astype(np.float32) + else: + x = arr[0] + x = x - np.max(x, axis=0, keepdims=True) + ex = np.exp(x) + prob = ex / np.clip(np.sum(ex, axis=0, keepdims=True), 1e-6, None) + bg = prob[0] if prob.shape[0] > 1 else prob[0] + hm = 1.0 - bg + return hm.astype(np.float32) + if arr.ndim == 3: + if arr.shape[0] == 1 and arr.shape[1] >= 8 and arr.shape[2] >= 8: + return arr[0].astype(np.float32) + if arr.ndim == 2 and arr.shape[0] >= 8 and arr.shape[1] >= 8: + return arr.astype(np.float32) + + # 2) Try keypoints and boxes + heat = np.zeros((Ht, Wt), dtype=np.float32) + + def draw_gaussian(hm, cx, cy, sigma=2.5, amp=1.0): + r = max(1, int(3 * sigma)) + xs = np.arange(-r, r + 1, dtype=np.float32) + ys = np.arange(-r, r + 1, dtype=np.float32) + gx = np.exp(-(xs**2) / (2 * sigma * sigma)) + gy = np.exp(-(ys**2) / (2 * sigma * sigma)) + g = np.outer(gy, gx) * float(amp) + x0 = int(round(cx)) - r + y0 = int(round(cy)) - r + x1 = x0 + g.shape[1] + y1 = y0 + g.shape[0] + if x1 < 0 or y1 < 0 or x0 >= Wt or y0 >= Ht: + return + xs0 = max(0, x0) + ys0 = max(0, y0) + xs1 = min(Wt, x1) + ys1 = min(Ht, y1) + gx0 = xs0 - x0 + gy0 = ys0 - y0 + gx1 = gx0 + (xs1 - xs0) + gy1 = gy0 + (ys1 - ys0) + hm[ys0:ys1, xs0:xs1] = np.maximum(hm[ys0:ys1, xs0:xs1], g[gy0:gy1, gx0:gx1]) + + def draw_soft_rect(hm, x0, y0, x1, y1, edge=3.0): + x0, y0, x1, y1 = int(x0), int(y0), int(x1), int(y1) + if x1 <= 0 or y1 <= 0 or x0 >= Wt or y0 >= Ht: + return + xs0 = max(0, min(x0, x1)) + ys0 = max(0, min(y0, y1)) + xs1 = min(Wt, max(x0, x1)) + ys1 = min(Ht, max(y0, y1)) + if xs1 - xs0 <= 0 or ys1 - ys0 <= 0: + return + hm[ys0:ys1, xs0:xs1] = np.maximum(hm[ys0:ys1, xs0:xs1], 1.0) + # feather edges with simple blur-like falloff + if edge > 0: + rad = int(edge) + if rad > 0: + # quick separable triangle filter + line = np.linspace(0, 1, rad + 1, dtype=np.float32)[1:] + for d in range(1, rad + 1): + w = line[d - 1] + if ys0 - d >= 0: + hm[ys0 - d:ys0, xs0:xs1] = np.maximum(hm[ys0 - d:ys0, xs0:xs1], w) + if ys1 + d <= Ht: + hm[ys1:ys1 + d, xs0:xs1] = np.maximum(hm[ys1:ys1 + d, xs0:xs1], w) + if xs0 - d >= 0: + hm[max(0, ys0 - d):min(Ht, ys1 + d), xs0 - d:xs0] = np.maximum( + hm[max(0, ys0 - d):min(Ht, ys1 + d), xs0 - d:xs0], w) + if xs1 + d <= Wt: + hm[max(0, ys0 - d):min(Ht, ys1 + d), xs1:xs1 + d] = np.maximum( + hm[max(0, ys0 - d):min(Ht, ys1 + d), xs1:xs1 + d], w) + + # Inspect outputs to find plausible keypoints/boxes + for out in outputs: + try: + arr = np.asarray(out) + except Exception: + continue + arr = to_float(arr) + if arr is None: + continue + a = arr + # Squeeze batch dims like [1,N,4] -> [N,4] + while a.ndim > 2 and a.shape[0] == 1: + a = np.squeeze(a, axis=0) + # Keypoints: [N,2] or [N,3] or [K, N, 2/3] (relax N limit; subsample if huge) + if a.ndim == 2 and a.shape[-1] in (2, 3): + pts = a + elif a.ndim == 3 and a.shape[-1] in (2, 3): + pts = a.reshape(-1, a.shape[-1]) + else: + pts = None + if pts is not None: + # Coordinates range guess: if max>1.2 -> absolute; else normalized + maxv = float(np.nanmax(np.abs(pts[:, :2]))) if pts.size else 0.0 + for px, py, *rest in pts: + if np.isnan(px) or np.isnan(py): + continue + if maxv <= 1.2: + cx = float(px) * (Wt - 1) + cy = float(py) * (Ht - 1) + else: + cx = float(px) + cy = float(py) + base_sig = max(1.5, min(Ht, Wt) / 128.0) + if _ONNX_KPTS_ENABLE: + draw_gaussian(heat, cx, cy, sigma=base_sig * float(_ONNX_KPTS_SIGMA), amp=float(_ONNX_KPTS_GAIN)) + else: + draw_gaussian(heat, cx, cy, sigma=base_sig) + continue + + # Wholebody-style packed keypoints: [N, K*3] with triples (x,y,conf) + if _ONNX_KPTS_ENABLE and a.ndim == 2 and a.shape[-1] >= 6 and (a.shape[-1] % 3) == 0: + K = a.shape[-1] // 3 + if K >= 5 and K <= 256: + # Guess coordinate range once + with np.errstate(invalid='ignore'): + maxv = float(np.nanmax(np.abs(a[:, :2]))) if a.size else 0.0 + for i in range(a.shape[0]): + row = a[i] + kp = row.reshape(K, 3) + for (px, py, pc) in kp: + if np.isnan(px) or np.isnan(py): + continue + if np.isfinite(pc) and pc < float(_ONNX_KPTS_CONF): + continue + if maxv <= 1.2: + cx = float(px) * (Wt - 1) + cy = float(py) * (Ht - 1) + else: + cx = float(px) + cy = float(py) + base_sig = max(1.0, min(Ht, Wt) / 128.0) + draw_gaussian(heat, cx, cy, sigma=base_sig * float(_ONNX_KPTS_SIGMA), amp=float(_ONNX_KPTS_GAIN)) + continue + # Boxes: [N,4+] (x0,y0,x1,y1) or [N, (x,y,w,h, [conf, ...])]; relax N limit (handle YOLO-style outputs) + if a.ndim == 2 and a.shape[-1] >= 4: + boxes = a + elif a.ndim == 3 and a.shape[-1] >= 4: + # choose the smallest first two dims as N + if a.shape[0] == 1: + boxes = a.reshape(-1, a.shape[-1]) + else: + boxes = a.reshape(-1, a.shape[-1]) + else: + boxes = None + if boxes is not None: + # Optional score gating (try to find a confidence column) + score = None + if boxes.shape[-1] >= 6: + score = boxes[:, 4] + # if classes follow, mix in best class prob + try: + score = score * np.max(boxes[:, 5:], axis=-1) + except Exception: + pass + elif boxes.shape[-1] == 5: + score = boxes[:, 4] + # Keep top-K by score if available + if score is not None: + try: + order = np.argsort(-score) + keep = order[: min(64, order.shape[0])] + boxes = boxes[keep] + score = score[keep] + except Exception: + score = None + + xy = boxes[:, :4] + maxv = float(np.nanmax(np.abs(xy))) if xy.size else 0.0 + if maxv <= 1.2: + x0 = xy[:, 0] * (Wt - 1) + y0 = xy[:, 1] * (Ht - 1) + x1 = xy[:, 2] * (Wt - 1) + y1 = xy[:, 3] * (Ht - 1) + else: + x0, y0, x1, y1 = xy[:, 0], xy[:, 1], xy[:, 2], xy[:, 3] + # Heuristic: if many boxes are inverted, treat as [x,y,w,h] + invalid = np.sum((x1 <= x0) | (y1 <= y0)) + if invalid > 0.5 * x0.shape[0]: + x, y, w, h = x0, y0, x1, y1 + x0 = x - w * 0.5 + y0 = y - h * 0.5 + x1 = x + w * 0.5 + y1 = y + h * 0.5 + for i in range(x0.shape[0]): + if score is not None and np.isfinite(score[i]) and score[i] < 0.2: + continue + draw_soft_rect(heat, x0[i], y0[i], x1[i], y1[i], edge=3.0) + + # Embedded keypoints in YOLO-style rows: try to parse trailing triples (x,y,conf) + if _ONNX_KPTS_ENABLE and boxes.shape[-1] > 6: + D = boxes.shape[-1] + for i in range(boxes.shape[0]): + row = boxes[i] + parsed = False + # try [xyxy, conf, cls, kpts] or [xyxy, conf, kpts] or [xyxy, kpts] + for offset in (6, 5, 4): + t = D - offset + if t >= 6 and t % 3 == 0: + k = t // 3 + kp = row[offset:offset + 3 * k].reshape(k, 3) + parsed = True + break + if not parsed: + continue + for (px, py, pc) in kp: + if np.isnan(px) or np.isnan(py): + continue + if pc < float(_ONNX_KPTS_CONF): + continue + if maxv <= 1.2: + cx = float(px) * (Wt - 1) + cy = float(py) * (Ht - 1) + else: + cx = float(px) + cy = float(py) + base_sig = max(1.0, min(Ht, Wt) / 128.0) + draw_gaussian(heat, cx, cy, sigma=base_sig * float(_ONNX_KPTS_SIGMA), amp=float(_ONNX_KPTS_GAIN)) + + if heat.max() > 0: + heat = np.clip(heat, 0.0, 1.0) + return heat + return None + + +def _onnx_build_mask(image_bhwc: torch.Tensor, preview: int, sensitivity: float, models_dir: str, anomaly_gain: float = 1.0) -> torch.Tensor: + """Deprecated: ONNX path removed. Returns zero mask of input size.""" + B, H, W, C = image_bhwc.shape + return torch.zeros((B, H, W, 1), device=image_bhwc.device, dtype=image_bhwc.dtype) + if not _try_init_onnx(models_dir): + return torch.zeros((image_bhwc.shape[0], image_bhwc.shape[1], image_bhwc.shape[2], 1), device=image_bhwc.device, dtype=image_bhwc.dtype) + + if not _ONNX_SESS: + return torch.zeros((image_bhwc.shape[0], image_bhwc.shape[1], image_bhwc.shape[2], 1), device=image_bhwc.device, dtype=image_bhwc.dtype) + + B, H, W, C = image_bhwc.shape + device = image_bhwc.device + dtype = image_bhwc.dtype + + # Process per-batch image + masks = [] + img_cpu = image_bhwc.detach().to('cpu') + for b in range(B): + masks_b = [] + # Prepare input resized square preview + target = int(max(16, min(1024, preview))) + xb = img_cpu[b].movedim(-1, 0).unsqueeze(0) # 1,C,H,W + x_stretch = F.interpolate(xb, size=(target, target), mode='bilinear', align_corners=False).clamp(0, 1) + x_letter = _letterbox_nchw(xb, target).clamp(0, 1) + # Try four variants: stretch RGB, letterbox RGB, stretch BGR, letterbox BGR + variants = [ + ("stretch-RGB", x_stretch), + ("letterbox-RGB", x_letter), + ("stretch-BGR", x_stretch[:, [2, 1, 0], :, :]), + ("letterbox-BGR", x_letter[:, [2, 1, 0], :, :]), + ] + if _ONNX_DEBUG: + try: + print(f"[CADE2.5][ONNX] Build mask for image[{b}] -> preview {target}x{target}") + except Exception: + pass + + for name, sess in list(_ONNX_SESS.items()): + try: + inputs = sess.get_inputs() + if not inputs: + continue + in_name = inputs[0].name + in_shape = inputs[0].shape if hasattr(inputs[0], 'shape') else None + # Choose layout automatically based on the presence of channel dim=3 + if isinstance(in_shape, (list, tuple)) and len(in_shape) == 4: + dim_vals = [] + for d in in_shape: + try: + dim_vals.append(int(d)) + except Exception: + dim_vals.append(-1) + if dim_vals[-1] == 3: + layout = "NHWC" + else: + layout = "NCHW" + else: + layout = "NCHW?" + if _ONNX_DEBUG: + try: + print(f"[CADE2.5][ONNX] Model '{name}' in_shape={in_shape} layout={layout}") + except Exception: + pass + # Try multiple input variants and scales + hm = None + chosen = None + for vname, vx in variants: + if layout.startswith("NHWC"): + xin = vx.permute(0, 2, 3, 1) + else: + xin = vx + for scale in (1.0, 255.0): + inp = (xin * float(scale)).numpy().astype(np.float32) + feed = {in_name: inp} + outs = sess.run(None, feed) + if _ONNX_DEBUG: + try: + shapes = [] + for o in outs: + try: + shapes.append(tuple(np.asarray(o).shape)) + except Exception: + shapes.append("?") + print(f"[CADE2.5][ONNX] '{name}' {vname} scale={scale} -> outs shapes {shapes}") + except Exception: + pass + hm = _try_heatmap_from_outputs(outs, (target, target)) + if _ONNX_DEBUG: + try: + if hm is None: + print(f"[CADE2.5][ONNX] '{name}' {vname} scale={scale}: no spatial heatmap detected") + else: + print(f"[CADE2.5][ONNX] '{name}' {vname} scale={scale}: heat stats min={np.min(hm):.4f} max={np.max(hm):.4f} mean={np.mean(hm):.4f}") + except Exception: + pass + if hm is not None and np.max(hm) > 0: + chosen = (vname, scale) + break + if hm is not None and np.max(hm) > 0: + break + if hm is None: + continue + # Scale by sensitivity and optional anomaly gain + gain = float(max(0.0, sensitivity)) + if 'anomaly' in name.lower(): + gain *= float(max(0.0, anomaly_gain)) + hm = np.clip(hm * gain, 0.0, 1.0) + tmask = _np_to_mask_tensor(hm, H, W, device, dtype) + if tmask is not None: + masks_b.append(tmask) + if _ONNX_DEBUG: + try: + area = float(tmask.movedim(-1,1).mean().item()) + if chosen is not None: + vname, scale = chosen + print(f"[CADE2.5][ONNX] '{name}' via {vname} x{scale} area={area:.4f}") + else: + print(f"[CADE2.5][ONNX] '{name}' contribution area={area:.4f}") + except Exception: + pass + except Exception: + # Ignore failing models + continue + if not masks_b: + masks.append(torch.zeros((1, H, W, 1), device=device, dtype=dtype)) + else: + # Soft-OR fusion: 1 - prod(1 - m) + stack = torch.stack([masks_b[i] for i in range(len(masks_b))], dim=0) # M,1,H,W,1? actually B dims kept as 1 + fused = 1.0 - torch.prod(1.0 - stack.clamp(0, 1), dim=0) + # Light smoothing via bilinear down/up (anti alias) + ch = fused.permute(0, 3, 1, 2) # B=1,C=1,H,W + dd = F.interpolate(ch, scale_factor=0.5, mode='bilinear', align_corners=False, recompute_scale_factor=False) + uu = F.interpolate(dd, size=(H, W), mode='bilinear', align_corners=False) + fused = uu.permute(0, 2, 3, 1).clamp(0, 1) + if _ONNX_DEBUG: + try: + area = float(fused.movedim(-1,1).mean().item()) + print(f"[CADE2.5][ONNX] Fused area (image[{b}])={area:.4f}") + except Exception: + pass + masks.append(fused) + + return torch.cat(masks, dim=0) + +def _sampler_names(): + try: + import comfy.samplers + return comfy.samplers.KSampler.SAMPLERS + except Exception: + return ["euler"] + + +def _scheduler_names(): + try: + import comfy.samplers + scheds = list(comfy.samplers.KSampler.SCHEDULERS) + if "MGHybrid" not in scheds: + scheds.append("MGHybrid") + return scheds + except Exception: + return ["normal", "MGHybrid"] + + +def safe_decode(vae, lat, tile=512, ovlp=64): + h, w = lat["samples"].shape[-2:] + if min(h, w) > 1024: + # Increase overlap for ultra-hires to reduce seam artifacts + ov = 128 if max(h, w) > 2048 else ovlp + return vae.decode_tiled(lat["samples"], tile_x=tile, tile_y=tile, overlap=ov) + return vae.decode(lat["samples"]) + + +def safe_encode(vae, img, tile=512, ovlp=64): + import math, torch.nn.functional as F + h, w = img.shape[1:3] + try: + stride = int(vae.spacial_compression_decode()) + except Exception: + stride = 8 + if stride <= 0: + stride = 8 + def _align_up(x, s): + return int(((x + s - 1) // s) * s) + Ht = _align_up(h, stride) + Wt = _align_up(w, stride) + x = img + if (Ht != h) or (Wt != w): + # pad on bottom/right using replicate to avoid black borders + pad_h = Ht - h + pad_w = Wt - w + x_nchw = img.movedim(-1, 1) + x_nchw = F.pad(x_nchw, (0, pad_w, 0, pad_h), mode='replicate') + x = x_nchw.movedim(1, -1) + if min(Ht, Wt) > 1024: + ov = 128 if max(Ht, Wt) > 2048 else ovlp + return vae.encode_tiled(x[:, :, :, :3], tile_x=tile, tile_y=tile, overlap=ov) + return vae.encode(x[:, :, :, :3]) + + + +def _gaussian_kernel(kernel_size: int, sigma: float, device=None): + x, y = torch.meshgrid( + torch.linspace(-1, 1, kernel_size, device=device), + torch.linspace(-1, 1, kernel_size, device=device), + indexing="ij", + ) + d = torch.sqrt(x * x + y * y) + g = torch.exp(-(d * d) / (2.0 * sigma * sigma)) + return g / g.sum() + + +def _sharpen_image(image: torch.Tensor, sharpen_radius: int, sigma: float, alpha: float): + if sharpen_radius == 0: + return (image,) + + image = image.to(model_management.get_torch_device()) + batch_size, height, width, channels = image.shape + + kernel_size = sharpen_radius * 2 + 1 + kernel = _gaussian_kernel(kernel_size, sigma, device=image.device) * -(alpha * 10) + kernel = kernel.to(dtype=image.dtype) + center = kernel_size // 2 + kernel[center, center] = kernel[center, center] - kernel.sum() + 1.0 + kernel = kernel.repeat(channels, 1, 1).unsqueeze(1) + + tensor_image = image.permute(0, 3, 1, 2) + tensor_image = F.pad(tensor_image, (sharpen_radius, sharpen_radius, sharpen_radius, sharpen_radius), 'reflect') + sharpened = F.conv2d(tensor_image, kernel, padding=center, groups=channels)[:, :, sharpen_radius:-sharpen_radius, sharpen_radius:-sharpen_radius] + sharpened = sharpened.permute(0, 2, 3, 1) + + result = torch.clamp(sharpened, 0, 1) + return (result.to(model_management.intermediate_device()),) + + +def _encode_clip_image(image: torch.Tensor, clip_vision, target_res: int) -> torch.Tensor: + # image: BHWC in [0,1] + img = image.movedim(-1, 1) # BCHW + img = F.interpolate(img, size=(target_res, target_res), mode="bilinear", align_corners=False) + img = (img * 2.0) - 1.0 + embeds = clip_vision.encode_image(img)["image_embeds"] + embeds = F.normalize(embeds, dim=-1) + return embeds + + +def _clip_cosine_distance(a: torch.Tensor, b: torch.Tensor) -> float: + if a.shape != b.shape: + m = min(a.shape[0], b.shape[0]) + a = a[:m] + b = b[:m] + sim = (a * b).sum(dim=-1).mean().clamp(-1.0, 1.0).item() + return 1.0 - sim + + +def _gaussian_blur_nchw(x: torch.Tensor, sigma: float = 1.0, radius: int = 1) -> torch.Tensor: + """Lightweight depthwise Gaussian blur for NCHW tensors. + Uses reflect padding and a normalized kernel built by _gaussian_kernel. + """ + if radius <= 0: + return x + ksz = radius * 2 + 1 + kernel = _gaussian_kernel(ksz, sigma, device=x.device).to(dtype=x.dtype) + kernel = kernel.repeat(x.shape[1], 1, 1).unsqueeze(1) # [C,1,K,K] + x_pad = F.pad(x, (radius, radius, radius, radius), mode='reflect') + y = F.conv2d(x_pad, kernel, padding=0, groups=x.shape[1]) + return y + + +def _letterbox_nchw(x: torch.Tensor, target: int, pad_val: float = 114.0 / 255.0) -> torch.Tensor: + """Letterbox a BCHW tensor to target x target with constant padding (YOLO-style). + Preserves aspect ratio, centers content, pads with pad_val. + """ + if x.ndim != 4: + return F.interpolate(x, size=(target, target), mode='bilinear', align_corners=False) + b, c, h, w = x.shape + if h == 0 or w == 0: + return F.interpolate(x, size=(target, target), mode='bilinear', align_corners=False) + r = float(min(target / max(1, h), target / max(1, w))) + nh = max(1, int(round(h * r))) + nw = max(1, int(round(w * r))) + y = F.interpolate(x, size=(nh, nw), mode='bilinear', align_corners=False) + pt = (target - nh) // 2 + pb = target - nh - pt + pl = (target - nw) // 2 + pr = target - nw - pl + if pt < 0 or pb < 0 or pl < 0 or pr < 0: + # Fallback stretch if rounding went weird + return F.interpolate(x, size=(target, target), mode='bilinear', align_corners=False) + return F.pad(y, (pl, pr, pt, pb), mode='constant', value=float(pad_val)) + + +def _fdg_filter(delta: torch.Tensor, low_gain: float, high_gain: float, sigma: float = 1.0, radius: int = 1) -> torch.Tensor: + """Frequency-Decoupled Guidance: split delta into low/high bands and reweight. + delta: [B,C,H,W] + """ + low = _gaussian_blur_nchw(delta, sigma=sigma, radius=radius) + high = delta - low + return low * float(low_gain) + high * float(high_gain) + + +def _fdg_split_three(delta: torch.Tensor, + sigma_lo: float = 0.8, + sigma_hi: float = 2.0, + radius: int = 1) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Tri-band split: returns (low, mid, high) for NCHW delta. + low = G(sigma_hi) + mid = G(sigma_lo) - G(sigma_hi) + high = delta - G(sigma_lo) + """ + sig_lo = float(max(0.05, sigma_lo)) + sig_hi = float(max(sig_lo + 1e-3, sigma_hi)) + blur_lo = _gaussian_blur_nchw(delta, sigma=sig_lo, radius=radius) + blur_hi = _gaussian_blur_nchw(delta, sigma=sig_hi, radius=radius) + low = blur_hi + mid = blur_lo - blur_hi + high = delta - blur_lo + return low, mid, high + + +def _fdg_energy_fraction(delta: torch.Tensor, sigma: float = 1.0, radius: int = 1) -> torch.Tensor: + """Return fraction of high-frequency energy: E_high / (E_low + E_high).""" + low = _gaussian_blur_nchw(delta, sigma=sigma, radius=radius) + high = delta - low + e_low = (low * low).mean(dim=(1, 2, 3), keepdim=True) + e_high = (high * high).mean(dim=(1, 2, 3), keepdim=True) + frac = e_high / (e_low + e_high + 1e-8) + return frac + + +def _wrap_model_with_guidance(model, guidance_mode: str, rescale_multiplier: float, momentum_beta: float, cfg_curve: float, perp_damp: float, use_zero_init: bool=False, zero_init_steps: int=0, fdg_low: float = 0.6, fdg_high: float = 1.3, fdg_sigma: float = 1.0, ze_zero_steps: int = 0, ze_adaptive: bool = False, ze_r_switch_hi: float = 0.6, ze_r_switch_lo: float = 0.45, fdg_low_adaptive: bool = False, fdg_low_min: float = 0.45, fdg_low_max: float = 0.7, fdg_ema_beta: float = 0.8, use_local_mask: bool = False, mask_inside: float = 1.0, mask_outside: float = 1.0, + midfreq_enable: bool = False, midfreq_gain: float = 0.0, midfreq_sigma_lo: float = 0.8, midfreq_sigma_hi: float = 2.0, + mahiro_plus_enable: bool = False, mahiro_plus_strength: float = 0.5, + eps_scale_enable: bool = False, eps_scale: float = 0.0): + + """Clone model and attach a cfg mixing function implementing RescaleCFG/FDG, CFGZero*/FD, or hybrid ZeResFDG. + guidance_mode: 'default' | 'RescaleCFG' | 'RescaleFDG' | 'CFGZero*' | 'CFGZeroFD' | 'ZeResFDG' + """ + if guidance_mode == "default": + return model + m = model.clone() + + # State for momentum and sigma normalization across steps + prev_delta = {"t": None} + sigma_seen = {"max": None, "min": None} + # Spectral switching/adaptive low state + spec_state = {"ema": None, "mode": "CFGZeroFD"} + + def cfg_func(args): + cond = args["cond"] + uncond = args["uncond"] + cond_scale = args["cond_scale"] + sigma = args.get("sigma", None) + x_orig = args.get("input", None) + + # Local spatial gain from CURRENT_ONNX_MASK_BCHW, resized to cond spatial size + def _local_gain_for(hw): + if not bool(use_local_mask): + return None + m = globals().get("CURRENT_ONNX_MASK_BCHW", None) + if m is None: + return None + try: + Ht, Wt = int(hw[0]), int(hw[1]) + g = m.to(device=cond.device, dtype=cond.dtype) + if g.shape[-2] != Ht or g.shape[-1] != Wt: + g = F.interpolate(g, size=(Ht, Wt), mode='bilinear', align_corners=False) + gi = float(mask_inside) + go = float(mask_outside) + gain = g * gi + (1.0 - g) * go # [B,1,H,W] + return gain + except Exception: + return None + + # Allow hybrid switch per-step + mode = guidance_mode + if guidance_mode == "ZeResFDG": + if bool(ze_adaptive): + try: + delta_raw = args["cond"] - args["uncond"] + frac_b = _fdg_energy_fraction(delta_raw, sigma=float(fdg_sigma), radius=1) # [B,1,1,1] + frac = float(frac_b.mean().clamp(0.0, 1.0).item()) + except Exception: + frac = 0.0 + if spec_state["ema"] is None: + spec_state["ema"] = frac + else: + beta = float(max(0.0, min(0.99, fdg_ema_beta))) + spec_state["ema"] = beta * float(spec_state["ema"]) + (1.0 - beta) * frac + r = float(spec_state["ema"]) + # Hysteresis: switch up/down with two thresholds + if spec_state["mode"] == "CFGZeroFD" and r >= float(ze_r_switch_hi): + spec_state["mode"] = "RescaleFDG" + elif spec_state["mode"] == "RescaleFDG" and r <= float(ze_r_switch_lo): + spec_state["mode"] = "CFGZeroFD" + mode = spec_state["mode"] + else: + try: + sigmas = args["model_options"]["transformer_options"]["sample_sigmas"] + matched_idx = (sigmas == args["timestep"][0]).nonzero() + if len(matched_idx) > 0: + current_idx = matched_idx.item() + else: + current_idx = 0 + except Exception: + current_idx = 0 + mode = "CFGZeroFD" if current_idx <= int(ze_zero_steps) else "RescaleFDG" + + if mode in ("CFGZero*", "CFGZeroFD"): + # Optional zero-init for the first N steps + if use_zero_init and "model_options" in args and args.get("timestep") is not None: + try: + sigmas = args["model_options"]["transformer_options"]["sample_sigmas"] + matched_idx = (sigmas == args["timestep"][0]).nonzero() + if len(matched_idx) > 0: + current_idx = matched_idx.item() + else: + # fallback lookup + current_idx = 0 + if current_idx <= int(zero_init_steps): + return cond * 0.0 + except Exception: + pass + # Project cond onto uncond subspace (batch-wise alpha) + bsz = cond.shape[0] + pos_flat = cond.view(bsz, -1) + neg_flat = uncond.view(bsz, -1) + dot = torch.sum(pos_flat * neg_flat, dim=1, keepdim=True) + denom = torch.sum(neg_flat * neg_flat, dim=1, keepdim=True).clamp_min(1e-8) + alpha = (dot / denom).view(bsz, *([1] * (cond.dim() - 1))) + resid = cond - uncond * alpha + # Adaptive low gain if enabled + low_gain_eff = float(fdg_low) + if bool(fdg_low_adaptive) and spec_state["ema"] is not None: + s = float(spec_state["ema"]) # 0..1 fraction of high-frequency energy + lmin = float(fdg_low_min) + lmax = float(fdg_low_max) + low_gain_eff = max(0.0, min(2.0, lmin + (lmax - lmin) * s)) + if mode == "CFGZeroFD": + resid = _fdg_filter(resid, low_gain=low_gain_eff, high_gain=fdg_high, sigma=float(fdg_sigma), radius=1) + # Apply local spatial gain to residual guidance + lg = _local_gain_for((cond.shape[-2], cond.shape[-1])) + if lg is not None: + resid = resid * lg.expand(-1, resid.shape[1], -1, -1) + noise_pred = uncond * alpha + cond_scale * resid + return noise_pred + + # RescaleCFG/FDG path (with optional momentum/perp damping and S-curve shaping) + delta = cond - uncond + pd = float(max(0.0, min(1.0, perp_damp))) + if pd > 0.0 and (prev_delta["t"] is not None) and (prev_delta["t"].shape == delta.shape): + prev = prev_delta["t"] + denom = (prev * prev).sum(dim=(1,2,3), keepdim=True).clamp_min(1e-6) + coeff = ((delta * prev).sum(dim=(1,2,3), keepdim=True) / denom) + parallel = coeff * prev + delta = delta - pd * parallel + beta = float(max(0.0, min(0.95, momentum_beta))) + if beta > 0.0: + if prev_delta["t"] is None or prev_delta["t"].shape != delta.shape: + prev_delta["t"] = delta.detach() + delta = (1.0 - beta) * delta + beta * prev_delta["t"] + prev_delta["t"] = delta.detach() + cond = uncond + delta + else: + prev_delta["t"] = delta.detach() + # After momentum: optionally apply FDG and rebuild cond + if mode == "RescaleFDG": + # Adaptive low gain if enabled + low_gain_eff = float(fdg_low) + if bool(fdg_low_adaptive) and spec_state["ema"] is not None: + s = float(spec_state["ema"]) # 0..1 + lmin = float(fdg_low_min) + lmax = float(fdg_low_max) + low_gain_eff = max(0.0, min(2.0, lmin + (lmax - lmin) * s)) + delta_fdg = _fdg_filter(delta, low_gain=low_gain_eff, high_gain=fdg_high, sigma=float(fdg_sigma), radius=1) + # Optional mid-frequency emphasis (band-pass) blended on top + if bool(midfreq_enable) and abs(float(midfreq_gain)) > 1e-6: + lo, mid, hi = _fdg_split_three(delta, sigma_lo=float(midfreq_sigma_lo), sigma_hi=float(midfreq_sigma_hi), radius=1) + # Respect local mask gain if present + lg = _local_gain_for((cond.shape[-2], cond.shape[-1])) + if lg is not None: + mid = mid * lg.expand(-1, mid.shape[1], -1, -1) + delta_fdg = delta_fdg + float(midfreq_gain) * mid + lg = _local_gain_for((cond.shape[-2], cond.shape[-1])) + if lg is not None: + delta_fdg = delta_fdg * lg.expand(-1, delta_fdg.shape[1], -1, -1) + cond = uncond + delta_fdg + else: + lg = _local_gain_for((cond.shape[-2], cond.shape[-1])) + if lg is not None: + delta = delta * lg.expand(-1, delta.shape[1], -1, -1) + cond = uncond + delta + + cond_scale_eff = cond_scale + if cfg_curve > 0.0 and (sigma is not None): + s = sigma + if s.ndim > 1: + s = s.flatten() + s_max = float(torch.max(s).item()) + s_min = float(torch.min(s).item()) + if sigma_seen["max"] is None: + sigma_seen["max"] = s_max + sigma_seen["min"] = s_min + else: + sigma_seen["max"] = max(sigma_seen["max"], s_max) + sigma_seen["min"] = min(sigma_seen["min"], s_min) + lo = max(1e-6, sigma_seen["min"]) + hi = max(lo * (1.0 + 1e-6), sigma_seen["max"]) + t = (torch.log(s + 1e-6) - torch.log(torch.tensor(lo, device=sigma.device))) / (torch.log(torch.tensor(hi, device=sigma.device)) - torch.log(torch.tensor(lo, device=sigma.device)) + 1e-6) + t = t.clamp(0.0, 1.0) + k = 6.0 * float(cfg_curve) + s_curve = torch.tanh((t - 0.5) * k) + gain = 1.0 + 0.15 * float(cfg_curve) * s_curve + if gain.ndim > 0: + gain = gain.mean().item() + cond_scale_eff = cond_scale * float(gain) + + # Epsilon scaling (exposure bias correction): early steps get multiplier closer to (1 + eps_scale) + eps_mult = 1.0 + if bool(eps_scale_enable) and (sigma is not None): + try: + s = sigma + if s.ndim > 1: + s = s.flatten() + s_max = float(torch.max(s).item()) + s_min = float(torch.min(s).item()) + if sigma_seen["max"] is None: + sigma_seen["max"] = s_max + sigma_seen["min"] = s_min + else: + sigma_seen["max"] = max(sigma_seen["max"], s_max) + sigma_seen["min"] = min(sigma_seen["min"], s_min) + lo = max(1e-6, sigma_seen["min"]) + hi = max(lo * (1.0 + 1e-6), sigma_seen["max"]) + t_lin = (torch.log(s + 1e-6) - torch.log(torch.tensor(lo, device=sigma.device))) / (torch.log(torch.tensor(hi, device=sigma.device)) - torch.log(torch.tensor(lo, device=sigma.device)) + 1e-6) + t_lin = t_lin.clamp(0.0, 1.0) + w_early = (1.0 - t_lin).mean().item() + eps_mult = float(1.0 + eps_scale * w_early) + except Exception: + eps_mult = float(1.0 + eps_scale) + + if sigma is None or x_orig is None: + return uncond + cond_scale * (cond - uncond) + sigma_ = sigma.view(sigma.shape[:1] + (1,) * (cond.ndim - 1)) + x = x_orig / (sigma_ * sigma_ + 1.0) + v_cond = ((x - (x_orig - cond)) * (sigma_ ** 2 + 1.0) ** 0.5) / (sigma_) + v_uncond = ((x - (x_orig - uncond)) * (sigma_ ** 2 + 1.0) ** 0.5) / (sigma_) + v_cfg = v_uncond + cond_scale_eff * (v_cond - v_uncond) + ro_pos = torch.std(v_cond, dim=(1, 2, 3), keepdim=True) + ro_cfg = torch.std(v_cfg, dim=(1, 2, 3), keepdim=True).clamp_min(1e-6) + v_rescaled = v_cfg * (ro_pos / ro_cfg) + v_final = float(rescale_multiplier) * v_rescaled + (1.0 - float(rescale_multiplier)) * v_cfg + eps = x_orig - (x - (v_final * eps_mult) * sigma_ / (sigma_ * sigma_ + 1.0) ** 0.5) + return eps + + m.set_model_sampler_cfg_function(cfg_func, disable_cfg1_optimization=True) + + # Optional directional post-mix inspired by Mahiro (global, no ONNX) + if bool(mahiro_plus_enable): + s_clamp = float(max(0.0, min(1.0, mahiro_plus_strength))) + mb_state = {"ema": None} + + def _sqrt_sign(x: torch.Tensor) -> torch.Tensor: + return x.sign() * torch.sqrt(x.abs().clamp_min(1e-12)) + + def _hp_split(x: torch.Tensor, radius: int = 1, sigma: float = 1.0): + low = _gaussian_blur_nchw(x, sigma=sigma, radius=radius) + high = x - low + return low, high + + def _sched_gain(args) -> float: + # Gentle mid-steps boost: triangle peak at the middle of schedule + try: + sigmas = args["model_options"]["transformer_options"]["sample_sigmas"] + idx_t = args.get("timestep", None) + if idx_t is None: + return 1.0 + matched = (sigmas == idx_t[0]).nonzero() + if len(matched) == 0: + return 1.0 + i = float(matched.item()) + n = float(sigmas.shape[0]) + if n <= 1: + return 1.0 + phase = i / (n - 1.0) + tri = 1.0 - abs(2.0 * phase - 1.0) + return float(0.6 + 0.4 * tri) # 0.6 at edges -> 1.0 mid + except Exception: + return 1.0 + + def mahiro_plus_post(args): + try: + scale = args.get('cond_scale', 1.0) + cond_p = args['cond_denoised'] + uncond_p = args['uncond_denoised'] + cfg = args['denoised'] + + # Orthogonalize positive to negative direction (batch-wise) + bsz = cond_p.shape[0] + pos_flat = cond_p.view(bsz, -1) + neg_flat = uncond_p.view(bsz, -1) + dot = torch.sum(pos_flat * neg_flat, dim=1, keepdim=True) + denom = torch.sum(neg_flat * neg_flat, dim=1, keepdim=True).clamp_min(1e-8) + alpha = (dot / denom).view(bsz, *([1] * (cond_p.dim() - 1))) + c_orth = cond_p - uncond_p * alpha + + leap_raw = float(scale) * c_orth + # Light high-pass emphasis for detail, protect low-frequency tone + low, high = _hp_split(leap_raw, radius=1, sigma=1.0) + leap = 0.35 * low + 1.00 * high + + # Directional agreement (global cosine over flattened dims) + u_leap = float(scale) * uncond_p + merge = 0.5 * (leap + cfg) + nu = _sqrt_sign(u_leap).flatten(1) + nm = _sqrt_sign(merge).flatten(1) + sim = F.cosine_similarity(nu, nm, dim=1).mean() + a = torch.clamp((sim + 1.0) * 0.5, 0.0, 1.0) + # Small EMA for temporal smoothness + if mb_state["ema"] is None: + mb_state["ema"] = float(a) + else: + mb_state["ema"] = 0.8 * float(mb_state["ema"]) + 0.2 * float(a) + a_eff = float(mb_state["ema"]) + w = a_eff * cfg + (1.0 - a_eff) * leap + + # Gentle energy match to CFG + dims = tuple(range(1, w.dim())) + ro_w = torch.std(w, dim=dims, keepdim=True).clamp_min(1e-6) + ro_cfg = torch.std(cfg, dim=dims, keepdim=True).clamp_min(1e-6) + w_res = w * (ro_cfg / ro_w) + + # Schedule gain over steps (mid stronger) + s_eff = s_clamp * _sched_gain(args) + out = (1.0 - s_eff) * cfg + s_eff * w_res + return out + except Exception: + return args['denoised'] + + try: + m.set_model_sampler_post_cfg_function(mahiro_plus_post) + except Exception: + pass + + # Quantile clamp stabilizer (per-sample): soft range limit for denoised tensor + # Always on, under the hood. Helps prevent rare exploding values. + def _qclamp_post(args): + try: + x = args.get("denoised", None) + if x is None: + return args["denoised"] + dt = x.dtype + xf = x.to(dtype=torch.float32) + B = xf.shape[0] + lo_q, hi_q = 0.001, 0.999 + out = [] + for i in range(B): + t = xf[i].reshape(-1) + try: + lo = torch.quantile(t, lo_q) + hi = torch.quantile(t, hi_q) + except Exception: + n = t.numel() + k_lo = max(1, int(n * lo_q)) + k_hi = max(1, int(n * hi_q)) + lo = torch.kthvalue(t, k_lo).values + hi = torch.kthvalue(t, k_hi).values + out.append(xf[i].clamp(min=lo, max=hi)) + y = torch.stack(out, dim=0).to(dtype=dt) + return y + except Exception: + return args["denoised"] + + try: + m.set_model_sampler_post_cfg_function(_qclamp_post) + except Exception: + pass + + return m + + +# --- AQClip-Lite: adaptive soft quantile clipping in latent space (tile overlap) --- +@torch.no_grad() +def _aqclip_lite(latent_bchw: torch.Tensor, + tile: int = 32, + stride: int = 16, + alpha: float = 2.0, + ema_state: dict | None = None, + ema_beta: float = 0.8, + H_override: torch.Tensor | None = None) -> tuple[torch.Tensor, dict]: + try: + z = latent_bchw + B, C, H, W = z.shape + dev, dt = z.device, z.dtype + ksize = max(8, min(int(tile), min(H, W))) + kstride = max(1, min(int(stride), ksize)) + + # Confidence map: attention entropy override or gradient proxy + if (H_override is not None) and isinstance(H_override, torch.Tensor): + hsrc = H_override.to(device=dev, dtype=dt) + if hsrc.dim() == 3: + hsrc = hsrc.unsqueeze(1) + gpool = F.avg_pool2d(hsrc, kernel_size=ksize, stride=kstride) + else: + zm = z.mean(dim=1, keepdim=True) + kx = torch.tensor([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], device=dev, dtype=dt).view(1, 1, 3, 3) + ky = torch.tensor([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], device=dev, dtype=dt).view(1, 1, 3, 3) + gx = F.conv2d(zm, kx, padding=1) + gy = F.conv2d(zm, ky, padding=1) + gmag = torch.sqrt(gx * gx + gy * gy) + gpool = F.avg_pool2d(gmag, kernel_size=ksize, stride=kstride) + gmax = gpool.amax(dim=(2, 3), keepdim=True).clamp_min(1e-6) + Hn = (gpool / gmax).squeeze(1) # B,h',w' + L = Hn.shape[1] * Hn.shape[2] + Hn = Hn.reshape(B, L) + + # Map confidence -> quantiles + ql = 0.5 * (Hn ** 2) + qh = 1.0 - 0.5 * ((1.0 - Hn) ** 2) + + # Per-tile mean/std + unf = F.unfold(z, kernel_size=ksize, stride=kstride) # B, C*ksize*ksize, L + M = unf.shape[1] + mu = unf.mean(dim=1).to(torch.float32) # B,L + var = (unf.to(torch.float32) - mu.unsqueeze(1)).pow(2).mean(dim=1) + sigma = (var + 1e-12).sqrt() + + # Normal inverse approximation: ndtri(q) = sqrt(2)*erfinv(2q-1) + def _ndtri(q: torch.Tensor) -> torch.Tensor: + return (2.0 ** 0.5) * torch.special.erfinv(q.mul(2.0).sub(1.0).clamp(-0.999999, 0.999999)) + k_neg = _ndtri(ql).abs() + k_pos = _ndtri(qh).abs() + lo = mu - k_neg * sigma + hi = mu + k_pos * sigma + + # EMA smooth + if ema_state is None: + ema_state = {} + b = float(max(0.0, min(0.999, ema_beta))) + if 'lo' in ema_state and 'hi' in ema_state and ema_state['lo'].shape == lo.shape: + lo = b * ema_state['lo'] + (1.0 - b) * lo + hi = b * ema_state['hi'] + (1.0 - b) * hi + ema_state['lo'] = lo.detach() + ema_state['hi'] = hi.detach() + + # Soft tanh clip (vectorized in unfold domain) + mid = (lo + hi) * 0.5 + half = (hi - lo) * 0.5 + half = half.clamp_min(1e-6) + y = (unf.to(torch.float32) - mid.unsqueeze(1)) / half.unsqueeze(1) + y = torch.tanh(float(alpha) * y) + unf_clipped = mid.unsqueeze(1) + half.unsqueeze(1) * y + unf_clipped = unf_clipped.to(dt) + + out = F.fold(unf_clipped, output_size=(H, W), kernel_size=ksize, stride=kstride) + ones = torch.ones((B, M, L), device=dev, dtype=dt) + w = F.fold(ones, output_size=(H, W), kernel_size=ksize, stride=kstride).clamp_min(1e-6) + out = out / w + return out, ema_state + except Exception: + return latent_bchw, (ema_state or {}) + +class ComfyAdaptiveDetailEnhancer25: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ("MODEL", {}), + "positive": ("CONDITIONING", {}), + "negative": ("CONDITIONING", {}), + "vae": ("VAE", {}), + "latent": ("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, "step": 0.1}), + "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.0001}), + "sampler_name": (_sampler_names(), {"default": _sampler_names()[0]}), + "scheduler": (_scheduler_names(), {"default": _scheduler_names()[0]}), + "iterations": ("INT", {"default": 1, "min": 1, "max": 1000}), + "steps_delta": ("FLOAT", {"default": 0.0, "min": -1000.0, "max": 1000.0, "step": 0.01}), + "cfg_delta": ("FLOAT", {"default": 0.0, "min": -100.0, "max": 100.0, "step": 0.01}), + "denoise_delta": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.0001}), + "apply_sharpen": ("BOOLEAN", {"default": False}), + "apply_upscale": ("BOOLEAN", {"default": False}), + "apply_ids": ("BOOLEAN", {"default": False}), + "clip_clean": ("BOOLEAN", {"default": False}), + "ids_strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), + "upscale_method": (MagicUpscaleModule.upscale_methods, {"default": "lanczos"}), + "scale_by": ("FLOAT", {"default": 1.2, "min": 1.0, "max": 8.0, "step": 0.01}), + "scale_delta": ("FLOAT", {"default": 0.0, "min": -8.0, "max": 8.0, "step": 0.01}), + "noise_offset": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 0.5, "step": 0.01}), + "threshold": ("FLOAT", {"default": 0.03, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "RMS latent drift threshold (smaller = more damping)."}), + }, + "optional": { + "Sharpnes_strenght": ("FLOAT", {"default": 0.300, "min": 0.0, "max": 1.0, "step": 0.001}), + "latent_compare": ("BOOLEAN", {"default": False, "tooltip": "Use latent drift to gently damp params (safer than overwriting latents)."}), + "accumulation": (["default", "fp32+fp16", "fp32+fp32"], {"default": "default", "tooltip": "Override SageAttention PV accumulation mode for this node run."}), + "reference_clean": ("BOOLEAN", {"default": False, "tooltip": "Use CLIP-Vision similarity to a reference image to stabilize output."}), + "reference_image": ("IMAGE", {}), + "clip_vision": ("CLIP_VISION", {}), + "ref_preview": ("INT", {"default": 224, "min": 64, "max": 512, "step": 16}), + "ref_threshold": ("FLOAT", {"default": 0.03, "min": 0.0, "max": 0.2, "step": 0.001}), + "ref_cooldown": ("INT", {"default": 1, "min": 1, "max": 8}), + + # ONNX detectors removed + + # Guidance controls + "guidance_mode": (["default", "RescaleCFG", "RescaleFDG", "CFGZero*", "CFGZeroFD", "ZeResFDG"], {"default": "RescaleCFG", "tooltip": "Rescale (stable), RescaleFDG (spectral), CFGZero*, CFGZeroFD, or hybrid ZeResFDG."}), + "rescale_multiplier": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Blend between rescaled and plain CFG (like comfy RescaleCFG)."}), + "momentum_beta": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 0.95, "step": 0.01, "tooltip": "EMA momentum in eps-space for (cond-uncond), 0 to disable."}), + "cfg_curve": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "S-curve shaping of cond_scale across steps (0=flat)."}), + "perp_damp": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Remove a small portion of the component parallel to previous delta (0-1)."}), + + # NAG (Normalized Attention Guidance) toggles + "use_nag": ("BOOLEAN", {"default": False, "tooltip": "Apply NAG inside CrossAttention (positive branch) during this node."}), + "nag_scale": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 50.0, "step": 0.1}), + "nag_tau": ("FLOAT", {"default": 2.5, "min": 0.0, "max": 10.0, "step": 0.01}), + "nag_alpha": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01}), + + # AQClip-Lite (adaptive latent clipping) + "aqclip_enable": ("BOOLEAN", {"default": False, "tooltip": "Adaptive soft tile clipping with overlap (reduces spikes on uncertain regions)."}), + "aq_tile": ("INT", {"default": 32, "min": 8, "max": 128, "step": 1}), + "aq_stride": ("INT", {"default": 16, "min": 4, "max": 128, "step": 1}), + "aq_alpha": ("FLOAT", {"default": 2.0, "min": 0.5, "max": 4.0, "step": 0.1}), + "aq_ema_beta": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 0.99, "step": 0.01}), + "aq_attn": ("BOOLEAN", {"default": False, "tooltip": "Use attention entropy as confidence (requires patched attention)."}), + + # CFGZero* extras + "use_zero_init": ("BOOLEAN", {"default": False, "tooltip": "For CFGZero*, zero out first few steps."}), + "zero_init_steps": ("INT", {"default": 0, "min": 0, "max": 20, "step": 1}), + + # FDG controls (placed last to avoid reordering existing fields) + "fdg_low": ("FLOAT", {"default": 0.6, "min": 0.0, "max": 2.0, "step": 0.01, "tooltip": "Low-frequency gain (<1 to restrain masses)."}), + "fdg_high": ("FLOAT", {"default": 1.3, "min": 0.5, "max": 2.5, "step": 0.01, "tooltip": "High-frequency gain (>1 to boost details)."}), + "fdg_sigma": ("FLOAT", {"default": 1.0, "min": 0.5, "max": 2.5, "step": 0.05, "tooltip": "Gaussian sigma for FDG low-pass split."}), + "ze_res_zero_steps": ("INT", {"default": 2, "min": 0, "max": 20, "step": 1, "tooltip": "Hybrid: number of initial steps to use CFGZeroFD before switching to RescaleFDG."}), + + # Adaptive spectral switch (ZeRes) and adaptive low gain + "ze_adaptive": ("BOOLEAN", {"default": False, "tooltip": "Enable spectral switch: CFGZeroFD, RescaleFDG by HF/LF ratio (EMA)."}), + "ze_r_switch_hi": ("FLOAT", {"default": 0.60, "min": 0.10, "max": 0.95, "step": 0.01, "tooltip": "Switch to RescaleFDG when EMA fraction of high-frequency."}), + "ze_r_switch_lo": ("FLOAT", {"default": 0.45, "min": 0.05, "max": 0.90, "step": 0.01, "tooltip": "Switch back to CFGZeroFD when EMA fraction (hysteresis)."}), + "fdg_low_adaptive": ("BOOLEAN", {"default": False, "tooltip": "Adapt fdg_low by HF fraction (EMA)."}), + "fdg_low_min": ("FLOAT", {"default": 0.45, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Lower bound for adaptive fdg_low."}), + "fdg_low_max": ("FLOAT", {"default": 0.70, "min": 0.0, "max": 2.0, "step": 0.01, "tooltip": "Upper bound for adaptive fdg_low."}), + "fdg_ema_beta": ("FLOAT", {"default": 0.80, "min": 0.0, "max": 0.99, "step": 0.01, "tooltip": "EMA smoothing for spectral ratio (higher = smoother)."}), + + # ONNX local guidance and keypoints removed + + # Muse Blend global directional post-mix + "muse_blend": ("BOOLEAN", {"default": False, "tooltip": "Enable Muse Blend (Mahiro+): gentle directional positive blend (global)."}), + "muse_blend_strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Overall influence of Muse Blend over baseline CFG (0..1)."}), + # Exposure Bias Correction (epsilon scaling) + "eps_scale_enable": ("BOOLEAN", {"default": False, "tooltip": "Exposure Bias Correction: scale predicted noise early in schedule."}), + "eps_scale": ("FLOAT", {"default": 0.005, "min": -1.0, "max": 1.0, "step": 0.0005, "tooltip": "Signed scaling near early steps (recommended ~0.0045; use with care)."}), + # KV pruning (self-attention speedup) + "kv_prune_enable": ("BOOLEAN", {"default": False, "tooltip": "Speed: prune K/V tokens in self-attention by energy (safe on hi-res blocks)."}), + "kv_keep": ("FLOAT", {"default": 0.85, "min": 0.5, "max": 1.0, "step": 0.01, "tooltip": "Fraction of tokens to keep when KV pruning is enabled."}), + "kv_min_tokens": ("INT", {"default": 128, "min": 1, "max": 16384, "step": 1, "tooltip": "Minimum sequence length to apply KV pruning."}), + "clipseg_enable": ("BOOLEAN", {"default": False, "tooltip": "Use CLIPSeg to build a text-driven mask (e.g., 'eyes | hands | face')."}), + "clipseg_text": ("STRING", {"default": "", "multiline": False}), + "clipseg_preview": ("INT", {"default": 224, "min": 64, "max": 512, "step": 16}), + "clipseg_threshold": ("FLOAT", {"default": 0.40, "min": 0.0, "max": 1.0, "step": 0.05}), + "clipseg_blur": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 15.0, "step": 0.1}), + "clipseg_dilate": ("INT", {"default": 4, "min": 0, "max": 10, "step": 1}), + "clipseg_gain": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 3.0, "step": 0.01}), + "clipseg_blend": (["fuse", "replace", "intersect"], {"default": "fuse", "tooltip": "How to combine CLIPSeg with any pre-mask (if present)."}), + "clipseg_ref_gate": ("BOOLEAN", {"default": False, "tooltip": "If reference provided, boost mask when far from reference (CLIP-Vision)."}), + "clipseg_ref_threshold": ("FLOAT", {"default": 0.03, "min": 0.0, "max": 0.2, "step": 0.001}), + + # Polish mode (final hi-res refinement) + "polish_enable": ("BOOLEAN", {"default": False, "tooltip": "Polish: keep low-frequency shape from reference while allowing high-frequency details to refine."}), + "polish_keep_low": ("FLOAT", {"default": 0.4, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "How much low-frequency (global form, lighting) to take from reference image (0=use current, 1=use reference)."}), + "polish_edge_lock": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Edge lock strength: protects edges from sideways drift (0=off, 1=strong)."}), + "polish_sigma": ("FLOAT", {"default": 1.0, "min": 0.3, "max": 3.0, "step": 0.1, "tooltip": "Radius for low/high split: larger keeps bigger shapes as 'low' (global form)."}), + "polish_start_after": ("INT", {"default": 1, "min": 0, "max": 3, "step": 1, "tooltip": "Enable polish after N iterations (0=immediately)."}), + "polish_keep_low_ramp": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Starting share of low-frequency mix; ramps to polish_keep_low over remaining iterations."}), + + }, + } + + RETURN_TYPES = ("LATENT", "IMAGE", "INT", "FLOAT", "FLOAT", "IMAGE") + RETURN_NAMES = ("LATENT", "IMAGE", "steps", "cfg", "denoise", "mask_preview") + FUNCTION = "apply_cade2" + CATEGORY = "MagicNodes" + + def apply_cade2(self, model, vae, positive, negative, latent, seed, steps, cfg, denoise, + sampler_name, scheduler, noise_offset, iterations=1, steps_delta=0.0, + cfg_delta=0.0, denoise_delta=0.0, apply_sharpen=False, + apply_upscale=False, apply_ids=False, clip_clean=False, + ids_strength=0.5, upscale_method="lanczos", scale_by=1.2, scale_delta=0.0, + Sharpnes_strenght=0.300, threshold=0.03, latent_compare=False, accumulation="default", + reference_clean=False, reference_image=None, clip_vision=None, ref_preview=224, ref_threshold=0.03, ref_cooldown=1, + guidance_mode="RescaleCFG", rescale_multiplier=0.7, momentum_beta=0.0, cfg_curve=0.0, perp_damp=0.0, + use_nag=False, nag_scale=4.0, nag_tau=2.5, nag_alpha=0.25, + aqclip_enable=False, aq_tile=32, aq_stride=16, aq_alpha=2.0, aq_ema_beta=0.8, aq_attn=False, + use_zero_init=False, zero_init_steps=0, + fdg_low=0.6, fdg_high=1.3, fdg_sigma=1.0, ze_res_zero_steps=2, + ze_adaptive=False, ze_r_switch_hi=0.60, ze_r_switch_lo=0.45, + fdg_low_adaptive=False, fdg_low_min=0.45, fdg_low_max=0.70, fdg_ema_beta=0.80, + muse_blend=False, muse_blend_strength=0.5, + eps_scale_enable=False, eps_scale=0.005, + clipseg_enable=False, clipseg_text="", clipseg_preview=224, + clipseg_threshold=0.40, clipseg_blur=7.0, clipseg_dilate=4, + clipseg_gain=1.0, clipseg_blend="fuse", clipseg_ref_gate=False, clipseg_ref_threshold=0.03, + polish_enable=False, polish_keep_low=0.4, polish_edge_lock=0.2, polish_sigma=1.0, + polish_start_after=1, polish_keep_low_ramp=0.2, + kv_prune_enable=False, kv_keep=0.85, kv_min_tokens=128): + # Hard reset of any sticky globals from prior runs + try: + global CURRENT_ONNX_MASK_BCHW + CURRENT_ONNX_MASK_BCHW = None + except Exception: + pass + + image = safe_decode(vae, latent) + + tuned_steps, tuned_cfg, tuned_denoise = AdaptiveSamplerHelper().tune( + image, steps, cfg, denoise) + + current_steps = tuned_steps + current_cfg = tuned_cfg + current_denoise = tuned_denoise + # Work on a detached copy to avoid mutating input latent across runs + try: + current_latent = {"samples": latent["samples"].clone()} + except Exception: + current_latent = {"samples": latent["samples"]} + current_scale = scale_by + + ref_embed = None + if reference_clean and (clip_vision is not None) and (reference_image is not None): + try: + ref_embed = _encode_clip_image(reference_image, clip_vision, ref_preview) + except Exception: + ref_embed = None + + # Pre-disable any lingering NAG patch from previous runs and set PV accumulation for this node + try: + sa_patch.enable_crossattention_nag_patch(False) + except Exception: + pass + prev_accum = getattr(sa_patch, "CURRENT_PV_ACCUM", None) + sa_patch.CURRENT_PV_ACCUM = None if accumulation == "default" else accumulation + # Enable NAG patch if requested + try: + sa_patch.enable_crossattention_nag_patch(bool(use_nag), float(nag_scale), float(nag_tau), float(nag_alpha)) + except Exception: + pass + + # Enable attention-entropy probe for AQClip Attn-mode + try: + if hasattr(sa_patch, "enable_attention_entropy_capture"): + sa_patch.enable_attention_entropy_capture(bool(aq_attn), max_tokens=1024, max_heads=4) + except Exception: + pass + + # Visual separation and start marker + try: + print("") + except Exception: + pass + try: + print("\x1b[32m==== Starting main job ====\x1b[0m") + except Exception: + pass + + # Enable KV pruning (self-attention) if requested + try: + if hasattr(sa_patch, "set_kv_prune"): + sa_patch.set_kv_prune(bool(kv_prune_enable), float(kv_keep), int(kv_min_tokens)) + except Exception: + pass + + mask_last = None + try: + with torch.inference_mode(): + __cade_noop = 0 # ensure non-empty with-block + + # Preflight: reset sticky state and build external masks once (CPU-pinned) + try: + CURRENT_ONNX_MASK_BCHW = None + except Exception: + pass + pre_mask = None + pre_area = 0.0 + # ONNX mask removed + # Build CLIPSeg mask once + if bool(clipseg_enable) and isinstance(clipseg_text, str) and clipseg_text.strip() != "": + try: + cmask = _clipseg_build_mask(image, clipseg_text, int(clipseg_preview), float(clipseg_threshold), float(clipseg_blur), int(clipseg_dilate), float(clipseg_gain), None, None, float(clipseg_ref_threshold)) + if cmask is not None: + if pre_mask is None: + pre_mask = cmask + else: + if clipseg_blend == "replace": + pre_mask = cmask + elif clipseg_blend == "intersect": + pre_mask = (pre_mask * cmask).clamp(0, 1) + else: + pre_mask = (1.0 - (1.0 - pre_mask) * (1.0 - cmask)).clamp(0, 1) + except Exception: + pass + if pre_mask is not None: + mask_last = pre_mask + om = pre_mask.movedim(-1, 1) + pre_area = float(om.mean().item()) + # One-time gentle damping from area + try: + if pre_area > 0.005: + damp = 1.0 - min(0.10, 0.02 + pre_area * 0.08) + current_denoise = max(0.10, current_denoise * damp) + current_cfg = max(1.0, current_cfg * (1.0 - 0.005)) + except Exception: + pass + # Compact status + try: + clipseg_status = "on" if bool(clipseg_enable) and isinstance(clipseg_text, str) and clipseg_text.strip() != "" else "off" + # print preflight info only in debug sessions (muted by default) + if False: + print(f"[CADE2.5][preflight] clipseg={clipseg_status} device={'cpu' if _CLIPSEG_FORCE_CPU else _CLIPSEG_DEV} mask_area={pre_area:.4f}") + except Exception: + pass + # Freeze per-iteration external mask rebuild + clipseg_enable = False + # Depth gate cache for micro-detail injection (reuse per resolution) + depth_gate_cache = {"size": None, "mask": None} + for i in range(iterations): + if i % 2 == 0: + clear_gpu_and_ram_cache() + + prev_samples = current_latent["samples"].clone().detach() + + iter_seed = seed + i * 7777 + if noise_offset > 0.0: + # Deterministic noise offset tied to iter_seed + fade = 1.0 - (i / max(1, iterations)) + try: + gen = torch.Generator(device='cpu') + except Exception: + gen = torch.Generator() + gen.manual_seed(int(iter_seed) & 0xFFFFFFFF) + eps = torch.randn( + size=current_latent["samples"].shape, + dtype=current_latent["samples"].dtype, + device='cpu', + generator=gen, + ).to(current_latent["samples"].device) + current_latent["samples"] += (noise_offset * fade) * eps + + # ONNX pre-sampling detectors removed + + # CLIPSeg mask (optional) + try: + if bool(clipseg_enable) and isinstance(clipseg_text, str) and clipseg_text.strip() != "": + img_prev2 = safe_decode(vae, current_latent) + cmask = _clipseg_build_mask(img_prev2, clipseg_text, int(clipseg_preview), float(clipseg_threshold), float(clipseg_blur), int(clipseg_dilate), float(clipseg_gain), ref_embed if bool(clipseg_ref_gate) else None, clip_vision if bool(clipseg_ref_gate) else None, float(clipseg_ref_threshold)) + if cmask is not None: + if mask_last is None: + fused = cmask + else: + if clipseg_blend == "replace": + fused = cmask + elif clipseg_blend == "intersect": + fused = (mask_last * cmask).clamp(0, 1) + else: + fused = (1.0 - (1.0 - mask_last) * (1.0 - cmask)).clamp(0, 1) + mask_last = fused + om = fused.movedim(-1, 1) + area = float(om.mean().item()) + if area > 0.005: + damp = 1.0 - min(0.10, 0.02 + area * 0.08) + current_denoise = max(0.10, current_denoise * damp) + current_cfg = max(1.0, current_cfg * (1.0 - 0.005)) + # No local guidance toggles here; keep optional mask hook clear + except Exception: + pass + + # Guidance override via cfg_func when requested + sampler_model = _wrap_model_with_guidance( + model, guidance_mode, rescale_multiplier, momentum_beta, cfg_curve, perp_damp, + use_zero_init=bool(use_zero_init), zero_init_steps=int(zero_init_steps), + fdg_low=float(fdg_low), fdg_high=float(fdg_high), fdg_sigma=float(fdg_sigma), + midfreq_enable=bool(False), midfreq_gain=float(0.0), midfreq_sigma_lo=float(0.8), midfreq_sigma_hi=float(2.0), + ze_zero_steps=int(ze_res_zero_steps), + ze_adaptive=bool(ze_adaptive), ze_r_switch_hi=float(ze_r_switch_hi), ze_r_switch_lo=float(ze_r_switch_lo), + fdg_low_adaptive=bool(fdg_low_adaptive), fdg_low_min=float(fdg_low_min), fdg_low_max=float(fdg_low_max), fdg_ema_beta=float(fdg_ema_beta), + mahiro_plus_enable=bool(muse_blend), mahiro_plus_strength=float(muse_blend_strength), + eps_scale_enable=bool(eps_scale_enable), eps_scale=float(eps_scale) + ) + + if str(scheduler) == "MGHybrid": + try: + # Build ZeSmart hybrid sigmas with safe defaults + sigmas = _build_hybrid_sigmas( + sampler_model, int(current_steps), str(sampler_name), "hybrid", + mix=0.5, denoise=float(current_denoise), jitter=0.01, seed=int(iter_seed), + _debug=False, tail_smooth=0.15, auto_hybrid_tail=True, auto_tail_strength=0.4, + ) + # Prepare latent + noise like in MG_ZeSmartSampler + lat_img = current_latent["samples"] + lat_img = _sample.fix_empty_latent_channels(sampler_model, lat_img) + batch_inds = current_latent.get("batch_index", None) + noise = _sample.prepare_noise(lat_img, int(iter_seed), batch_inds) + noise_mask = current_latent.get("noise_mask", None) + callback = nodes.latent_preview.prepare_callback(sampler_model, int(current_steps)) + disable_pbar = not _utils.PROGRESS_BAR_ENABLED + sampler_obj = _samplers.sampler_object(str(sampler_name)) + samples = _sample.sample_custom( + sampler_model, noise, float(current_cfg), sampler_obj, sigmas, + positive, negative, lat_img, + noise_mask=noise_mask, callback=callback, + disable_pbar=disable_pbar, seed=int(iter_seed) + ) + current_latent = {**current_latent} + current_latent["samples"] = samples + except Exception as e: + # Fallback to original path if anything goes wrong + print(f"[CADE2.5][MGHybrid] fallback to common_ksampler due to: {e}") + current_latent, = nodes.common_ksampler( + sampler_model, iter_seed, int(current_steps), current_cfg, sampler_name, _scheduler_names()[0], + positive, negative, current_latent, denoise=current_denoise) + else: + current_latent, = nodes.common_ksampler( + sampler_model, iter_seed, int(current_steps), current_cfg, sampler_name, scheduler, + positive, negative, current_latent, denoise=current_denoise) + + if bool(latent_compare): + latent_diff = current_latent["samples"] - prev_samples + rms = torch.sqrt(torch.mean(latent_diff * latent_diff)) + drift = float(rms.item()) + if drift > float(threshold): + overshoot = max(0.0, drift - float(threshold)) + damp = 1.0 - min(0.15, overshoot * 2.0) + current_denoise = max(0.20, current_denoise * damp) + cfg_damp = 0.997 if damp > 0.9 else 0.99 + current_cfg = max(1.0, current_cfg * cfg_damp) + + # AQClip-Lite: adaptive soft clipping in latent space (before decode) + try: + if bool(aqclip_enable): + if 'aq_state' not in locals(): + aq_state = None + H_override = None + if bool(aq_attn) and hasattr(sa_patch, "get_attention_entropy_map"): + try: + Hm = sa_patch.get_attention_entropy_map(clear=False) + if Hm is not None: + H_override = F.interpolate(Hm, size=(current_latent["samples"].shape[-2], current_latent["samples"].shape[-1]), mode="bilinear", align_corners=False) + except Exception: + H_override = None + z_new, aq_state = _aqclip_lite( + current_latent["samples"], + tile=int(aq_tile), stride=int(aq_stride), + alpha=float(aq_alpha), ema_state=aq_state, ema_beta=float(aq_ema_beta), + H_override=H_override, + ) + current_latent["samples"] = z_new + except Exception: + pass + + image = safe_decode(vae, current_latent) + + # Polish mode: keep global form (low frequencies) from reference while letting details refine + if bool(polish_enable) and (i >= int(polish_start_after)): + try: + # Prepare tensors + img = image + ref = reference_image if (reference_image is not None) else img + if ref.shape[1] != img.shape[1] or ref.shape[2] != img.shape[2]: + # resize reference to match current image + ref_n = ref.movedim(-1, 1) + ref_n = F.interpolate(ref_n, size=(img.shape[1], img.shape[2]), mode='bilinear', align_corners=False) + ref = ref_n.movedim(1, -1) + x = img.movedim(-1, 1) + r = ref.movedim(-1, 1) + # Low/high split via Gaussian blur + rad = max(1, int(round(float(polish_sigma) * 2))) + low_x = _gaussian_blur_nchw(x, sigma=float(polish_sigma), radius=rad) + low_r = _gaussian_blur_nchw(r, sigma=float(polish_sigma), radius=rad) + high_x = x - low_x + # Mix low from reference and current with ramp + # a starts from polish_keep_low_ramp and linearly ramps to polish_keep_low over remaining iterations + try: + denom = max(1, int(iterations) - int(polish_start_after)) + t = max(0.0, min(1.0, (i - int(polish_start_after)) / denom)) + except Exception: + t = 1.0 + a0 = float(polish_keep_low_ramp) + at = float(polish_keep_low) + a = a0 + (at - a0) * t + low_mix = low_r * a + low_x * (1.0 - a) + new = low_mix + high_x + # Micro-detail injection on tail: very light HF boost gated by edges+depth + try: + phase = (i + 1) / max(1, int(iterations)) + # ramp starts late (>=0.70 of iterations), slightly earlier and wider + ramp = max(0.0, min(1.0, (phase - 0.70) / 0.30)) + if ramp > 0.0: + # fine-scale high-pass + micro = x - _gaussian_blur_nchw(x, sigma=0.6, radius=1) + # edge gate: suppress near strong edges to avoid halos + gray = x.mean(dim=1, keepdim=True) + sobel_x = torch.tensor([[[-1,0,1],[-2,0,2],[-1,0,1]]], dtype=gray.dtype, device=gray.device).unsqueeze(1) + sobel_y = torch.tensor([[[-1,-2,-1],[0,0,0],[1,2,1]]], dtype=gray.dtype, device=gray.device).unsqueeze(1) + gx = F.conv2d(gray, sobel_x, padding=1) + gy = F.conv2d(gray, sobel_y, padding=1) + mag = torch.sqrt(gx*gx + gy*gy) + m_edge = (mag - mag.amin()) / (mag.amax() - mag.amin() + 1e-8) + g_edge = (1.0 - m_edge).clamp(0.0, 1.0).pow(0.65) # prefer flats/meso-areas + # depth gate: prefer nearer surfaces when depth is available + try: + sz = (int(img.shape[1]), int(img.shape[2])) + if depth_gate_cache.get("size") != sz or depth_gate_cache.get("mask") is None: + model_path = os.path.join(os.path.dirname(__file__), '..', 'depth-anything', 'depth_anything_v2_vitl.pth') + dm = _cf_build_depth_map(img, res=512, model_path=model_path, hires_mode=True) + depth_gate_cache = {"size": sz, "mask": dm} + dm = depth_gate_cache.get("mask") + if dm is not None: + g_depth = (dm.movedim(-1, 1).clamp(0,1)) ** 1.35 + else: + g_depth = torch.ones_like(g_edge) + except Exception: + g_depth = torch.ones_like(g_edge) + g = (g_edge * g_depth).clamp(0.0, 1.0) + micro_boost = 0.018 * ramp # very gentle, slightly higher + new = new + micro_boost * (micro * g) + except Exception: + pass + # Edge-lock: protect edges from drift by biasing toward low_mix along edges + el = float(polish_edge_lock) + if el > 1e-6: + # Sobel edge magnitude on grayscale + gray = x.mean(dim=1, keepdim=True) + sobel_x = torch.tensor([[[-1,0,1],[-2,0,2],[-1,0,1]]], dtype=gray.dtype, device=gray.device).unsqueeze(1) + sobel_y = torch.tensor([[[-1,-2,-1],[0,0,0],[1,2,1]]], dtype=gray.dtype, device=gray.device).unsqueeze(1) + gx = F.conv2d(gray, sobel_x, padding=1) + gy = F.conv2d(gray, sobel_y, padding=1) + mag = torch.sqrt(gx*gx + gy*gy) + m = (mag - mag.amin()) / (mag.amax() - mag.amin() + 1e-8) + # Blend toward low_mix near edges + new = new * (1.0 - el*m) + (low_mix) * (el*m) + img2 = new.movedim(1, -1).clamp(0,1) + # Feed back to latent for next steps + current_latent = {"samples": safe_encode(vae, img2)} + image = img2 + except Exception: + pass + + # ONNX detectors removed + + if reference_clean and (ref_embed is not None) and (i % max(1, ref_cooldown) == 0): + try: + cur_embed = _encode_clip_image(image, clip_vision, ref_preview) + dist = _clip_cosine_distance(cur_embed, ref_embed) + if dist > ref_threshold: + current_denoise = max(0.10, current_denoise * 0.9) + current_cfg = max(1.0, current_cfg * 0.99) + except Exception: + pass + + if apply_upscale and current_scale != 1.0: + current_latent, image = MagicUpscaleModule().process_upscale( + current_latent, vae, upscale_method, current_scale) + # After upscale at large sizes, add a tiny HF sprinkle gated by edges+depth + try: + H, W = int(image.shape[1]), int(image.shape[2]) + if max(H, W) > 1536: + blur = _gaussian_blur(image, radius=1.0, sigma=0.8) + hf = (image - blur).clamp(-1, 1) + # Edge gate in image space (luma Sobel) + lum = (0.2126 * image[..., 0] + 0.7152 * image[..., 1] + 0.0722 * image[..., 2]) + kx = torch.tensor([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], device=lum.device, dtype=lum.dtype).view(1, 1, 3, 3) + ky = torch.tensor([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], device=lum.device, dtype=lum.dtype).view(1, 1, 3, 3) + g = torch.sqrt(F.conv2d(lum.unsqueeze(1), kx, padding=1)**2 + F.conv2d(lum.unsqueeze(1), ky, padding=1)**2).squeeze(1) + m = (g - g.amin()) / (g.amax() - g.amin() + 1e-8) + g_edge = (1.0 - m).clamp(0,1).pow(0.5).unsqueeze(-1) + # Depth gate (once per resolution) + try: + sz = (H, W) + if depth_gate_cache.get("size") != sz or depth_gate_cache.get("mask") is None: + model_path = os.path.join(os.path.dirname(__file__), '..', 'depth-anything', 'depth_anything_v2_vitl.pth') + dm = _cf_build_depth_map(image, res=512, model_path=model_path, hires_mode=True) + depth_gate_cache = {"size": sz, "mask": dm} + dm = depth_gate_cache.get("mask") + if dm is not None: + g_depth = dm.clamp(0,1) ** 1.2 + else: + g_depth = torch.ones_like(g_edge) + except Exception: + g_depth = torch.ones_like(g_edge) + g_tot = (g_edge * g_depth).clamp(0,1) + image = (image + 0.045 * hf * g_tot).clamp(0,1) + except Exception: + pass + current_cfg = max(4.0, current_cfg * (1.0 / current_scale)) + current_denoise = max(0.15, current_denoise * (1.0 / current_scale)) + + current_steps = max(1, current_steps - steps_delta) + current_cfg = max(0.0, current_cfg - cfg_delta) + current_denoise = max(0.0, current_denoise - denoise_delta) + current_scale = max(1.0, current_scale - scale_delta) + + if apply_upscale and current_scale != 1.0 and max(image.shape[1:3]) > 1024: + current_latent = {"samples": safe_encode(vae, image)} + + finally: + # Always disable NAG patch and clear local mask, even on errors + try: + sa_patch.enable_crossattention_nag_patch(False) + except Exception: + pass + try: + sa_patch.CURRENT_PV_ACCUM = prev_accum + except Exception: + pass + try: + CURRENT_ONNX_MASK_BCHW = None + except Exception: + pass + + if apply_ids: + image, = IntelligentDetailStabilizer().stabilize(image, ids_strength) + + if apply_sharpen: + image, = _sharpen_image(image, 2, 1.0, Sharpnes_strenght) + + # Mask preview as IMAGE (RGB) + if mask_last is None: + mask_last = torch.zeros((image.shape[0], image.shape[1], image.shape[2], 1), device=image.device, dtype=image.dtype) + onnx_mask_img = mask_last.repeat(1, 1, 1, 3).clamp(0, 1) + + # Final pass: remove isolated hot whites ("fireflies") without touching real edges/highlights + try: + image = _despeckle_fireflies(image, thr=0.998, max_iso=4.0/9.0, grad_gate=0.15) + except Exception: + pass + + # Cleanup KV pruning state to avoid leaking into other nodes + try: + if hasattr(sa_patch, "set_kv_prune"): + sa_patch.set_kv_prune(False, 1.0, int(kv_min_tokens)) + except Exception: + pass + + return current_latent, image, int(current_steps), float(current_cfg), float(current_denoise), onnx_mask_img + + + diff --git a/mod/hard/mg_controlfusion.py b/mod/hard/mg_controlfusion.py new file mode 100644 index 0000000000000000000000000000000000000000..9016908e028f3d6074d5f3e868d25c4861daa8d7 --- /dev/null +++ b/mod/hard/mg_controlfusion.py @@ -0,0 +1,519 @@ +import os +import sys +import math +import torch +import torch.nn.functional as F +import numpy as np + +import comfy.model_management as model_management + + +_DEPTH_INIT = False +_DEPTH_MODEL = None +_DEPTH_PROC = None + + +def _insert_aux_path(): + try: + base = os.path.dirname(os.path.dirname(__file__)) # .../custom_nodes + aux_root = os.path.join(base, 'comfyui_controlnet_aux') + aux_src = os.path.join(aux_root, 'src') + for p in (aux_src, aux_root): + if os.path.isdir(p) and p not in sys.path: + sys.path.insert(0, p) + except Exception: + pass + + +def _try_init_depth_anything(model_path: str): + global _DEPTH_INIT, _DEPTH_MODEL, _DEPTH_PROC + if _DEPTH_INIT: + return _DEPTH_MODEL is not None + _DEPTH_INIT = True + # Prefer our vendored implementation first + try: + from ...vendor.depth_anything_v2.dpt import DepthAnythingV2 # type: ignore + # Guess config from filename + fname = os.path.basename(model_path or '') + cfgs = { + 'depth_anything_v2_vits.pth': dict(encoder='vits', features=64, out_channels=[48,96,192,384]), + 'depth_anything_v2_vitb.pth': dict(encoder='vitb', features=128, out_channels=[96,192,384,768]), + 'depth_anything_v2_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]), + 'depth_anything_v2_vitg.pth': dict(encoder='vitg', features=384, out_channels=[1536,1536,1536,1536]), + 'depth_anything_v2_metric_vkitti_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]), + 'depth_anything_v2_metric_hypersim_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]), + } + # fallback to vitl if unknown + cfg = cfgs.get(fname, cfgs['depth_anything_v2_vitl.pth']) + device = 'cuda' if torch.cuda.is_available() else 'cpu' + m = DepthAnythingV2(**cfg) + sd = torch.load(model_path, map_location='cpu') + m.load_state_dict(sd) + _DEPTH_MODEL = m.to(device).eval() + _DEPTH_PROC = True + return True + except Exception: + # Try local checkout of comfyui_controlnet_aux (if present) + _insert_aux_path() + try: + from custom_controlnet_aux.depth_anything_v2.dpt import DepthAnythingV2 # type: ignore + fname = os.path.basename(model_path or '') + cfgs = { + 'depth_anything_v2_vits.pth': dict(encoder='vits', features=64, out_channels=[48,96,192,384]), + 'depth_anything_v2_vitb.pth': dict(encoder='vitb', features=128, out_channels=[96,192,384,768]), + 'depth_anything_v2_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]), + 'depth_anything_v2_vitg.pth': dict(encoder='vitg', features=384, out_channels=[1536,1536,1536,1536]), + 'depth_anything_v2_metric_vkitti_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]), + 'depth_anything_v2_metric_hypersim_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]), + } + cfg = cfgs.get(fname, cfgs['depth_anything_v2_vitl.pth']) + device = 'cuda' if torch.cuda.is_available() else 'cpu' + m = DepthAnythingV2(**cfg) + sd = torch.load(model_path, map_location='cpu') + m.load_state_dict(sd) + _DEPTH_MODEL = m.to(device).eval() + _DEPTH_PROC = True + return True + except Exception: + # Fallback: packaged auxiliary API + try: + from controlnet_aux.depth_anything import DepthAnythingDetector, DepthAnythingV2 # type: ignore + device = 'cuda' if torch.cuda.is_available() else 'cpu' + _DEPTH_MODEL = DepthAnythingV2(model_path=model_path, device=device) + _DEPTH_PROC = True + return True + except Exception: + _DEPTH_MODEL = None + _DEPTH_PROC = False + return False + + +def _build_depth_map(image_bhwc: torch.Tensor, res: int, model_path: str, hires_mode: bool = True) -> torch.Tensor: + B, H, W, C = image_bhwc.shape + dev = image_bhwc.device + dtype = image_bhwc.dtype + # Choose target min-side for processing. In hires mode we allow higher caps and keep aspect. + # DepthAnything v2 can be memory-hungry on large inputs; cap min-side at 1024 + cap = 1024 + target = int(max(16, min(cap, res))) + if _try_init_depth_anything(model_path): + try: + # to CPU uint8 + img = image_bhwc.detach().to('cpu') + x = img[0].movedim(-1, 0).unsqueeze(0) + # keep aspect ratio: scale so that min(H,W) == target + _, Cc, Ht, Wt = x.shape + min_side = max(1, min(Ht, Wt)) + scale = float(target) / float(min_side) + out_h = max(1, int(round(Ht * scale))) + out_w = max(1, int(round(Wt * scale))) + x = F.interpolate(x, size=(out_h, out_w), mode='bilinear', align_corners=False) + # make channels-last and ensure contiguous layout for OpenCV + arr = (x[0].movedim(0, -1).contiguous().numpy() * 255.0).astype('uint8') + # Prefer direct DepthAnythingV2 inference if model has infer_image + if hasattr(_DEPTH_MODEL, 'infer_image'): + import cv2 + # Drive input_size from desired depth resolution (min side), let DA keep aspect + input_sz = int(max(224, min(cap, res))) + depth = _DEPTH_MODEL.infer_image(cv2.cvtColor(arr, cv2.COLOR_RGB2BGR), input_size=input_sz, max_depth=20.0) + d = np.asarray(depth, dtype=np.float32) + # Normalize DepthAnythingV2 output (0..max_depth) to 0..1 + d = d / 20.0 + else: + depth = _DEPTH_MODEL(arr) + d = np.asarray(depth, dtype=np.float32) + if d.max() > 1.0: + d = d / 255.0 + d = torch.from_numpy(d)[None, None] # 1,1,h,w + d = F.interpolate(d, size=(H, W), mode='bilinear', align_corners=False) + d = d[0, 0].to(device=dev, dtype=dtype) + d = d.clamp(0, 1) + return d + except Exception: + pass + # Fallback pseudo-depth: luminance + gentle blur + lum = (0.2126 * image_bhwc[..., 0] + 0.7152 * image_bhwc[..., 1] + 0.0722 * image_bhwc[..., 2]).to(dtype=dtype) + x = lum.movedim(-1, 0).unsqueeze(0) if lum.ndim == 3 else lum.unsqueeze(0).unsqueeze(0) + x = F.interpolate(x, size=(H, W), mode='bilinear', align_corners=False) + x = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1) + return x[0, 0].clamp(0, 1) + + +def _pyracanny(image_bhwc: torch.Tensor, + low: int, + high: int, + res: int, + thin_iter: int = 0, + edge_boost: float = 0.0, + smart_tune: bool = False, + smart_boost: float = 0.2, + preserve_aspect: bool = True) -> torch.Tensor: + try: + import cv2 + except Exception: + # Fallback: simple Sobel magnitude + x = image_bhwc.movedim(-1, 1) + xg = x.mean(dim=1, keepdim=True) + gx = F.conv2d(xg, torch.tensor([[[-1, 0, 1],[-2,0,2],[-1,0,1]]], dtype=x.dtype, device=x.device).unsqueeze(1), padding=1) + gy = F.conv2d(xg, torch.tensor([[[-1,-2,-1],[0,0,0],[1,2,1]]], dtype=x.dtype, device=x.device).unsqueeze(1), padding=1) + mag = torch.sqrt(gx*gx + gy*gy) + mag = (mag - mag.amin())/(mag.amax()-mag.amin()+1e-6) + return mag[0,0].clamp(0,1) + B,H,W,C = image_bhwc.shape + img = (image_bhwc.detach().to('cpu')[0].contiguous().numpy()*255.0).astype('uint8') + cap = 4096 + target = int(max(64, min(cap, res))) + if preserve_aspect: + scale = float(target) / float(max(1, min(H, W))) + out_h = max(8, int(round(H * scale))) + out_w = max(8, int(round(W * scale))) + img_res = cv2.resize(img, (out_w, out_h), interpolation=cv2.INTER_LINEAR) + else: + img_res = cv2.resize(img, (target, target), interpolation=cv2.INTER_LINEAR) + gray = cv2.cvtColor(img_res, cv2.COLOR_RGB2GRAY) + pyr_scales = [1.0, 0.5, 0.25] + acc = None + for s in pyr_scales: + if preserve_aspect: + sz = (max(8, int(round(img_res.shape[1]*s))), max(8, int(round(img_res.shape[0]*s)))) + else: + sz = (max(8, int(target*s)), max(8, int(target*s))) + g = cv2.resize(gray, sz, interpolation=cv2.INTER_AREA) + g = cv2.GaussianBlur(g, (5,5), 0) + e = cv2.Canny(g, threshold1=int(low*s), threshold2=int(high*s)) + e = cv2.resize(e, (W, H), interpolation=cv2.INTER_LINEAR) + e = (e.astype(np.float32)/255.0) + acc = e if acc is None else np.maximum(acc, e) + # Estimate density and sharpness for smart tuning + edensity_pre = None + try: + edensity_pre = float(np.mean(acc)) if acc is not None else None + except Exception: + edensity_pre = None + lap_var = None + try: + g32 = gray.astype(np.float32) / 255.0 + lap = cv2.Laplacian(g32, cv2.CV_32F) + lap_var = float(lap.var()) + except Exception: + lap_var = None + + # optional thinning + try: + thin_iter_eff = int(thin_iter) + if smart_tune: + # simple heuristic: more thinning on high res and dense edges + auto = 0 + if target >= 1024: + auto += 1 + if target >= 1400: + auto += 1 + if edensity_pre is not None and edensity_pre > 0.12: + auto += 1 + if edensity_pre is not None and edensity_pre < 0.05: + auto = max(0, auto - 1) + thin_iter_eff = max(thin_iter_eff, min(3, auto)) + if thin_iter_eff > 0: + import cv2 + if hasattr(cv2, 'ximgproc') and hasattr(cv2.ximgproc, 'thinning'): + th = acc.copy() + th = (th*255).astype('uint8') + th = cv2.ximgproc.thinning(th) + acc = th.astype(np.float32)/255.0 + else: + # simple erosion-based thinning approximation + kernel = np.ones((3,3), np.uint8) + t = (acc*255).astype('uint8') + for _ in range(int(thin_iter_eff)): + t = cv2.erode(t, kernel, iterations=1) + acc = t.astype(np.float32)/255.0 + except Exception: + pass + # optional edge boost (unsharp on edge map) + # We fix a gentle boost for micro‑contrast; smart_tune may nudge it slightly + boost_eff = 0.10 + if smart_tune: + try: + lv = 0.0 if lap_var is None else max(0.0, min(1.0, lap_var / 2.0)) + dens = 0.0 if edensity_pre is None else float(max(0.0, min(1.0, edensity_pre))) + boost_eff = max(0.05, min(0.20, boost_eff + (1.0 - dens) * 0.05 + (1.0 - lv) * 0.02)) + except Exception: + pass + if boost_eff and boost_eff != 0.0: + try: + import cv2 + blur = cv2.GaussianBlur(acc, (0,0), sigmaX=1.0) + acc = np.clip(acc + float(boost_eff)*(acc - blur), 0.0, 1.0) + except Exception: + pass + ed = torch.from_numpy(acc).to(device=image_bhwc.device, dtype=image_bhwc.dtype) + return ed.clamp(0,1) + + +def _blend(depth: torch.Tensor, edges: torch.Tensor, mode: str, factor: float) -> torch.Tensor: + depth = depth.clamp(0,1) + edges = edges.clamp(0,1) + if mode == 'max': + return torch.maximum(depth, edges) + if mode == 'edge_over_depth': + # edges override depth (edge=1) while preserving depth elsewhere + return (depth * (1.0 - edges) + edges).clamp(0,1) + # normal + f = float(max(0.0, min(1.0, factor))) + return (depth*(1.0-f) + edges*f).clamp(0,1) + + +def _apply_controlnet_separate(positive, negative, control_net, image_bhwc: torch.Tensor, + strength_pos: float, strength_neg: float, + start_percent: float, end_percent: float, vae=None, + apply_to_uncond: bool = False, + stack_prev_control: bool = False): + control_hint = image_bhwc.movedim(-1,1) + out_pos = [] + out_neg = [] + # POS + for t in positive: + d = t[1].copy() + prev = d.get('control', None) if stack_prev_control else None + c_net = control_net.copy().set_cond_hint(control_hint, float(strength_pos), (start_percent, end_percent), vae=vae, extra_concat=[]) + c_net.set_previous_controlnet(prev) + d['control'] = c_net + d['control_apply_to_uncond'] = bool(apply_to_uncond) + out_pos.append([t[0], d]) + # NEG + for t in negative: + d = t[1].copy() + prev = d.get('control', None) if stack_prev_control else None + c_net = control_net.copy().set_cond_hint(control_hint, float(strength_neg), (start_percent, end_percent), vae=vae, extra_concat=[]) + c_net.set_previous_controlnet(prev) + d['control'] = c_net + d['control_apply_to_uncond'] = bool(apply_to_uncond) + out_neg.append([t[0], d]) + return out_pos, out_neg + + +class MG_ControlFusion: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE", {"tooltip": "Input RGB image (B,H,W,3) in 0..1."}), + "positive": ("CONDITIONING", {"tooltip": "Positive conditioning to apply ControlNet to."}), + "negative": ("CONDITIONING", {"tooltip": "Negative conditioning to apply ControlNet to."}), + "control_net": ("CONTROL_NET", {"tooltip": "ControlNet module receiving the fused mask as hint."}), + "vae": ("VAE", {"tooltip": "VAE used by ControlNet when encoding the hint."}), + }, + "optional": { + "enable_depth": ("BOOLEAN", {"default": True, "tooltip": "Enable depth map fusion (Depth Anything v2 if available)."}), + "depth_model_path": ("STRING", {"default": os.path.join(os.path.dirname(os.path.dirname(__file__)), 'MagicNodes','depth-anything','depth_anything_v2_vitl.pth') if False else os.path.join(os.path.dirname(__file__), '..','depth-anything','depth_anything_v2_vitl.pth'), "tooltip": "Path to Depth Anything v2 .pth weights (vits/vitb/vitl/vitg)."}), + "depth_resolution": ("INT", {"default": 768, "min": 64, "max": 1024, "step": 64, "tooltip": "Depth min-side resolution (cap 1024). In Hi‑Res mode drives DepthAnything input_size."}), + "enable_pyra": ("BOOLEAN", {"default": True, "tooltip": "Enable PyraCanny edge detector."}), + "pyra_low": ("INT", {"default": 109, "min": 0, "max": 255, "tooltip": "Canny low threshold (0..255)."}), + "pyra_high": ("INT", {"default": 147, "min": 0, "max": 255, "tooltip": "Canny high threshold (0..255)."}), + "pyra_resolution": ("INT", {"default": 1024, "min": 64, "max": 4096, "step": 64, "tooltip": "Working resolution for edges (min side, keeps aspect)."}), + "edge_thin_iter": ("INT", {"default": 0, "min": 0, "max": 10, "step": 1, "tooltip": "Thinning iterations for edges (skeletonize). 0 = off."}), + "edge_alpha": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Opacity for edges before blending (0..1)."}), + "edge_boost": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Deprecated: internal boost fixed (~0.10); use edge_alpha instead."}), + "smart_tune": ("BOOLEAN", {"default": False, "tooltip": "Auto-adjust thinning/boost from image edge density and sharpness."}), + "smart_boost": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Scale for auto edge boost when Smart Tune is on."}), + "blend_mode": (["normal","max","edge_over_depth"], {"default": "normal", "tooltip": "Depth+edges merge: normal (mix), max (strongest), edge_over_depth (edges overlay)."}), + "blend_factor": ("FLOAT", {"default": 0.02, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Blend strength for edges into depth (depends on mode)."}), + "strength_pos": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01, "tooltip": "ControlNet strength for positive branch."}), + "strength_neg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01, "tooltip": "ControlNet strength for negative branch."}), + "start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Start percentage along the sampling schedule."}), + "end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "End percentage along the sampling schedule."}), + "preview_res": ("INT", {"default": 1024, "min": 256, "max": 2048, "step": 64, "tooltip": "Preview minimum side (keeps aspect ratio)."}), + "mask_brightness": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Preview brightness multiplier (visualization only)."}), + "preview_show_strength": ("BOOLEAN", {"default": True, "tooltip": "Multiply preview by ControlNet strength for visualization."}), + "preview_strength_branch": (["positive","negative","max","avg"], {"default": "max", "tooltip": "Which strength to reflect in preview (display only)."}), + "hires_mask_auto": ("BOOLEAN", {"default": True, "tooltip": "High‑res mask: keep aspect ratio, scale by minimal side for depth/edges, and drive DepthAnything with your depth_resolution (no 2K cap)."}), + "apply_to_uncond": ("BOOLEAN", {"default": False, "tooltip": "Apply ControlNet hint to the unconditional branch as well (stronger global hold on very large images)."}), + "stack_prev_control": ("BOOLEAN", {"default": False, "tooltip": "Chain with any previously attached ControlNet in the conditioning (advanced). Off = replace to avoid memory bloat."}), + # Split apply: chain Depth and Edges with separate schedules/strengths (fixed order: depth -> edges) + "split_apply": ("BOOLEAN", {"default": False, "tooltip": "Apply Depth and Edges as two chained ControlNets (fixed order: depth then edges)."}), + "edge_start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Edges start percent (when split is enabled)."}), + "edge_end_percent": ("FLOAT", {"default": 0.6, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Edges end percent (when split is enabled)."}), + "depth_start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Depth start percent (when split is enabled)."}), + "depth_end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Depth end percent (when split is enabled)."}), + "edge_strength_mul": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 3.0, "step": 0.01, "tooltip": "Multiply global strength for Edges when split is enabled."}), + "depth_strength_mul": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 3.0, "step": 0.01, "tooltip": "Multiply global strength for Depth when split is enabled."}), + # Extra edge controls (bottom) + "edge_width": ("FLOAT", {"default": 0.0, "min": -0.5, "max": 1.5, "step": 0.05, "tooltip": "Edge thickness adjust: negative thins, positive thickens."}), + "edge_smooth": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.05, "tooltip": "Small smooth on edges to reduce pixelation (0..1)."}), + "edge_single_line": ("BOOLEAN", {"default": False, "tooltip": "Try to collapse double outlines into a single centerline."}), + "edge_single_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Strength of single-line collapse (0..1). 0 = off, 1 = strong."}), + "edge_depth_gate": ("BOOLEAN", {"default": False, "tooltip": "Weigh edges by depth so distant lines are fainter."}), + "edge_depth_gamma": ("FLOAT", {"default": 1.5, "min": 0.2, "max": 4.0, "step": 0.1, "tooltip": "Gamma for depth gating: edges *= (1−depth)^gamma."}), + } + } + + RETURN_TYPES = ("CONDITIONING","CONDITIONING","IMAGE") + RETURN_NAMES = ("positive","negative","mask_preview") + FUNCTION = "apply" + CATEGORY = "MagicNodes" + + def apply(self, image, positive, negative, control_net, vae, + enable_depth=True, depth_model_path="", depth_resolution=1024, + enable_pyra=True, pyra_low=109, pyra_high=147, pyra_resolution=1024, + edge_thin_iter=0, edge_alpha=1.0, edge_boost=0.0, + smart_tune=False, smart_boost=0.2, + blend_mode="normal", blend_factor=0.02, + strength_pos=1.0, strength_neg=1.0, start_percent=0.0, end_percent=1.0, + preview_res=1024, mask_brightness=1.0, + preview_show_strength=True, preview_strength_branch="max", + hires_mask_auto=True, apply_to_uncond=False, stack_prev_control=False, + edge_width=0.0, edge_smooth=0.0, edge_single_line=False, edge_single_strength=0.0, + edge_depth_gate=False, edge_depth_gamma=1.5, + split_apply=False, edge_start_percent=0.0, edge_end_percent=0.6, + depth_start_percent=0.0, depth_end_percent=1.0, + edge_strength_mul=1.0, depth_strength_mul=1.0): + + dev = image.device + dtype = image.dtype + B,H,W,C = image.shape + # Build depth/edges + depth = None + edges = None + if enable_depth: + model_path = depth_model_path or os.path.join(os.path.dirname(__file__), '..','depth-anything','depth_anything_v2_vitl.pth') + depth = _build_depth_map(image, int(depth_resolution), model_path, bool(hires_mask_auto)) + if enable_pyra: + edges = _pyracanny(image, + int(pyra_low), int(pyra_high), int(pyra_resolution), + int(edge_thin_iter), float(edge_boost), + bool(smart_tune), float(smart_boost), bool(hires_mask_auto)) + if depth is None and edges is None: + # Nothing to do: return inputs and zero preview + prev = torch.zeros((B, max(H,1), max(W,1), 3), device=dev, dtype=dtype) + return positive, negative, prev + + if depth is None: + depth = torch.zeros_like(edges) + if edges is None: + edges = torch.zeros_like(depth) + + # Edge post-process: width/single-line/smooth + def _edges_post(acc_t: torch.Tensor) -> torch.Tensor: + try: + import cv2, numpy as _np + acc = acc_t.detach().to('cpu').numpy() + img = (acc*255.0).astype(_np.uint8) + k = _np.ones((3,3), _np.uint8) + # Adjust thickness + w = float(edge_width) + if abs(w) > 1e-6: + it = int(abs(w)) + frac = abs(w) - it + op = cv2.dilate if w > 0 else cv2.erode + y = img.copy() + for _ in range(max(0, it)): + y = op(y, k, iterations=1) + if frac > 1e-6: + y2 = op(y, k, iterations=1) + y = ((1.0-frac)*y.astype(_np.float32) + frac*y2.astype(_np.float32)).astype(_np.uint8) + img = y + # Collapse double lines to single centerline + if bool(edge_single_line) and float(edge_single_strength) > 1e-6: + try: + s = float(edge_single_strength) + close = cv2.morphologyEx(img, cv2.MORPH_CLOSE, k, iterations=1) + if hasattr(cv2, 'ximgproc') and hasattr(cv2.ximgproc, 'thinning'): + sk = cv2.ximgproc.thinning(close) + else: + # limited-iteration morphological skeletonization + iters = max(1, int(round(2 + 6*s))) + sk = _np.zeros_like(close) + src = close.copy() + elem = cv2.getStructuringElement(cv2.MORPH_CROSS, (3,3)) + for _ in range(iters): + er = cv2.erode(src, elem, iterations=1) + op = cv2.morphologyEx(er, cv2.MORPH_OPEN, elem) + tmp = cv2.subtract(er, op) + sk = cv2.bitwise_or(sk, tmp) + src = er + if not _np.any(src): + break + # Blend skeleton back with original according to strength + img = ((_np.float32(1.0 - s) * img.astype(_np.float32)) + (_np.float32(s) * sk.astype(_np.float32))).astype(_np.uint8) + except Exception: + pass + # Smooth + if float(edge_smooth) > 1e-6: + sigma = max(0.1, min(2.0, float(edge_smooth) * 1.2)) + img = cv2.GaussianBlur(img, (0,0), sigmaX=sigma) + out = torch.from_numpy((img.astype(_np.float32)/255.0)).to(device=acc_t.device, dtype=acc_t.dtype) + return out.clamp(0,1) + except Exception: + # Torch fallback: light blur-only + if float(edge_smooth) > 1e-6: + s = max(1, int(round(float(edge_smooth)*2))) + return F.avg_pool2d(acc_t.unsqueeze(0).unsqueeze(0), kernel_size=2*s+1, stride=1, padding=s)[0,0].clamp(0,1) + return acc_t + + edges = _edges_post(edges) + + # Depth gating of edges + if bool(edge_depth_gate): + # Inverted gating per feedback: use depth^gamma (nearer = stronger if depth is larger) + g = (depth.clamp(0,1)) ** float(edge_depth_gamma) + edges = (edges * g).clamp(0,1) + + # Apply edge alpha before blending + edges = (edges * float(edge_alpha)).clamp(0,1) + + fused = _blend(depth, edges, str(blend_mode), float(blend_factor)) + # Apply as split (Edges then Depth) or single fused hint + if bool(split_apply): + # Fixed order for determinism: Depth first, then Edges + hint_edges = edges.unsqueeze(-1).repeat(1,1,1,3) + hint_depth = depth.unsqueeze(-1).repeat(1,1,1,3) + # Depth first + pos_mid, neg_mid = _apply_controlnet_separate( + positive, negative, control_net, hint_depth, + float(strength_pos) * float(depth_strength_mul), + float(strength_neg) * float(depth_strength_mul), + float(depth_start_percent), float(depth_end_percent), vae, + bool(apply_to_uncond), True + ) + # Then edges + pos_out, neg_out = _apply_controlnet_separate( + pos_mid, neg_mid, control_net, hint_edges, + float(strength_pos) * float(edge_strength_mul), + float(strength_neg) * float(edge_strength_mul), + float(edge_start_percent), float(edge_end_percent), vae, + bool(apply_to_uncond), True + ) + else: + hint = fused.unsqueeze(-1).repeat(1,1,1,3) + pos_out, neg_out = _apply_controlnet_separate( + positive, negative, control_net, hint, + float(strength_pos), float(strength_neg), + float(start_percent), float(end_percent), vae, + bool(apply_to_uncond), bool(stack_prev_control) + ) + # Build preview: keep aspect ratio, set minimal side + prev_res = int(max(256, min(2048, preview_res))) + scale = prev_res / float(min(H, W)) + out_h = max(1, int(round(H * scale))) + out_w = max(1, int(round(W * scale))) + prev = F.interpolate(fused.unsqueeze(0).unsqueeze(0), size=(out_h, out_w), mode='bilinear', align_corners=False)[0,0] + # Optionally reflect ControlNet strength in preview (display only) + if bool(preview_show_strength): + br = str(preview_strength_branch) + sp = float(strength_pos) + sn = float(strength_neg) + if br == 'negative': + s_vis = sn + elif br == 'max': + s_vis = max(sp, sn) + elif br == 'avg': + s_vis = 0.5 * (sp + sn) + else: + s_vis = sp + # clamp for display range + s_vis = max(0.0, min(1.0, s_vis)) + prev = prev * s_vis + # Apply visualization brightness only for preview + prev = (prev * float(mask_brightness)).clamp(0.0, 1.0) + prev = prev.unsqueeze(-1).repeat(1,1,3).to(device=dev, dtype=dtype).unsqueeze(0) + return (pos_out, neg_out, prev) diff --git a/mod/hard/mg_ids.py b/mod/hard/mg_ids.py new file mode 100644 index 0000000000000000000000000000000000000000..5fee22166bd1c9b0e347ff5cec7eb2735db9887b --- /dev/null +++ b/mod/hard/mg_ids.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import numpy as np +import torch + +try: + from scipy.ndimage import gaussian_filter as _scipy_gaussian_filter + _HAVE_SCIPY = True +except Exception: + _HAVE_SCIPY = False + + +def _torch_gaussian_blur(image: torch.Tensor, sigma: float) -> torch.Tensor: + # image: BHWC in [0,1] + if sigma <= 0.0: + return image + device = image.device + dtype = image.dtype + radius = max(1, int(3.0 * float(sigma))) + ksize = radius * 2 + 1 + x = torch.arange(-radius, radius + 1, device=device, dtype=dtype) + g1 = torch.exp(-(x * x) / (2.0 * (sigma ** 2))) + g1 = (g1 / g1.sum()).view(1, 1, 1, -1) + g2 = g1.transpose(2, 3) + xch = image.movedim(-1, 1) # BCHW + pad = (radius, radius, radius, radius) + out = torch.nn.functional.conv2d(torch.nn.functional.pad(xch, pad, mode="reflect"), g1.repeat(xch.shape[1], 1, 1, 1), groups=xch.shape[1]) + out = torch.nn.functional.conv2d(torch.nn.functional.pad(out, pad, mode="reflect"), g2.repeat(out.shape[1], 1, 1, 1), groups=out.shape[1]) + return out.movedim(1, -1) + + +class IntelligentDetailStabilizer: + """Alias-preserving move of IDS into mod/ as mg_ids.py. + Keeps class/key name for backward compatibility. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE", {}), + "ids_strength": ( + "FLOAT", + {"default": 0.5, "min": -1.0, "max": 1.0, "step": 0.01}, + ), + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("IMAGE",) + FUNCTION = "stabilize" + CATEGORY = "MagicNodes" + + def stabilize(self, image: torch.Tensor, ids_strength: float = 0.5): + sigma = max(float(ids_strength) * 2.0, 1e-3) + if _HAVE_SCIPY: + img_np = image.detach().cpu().numpy() + denoised = _scipy_gaussian_filter(img_np, sigma=(0, sigma, sigma, 0)) + blurred = _scipy_gaussian_filter(denoised, sigma=(0, 1.0, 1.0, 0)) + sharpen = denoised + ids_strength * (denoised - blurred) + sharpen = np.clip(sharpen, 0.0, 1.0) + out = torch.from_numpy(sharpen).to(image.device, dtype=image.dtype) + else: + denoised = _torch_gaussian_blur(image, sigma=sigma) + blurred = _torch_gaussian_blur(denoised, sigma=1.0) + out = (denoised + ids_strength * (denoised - blurred)).clamp(0, 1) + return (out,) diff --git a/mod/hard/mg_upscale_module.py b/mod/hard/mg_upscale_module.py new file mode 100644 index 0000000000000000000000000000000000000000..6477ee4751f5b904b13fcc0384d91d9771439aee --- /dev/null +++ b/mod/hard/mg_upscale_module.py @@ -0,0 +1,72 @@ +import comfy.utils +import torch +import gc +import logging +import comfy.model_management as model_management + + +def clear_gpu_and_ram_cache(): + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + torch.cuda.ipc_collect() + + +def _smart_decode(vae, latent, tile_size=512): + try: + images = vae.decode(latent["samples"]) + except model_management.OOM_EXCEPTION: + logging.warning("VAE decode OOM, using tiled decode") + compression = vae.spacial_compression_decode() + images = vae.decode_tiled( + latent["samples"], + tile_x=tile_size // compression, + tile_y=tile_size // compression, + overlap=(tile_size // 4) // compression, + ) + if len(images.shape) == 5: + images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1]) + return images + + +class MagicUpscaleModule: + """Moved into mod/ as mg_upscale_module keeping class/key name.""" + upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"] + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "samples": ("LATENT", {}), + "vae": ("VAE", {}), + "upscale_method": (cls.upscale_methods, {"default": "bilinear"}), + "scale_by": ("FLOAT", {"default": 1.2, "min": 0.01, "max": 8.0, "step": 0.01}), + } + } + + RETURN_TYPES = ("LATENT", "IMAGE") + RETURN_NAMES = ("LATENT", "Upscaled Image") + FUNCTION = "process_upscale" + CATEGORY = "MagicNodes" + + def process_upscale(self, samples, vae, upscale_method, scale_by): + clear_gpu_and_ram_cache() + images = _smart_decode(vae, samples) + samples_t = images.movedim(-1, 1) + width = round(samples_t.shape[3] * scale_by) + height = round(samples_t.shape[2] * scale_by) + # Align to VAE stride to avoid border artifacts/shape drift + try: + stride = int(vae.spacial_compression_decode()) + except Exception: + stride = 8 + if stride <= 0: + stride = 8 + def _align_up(x, s): + return int(((x + s - 1) // s) * s) + width_al = _align_up(width, stride) + height_al = _align_up(height, stride) + up = comfy.utils.common_upscale(samples_t, width_al, height_al, upscale_method, "disabled") + up = up.movedim(1, -1) + encoded = vae.encode(up[:, :, :, :3]) + return ({"samples": encoded}, up) diff --git a/mod/hard/mg_zesmart_sampler_v1_1.py b/mod/hard/mg_zesmart_sampler_v1_1.py new file mode 100644 index 0000000000000000000000000000000000000000..388e0b6b53c98ef560cede4b751b2bfeed6d674d --- /dev/null +++ b/mod/hard/mg_zesmart_sampler_v1_1.py @@ -0,0 +1,210 @@ +from __future__ import annotations + +import math +import torch +import torch.nn.functional as F # noqa: F401 + +import comfy.utils as _utils +import comfy.sample as _sample +import comfy.samplers as _samplers +from comfy.k_diffusion import sampling as _kds + +import nodes # latent preview callback + + +def _smoothstep01(x: torch.Tensor) -> torch.Tensor: + return x * x * (3.0 - 2.0 * x) + + +def _build_hybrid_sigmas(model, steps: int, base_sampler: str, mode: str, + mix: float, denoise: float, jitter: float, seed: int, + _debug: bool = False, tail_smooth: float = 0.0, + auto_hybrid_tail: bool = True, auto_tail_strength: float = 0.35): + """Return 1D tensor of sigmas (len == steps+1), strictly descending and ending with 0. + + mode: 'karras' | 'beta' | 'hybrid'. If 'hybrid', blend tail toward beta by `mix`. + We DO NOT apply 'drop penultimate' until the very end to preserve denoise math. + """ + ms = model.get_model_object("model_sampling") + steps = int(steps) + assert steps >= 1 + + # --- base tracks --- + sig_k = _samplers.calculate_sigmas(ms, "karras", steps) + sig_b = _samplers.calculate_sigmas(ms, "beta", steps) + + mode = str(mode).lower() + if mode == "karras": + sig = sig_k + elif mode == "beta": + sig = sig_b + else: + n = sig_k.shape[0] + t = torch.linspace(0.0, 1.0, n, device=sig_k.device, dtype=sig_k.dtype) + m = float(max(0.0, min(1.0, mix))) + eps = 1e-6 if m < 1e-6 else m + w = torch.clamp((t - (1.0 - m)) / eps, 0.0, 1.0) + w = _smoothstep01(w) + sig = sig_k * (1.0 - w) + sig_b * w + + # --- Comfy denoise semantics: recompute a "full" track and take the tail of desired length --- + sig_k_base = sig_k + sig_b_base = sig_b + if denoise is not None and 0.0 < float(denoise) < 0.999999: + new_steps = max(1, int(steps / max(1e-6, float(denoise)))) + sk = _samplers.calculate_sigmas(ms, "karras", new_steps) + sb = _samplers.calculate_sigmas(ms, "beta", new_steps) + if mode == "karras": + sig_full = sk + elif mode == "beta": + sig_full = sb + else: + n2 = sk.shape[0] + t2 = torch.linspace(0.0, 1.0, n2, device=sk.device, dtype=sk.dtype) + m = float(max(0.0, min(1.0, mix))) + eps = 1e-6 if m < 1e-6 else m + w2 = torch.clamp((t2 - (1.0 - m)) / eps, 0.0, 1.0) + w2 = _smoothstep01(w2) + sig_full = sk * (1.0 - w2) + sb * w2 + need = steps + 1 + if sig_full.shape[0] >= need: + sig = sig_full[-need:] + sig_k_base = sk[-need:] + sig_b_base = sb[-need:] + else: + # Worst case: trust what we got; we will still guarantee the last sigma is zero later + sig = sig_full + tail = min(need, sk.shape[0]) + sig_k_base = sk[-tail:] + sig_b_base = sb[-tail:] + + # --- auto-hybrid tail: blend beta into the tail when the steps become brittle --- + if bool(auto_hybrid_tail) and sig.numel() > 2: + n = sig.shape[0] + t = torch.linspace(0.0, 1.0, n, device=sig.device, dtype=sig.dtype) + m = float(max(0.0, min(1.0, mix))) + if mode == "hybrid": + eps = 1e-6 if m < 1e-6 else m + w_m = torch.clamp((t - (1.0 - m)) / eps, 0.0, 1.0) + w_m = _smoothstep01(w_m) + elif mode == "beta": + w_m = torch.ones_like(t) + else: + w_m = torch.zeros_like(t) + dif = (sig[1:] - sig[:-1]).abs() / sig[:-1].abs().clamp_min(1e-8) + dif = torch.cat([dif, dif[-1:]], dim=0) + dif = (dif - dif.min()) / (dif.max() - dif.min() + 1e-8) + ramp = _smoothstep01(torch.clamp((t - 0.7) / 0.3, 0.0, 1.0)) + w_a = dif * ramp + g = float(max(0.0, min(1.0, auto_tail_strength))) + u = w_m + g * w_a - w_m * g * w_a + sig = sig_k_base * (1.0 - u) + sig_b_base * u + + # --- tiny schedule jitter --- + j = float(max(0.0, min(0.1, float(jitter)))) + if j > 0.0 and sig.numel() > 1: + gen = torch.Generator(device='cpu') + gen.manual_seed(int(seed) ^ 0x5EEDCAFE) + noise = torch.randn(sig.shape, generator=gen, device='cpu').to(sig.device, sig.dtype) + amp = j * float(sig[0].item() - sig[-1].item()) * 1e-3 + sig = sig + noise * amp + sig, _ = torch.sort(sig, descending=True) + + # --- hard guarantee of ending with exact zero --- + if sig[-1].abs() > 1e-12: + sig = torch.cat([sig[:-1], sig.new_zeros(1)], dim=0) + + # --- and only now drop-penultimate for respective samplers --- + # --- gentle smoothing of sigma tail (adaptive, safe for monotonic decrease) --- + ts = float(max(0.0, min(1.0, tail_smooth))) + if ts > 0.0 and sig.numel() > 2: + s = sig.clone() + n = int(s.shape[0]) + t = torch.linspace(0.0, 1.0, n, device=s.device, dtype=s.dtype) + w = (t.pow(2) * ts).clamp(0.0, 1.0) + for i in range(n - 2, -1, -1): + a = float(min(0.5, 0.5 * w[i].item())) + s[i] = (1.0 - a) * s[i] + a * s[i + 1] + sig = s + + if base_sampler in _samplers.KSampler.DISCARD_PENULTIMATE_SIGMA_SAMPLERS and sig.numel() >= 2: + sig = torch.cat([sig[:-2], sig[-1:]], dim=0) + + sig = sig.to(model.load_device) + + # Lightweight debug: schedule summary + if _debug: + try: + desc_ok = bool((sig[:-1] > sig[1:]).all().item()) if sig.numel() > 1 else True + head = ", ".join(f"{float(v):.4g}" for v in sig[:3].tolist()) if sig.numel() >= 3 else \ + ", ".join(f"{float(v):.4g}" for v in sig.tolist()) + tail = ", ".join(f"{float(v):.4g}" for v in sig[-3:].tolist()) if sig.numel() >= 3 else head + print(f"[ZeSmart][dbg] sigmas len={sig.numel()} desc={desc_ok} first={float(sig[0]):.6g} last={float(sig[-1]):.6g}") + print(f"[ZeSmart][dbg] head: [{head}] tail: [{tail}]") + except Exception: + pass + + return sig + + +class MG_ZeSmartSampler: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ("MODEL", {}), + "seed": ("INT", {"default": 0, "min": 0, "max": 2**63-1, "control_after_generate": True}), + "steps": ("INT", {"default": 20, "min": 1, "max": 4096}), + "cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 50.0, "step": 0.1}), + "base_sampler": (_samplers.KSampler.SAMPLERS, {"default": "dpmpp_2m"}), + "schedule": (["karras", "beta", "hybrid"], {"default": "hybrid", "tooltip": "Sigma curve: karras — soft start; beta — stable tail; hybrid — their mix."}), + "positive": ("CONDITIONING", {}), + "negative": ("CONDITIONING", {}), + "latent": ("LATENT", {}), + }, + "optional": { + "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Path shortening: 1.0 = full; <1.0 = take the last steps only. Useful for inpaint/mixing."}), + "hybrid_mix": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "For schedule=hybrid: tail fraction blended toward beta (0=karras, 1=beta)."}), + "jitter_sigma": ("FLOAT", {"default": 0.01, "min": 0.0, "max": 0.1, "step": 0.001, "tooltip": "Tiny sigma jitter to kill moiré/banding on backgrounds. 0–0.02 is usually enough."}), + "tail_smooth": ("FLOAT", {"default": 0.15, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Smooth the sigma tail — reduces wobble/banding. Too high may soften details."}), + "auto_hybrid_tail": ("BOOLEAN", {"default": True, "tooltip": "Auto‑blend beta on the tail when steps become brittle."}), + "auto_tail_strength": ("FLOAT", {"default": 0.4, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Strength of auto beta‑mix on the tail (0=off, 1=max)."}), + "debug_probe": ("BOOLEAN", {"default": False, "tooltip": "Print sigma summary (length, first/last, head/tail)."}), + } + } + + RETURN_TYPES = ("LATENT",) + RETURN_NAMES = ("LATENT",) + FUNCTION = "apply" + CATEGORY = "MagicNodes/Experimental" + + def apply(self, model, seed, steps, cfg, base_sampler, schedule, + positive, negative, latent, denoise=1.0, hybrid_mix=0.5, + jitter_sigma=0.02, tail_smooth=0.07, + auto_hybrid_tail=True, auto_tail_strength=0.3, + debug_probe=False): + # Prepare latent + noise + lat_img = latent["samples"] + lat_img = _sample.fix_empty_latent_channels(model, lat_img) + batch_inds = latent.get("batch_index", None) + noise = _sample.prepare_noise(lat_img, seed, batch_inds) + noise_mask = latent.get("noise_mask", None) + + # Custom sigmas + sigmas = _build_hybrid_sigmas(model, int(steps), str(base_sampler), str(schedule), + float(hybrid_mix), float(denoise), float(jitter_sigma), int(seed), + _debug=bool(debug_probe), tail_smooth=float(tail_smooth), + auto_hybrid_tail=bool(auto_hybrid_tail), + auto_tail_strength=float(auto_tail_strength)) + + # Use native sampler; all tweaks happen in sigma schedule only. + sampler_obj = _samplers.sampler_object(str(base_sampler)) + callback = nodes.latent_preview.prepare_callback(model, int(steps)) + disable_pbar = not _utils.PROGRESS_BAR_ENABLED + samples = _sample.sample_custom(model, noise, float(cfg), sampler_obj, sigmas, + positive, negative, lat_img, + noise_mask=noise_mask, callback=callback, + disable_pbar=disable_pbar, seed=seed) + out = {**latent} + out["samples"] = samples + return (out,) diff --git a/mod/mg_combinode.py b/mod/mg_combinode.py new file mode 100644 index 0000000000000000000000000000000000000000..fb2b3b2c3391ae9d8699c8bea7b183c74c12a922 --- /dev/null +++ b/mod/mg_combinode.py @@ -0,0 +1,448 @@ +import comfy.sd +import comfy.clip_vision +import folder_paths +import comfy.utils +import torch +import random +from datetime import datetime +import random +import gc +import os +import json +import re + +from .hard.mg_upscale_module import clear_gpu_and_ram_cache + +# Module level caches to reuse loaded models and LoRAs between invocations +_checkpoint_cache = {} +_loaded_checkpoint = None +_lora_cache = {} +_active_lora_names = set() + + +def _clear_unused_loras(active_names): + """Remove unused LoRAs from cache and clear GPU memory.""" + unused = [n for n in _lora_cache if n not in active_names] + for n in unused: + del _lora_cache[n] + if unused: + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + +def _load_checkpoint(path): + """Load checkpoint from cache or disk.""" + if path in _checkpoint_cache: + return _checkpoint_cache[path] + model, clip, vae = comfy.sd.load_checkpoint_guess_config( + path, + output_vae=True, + output_clip=True, + embedding_directory=folder_paths.get_folder_paths("embeddings"), + )[:3] + _checkpoint_cache[path] = (model, clip, vae) + return model, clip, vae + + +def _unload_old_checkpoint(current_path): + """Unload checkpoint if it's different from the current one.""" + global _loaded_checkpoint + if _loaded_checkpoint and _loaded_checkpoint != current_path: + _checkpoint_cache.pop(_loaded_checkpoint, None) + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + _loaded_checkpoint = current_path + + + +class MagicNodesCombiNode: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + + + + # --- Checkpoint --- + "use_checkpoint": ("BOOLEAN", {"default": True}), + "checkpoint": (folder_paths.get_filename_list("checkpoints"), {}), + "clear_cache": ("BOOLEAN", {"default": False}), + + # --- LoRA 1 --- + "use_lora_1": ("BOOLEAN", {"default": True}), + "lora_1": (folder_paths.get_filename_list("loras"), {}), + "strength_model_1": ("FLOAT", {"default": 1.0, "min": -1.5, "max": 1.5, "step": 0.01,}), + "strength_clip_1": ("FLOAT", {"default": 1.0, "min": -1.5, "max": 1.5, "step": 0.01,}), + + # --- LoRA 2 --- + "use_lora_2": ("BOOLEAN", {"default": False}), + "lora_2": (folder_paths.get_filename_list("loras"), {}), + "strength_model_2": ("FLOAT", {"default": 0.0, "min": -1.5, "max": 1.5, "step": 0.01,}), + "strength_clip_2": ("FLOAT", {"default": 0.0, "min": -1.5, "max": 1.5, "step": 0.01,}), + + # --- LoRA 3 --- + "use_lora_3": ("BOOLEAN", {"default": False}), + "lora_3": (folder_paths.get_filename_list("loras"), {}), + "strength_model_3": ("FLOAT", {"default": 0.0, "min": -1.5, "max": 1.5, "step": 0.01,}), + "strength_clip_3": ("FLOAT", {"default": 0.0, "min": -1.5, "max": 1.5, "step": 0.01,}), + + # --- LoRA 4 --- + "use_lora_4": ("BOOLEAN", {"default": False}), + "lora_4": (folder_paths.get_filename_list("loras"), {}), + "strength_model_4": ("FLOAT", {"default": 0.0, "min": -1.5, "max": 1.5, "step": 0.01,}), + "strength_clip_4": ("FLOAT", {"default": 0.0, "min": -1.5, "max": 1.5, "step": 0.01,}), + + # --- LoRA 5 --- + "use_lora_5": ("BOOLEAN", {"default": False}), + "lora_5": (folder_paths.get_filename_list("loras"), {}), + "strength_model_5": ("FLOAT", {"default": 0.0, "min": -1.5, "max": 1.5, "step": 0.01,}), + "strength_clip_5": ("FLOAT", {"default": 0.0, "min": -1.5, "max": 1.5, "step": 0.01,}), + + # --- LoRA 6 --- + "use_lora_6": ("BOOLEAN", {"default": False}), + "lora_6": (folder_paths.get_filename_list("loras"), {}), + "strength_model_6": ("FLOAT", {"default": 0.0, "min": -1.5, "max": 1.5, "step": 0.01,}), + "strength_clip_6": ("FLOAT", {"default": 0.0, "min": -1.5, "max": 1.5, "step": 0.01,}), + }, + "optional": { + "model_in": ("MODEL", {}), + "clip_in": ("CLIP", {}), + "vae_in": ("VAE", {}), + + # --- Prompts --- (controlled dynamic expansion inside node for determinism) + "positive_prompt": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": False}), + "negative_prompt": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": False}), + + # Optional external conditioning (bypass internal text encode) + "positive_in": ("CONDITIONING", {}), + "negative_in": ("CONDITIONING", {}), + + # --- CLIP Layers --- + "clip_set_last_layer_positive": ("INT", {"default": -2, "min": -20, "max": 0}), + "clip_set_last_layer_negative": ("INT", {"default": -2, "min": -20, "max": 0}), + + # --- Recipes --- + "recipe_slot": (["Off", "Slot 1", "Slot 2", "Slot 3", "Slot 4"], {"default": "Off", "tooltip": "Choose slot to save/load assembled setup."}), + "recipe_save": ("BOOLEAN", {"default": False, "tooltip": "Save current setup into the selected slot."}), + "recipe_use": ("BOOLEAN", {"default": False, "tooltip": "Load and apply setup from the selected slot for this run."}), + + # --- Standard pipeline (match classic node order for CLIP) --- + "standard_pipeline": ("BOOLEAN", {"default": False, "tooltip": "Use vanilla order for CLIP: Set Last Layer -> Load LoRA -> Encode (same CLIP logic as standard ComfyUI)."}), + + # CLIP LoRA gains per branch (effective only when standard_pipeline=true) + "clip_lora_pos_gain": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 3.0, "step": 0.01, "tooltip": "Multiplier for CLIP-LoRA strength on positive branch (standard pipeline)."}), + "clip_lora_neg_gain": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 3.0, "step": 0.01, "tooltip": "Multiplier for CLIP-LoRA strength on negative branch (standard pipeline)."}), + + # Deterministic dynamic prompts + "dynamic_pos": ("BOOLEAN", {"default": False, "tooltip": "Deterministically expand choices in positive prompt (uses dyn_seed)."}), + "dynamic_neg": ("BOOLEAN", {"default": False, "tooltip": "Deterministically expand choices in negative prompt (uses dyn_seed)."}), + "dyn_seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFF, "tooltip": "Seed for dynamic prompt expansion (same seed used for both prompts)."}), + "dynamic_break_freeze": ("BOOLEAN", {"default": True, "tooltip": "If enabled, do not expand choices before the first |BREAK| marker; dynamic applies only after it."}), + "show_expanded_prompts": ("BOOLEAN", {"default": False, "tooltip": "Print expanded Positive/Negative prompts to console when dynamic is enabled."}), + "save_expanded_prompts": ("BOOLEAN", {"default": False, "tooltip": "Save expanded prompts to mod/dynPrompt/SEED_dd_mm_yyyy.txt when dynamic is enabled."}), + } + } + + + RETURN_TYPES = ("MODEL", "CLIP", "CONDITIONING", "CONDITIONING", "VAE") + RETURN_NAMES = ("MODEL", "CLIP", "Positive", "Negative", "VAE") + #RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "VAE") + #RETURN_NAMES = ("MODEL", "Positive", "Negative", "VAE") + FUNCTION = "apply_magic_node" + CATEGORY = "MagicNodes" + + def apply_magic_node(self, model_in=None, clip_in=None, checkpoint=None, + use_checkpoint=True, clear_cache=False, + use_lora_1=True, lora_1=None, strength_model_1=1.0, strength_clip_1=1.0, + use_lora_2=False, lora_2=None, strength_model_2=0.0, strength_clip_2=0.0, + use_lora_3=False, lora_3=None, strength_model_3=0.0, strength_clip_3=0.0, + use_lora_4=False, lora_4=None, strength_model_4=0.0, strength_clip_4=0.0, + use_lora_5=False, lora_5=None, strength_model_5=0.0, strength_clip_5=0.0, + use_lora_6=False, lora_6=None, strength_model_6=0.0, strength_clip_6=0.0, + positive_prompt="", negative_prompt="", + clip_set_last_layer_positive=-2, clip_set_last_layer_negative=-2, + vae_in=None, + recipe_slot="Off", recipe_save=False, recipe_use=False, + standard_pipeline=False, + clip_lora_pos_gain=1.0, clip_lora_neg_gain=1.0, + positive_in=None, negative_in=None, + dynamic_pos=False, dynamic_neg=False, dyn_seed=0, dynamic_break_freeze=True, + show_expanded_prompts=False, save_expanded_prompts=False): + + global _loaded_checkpoint + + # hard scrub of checkpoint cache each call (prevent hidden state) + _checkpoint_cache.clear() + if clear_cache: + _lora_cache.clear() + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + # Recipe helpers + def _recipes_path(): + base = os.path.join(os.path.dirname(__file__), "state") + os.makedirs(base, exist_ok=True) + return os.path.join(base, "combinode_recipes.json") + def _recipes_load(): + try: + with open(_recipes_path(), "r", encoding="utf-8") as f: + return json.load(f) + except Exception: + return {} + def _recipes_save(data: dict): + try: + with open(_recipes_path(), "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + except Exception: + pass + + # Apply recipe if requested + slot_idx = {"Off": 0, "Slot 1": 1, "Slot 2": 2, "Slot 3": 3, "Slot 4": 4}.get(str(recipe_slot), 0) + if slot_idx and bool(recipe_use): + rec = _recipes_load().get(str(slot_idx), None) + if rec is not None: + try: + use_checkpoint = rec.get("use_checkpoint", use_checkpoint) + checkpoint = rec.get("checkpoint", checkpoint) + clip_set_last_layer_positive = rec.get("clip_pos", clip_set_last_layer_positive) + clip_set_last_layer_negative = rec.get("clip_neg", clip_set_last_layer_negative) + positive_prompt = rec.get("pos_text", positive_prompt) + negative_prompt = rec.get("neg_text", negative_prompt) + rls = rec.get("loras", []) + if len(rls) >= 4: + (use_lora_1, lora_1, strength_model_1, strength_clip_1) = rls[0] + (use_lora_2, lora_2, strength_model_2, strength_clip_2) = rls[1] + (use_lora_3, lora_3, strength_model_3, strength_clip_3) = rls[2] + (use_lora_4, lora_4, strength_model_4, strength_clip_4) = rls[3] + if len(rls) >= 5: + (use_lora_5, lora_5, strength_model_5, strength_clip_5) = rls[4] + if len(rls) >= 6: + (use_lora_6, lora_6, strength_model_6, strength_clip_6) = rls[5] + print(f"[CombiNode] Loaded recipe Slot {slot_idx}.") + except Exception: + print(f"[CombiNode] Failed to apply recipe Slot {slot_idx}.") + + # Prompt normalization helper (keeps '|' intact) + def _norm_prompt(s: str) -> str: + if not isinstance(s, str) or not s: + return s or "" + s2 = s.replace("\r", " ").replace("\n", " ") + s2 = re.sub(r"\s+", " ", s2) + s2 = re.sub(r"\s*,\s*", ", ", s2) + s2 = re.sub(r"(,\s*){2,}", ", ", s2) + return s2.strip() + + # Deterministic dynamic prompt expansion: supports {...}, (...), [...] with '|' choices + def _expand_dynamic(text: str, seed_val: int, freeze_before_break: bool = True) -> str: + if not isinstance(text, str) or (text.find('|') < 0): + return text + # Honor |BREAK|: keep first segment intact when requested + if freeze_before_break and ('|BREAK|' in text): + pre, post = text.split('|BREAK|', 1) + return pre + '|BREAK|' + _expand_dynamic(post, seed_val, freeze_before_break=False) + rng = random.Random(int(seed_val) & 0xFFFFFFFF) + def _expand_pattern(t: str, pat: re.Pattern) -> str: + prev = None + cur = t + while prev != cur: + prev = cur + def repl(m): + body = m.group(1) + choices = [c.strip() for c in body.split('|') if c.strip()] + if not choices: + return m.group(0) + return rng.choice(choices) + cur = pat.sub(repl, cur) + return cur + for rx in ( + re.compile(r"\{([^{}]+)\}"), + re.compile(r"\(([^()]+)\)"), + re.compile(r"\[([^\[\]]+)\]"), + ): + text = _expand_pattern(text, rx) + return text + + # Precompute expanded (or original) texts once + pos_text_expanded = _norm_prompt(_expand_dynamic(positive_prompt, int(dyn_seed), bool(dynamic_break_freeze)) if bool(dynamic_pos) else positive_prompt) + neg_text_expanded = _norm_prompt(_expand_dynamic(negative_prompt, int(dyn_seed), bool(dynamic_break_freeze)) if bool(dynamic_neg) else negative_prompt) + + if use_checkpoint and checkpoint: + checkpoint_path = folder_paths.get_full_path_or_raise("checkpoints", checkpoint) + _unload_old_checkpoint(checkpoint_path) + base_model, base_clip, vae = _load_checkpoint(checkpoint_path) + model = base_model.clone() + clip = base_clip.clone() + clip_clean = base_clip.clone() # keep pristine CLIP for standard pipeline path + + elif model_in and clip_in: + _unload_old_checkpoint(None) + model = model_in.clone() + clip = clip_in.clone() + clip_clean = clip_in.clone() + vae = vae_in + else: + raise Exception("No model selected!") + + # single clear at start is enough; avoid double-clearing here + + # Применение цепочки LoRA + loras = [ + (use_lora_1, lora_1, strength_model_1, strength_clip_1), + (use_lora_2, lora_2, strength_model_2, strength_clip_2), + (use_lora_3, lora_3, strength_model_3, strength_clip_3), + (use_lora_4, lora_4, strength_model_4, strength_clip_4), + (use_lora_5, lora_5, strength_model_5, strength_clip_5), + (use_lora_6, lora_6, strength_model_6, strength_clip_6), + ] + + active_lora_paths = [] + lora_stack = [] # list of (lora_file, sc, sm) + defer_clip = bool(standard_pipeline) + for use_lora, lora_name, sm, sc in loras: + if use_lora and lora_name: + lora_path = folder_paths.get_full_path_or_raise("loras", lora_name) + active_lora_paths.append(lora_path) + # keep lora object to avoid reloading + if lora_path in _lora_cache: + lora_file = _lora_cache[lora_path] + else: + lora_file = comfy.utils.load_torch_file(lora_path, safe_load=True) + _lora_cache[lora_path] = lora_file + lora_stack.append((lora_file, float(sc), float(sm))) + sc_apply = 0.0 if defer_clip else sc + model, clip = comfy.sd.load_lora_for_models(model, clip, lora_file, sm, sc_apply) + + _clear_unused_loras(active_lora_paths) + # Warn about duplicate LoRA selections across slots + try: + counts = {} + for p in active_lora_paths: + counts[p] = counts.get(p, 0) + 1 + dups = [p for p, c in counts.items() if c > 1] + if dups: + print(f"[CombiNode] Duplicate LoRA detected across slots: {len(dups)} file(s).") + except Exception: + pass + + # Embeddings Positive и Negative + # Standard pipeline: optionally use a shared CLIP after clip_layer + CLIP-LoRA + # Select CLIP source for encoding: pristine when standard pipeline is enabled + src_clip = clip_clean if bool(standard_pipeline) else clip + + pos_gain = float(clip_lora_pos_gain) + neg_gain = float(clip_lora_neg_gain) + skips_equal = int(clip_set_last_layer_positive) == int(clip_set_last_layer_negative) + # Use shared CLIP only if gains are equal and skips equal + use_shared = bool(standard_pipeline) and skips_equal and (abs(pos_gain - neg_gain) < 1e-6) + + if (positive_in is None) and (negative_in is None) and use_shared: + shared_clip = src_clip.clone() + shared_clip.clip_layer(clip_set_last_layer_positive) + for lora_file, sc, sm in lora_stack: + try: + _m_unused, shared_clip = comfy.sd.load_lora_for_models(model, shared_clip, lora_file, 0.0, sc * pos_gain) + except Exception: + pass + tokens_pos = shared_clip.tokenize(pos_text_expanded) + cond_pos = shared_clip.encode_from_tokens_scheduled(tokens_pos) + tokens_neg = shared_clip.tokenize(neg_text_expanded) + cond_neg = shared_clip.encode_from_tokens_scheduled(tokens_neg) + else: + # CLIP Set Last Layer + Positive conditioning + clip_pos = src_clip.clone() + clip_pos.clip_layer(clip_set_last_layer_positive) + if bool(standard_pipeline): + for lora_file, sc, sm in lora_stack: + try: + _m_unused, clip_pos = comfy.sd.load_lora_for_models(model, clip_pos, lora_file, 0.0, sc * pos_gain) + except Exception: + pass + if positive_in is not None: + cond_pos = positive_in + else: + tokens_pos = clip_pos.tokenize(pos_text_expanded) + cond_pos = clip_pos.encode_from_tokens_scheduled(tokens_pos) + + # CLIP Set Last Layer + Negative conditioning + clip_neg = src_clip.clone() + clip_neg.clip_layer(clip_set_last_layer_negative) + if bool(standard_pipeline): + for lora_file, sc, sm in lora_stack: + try: + _m_unused, clip_neg = comfy.sd.load_lora_for_models(model, clip_neg, lora_file, 0.0, sc * neg_gain) + except Exception: + pass + if negative_in is not None: + cond_neg = negative_in + else: + tokens_neg = clip_neg.tokenize(neg_text_expanded) + cond_neg = clip_neg.encode_from_tokens_scheduled(tokens_neg) + + # Optional: show/save expanded prompts if dynamic used anywhere + dyn_used = bool(dynamic_pos) or bool(dynamic_neg) + if dyn_used and (bool(show_expanded_prompts) or bool(save_expanded_prompts)): + # Console print + if bool(show_expanded_prompts): + try: + print(f"[CombiNode] Expanded prompts (dyn_seed={int(dyn_seed)}):") + def _print_block(name, src, expanded): + print(name + ":") + if bool(dynamic_break_freeze) and ('|BREAK|' in src) and ((name=="Positive" and bool(dynamic_pos)) or (name=="Negative" and bool(dynamic_neg))): + print(" static") + print(" " + expanded) + _print_block("Positive", positive_prompt, pos_text_expanded) + _print_block("Negative", negative_prompt, neg_text_expanded) + except Exception: + pass + # File save + if bool(save_expanded_prompts): + try: + base = os.path.join(os.path.dirname(__file__), "dynPrompt") + os.makedirs(base, exist_ok=True) + now = datetime.now() + fname = f"{int(dyn_seed)}_{now.day:02d}_{now.month:02d}_{now.year}.txt" + path = os.path.join(base, fname) + lines = [] + def _append_block(name, src, expanded): + lines.append(name + ":\n") + if bool(dynamic_break_freeze) and ('|BREAK|' in src) and ((name=="Positive" and bool(dynamic_pos)) or (name=="Negative" and bool(dynamic_neg))): + lines.append("static\n") + lines.append(expanded + "\n\n") + _append_block("Positive", positive_prompt, pos_text_expanded) + _append_block("Negative", negative_prompt, neg_text_expanded) + with open(path, 'w', encoding='utf-8') as f: + f.writelines(lines) + except Exception: + pass + + # Save recipe if requested + if slot_idx and bool(recipe_save): + data = _recipes_load() + data[str(slot_idx)] = { + "use_checkpoint": bool(use_checkpoint), + "checkpoint": checkpoint, + "clip_pos": int(clip_set_last_layer_positive), + "clip_neg": int(clip_set_last_layer_negative), + "pos_text": str(positive_prompt), + "neg_text": str(negative_prompt), + "loras": [ + [bool(use_lora_1), lora_1, float(strength_model_1), float(strength_clip_1)], + [bool(use_lora_2), lora_2, float(strength_model_2), float(strength_clip_2)], + [bool(use_lora_3), lora_3, float(strength_model_3), float(strength_clip_3)], + [bool(use_lora_4), lora_4, float(strength_model_4), float(strength_clip_4)], + [bool(use_lora_5), lora_5, float(strength_model_5), float(strength_clip_5)], + [bool(use_lora_6), lora_6, float(strength_model_6), float(strength_clip_6)], + ], + } + _recipes_save(data) + print(f"[CombiNode] Saved recipe Slot {slot_idx}.") + + # Return the CLIP instance consistent with encoding path + return (model, src_clip if bool(standard_pipeline) else clip, cond_pos, cond_neg, vae) + + + + diff --git a/mod/mg_sagpu_attention.py b/mod/mg_sagpu_attention.py new file mode 100644 index 0000000000000000000000000000000000000000..1fa01835f3d93cddbbea004fdbf0418392a47148 --- /dev/null +++ b/mod/mg_sagpu_attention.py @@ -0,0 +1,537 @@ +from comfy.ldm.modules import attention as comfy_attention +import logging +import comfy.model_patcher +import comfy.utils +import comfy.sd +import torch +import comfy.model_management as mm +from comfy.cli_args import args + +sageattn_modes = [ + "disabled", + "auto", + "auto_speed", + "auto_quality", + "sageattn_qk_int8_pv_fp16_cuda", + "sageattn_qk_int8_pv_fp16_triton", + "sageattn_qk_int8_pv_fp8_cuda", + "sageattn_qk_int8_pv_fp8_cuda++", +] + +_initialized = False +# Avoid spamming logs each attention call +_sage_warned_once = False +_sage_generic_warned_once = False +_original_functions = {} + +# Runtime override knobs (may be set by other nodes, e.g., CADE2 Beta) +# CURRENT_PV_ACCUM can be None, "fp32+fp16" or "fp32+fp32" +CURRENT_PV_ACCUM = None + +# Lightweight attention-entropy probe (for AQClip Attn-mode) +_attn_entropy_enabled = False +_attn_entropy_last = None # torch.Tensor | None, shape (B,1,h',w') in [0,1] +_attn_probe_heads_cap = 4 +_attn_probe_tokens_cap = 1024 + +def enable_attention_entropy_capture(enable: bool, max_tokens: int = 1024, max_heads: int = 4): + """Toggle capturing a tiny attention entropy map during optimized_attention. + Stores a normalized map per forward pass; consumer may upsample to latent size. + """ + global _attn_entropy_enabled, _attn_probe_tokens_cap, _attn_probe_heads_cap, _attn_entropy_last + _attn_entropy_enabled = bool(enable) + _attn_probe_tokens_cap = int(max(128, min(16384, max_tokens))) + _attn_probe_heads_cap = int(max(1, min(32, max_heads))) + if not _attn_entropy_enabled: + _attn_entropy_last = None + +def get_attention_entropy_map(clear: bool = False): + """Return last captured attention entropy map (B,1,h',w') in [0,1] or None.""" + global _attn_entropy_last + out = _attn_entropy_last + if clear: + _attn_entropy_last = None + return out + +# ------------------------ KV pruning (self-attention) ------------------------ +_kv_prune_enabled = False +_kv_prune_keep = 0.85 +_kv_prune_min_tokens = 128 + +def set_kv_prune(enable: bool, keep: float = 0.85, min_tokens: int = 128): + """Enable lightweight K/V token pruning inside optimized attention. + - Applies only to self-attention (len(Q)==len(K)). + - Keeps top-`keep` fraction of keys/values by L2 energy of K, averaged over heads. + - Skips pruning when an attention mask is provided (shape mismatch risk). + """ + global _kv_prune_enabled, _kv_prune_keep, _kv_prune_min_tokens + _kv_prune_enabled = bool(enable) + try: + _kv_prune_keep = float(max(0.5, min(1.0, keep))) + except Exception: + _kv_prune_keep = 0.85 + try: + _kv_prune_min_tokens = int(max(1, min_tokens)) + except Exception: + _kv_prune_min_tokens = 128 + +if not _initialized: + _original_functions["orig_attention"] = comfy_attention.optimized_attention + _original_functions["original_patch_model"] = comfy.model_patcher.ModelPatcher.patch_model + _original_functions["original_load_lora_for_models"] = comfy.sd.load_lora_for_models + _initialized = True + +class MGSagpuBaseLoader: + original_linear = None + cublas_patched = False + + @torch.compiler.disable() + def _patch_modules(self, patch_cublaslinear, sage_attention): + from comfy.ops import disable_weight_init, CastWeightBiasOp, cast_bias_weight + + if sage_attention != "disabled": + print("Patching comfy attention to use sageattn") + try: + from sageattention import sageattn + from sageattention import ( + sageattn_qk_int8_pv_fp16_cuda, + sageattn_qk_int8_pv_fp16_triton, + sageattn_qk_int8_pv_fp8_cuda, + sageattn_qk_int8_pv_fp8_cuda_sm90, + ) + except ImportError: + from SageAttention import sageattn + from SageAttention import ( + sageattn_qk_int8_pv_fp16_cuda, + sageattn_qk_int8_pv_fp16_triton, + sageattn_qk_int8_pv_fp8_cuda, + sageattn_qk_int8_pv_fp8_cuda_sm90, + ) + def set_sage_func(sage_attention): + # Helper: pick best kernel for current GPU + def select_auto(quality: bool): + def _auto(q, k, v, is_causal=False, attn_mask=None, tensor_layout="NHD"): + major, minor = torch.cuda.get_device_capability(torch.cuda.current_device()) if torch.cuda.is_available() else (0, 0) + try: + if major == 12 and minor == 0: + # RTX 50 series + pv = "fp32+fp32" if quality else "fp32+fp16" + return sageattn_qk_int8_pv_fp8_cuda(q, k, v, is_causal=is_causal, attn_mask=attn_mask, pv_accum_dtype=pv, tensor_layout=tensor_layout) + elif major == 9: + # H100 family + pv = "fp32+fp32" if quality else "fp32+fp32" + return sageattn_qk_int8_pv_fp8_cuda_sm90(q, k, v, is_causal=is_causal, attn_mask=attn_mask, pv_accum_dtype=pv, tensor_layout=tensor_layout) + elif major == 8 and minor == 9: + pv = "fp32+fp32" if quality else "fp32+fp16" + return sageattn_qk_int8_pv_fp8_cuda(q, k, v, is_causal=is_causal, attn_mask=attn_mask, pv_accum_dtype=pv, tensor_layout=tensor_layout) + elif major == 8 and minor in (0, 6): + # Ampere + # Prefer CUDA kernel when possible + return sageattn_qk_int8_pv_fp16_cuda(q, k, v, is_causal=is_causal, attn_mask=attn_mask, pv_accum_dtype="fp32", tensor_layout=tensor_layout) + except Exception: + pass + # Generic auto (library decides), works across arch when available + return sageattn(q, k, v, is_causal=is_causal, attn_mask=attn_mask, tensor_layout=tensor_layout) + return _auto + if sage_attention == "auto": + return select_auto(quality=False) + if sage_attention == "auto_speed": + return select_auto(quality=False) + if sage_attention == "auto_quality": + return select_auto(quality=True) + elif sage_attention == "sageattn_qk_int8_pv_fp16_cuda": + def func(q, k, v, is_causal=False, attn_mask=None, tensor_layout="NHD"): + return sageattn_qk_int8_pv_fp16_cuda(q, k, v, is_causal=is_causal, attn_mask=attn_mask, pv_accum_dtype="fp32", tensor_layout=tensor_layout) + return func + elif sage_attention == "sageattn_qk_int8_pv_fp16_triton": + def func(q, k, v, is_causal=False, attn_mask=None, tensor_layout="NHD"): + return sageattn_qk_int8_pv_fp16_triton(q, k, v, is_causal=is_causal, attn_mask=attn_mask, tensor_layout=tensor_layout) + return func + elif sage_attention == "sageattn_qk_int8_pv_fp8_cuda": + def func(q, k, v, is_causal=False, attn_mask=None, tensor_layout="NHD"): + return sageattn_qk_int8_pv_fp8_cuda(q, k, v, is_causal=is_causal, attn_mask=attn_mask, pv_accum_dtype="fp32+fp32", tensor_layout=tensor_layout) + return func + elif sage_attention == "sageattn_qk_int8_pv_fp8_cuda++": + # using imported sageattn_qk_int8_pv_fp8_cuda above (name alias consistent for both module names) + # This variant requires SM89 (Ada 8.9). On newer GPUs (e.g., SM90), + # fall back to generic auto selection to avoid kernel assertion. + try: + if torch.cuda.is_available(): + major, minor = torch.cuda.get_device_capability(torch.cuda.current_device()) + if not (major == 8 and minor == 9): + logging.warning(f"sageattn_qk_int8_pv_fp8_cuda++ requires SM89, but detected SM{major}{minor}. Falling back to auto kernel selection.") + def func(q, k, v, is_causal=False, attn_mask=None, tensor_layout="NHD"): + return sageattn(q, k, v, is_causal=is_causal, attn_mask=attn_mask, tensor_layout=tensor_layout) + return func + except Exception: + pass + def func(q, k, v, is_causal=False, attn_mask=None, tensor_layout="NHD"): + return sageattn_qk_int8_pv_fp8_cuda(q, k, v, is_causal=is_causal, attn_mask=attn_mask, pv_accum_dtype="fp32+fp16", tensor_layout=tensor_layout) + return func + + sage_func = set_sage_func(sage_attention) + + @torch.compiler.disable() + def attention_sage(q, k, v, heads, mask=None, attn_precision=None, skip_reshape=False, skip_output_reshape=False, transformer_options=None, **kwargs): + if skip_reshape: + b, _, _, dim_head = q.shape + tensor_layout="HND" + else: + b, _, dim_head = q.shape + dim_head //= heads + q, k, v = map( + lambda t: t.view(b, -1, heads, dim_head), + (q, k, v), + ) + tensor_layout="NHD" + if mask is not None: + # add a batch dimension if there isn't already one + if mask.ndim == 2: + mask = mask.unsqueeze(0) + # add a heads dimension if there isn't already one + if mask.ndim == 3: + mask = mask.unsqueeze(1) + # Prefer trying sage kernels; allow runtime overrides via transformer_options or CURRENT_PV_ACCUM + + # Optional K/V pruning for self-attention (token-level top-k) + try: + if _kv_prune_enabled and (mask is None): + import math + if tensor_layout == "NHD": + # q,k,v: B,N,H,D + Bn, Nq, Hn, Dh = q.shape + Nk = k.shape[1] + if Nq == Nk and Nk >= _kv_prune_min_tokens: + keep = max(1, int(math.ceil(float(_kv_prune_keep) * Nk))) + if keep < Nk: + # importance: mean over heads of L2 norm of K per token + imp = (k.pow(2).sum(dim=-1)).mean(dim=2) # B,N + top = torch.topk(imp, k=keep, dim=1, largest=True, sorted=False).indices + idx = top.unsqueeze(-1).unsqueeze(-1).expand(Bn, keep, Hn, Dh) + k = torch.gather(k, dim=1, index=idx) + v = torch.gather(v, dim=1, index=idx) + else: + # HND: q,k,v: B,H,N,D + Bb, Hn, Nq, Dh = q.shape + Nk = k.shape[2] + if Nq == Nk and Nk >= _kv_prune_min_tokens: + keep = max(1, int(math.ceil(float(_kv_prune_keep) * Nk))) + if keep < Nk: + imp = (k.pow(2).sum(dim=-1)).mean(dim=1) # B,N + top = torch.topk(imp, k=keep, dim=1, largest=True, sorted=False).indices + idx = top.unsqueeze(1).unsqueeze(-1).expand(Bb, Hn, keep, Dh) + k = torch.gather(k, dim=2, index=idx) + v = torch.gather(v, dim=2, index=idx) + except Exception: + # On any issue, skip pruning silently + pass + + try: + pv_override = None + if transformer_options and isinstance(transformer_options, dict): + so = transformer_options.get("sageattn") + if isinstance(so, dict): + pv_override = so.get("pv_accum_dtype", None) + if pv_override is None: + pv_override = CURRENT_PV_ACCUM + + if pv_override is not None: + out = sageattn(q, k, v, attn_mask=mask, is_causal=False, tensor_layout=tensor_layout, pv_accum_dtype=pv_override) + else: + out = sage_func(q, k, v, attn_mask=mask, is_causal=False, tensor_layout=tensor_layout) + except Exception as e: + global _sage_generic_warned_once + if not _sage_generic_warned_once: + logging.warning(f"Error running sage attention: {e}. Falling back.") + _sage_generic_warned_once = True + try: + out = sageattn(q, k, v, attn_mask=mask, is_causal=False, tensor_layout=tensor_layout) + except Exception: + # Final fallback to PyTorch attention, silent after first warning + if tensor_layout == "NHD": + q, k, v = map(lambda t: t.transpose(1, 2), (q, k, v)) + return comfy_attention.attention_pytorch(q, k, v, heads, mask=mask, skip_reshape=True, skip_output_reshape=skip_output_reshape, transformer_options=transformer_options, **kwargs) + # Optional tiny attention-entropy probe (avoid heavy compute) + try: + if _attn_entropy_enabled: + import torch + with torch.inference_mode(): + if tensor_layout == "HND": + # q: B,H,N,D -> B,N,H,D for uniform handling + q_probe = q.transpose(1, 2) + k_probe = k.transpose(1, 2) + else: + q_probe = q + k_probe = k + B_, N_, H_, Dh = q_probe.shape + # Cap heads and tokens + h_cap = min(H_, _attn_probe_heads_cap) + step = max(1, N_ // _attn_probe_tokens_cap) + q_s = q_probe[:, ::step, :h_cap, :].transpose(1, 2) # B,h,q,d + k_s = k_probe[:, ::step, :h_cap, :].transpose(1, 2) # B,h,k,d + scale = (float(Dh) ** -0.5) + # logits: B,h,q,k + logits = torch.matmul(q_s * scale, k_s.transpose(-1, -2)) + p = torch.softmax(logits, dim=-1) + # entropy per query + eps = 1e-9 + Hq = -(p * (p.clamp_min(eps).log())).sum(dim=-1) # B,h,q + Hq = Hq.mean(dim=1) # B,q + # reshape to approx grid + import math + Q = Hq.shape[-1] + w = int(math.sqrt(Q)) + w = max(1, w) + h = max(1, Q // w) + if h * w > Q: + Hq = Hq[..., : (h * w)] + elif h * w < Q: + # pad with last + pad = (h * w) - Q + if pad > 0: + Hq = torch.cat([Hq, Hq[..., -1:].expand(B_, pad)], dim=-1) + Hmap = Hq.reshape(B_, 1, h, w) + # normalize per-sample to [0,1] + Hmin = Hmap.amin(dim=(2, 3), keepdim=True) + Hmax = Hmap.amax(dim=(2, 3), keepdim=True) + Hn = (Hmap - Hmin) / (Hmax - Hmin + 1e-6) + global _attn_entropy_last + _attn_entropy_last = Hn.detach() + except Exception: + pass + + if tensor_layout == "HND": + if not skip_output_reshape: + out = ( + out.transpose(1, 2).reshape(b, -1, heads * dim_head) + ) + else: + if skip_output_reshape: + out = out.transpose(1, 2) + else: + out = out.reshape(b, -1, heads * dim_head) + return out + + comfy_attention.optimized_attention = attention_sage + comfy.ldm.hunyuan_video.model.optimized_attention = attention_sage + comfy.ldm.flux.math.optimized_attention = attention_sage + comfy.ldm.genmo.joint_model.asymm_models_joint.optimized_attention = attention_sage + comfy.ldm.cosmos.blocks.optimized_attention = attention_sage + comfy.ldm.wan.model.optimized_attention = attention_sage + + else: + print("Restoring initial comfy attention") + comfy_attention.optimized_attention = _original_functions.get("orig_attention") + comfy.ldm.hunyuan_video.model.optimized_attention = _original_functions.get("orig_attention") + comfy.ldm.flux.math.optimized_attention = _original_functions.get("orig_attention") + comfy.ldm.genmo.joint_model.asymm_models_joint.optimized_attention = _original_functions.get("orig_attention") + comfy.ldm.cosmos.blocks.optimized_attention = _original_functions.get("orig_attention") + comfy.ldm.wan.model.optimized_attention = _original_functions.get("orig_attention") + + if patch_cublaslinear: + if not MGSagpuBaseLoader.cublas_patched: + MGSagpuBaseLoader.original_linear = disable_weight_init.Linear + try: + from cublas_ops import CublasLinear + except ImportError: + raise Exception("Can't import 'torch-cublas-hgemm', install it from here https://github.com/aredden/torch-cublas-hgemm") + + class PatchedLinear(CublasLinear, CastWeightBiasOp): + def reset_parameters(self): + pass + + def forward_comfy_cast_weights(self, input): + weight, bias = cast_bias_weight(self, input) + return torch.nn.functional.linear(input, weight, bias) + + def forward(self, *args, **kwargs): + if self.comfy_cast_weights: + return self.forward_comfy_cast_weights(*args, **kwargs) + else: + return super().forward(*args, **kwargs) + + disable_weight_init.Linear = PatchedLinear + MGSagpuBaseLoader.cublas_patched = True + else: + if MGSagpuBaseLoader.cublas_patched: + disable_weight_init.Linear = MGSagpuBaseLoader.original_linear + MGSagpuBaseLoader.cublas_patched = False + +from comfy.patcher_extension import CallbacksMP +class MGSagpuAttention(MGSagpuBaseLoader): + @classmethod + def INPUT_TYPES(s): + return {"required": { + "model": ("MODEL",), + "sage_attention": (sageattn_modes, {"default": False, "tooltip": "Global patch comfy attention to use sageattn, once patched to revert back to normal you would need to run this node again with disabled option."}), + }} + + RETURN_TYPES = ("MODEL", ) + FUNCTION = "patch" + DESCRIPTION = "Node for patching attention mode. This doesn't use the model patching system and thus can't be disabled without running the node again with 'disabled' option." + EXPERIMENTAL = False + CATEGORY = "MagicNodes" + + def patch(self, model, sage_attention): + model_clone = model.clone() + @torch.compiler.disable() + def patch_attention_enable(model): + self._patch_modules(False, sage_attention) + @torch.compiler.disable() + def patch_attention_disable(model): + self._patch_modules(False, "disabled") + + model_clone.add_callback(CallbacksMP.ON_PRE_RUN, patch_attention_enable) + model_clone.add_callback(CallbacksMP.ON_CLEANUP, patch_attention_disable) + + return model_clone, + + + +# Legacy compile helpers removed + +# Legacy video helpers removed +import inspect as _inspect +try: + from comfy.ldm.modules import attention as _cm_attn +except Exception as _e: + _cm_attn = None + +_nag_patch_active = False +_nag_params = {"scale": 5.0, "tau": 2.5, "alpha": 0.25} +_original_functions.setdefault("orig_crossattn_forward", None) +_original_functions.setdefault("orig_crossattn_sig", None) + +def _call_orig_crossattn(self, x, context=None, **kwargs): + #\"\"\"Call the original CrossAttention.forward with kwargs filtered to its signature.\"\"\" + f = _original_functions.get("orig_crossattn_forward", None) + if f is None: + # Should not happen; just try current method + return self.__class__.forward(self, x, context=context, **kwargs) + sig = _original_functions.get("orig_crossattn_sig", None) + if sig is None: + try: + sig = _inspect.signature(f) + _original_functions["orig_crossattn_sig"] = sig + except Exception: + sig = None + if sig is not None: + allowed = set(sig.parameters.keys()) + fkwargs = {k: v for k, v in kwargs.items() if k in allowed} + else: + fkwargs = kwargs + try: + return f(self, x, context=context, **fkwargs) + except TypeError: + # Some builds have (x, context=None, value=None, mask=None) only + fkwargs.pop("attn_precision", None) + fkwargs.pop("transformer_options", None) + try: + return f(self, x, context=context, **fkwargs) + except Exception: + # Give up; call current method (unpatched) to avoid crashing + return self.__class__.forward(self, x, context=context, **kwargs) + +def _kj_crossattn_forward_nag(self, x, context=None, value=None, mask=None, **kwargs): + # If patch not active or context not having cond/uncond, defer to original. + if (not _nag_patch_active) or (_cm_attn is None): + return _call_orig_crossattn(self, x, context=context, value=value, mask=mask, **kwargs) + try: + if context is None or not torch.is_tensor(context): + return _call_orig_crossattn(self, x, context=context, value=value, mask=mask, **kwargs) + + # Expect batch 2 with [uncond, cond]; if not, fall back + if context.shape[0] < 2: + return _call_orig_crossattn(self, x, context=context, value=value, mask=mask, **kwargs) + + # Split branches. In most samplers order is [uncond, cond]. + # If x has batch==2, split it likewise; else use the same x for both calls. + x_has_pair = (torch.is_tensor(x) and x.shape[0] == 2) + x_u = x[0:1] if x_has_pair else x + x_c = x[1:2] if x_has_pair else x + + c_u, c_c = context[0:1], context[1:2] + + # value may also be batched + v = kwargs.get("value", value) + if torch.is_tensor(v) and v.shape[0] == 2: + v_u, v_c = v[0:1], v[1:2] + else: + v_u = v_c = v + + # Get per-branch outputs using the ORIGINAL forward + # - Neg branch (for real uncond stream) + out_u = _call_orig_crossattn(self, x_u, context=c_u, value=v_u, mask=mask, **kwargs) + # - Pos branch + z_pos = _call_orig_crossattn(self, x_c, context=c_c, value=v_c, mask=mask, **kwargs) + # - "Neg guidance" term computed with *positive query but negative context* + z_neg = _call_orig_crossattn(self, x_c, context=c_u, value=v_u, mask=mask, **kwargs) + + # NAG mixing in the attention output space + phi = float(_nag_params.get("scale", 5.0)) + tau = float(_nag_params.get("tau", 2.5)) + alpha = float(_nag_params.get("alpha", 0.25)) + + g = z_pos * phi - z_neg * (phi - 1.0) + # L1-norm based clipping to limit deviation from Z+ + def _l1_norm(t): + return torch.sum(torch.abs(t), dim=-1, keepdim=True).clamp_min(1e-6) + s_pos = _l1_norm(z_pos) + s_g = _l1_norm(g) + scale = (s_pos * tau) / s_g + g = torch.where((s_g > s_pos * tau), g * scale, g) + + z_guided = g * alpha + z_pos * (1.0 - alpha) + if x_has_pair: + return torch.cat([out_u, z_guided], dim=0) + else: + return z_guided + except Exception as e: + # If anything goes wrong, use the original forward. + return _call_orig_crossattn(self, x, context=context, value=value, mask=mask, **kwargs) + +def enable_crossattention_nag_patch(enable: bool, nag_scale: float = 5.0, nag_tau: float = 2.5, nag_alpha: float = 0.25): + #\"\"\"Enable/disable a safe CrossAttention forward wrapper that applies NAG to the positive branch only. + #This does not modify model weights and is fully reversible. The wrapper preserves + #unknown kwargs (filters per-signature) to avoid errors on older Comfy builds. + #\"\"\" + global _nag_patch_active, _nag_params + if _cm_attn is None: + return False + if enable: + _nag_params = {"scale": float(nag_scale), "tau": float(nag_tau), "alpha": float(nag_alpha)} + if _original_functions.get("orig_crossattn_forward", None) is None: + try: + _original_functions["orig_crossattn_forward"] = _cm_attn.CrossAttention.forward + try: + _original_functions["orig_crossattn_sig"] = _inspect.signature(_cm_attn.CrossAttention.forward) + except Exception: + _original_functions["orig_crossattn_sig"] = None + except Exception: + return False + # Patch in our wrapper + try: + _cm_attn.CrossAttention.forward = _kj_crossattn_forward_nag + _nag_patch_active = True + return True + except Exception: + return False + else: + # Restore original if we have it + if _original_functions.get("orig_crossattn_forward", None) is not None: + try: + _cm_attn.CrossAttention.forward = _original_functions["orig_crossattn_forward"] + except Exception: + pass + _nag_patch_active = False + return True +# =============================================================================== + +PatchSageAttention = MGSagpuAttention + + + + + + + diff --git a/mod/mg_seed_latent.py b/mod/mg_seed_latent.py new file mode 100644 index 0000000000000000000000000000000000000000..b2b6fed3d7fedc8d0942b1f8c093196dc0ef59f2 --- /dev/null +++ b/mod/mg_seed_latent.py @@ -0,0 +1,54 @@ +""" +Simple latent generator for ComfyUI. +The ``MagicSeedLatent`` class creates a random latent tensor of the specified size. +If ``mix_image`` is enabled, the input image is encoded with a VAE and mixed with noise. +""" + +from __future__ import annotations + +import torch + + +class MagicSeedLatent: + """Generate a latent tensor with optional image mixing.""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "width": ("INT", {"default": 512, "min": 8, "max": 4096, "step": 8}), + "height": ("INT", {"default": 512, "min": 8, "max": 4096, "step": 8}), + "batch_size": ("INT", {"default": 1, "min": 1, "max": 64}), + "sigma": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1}), + "bias": ("FLOAT", {"default": 0.0, "min": -10.0, "max": 10.0, "step": 0.1}), + "mix_image": ("BOOLEAN", {"default": False}), + }, + "optional": { + "vae": ("VAE", {}), + "image": ("IMAGE", {}), + }, + } + + RETURN_TYPES = ("LATENT",) + RETURN_NAMES = ("LATENT",) + FUNCTION = "generate" + CATEGORY = "MagicNodes" + + def generate( + self, + width: int, + height: int, + batch_size: int, + sigma: float, + bias: float, + mix_image: bool = False, + vae=None, + image=None, + ): + """Generate a random latent tensor and optionally mix it with an image.""" + + lat = torch.randn(batch_size, 4, height // 8, width // 8) * sigma + bias + if mix_image and vae is not None and image is not None: + encoded = vae.encode(image[:, :, :, :3]) + lat = encoded + lat + return ({"samples": lat},) \ No newline at end of file diff --git a/mod/state/smart_seed.json b/mod/state/smart_seed.json new file mode 100644 index 0000000000000000000000000000000000000000..a9a6de551e75d1061898e74092489127ee3fa3a2 --- /dev/null +++ b/mod/state/smart_seed.json @@ -0,0 +1,1053 @@ +{ + "0x57d0f1ec15fc5bfe": 3, + "0xa6f60b56868575d2": 1, + "0x8779c336146e0822": 2, + "0xc6f12e6a70f68001": 1, + "0x9a0d92ab21e22502": 1, + "0xba5e6b1beb9f8963": 1, + "0x12166e6de7c010a": 2, + "0x617f65b591ef6cd7": 1, + "0xb4e55ed148e475a0": 1, + "0xecfd286eda30ced2": 1, + "0x2511be2a5a5b35c3": 1, + "0xa9228ea9fed25a32": 1, + "0x3eda68dcf4c46604": 1, + "0x24945b9047cdcc3e": 1, + "0x3d95ce1ac64434db": 1, + "0x4b73dddfcd33b79a": 1, + "0xc41925a930bf2010": 1, + "0x722c484a5b248977": 1, + "0x95bf37ad53f49b47": 1, + "0x75aaca641a35177a": 1, + "0x2ce87ea0cb626925": 1, + "0x53aba17f124be2a4": 1, + "0x731fd5513e6af9ef": 1, + "0x85f695dc969d5f29": 1, + "0x32ba50e6f9be3709": 1, + "0xdfdbd685aee26048": 1, + "0x5d126a0a73080e3e": 1, + "0x4839e6b926b01107": 1, + "0x105511739cf86445": 1, + "0xf37fc865bb662672": 1, + "0x346d7a872fa90913": 1, + "0x3bdd625c36aac2da": 1, + "0xe99afa03e4cc2b14": 1, + "0xfcbf95c2e210a8a1": 1, + "0xfede18c89ef8c51b": 1, + "0x8ac43ee60bfa64b9": 2, + "0x6675ec766d428ef6": 1, + "0x37a12713e22f4402": 1, + "0x1810ae388e1b5eb5": 1, + "0x21b40c152a70ccc6": 2, + "0xa33ee567b18192f": 1, + "0x64a799b378b6bbb0": 1, + "0x5124651f2ea8191": 1, + "0xb23fbf30e029b262": 4, + "0xac67ee5eb632629f": 1, + "0xcab77f007ad4d7f5": 1, + "0x375f0ec39e6d6eda": 1, + "0x211ecf358a70c7f7": 2, + "0x2f93f8c87259d1ac": 1, + "0x437cbf4f21c7d00a": 1, + "0xacb10d781999edd7": 1, + "0xd53378d989d41ab": 2, + "0x5a566f2e5766e93d": 1, + "0xc8c0a3495eeea414": 1, + "0x518dc538cd1ec452": 1, + "0x66b237e99917f216": 1, + "0x3cb1e5a97a76ff52": 1, + "0x8be94d4dca46f191": 1, + "0xb57b85cfd55cb316": 1, + "0xe067895fe4ac232f": 2, + "0x674615a3cf7d9839": 1, + "0xfde5781e788fe9ac": 1, + "0x48a4ea1367928dc3": 1, + "0xf2c3461ccf1c33c0": 3, + "0x293c6022a4c6c7c1": 1, + "0xbe39adb6bfb49c55": 1, + "0x27bd824a461aff22": 1, + "0x660163d9b9df943c": 2, + "0x869bada29ab5cafb": 1, + "0x186661ce6643dcd8": 1, + "0xed375182fc10aaf2": 1, + "0x2c69378619ccf294": 3, + "0x5862958938b3aae2": 1, + "0x6ccc6050bf664ce6": 1, + "0xd65b8a268368081c": 1, + "0xea7e353a45d0bdb9": 1, + "0x1e1bbabaeccf1812": 1, + "0xa41d551e5deb06b0": 1, + "0x4db4f53c5e560749": 1, + "0x9cbdf65f423e80db": 1, + "0xcb1e197a55d80ee8": 1, + "0x3cea0b3f48365940": 1, + "0xbbc6ac8c4b7ed362": 1, + "0xb457516aace07b17": 1, + "0xb7d75a1f49e4058d": 1, + "0x51bd94cd6e362371": 1, + "0xc1b78b7672fa357f": 1, + "0x95eef4c666013460": 1, + "0x6ab065d87e2f02c2": 2, + "0xc44ff110b7e3f663": 1, + "0xf8dfbafb448535b0": 1, + "0x7b1105ee38a933ac": 1, + "0x169847ca1efe7890": 1, + "0x22fa70b4b04d38b2": 1, + "0xabefed72b1e1b00f": 1, + "0xba2bf7b1b4cbc85b": 1, + "0x792282af8ecf5c48": 1, + "0xb6a40a4dc7f1e609": 1, + "0xac813249e15a0d4": 1, + "0xbf3a68ec003614e8": 1, + "0x8ed71e85871fabdc": 1, + "0x140b4ea3e30dbd1c": 2, + "0x1c33733a4739c9a": 1, + "0xeffcca755288c225": 1, + "0xd76ade2331cce030": 1, + "0x47342b969c31801b": 1, + "0x4bea22fc479efe51": 1, + "0xe2956738153e56ce": 1, + "0x6668e92d045c0b49": 1, + "0x903a9b268fd3e889": 4, + "0xbda88c654e59ae83": 1, + "0x7cb30e33c9bb5242": 1, + "0x4a3c3aa7139caed2": 1, + "0x2315ef283d4ca2a0": 2, + "0x9aa0388eb151af8f": 1, + "0x187afcf5e32e846b": 2, + "0x3a8ed2e50fd0a9d3": 1, + "0xc81805cdefdf11d4": 1, + "0x3075fc679c797e84": 1, + "0xa67fb28d6520a1a7": 1, + "0xe7dfa25f35a8a541": 1, + "0x5f09c8d0d8f847e7": 1, + "0x48cb0d60fa75dc9d": 1, + "0xd031a96e13a5cbf2": 4, + "0x16ec6997e1298fca": 1, + "0x4a6ccdd4a6c35797": 1, + "0xf882ff7e5c962d95": 1, + "0x4210b9ba2936ece5": 1, + "0xd7b199c1ef6fadd5": 2, + "0x9dfce65966e2b0ca": 1, + "0xb42b5588fc724732": 1, + "0xfc144657d1101ee3": 1, + "0x23532e04d7abd273": 1, + "0x3f433b93784e702": 1, + "0xb5a63236889528c2": 1, + "0x7320f291146fc014": 1, + "0x5f0cf2011c4d889a": 1, + "0xe03b8326182bf2b": 1, + "0xa46104502538d3f0": 1, + "0x2ecaa7c62d4fb3be": 2, + "0x16e50839ff624d20": 6, + "0xd780392c901deb32": 1, + "0x6a29433dd6a042ba": 1, + "0x5e3f2229b0516347": 1, + "0x546d73d03951d52f": 1, + "0x94e7f51182d91b02": 1, + "0xb574e91172a2737a": 1, + "0xc11bd6e93e2188ee": 1, + "0xda53ec2dc4ae0267": 1, + "0x60875d18ca112783": 1, + "0x45f17324bb4ab0fa": 1, + "0x2b705560712f9e41": 4, + "0xdf2a7731375228ae": 1, + "0x5bd2980318aee4ba": 1, + "0xf83c8ace011c5c3c": 1, + "0xeb78d80c09c518c7": 2, + "0xafefb09d226287ad": 1, + "0x1dbca41441b38e01": 1, + "0x546efde6d213b3d2": 3, + "0x69d4d099c00510e5": 1, + "0x164b8083429419af": 1, + "0x8845336c59551ffd": 5, + "0xd2c665b000276ef3": 1, + "0x136a4d2d278e9e6b": 1, + "0xce6ba88562631d37": 1, + "0x7f1cbb316649dbfb": 1, + "0x49d13445c9e879fb": 1, + "0x36ee6b8950e0e30c": 1, + "0x906409ff1872e0dd": 1, + "0x39e8e3163f0ad47c": 1, + "0xa787036909188ba0": 4, + "0x88032b20bfc8b8": 1, + "0xc2fb3055a59c55ac": 1, + "0xd845e51eaf30bace": 1, + "0xff60c81245a176c6": 1, + "0xb12bf9939d60a394": 1, + "0x334259d35fc13aa8": 1, + "0xedecceba5fc3ab3f": 1, + "0x31baca6e84c30d61": 7, + "0xa957c1bffdf675a2": 1, + "0x596b9aba3c23c65b": 1, + "0x6c253d5f85db593f": 1, + "0x8d0edddfcfc2c59f": 2, + "0x63bb33a93d06dbf5": 1, + "0x588a436578fe988d": 1, + "0x193d25eaa430e67c": 1, + "0x90adf959d76f958": 1, + "0x175b474b71d47019": 1, + "0x258d21ca24f51ca2": 1, + "0x2c289b768e19382b": 1, + "0x53d318c73d7ddfef": 2, + "0x361feb17a30afe06": 1, + "0xb1cda829783c1ef9": 1, + "0xc7d4f26cefcd4959": 1, + "0x8d49140da4e91714": 1, + "0xcc42476947f233bd": 1, + "0x18b4a8b560af9507": 1, + "0xd24a423a53d4c1aa": 1, + "0x15e90e76cd2b649d": 1, + "0xec89d187199f873a": 1, + "0x384ef6e5bb0a748": 1, + "0x24e98a6e1369857b": 1, + "0xd9eb5d000613355": 1, + "0xdf2092278b356272": 1, + "0x39417bc9c26bd916": 1, + "0x59e055249f8900fc": 1, + "0xe0205573c1bef3ae": 1, + "0x19e3758ac6de97db": 1, + "0xf7c7eb05454191d3": 1, + "0x9842cfcc95f90379": 1, + "0xf2c07cd92c346b7f": 1, + "0x4d3f90cea6981ab2": 1, + "0x4927e59f9bfc26e6": 1, + "0x4d0d27f1bb251c39": 1, + "0xb1bc9e134e4352b6": 1, + "0x19d606c592332077": 1, + "0x897cbb2a5a3b4c3a": 1, + "0x3bad7683b761a7cc": 1, + "0x18bcf4cac9b7d274": 1, + "0x905f79195df10459": 1, + "0xe423c60fc93189cc": 1, + "0xbb2995497607807d": 1, + "0xf3f647432a5ed077": 1, + "0x88e3e18ba47f9400": 1, + "0x24b5c952468b1534": 1, + "0xb2b576468a031b87": 1, + "0xdcf1c5f8fab7f5fc": 1, + "0xc24cf4b61c1515c7": 1, + "0xfea7055d347b5cfd": 1, + "0x4f002cb41a439d0a": 1, + "0xc5735c384a35ac2d": 1, + "0x5fb011eb4884167b": 1, + "0x1e32cf3c06e616a2": 1, + "0xd33c48d20ec98869": 1, + "0x4494b42d2d5c8dca": 1, + "0xc83186d6e0b32a2e": 1, + "0x235454d2608ec9ea": 1, + "0x65bf28f6126d2a59": 3, + "0xeec1428e8c4634d": 1, + "0xd2fee2f76bb47697": 1, + "0x3be99764f476bb63": 1, + "0x54d9c2ca5c26203": 1, + "0x302297eda48fbc68": 1, + "0xa6d3a212a12c50f0": 1, + "0xfc49900989e77cc5": 1, + "0x482bbffca21eff93": 2, + "0xb50fef1152e9e0b3": 1, + "0xa330212fd16b8516": 1, + "0x52617262bfaae31d": 1, + "0x9fe9f18b1bbedc6b": 1, + "0xea8e40d7cb6045d": 1, + "0xeb55cee1bd61b842": 1, + "0x6d950f22685e7b8e": 1, + "0x7e146f6504aeb3dd": 1, + "0x1364d69e9753f9ba": 1, + "0xdc66410291f40ad9": 1, + "0x6a7fa6fb73e044e3": 1, + "0xbb7172274f6ffcf2": 1, + "0xc1f3858909550954": 1, + "0xb6641ca6b231195d": 1, + "0xc5802e7f4a126ee6": 1, + "0xbd7f07710bebd35d": 1, + "0x74610b1e406f44c8": 1, + "0x99347c850ed51277": 1, + "0x83d0483f074f8da6": 1, + "0x99622bacec6ae935": 1, + "0x82edf6f7e04a3658": 1, + "0x151afd852ed5389": 2, + "0x68c6587feb995582": 1, + "0x6ce1485b9c076b99": 1, + "0xde20f2a7038bf7fc": 1, + "0x6a1b50d200036de7": 1, + "0x9661fc8e154f2f1": 1, + "0xe250dcd8834b98fb": 1, + "0x63793ab616155f8a": 1, + "0x73937445b9301374": 1, + "0x2e42c9becddd2bac": 1, + "0x1f5d0fbb5c6f1420": 1, + "0xd655410da12f96b3": 1, + "0xc846705502d4b920": 1, + "0x32bbd720985d9403": 1, + "0x4e7c24128c23c738": 1, + "0xadc96b66f18a0fac": 1, + "0x93a07e00d9305bd8": 1, + "0x3efaa9cb29d08282": 16, + "0xb5bb1e2978bab5be": 1, + "0xfa22548679e4820d": 1, + "0x2348290bfa38a29b": 1, + "0x522fe215e1431f6b": 1, + "0xd9abb61f8402e349": 1, + "0x36e905ff01ac1904": 1, + "0xd407f3ff45d9ca0a": 1, + "0xc223a7541462d750": 1, + "0xd2230b1e79ff8210": 1, + "0xda62cb554337cbe1": 1, + "0x22a8bfbca4d10a56": 1, + "0x8d1194e850719651": 1, + "0xaa8a7ded08d8fd91": 1, + "0xe8fc434bb7938d7f": 1, + "0x282c789381b0730f": 1, + "0xee36ba144eb352bc": 1, + "0x178a1aac46e6f6a5": 1, + "0x7c79f6e56c7828f9": 1, + "0xbf736588c1698c28": 1, + "0xad7abc28a881d019": 1, + "0x7745d4dd74fc7968": 1, + "0xfeb5c149320212c6": 1, + "0x2df61277b9c1ad73": 1, + "0xa3ef7884525bcb5a": 1, + "0x3adf38579be33dc": 1, + "0xc68efd01fcd6c276": 1, + "0x1028fc942e2edf79": 1, + "0x3b095aeb3bee5a4c": 1, + "0xe259fcd12757ed37": 1, + "0x72d0db26cd5ace04": 1, + "0xad208dc942902837": 1, + "0xfc965eef50f4548a": 1, + "0x245cf6ecdd135ca0": 1, + "0x70fee594fad615da": 1, + "0x63838c9d950f27ae": 1, + "0x363e6ed555ac7a68": 1, + "0xdc79c0b87daaaed5": 1, + "0xb3e9a5b0d68be049": 1, + "0xa96f54db3a97b5c3": 1, + "0xf1e833f73d0d240e": 1, + "0x932f328cc6686d79": 1, + "0x3c1aa8a6a0b34b8": 1, + "0x6208fd8a1b6ef1ba": 1, + "0x6f653f71707b388c": 2, + "0x7bcd51df150b2b2c": 1, + "0xcd124e737e9cf660": 1, + "0xf959d89c715d1ef9": 1, + "0x906e439f905d737b": 1, + "0xc9f761d667115fd5": 1, + "0xf8c4f640c6be9ba6": 1, + "0x20c5342ccfa6deae": 1, + "0x756607e02536c140": 1, + "0x7e0251360118c6ff": 1, + "0x5276c5bfe46aabb7": 1, + "0x4adfb8c20c6fb845": 1, + "0x661a5f242b09181c": 1, + "0x33c9f270b5502e0e": 1, + "0xf64e14c1f5b4a8c4": 1, + "0x2df9aba5f6075ee8": 1, + "0x8d79e5468aa68743": 1, + "0x3845ab9245028bbf": 1, + "0x9b64cee2041eb328": 1, + "0xcd6654832c5eeab": 1, + "0xa43ace499217d5c6": 1, + "0xa7096c747a55af68": 1, + "0xad9cd7806a7d9997": 1, + "0x6f97b97672e4c6f9": 1, + "0x63bc869939659ccc": 1, + "0xc982c51c678fdbf2": 1, + "0xaa7c7b3c378496f7": 1, + "0xd8fde97ca9fe0acd": 3, + "0x4b1e09c338ea8d89": 1, + "0x2241deee410c442c": 1, + "0x715459e3ff45b2ca": 1, + "0xe6b12728aca9b0e": 2, + "0x4b7698a99a5d60f7": 1, + "0x2151f01963dc6de": 1, + "0x3fd5ef343292dd7f": 1, + "0xcda499607a2d112e": 1, + "0x19f49091a446d6cf": 1, + "0xbbe9efc9f13a8801": 1, + "0xb9dc700cc6704f4e": 1, + "0x266e15e87c189cad": 1, + "0xea02213b45491d47": 1, + "0xf5d1bbc49fd49627": 1, + "0x61165ea8d06b570e": 1, + "0x527dc8384b2a4703": 1, + "0x25c3fef00eac3066": 1, + "0xdfbd60f5ee5ad79a": 1, + "0xd4138167bac4ab93": 1, + "0xd769a236c55dda65": 1, + "0xbf81d02b49f6ba4d": 1, + "0x400be041c5d02827": 1, + "0xb607682af698f2ff": 1, + "0x43911f7e4e22c10c": 1, + "0x93fcad63e71f19c3": 1, + "0x2d9c6fe97d72f537": 1, + "0x96d25cab81f868f9": 3, + "0x9b7ca90b1f603763": 1, + "0xf1f4c78e3d76dd77": 1, + "0xd67620b00edf4237": 1, + "0xdad4fd04fd989d82": 1, + "0x70d3d844d5fc3dd3": 6, + "0x71eb4a84f48a2e9": 1, + "0xa554db59f248e341": 1, + "0xacda14073c542eb8": 1, + "0x23823d234446eea9": 1, + "0x9dfa0012f0881822": 1, + "0x553513716673c9dc": 1, + "0xc6446e21c0e48903": 1, + "0x758212987a60b0a0": 1, + "0x8133610bf5dd4909": 1, + "0xe5671577c0ef431": 1, + "0xc2dce3a274cae3a2": 1, + "0xa6187ec73403f5a4": 1, + "0x8d5bedb96725874b": 1, + "0xb0e9c3f804ac5e2": 1, + "0x3cade1c1467176cf": 1, + "0xb271b66cd25b88ef": 1, + "0xb8f8734d8d934a56": 1, + "0xb2d8a5f933986590": 1, + "0xf594522497736e32": 1, + "0x74b1a44b38e2f442": 1, + "0x66916cf370a5080": 1, + "0x5c86a34623b4f2ec": 1, + "0xb6791f613dcf5e23": 1, + "0xe04110442dc7ccd4": 1, + "0x7f8c07d71e468d94": 1, + "0x6dbf4e350333f7cf": 1, + "0x4e9391cdac66f445": 1, + "0x9e9d23d2337be2d4": 1, + "0xb9d3c621a3cefb2f": 1, + "0x8477bccff4864f2a": 1, + "0x3a66a022b160fd8c": 1, + "0x3c3faf11e6ea0c46": 1, + "0xd773c6e950e8564": 1, + "0x596d422267be3fb7": 1, + "0xa01b94ad95677e83": 1, + "0x8f0fb89fa21c502d": 1, + "0x65950fbad4597562": 1, + "0x55495c04366b5db3": 1, + "0x534c004a54ec12f1": 1, + "0x7c002ed9a6f7abe0": 1, + "0xe87d2d0b071ea873": 1, + "0x7359f362a9806886": 1, + "0xa5f6dba46c828789": 1, + "0x58c0e3aa0a9e1d5": 1, + "0x7c7045c33eb3c470": 1, + "0xc98ddfa3d432a3c3": 1, + "0xfc13a50f1342d7b4": 1, + "0xd62b0909a241aae": 1, + "0x841ce6415df8979b": 1, + "0x2d01f1b72a734fda": 1, + "0xc9dd873ae154b143": 1, + "0x2c6e6f86e6a3a4e3": 1, + "0xb7c44a2175def125": 1, + "0x69a566b4e398bded": 1, + "0x4fe5b2497e74e5": 1, + "0x1bf4923a34f15cf7": 1, + "0x89cf2b1e0571b042": 1, + "0xf0fac69c1b4ba09": 1, + "0x86415075cc292a43": 1, + "0x17c512dc62096554": 1, + "0x5f4f82fff1a7723b": 1, + "0x73b4a0792986cff0": 1, + "0xac7047344ae8637b": 1, + "0xcf42eb2b4a2c6560": 2, + "0x2d74a6c5b01013eb": 1, + "0x99399857571eb9f1": 1, + "0x4934ac7835715144": 1, + "0x77da903db51f24c7": 1, + "0x4eeb3530fcf1ccbe": 1, + "0x712fe93d5a88db46": 1, + "0xaf65e93906ede024": 1, + "0x7e3b375d1a9815f5": 1, + "0xfabf2c05122b08d5": 1, + "0xb0f7a817035b5ff": 1, + "0xb91a8defd992362c": 1, + "0x3ec4c748c87ad9ca": 1, + "0x84164240ab16b192": 1, + "0xbb98cb9402ba545d": 1, + "0xa9ea7ae880948c18": 1, + "0x29fe0404761ace41": 1, + "0x3d8aebcebdbe99ca": 1, + "0xc80b94d085184262": 2, + "0xbb888a1dfcb9eda9": 1, + "0x522ce448e2d8c585": 1, + "0xa3ea1623f9abe995": 1, + "0x1fae328f15288106": 1, + "0xb002ec37462b192c": 1, + "0x926ffbfb04957436": 1, + "0x9065af6bf94e4abc": 1, + "0xfd3eaa49e1a362c": 1, + "0xb2579eb36d312766": 1, + "0xe67ef8d8f5b4f2e1": 1, + "0xdd1c7ea3d243f89b": 1, + "0x9fd93c2ffa6bfc05": 1, + "0xa5a0a7c29f83f8f6": 1, + "0x91b56db92d6c21": 1, + "0x32cfe3c4c08f6950": 1, + "0xf85cca62c1b658c8": 1, + "0x350f116baef70097": 1, + "0xced0b25057243e3b": 1, + "0xa13e2c4a53d2398d": 1, + "0xd8b273b886fef650": 1, + "0xe6a5bdba552419ce": 1, + "0x64dc896bb3992434": 1, + "0x6cd1f6f2cd0c93bc": 1, + "0x7615d3dbc18a7a2b": 1, + "0x9546798cb6cff64b": 1, + "0xd357a0bc3b652155": 1, + "0xe54553265d62c05a": 1, + "0xfdf34af2b5150709": 1, + "0x705bd784ea3c5027": 1, + "0x72c04a01020843b7": 1, + "0xab6a4412eb9d595c": 1, + "0x1efb5c4a867ee615": 1, + "0x308ee3a534c9534f": 1, + "0x8b0f98c080311214": 1, + "0xb0242af61464d12f": 1, + "0x1ff35a1432c9d533": 1, + "0x5eed6ed9384f6000": 1, + "0x2b301fab6e8fdb08": 1, + "0xac1e9136037734b3": 1, + "0x9b405890c8fdcf2b": 1, + "0x8a11960290e41d5b": 1, + "0x3dc053f80a774bac": 1, + "0xfc8e8dea8c61b65": 1, + "0xc64c03053a222cce": 1, + "0xd36b36debc9ea11f": 1, + "0x205b3af0eb725f17": 1, + "0xdf6121c41f338e73": 1, + "0xd1dd085b88a0149": 1, + "0x27d75102ae9277b8": 1, + "0xf971d36bf2f0414a": 1, + "0xc9e38d253d30fc9b": 1, + "0x79a4a4f78ac27b7e": 1, + "0x2be4c8c48b0c3f10": 2, + "0xfe29a330f7c31953": 2, + "0x6bf6d3c14f1fe07d": 1, + "0xc6b6033d3ba01f6f": 1, + "0xcf3479f87edbd318": 1, + "0x835f2b731172fba8": 1, + "0x95313e3fd63ec830": 2, + "0x1c611c420aeb1ac5": 1, + "0xbb04712b23bff546": 1, + "0x313129407b479d30": 1, + "0xe5dd0782ec8c9454": 1, + "0xcb6a6fe09e7b9b30": 1, + "0xbf61123966dcab8a": 1, + "0x3fa71a6f930e811": 1, + "0xb5814ebc4e52bc3f": 1, + "0xaef5561b1bdaeebf": 1, + "0x841afa0e41c1f6a9": 1, + "0xbef0a12024313052": 1, + "0x45f8c707e9f11932": 2, + "0xb2ecfce4d3083e62": 1, + "0x91117c8908313915": 1, + "0xf630d6eaf7febcac": 1, + "0x6a932dfe2da56901": 1, + "0xd55f3c1e5d5311fc": 1, + "0x259a856bade3e93": 7, + "0x1c8814c8500e0f7a": 1, + "0xd6bc3441ec446330": 1, + "0xf3d7510ba88792f": 2, + "0x2bbfab2cb2ef8122": 1, + "0xd4aacdcf04e66de7": 1, + "0xc76b63c0c64dd65e": 1, + "0xcd357ababff805c": 1, + "0x7d037c38c7545f8d": 1, + "0xb9d1d835b6771041": 1, + "0x54fc07a0475695e6": 1, + "0x93b90f57fbb63513": 1, + "0x8e65499c4303c683": 1, + "0x6b3dba43b3f14218": 1, + "0x191e876a42474cd4": 1, + "0x92a8780b544a1fe6": 1, + "0x50e450fb361df742": 1, + "0x5fc305ca6b822934": 1, + "0xc0e8f2671d802b4f": 1, + "0xa948dc88078ea5a9": 1, + "0xe64bfe0090014867": 1, + "0x6694213c6aa7dc86": 1, + "0x6cd88b4672770a4b": 1, + "0xe3abab1363130402": 1, + "0x37a53691d3aab1d7": 1, + "0xb45f52f679872731": 1, + "0x844e3d1f1886ba39": 1, + "0xcf411107daf285bb": 1, + "0x2e8af75c54836928": 2, + "0xb733d05d4a860112": 1, + "0xb398fcbfc3871d14": 1, + "0x6646f549da8be681": 1, + "0x13531a59837a3257": 1, + "0x7d54705dc133c944": 1, + "0xacdd775c940d08e6": 2, + "0x9b8c9318230bc2d8": 1, + "0x714d2869da4526f2": 1, + "0x19b30099684509e1": 1, + "0xcaaef227dbcfceba": 1, + "0xc18b0f471a803a60": 1, + "0xc4ed6bac365d0ef4": 1, + "0x5faa44144c9ce6b0": 2, + "0xe2f651b3adc22d03": 1, + "0x4c0d06012048b22e": 1, + "0x5a9b636b697bf447": 1, + "0x59ebcce261bb6254": 1, + "0xba2e78bab141e0e": 1, + "0xebd1dcb720f4c0a7": 1, + "0x206a651f81668e2": 1, + "0x8bfc7e19afb32cdd": 1, + "0x929c898aea840438": 1, + "0x5dc01e8755f53650": 1, + "0xca869a6344c10dc3": 1, + "0x14931ee38b8928cd": 1, + "0x19bfb5a7004386bd": 1, + "0x76efcab79557fb85": 1, + "0x5df9eedebf0486b0": 1, + "0xd81a183e6e9de72f": 1, + "0x761194f82c71b34f": 1, + "0x5e4d4e2aa5e5f699": 1, + "0x7e55022d48805d36": 1, + "0xe3f3ff0336335a64": 1, + "0x373a1a5c4d96bcd3": 3, + "0x350599eb8a34fdf4": 1, + "0xe13a946886440c80": 1, + "0xef1133d1f4df1d94": 1, + "0x1895cb7ca05cbadc": 1, + "0x62b18a8c6e7a78cc": 1, + "0x74ee5ed36e0c384f": 1, + "0x63c60d2731d77457": 3, + "0xce937d134efcd996": 1, + "0xf1788821396f04fc": 1, + "0x1f0b7317ae9ad73e": 1, + "0x285341ea4506fefd": 2, + "0xe95ed353f6440db4": 1, + "0xfb803457824e71b": 1, + "0x56cadf54e3347776": 1, + "0xa636be880bebd42e": 1, + "0xe1e69555ad78cd32": 1, + "0x7fa86e172446f8aa": 1, + "0x481a382f176e7903": 1, + "0x9c38918d7bc6d164": 1, + "0xcc7d67afcd30f180": 2, + "0x60bb3082129bacb6": 1, + "0x31ab602e578ba3a2": 1, + "0xe2b8b0f9ba3f68a": 1, + "0xa43bc4ad303050c5": 1, + "0x77c50d9bdae1fe82": 1, + "0x3ae23ba8a0193f51": 2, + "0xa2967a21b386b229": 1, + "0x7bbb41c5f61fa885": 1, + "0xf9eeb7ea2d60624b": 1, + "0x428f838325012c35": 3, + "0x5353ce0bfce52b6": 1, + "0x84bda41e7eb8400d": 2, + "0x34b44e11bfa7a6b6": 2, + "0xcf9e52c257342eb9": 1, + "0x3a78e97cefd5fc6c": 1, + "0xfa1fcf0fece6eb81": 1, + "0x3c4d4792bd86b1ba": 1, + "0x548b5798be295903": 2, + "0x8b5b56a3ecac10f9": 1, + "0x37f457c6f62e2071": 1, + "0x5bd71a93cba9f37c": 1, + "0x38cf3501d60d1563": 1, + "0xd5df008218ed9dae": 1, + "0x6c0a4e245c682d31": 1, + "0x8f4ea963c16d5070": 2, + "0x1a0996b8dd9c2f04": 1, + "0x592cf7c6f89c1938": 1, + "0xce0ad847bd8c8d82": 1, + "0x26917b45ef5cfc9f": 1, + "0xed80695d0c9be143": 1, + "0xf8042bcc853fd888": 2, + "0xbf998778640de57c": 1, + "0xa94b528804edcb80": 2, + "0x3c3328025e396a9": 2, + "0xf28de04a6cc941b1": 1, + "0xc95cf9043d0c56ab": 1, + "0x13a6da5fb681ff12": 1, + "0xc8ce229ed09040f2": 1, + "0xc925d7bbfd0d4092": 1, + "0xfa0580615339a803": 2, + "0x4e62111b5cf0878": 1, + "0x715db2d3525528e0": 1, + "0xf88b8a9ba327f105": 2, + "0x1b7bed06c42dc979": 1, + "0x22a2d79b8da12ee3": 1, + "0xe5bcabb75c2f8ee8": 1, + "0x35e5c3fea417ca1c": 1, + "0x6663c2a571e860fa": 1, + "0xde9bb443b23ab734": 1, + "0xc21818aad374dd81": 1, + "0xeaa8f361e2441dba": 1, + "0x4914b0d566629370": 1, + "0x5eda38a98861cdd6": 1, + "0xd1ac6bef67758b18": 1, + "0x2210086a579e2c1d": 1, + "0x9391c03c4e67dcb9": 1, + "0xc68d9200aa79dbdd": 1, + "0xe3f4058c30ee2947": 1, + "0x6cba2c96b280daf9": 1, + "0xc644f961b132b247": 2, + "0xd6616d27b426b31": 1, + "0x1c26b1fa09256ada": 3, + "0xe160b91755b201ae": 1, + "0x23f604d450a63d50": 1, + "0x7ca3976df2cd50ac": 2, + "0xa875c96058eec6ce": 1, + "0xb4d32cae41b35b5d": 1, + "0x24b451b46deb1c9a": 1, + "0xcafced5863a71e45": 1, + "0x2ad694391a087148": 1, + "0xc8c7b6ddfac9a328": 1, + "0x4a26be72817e2b78": 1, + "0xfe7b80c5add534a8": 1, + "0x5f61806e33381159": 1, + "0xc4059def6bea217c": 2, + "0x759d60ad90a43e47": 1, + "0xf5b3727aa6f620bd": 1, + "0x6b491144b9af0b97": 1, + "0x67a5d8e607de739d": 2, + "0x96f595b2a2d49235": 1, + "0xb31dc04fb3ade1f9": 3, + "0x2df1c40ed714b0c4": 1, + "0xc843b8f6ec2daa06": 1, + "0xbceda44e3b5a1fed": 1, + "0x8be678c07a6c7f78": 1, + "0xfb5a0097f57421eb": 2, + "0x924576d6e25baf5a": 1, + "0xfe8ebadd74f016aa": 1, + "0x7e58023533dafcea": 1, + "0xa864c31db1d658af": 2, + "0xbba91e1fc5c8090a": 1, + "0xbc274a58f6adb2ef": 1, + "0x51d26dc99a7b5fbe": 1, + "0x58e3a261023ae9e3": 3, + "0x353390477b275136": 1, + "0xc216591dc142bc17": 1, + "0x4915d8947974b194": 1, + "0xab4a184a0ee7a09": 2, + "0x12f5f3b56ad9e903": 1, + "0xe610482ea02f37d": 1, + "0x98633e310fa14c8f": 1, + "0xcf99149574d28cb": 1, + "0xc358720c54d8c465": 1, + "0xc332071bc05e155e": 1, + "0x1d6280d02a9c5100": 2, + "0x7e0123fe9c28d657": 1, + "0x4c577d2b82327a85": 1, + "0xb0e57714b0e9d1f9": 1, + "0x81cc5c3925a823e": 1, + "0x3497f23966ef166": 1, + "0x1c2fb0298b0f0090": 1, + "0x987e005ef825d521": 1, + "0x927bf4ad7504170e": 1, + "0x2ded51be82f01ddb": 1, + "0x55eff55df76a00fb": 1, + "0x3049a7d8c356211b": 1, + "0x5197faae99080532": 1, + "0x900ffa9785d1de19": 1, + "0xffb64b20e9e694da": 1, + "0x54689d1196981bea": 1, + "0x245037176a6917f2": 1, + "0xc6b833c44976caca": 1, + "0xfe856858a0600e8a": 1, + "0x3ab1277deaa7df65": 1, + "0x9a41920258e811b2": 1, + "0xaa33a75529ad87c2": 1, + "0xb5aaa2cbb41cc3b6": 1, + "0xdb111a82717bc63": 1, + "0xf08ad8c3a17cf35b": 1, + "0xbe5881fd2072606": 1, + "0xc2897dbbb82e9d45": 1, + "0x65ca155d25f01c86": 1, + "0x459797d353080bf6": 1, + "0x19c52b8f8acfa2d4": 1, + "0x3c7035a65a4e375b": 2, + "0xcc7cb2e79ec3bb17": 1, + "0x5fc5f0916ddf5001": 1, + "0x3e84721502ac109d": 1, + "0x3bc16fc650257647": 1, + "0xd7f20f2efe3d92c8": 1, + "0xc44d2b1068bbcecb": 1, + "0xfd005a15d667cbc5": 1, + "0xd328912bffbe80d0": 3, + "0xa45012b986d103": 1, + "0x192c3d50918de0de": 1, + "0xade05e3f3690da4e": 1, + "0xc5fa9871da26910d": 3, + "0x43e9091729ffc5a7": 1, + "0xfe9ae5f0b09dbdc0": 1, + "0x72fc8321d6b6fc12": 1, + "0x1482702d994fcb2e": 1, + "0x280b93c2b0315525": 1, + "0x3e862a280c319490": 1, + "0x6ec8df15ce83ec95": 1, + "0x4d53e6f60ab5ddaf": 1, + "0x4e732ea3c71eb64e": 1, + "0x1bd881bac20ac966": 1, + "0xd26afa6976afd9ac": 2, + "0xb37c85907f4021f9": 1, + "0x414b869c513fcf0": 1, + "0xc1cbc14bc17e6452": 4, + "0xd84469446ac2e2ba": 1, + "0x2b7ea4ca66bd8739": 1, + "0x63244d1e08f8b686": 1, + "0x2a076023c86c94a4": 4, + "0x9d2558ce0b570033": 2, + "0x7aa949dea8934096": 1, + "0xf2f2e9f583cf134b": 1, + "0xc08315048688f5ba": 1, + "0x5e2767f79b6123b9": 2, + "0x519522f66a04af8a": 1, + "0x5f6b7c0af1765698": 1, + "0xa5190cea49618ee1": 2, + "0x6c2fe5b6bbdaeeee": 1, + "0x2183c9cd6627554f": 1, + "0x7b399c224c9c6d3": 1, + "0x71e620b96fab7a6a": 1, + "0x425243e34dab0cf7": 1, + "0xe23024d11e517ba": 1, + "0xf833f6f64d0dca95": 1, + "0x1e4a575e7c7b5a11": 2, + "0x4b85c0f20dfeac2f": 1, + "0xe6033a6c3c4a6ff": 1, + "0x355ba6d21d74836f": 1, + "0x244e14401ecfbcd1": 3, + "0x5e29743bb6f3c0fb": 1, + "0x81b5c0ecd4b47f2c": 1, + "0x34ba9766656f3742": 1, + "0xbc2fb4478d1181c9": 1, + "0x6ef72e26814b67aa": 1, + "0xc28a9617cc887dba": 4, + "0xc0d429e2626dd675": 1, + "0x46a879460a552bab": 1, + "0x5e8131317bae5d26": 2, + "0xec34e34e925b3bec": 1, + "0x4e38acb3f91ea942": 1, + "0xb5320e082260e0d3": 1, + "0x3a192f6b153f0335": 1, + "0x8f459eb966702584": 1, + "0xfef49670bf1725dc": 1, + "0x259bd51040aad9df": 1, + "0xaf1e18ad87ae4aed": 1, + "0x46cbab1ed3a7f45a": 2, + "0xd84b9d66eee61039": 1, + "0x69562aaa9a004f4d": 1, + "0x36ad3f96ae84bc0c": 1, + "0x8ea7c2d5157defd0": 1, + "0x97ee68b205e49e0": 1, + "0x21833d698dc1ec35": 1, + "0xf9c8db272125787a": 1, + "0xb41e25dfd9b6eb73": 1, + "0x67659cd1215f0dba": 1, + "0xa84ba39ebee4cb19": 1, + "0xe761c7fc63b97282": 1, + "0xd705f497425ffb77": 1, + "0xc468c186e68ce0a0": 1, + "0x742244696c095a2f": 1, + "0xef768511e6cbdea5": 1, + "0xf03b383463dd6c97": 1, + "0x3f9e3bbbd665002": 1, + "0xbc70bdc3de349ce": 1, + "0xf23fcc6f1e365735": 1, + "0x8f724147d3f1d44d": 1, + "0xc50d3f62118c6ed2": 1, + "0x64d9d72e47f85c57": 1, + "0x5826642665432484": 1, + "0xbfd6bffd4752d532": 1, + "0xd66b9e5be40ec1c1": 1, + "0x62e7fdf441762f2b": 1, + "0xe7849cc824575e1e": 1, + "0xc241f920804dec4": 1, + "0xe5180c9da4824695": 1, + "0x2833c1fe85ed13b1": 1, + "0xa6c688ea702fb1a8": 1, + "0x107d73b2aa3f984a": 1, + "0xde77838ad12d1a8b": 1, + "0xd05555c3201c0435": 1, + "0x614e34ad087aace": 1, + "0x1512a62d4ab48b87": 1, + "0x8f6f7696a43630f6": 1, + "0xd3038f7d3fcb64cf": 1, + "0x9c216f4fe62be002": 1, + "0x21ec9c1e0a9d6551": 1, + "0xc4f53efd7a4a64b7": 1, + "0x50ac36801bfd5622": 1, + "0xd4e2a7ba4f094957": 1, + "0x68214356e6dfa537": 2, + "0x161df91672583cce": 1, + "0xc4273b12ed606d21": 1, + "0xba226974c520095": 1, + "0xfcb2926fc638b75d": 1, + "0x626ec7471b9925a5": 1, + "0x17a6c360cac76f9b": 1, + "0xcd0ea0fb11d49223": 1, + "0x5584d1c99ac1a391": 1, + "0xf220f18a4013d10a": 1, + "0xac8be3e76c3e79d4": 1, + "0x115d3688c45c01d4": 1, + "0x8d2831c933bd93f8": 1, + "0x33679cd84297056c": 1, + "0x5d6a2a9450b3f721": 1, + "0xfe84f02144dde097": 2, + "0xa369861c55523565": 1, + "0xf706e7b528240dc0": 1, + "0x157bf1ee152043bd": 1, + "0x1f944975a0f568b8": 2, + "0xee92296ce711df55": 1, + "0x160437eb003118e2": 1, + "0x26d616835bf3e654": 1, + "0x9a22118dd7299aa6": 1, + "0x735b6e5d68d5c8da": 1, + "0x8dd03dbaa12b534a": 1, + "0x2718b86049f80baf": 1, + "0x2308ac4ec96c9f77": 1, + "0x1cff1f3bfdf279d1": 1, + "0xff415a7e2777aa34": 1, + "0xb40ac91c23d7e0be": 1, + "0x44454836768e46f7": 1, + "0x4504d175b70b98d2": 1, + "0xa911af5e8efcae95": 1, + "0x47c6a9753b6cbae3": 2, + "0x330ed3336e505ff6": 1, + "0xb49a075122ae1cb9": 1, + "0x8e270d663e61024a": 1, + "0xc840530fc2c4ea96": 1, + "0xa11116f1dd43883b": 1, + "0xf139336be7965c90": 1, + "0xe47319e05ca2cec7": 1, + "0xbf43b9b3378908c1": 1, + "0x8a55057b7af2bdd4": 1, + "0x12b261385a59cdf5": 1, + "0x5757586a6904c7a": 1, + "0x2c470d4f87b89579": 1, + "0xcae0d092e005211": 1, + "0xd2c35c489cfc7827": 1, + "0x895ce4a70ab15031": 1, + "0x260c3c174e02fc3c": 1, + "0x28bf8c390d610f15": 1, + "0x68ba28eb88cd8697": 1, + "0xe8235911236e4004": 1, + "0xb2a5137e7a8f2726": 2, + "0xeb1e8b589b1cdac9": 1, + "0x9a23511695313efe": 2, + "0x28a1ffc92585d476": 1, + "0x752dc481a7cca247": 1, + "0xc73f1e4d0606db26": 2, + "0x3633202ecaf2ad00": 1, + "0x24e0a60ad2c9635f": 1, + "0x164612db04387d9": 1, + "0x46102946da8cd920": 1, + "0x150a9c1026f29e4c": 1, + "0x4bb56893d5e5a5b4": 1, + "0xbfc32e33a6e64291": 2, + "0xd9df95ed75229b4b": 1, + "0x257dc9e4f3b6c3d1": 1, + "0x4da3fea10a9a0427": 1, + "0xdb71b0308596faad": 1, + "0x187b7ac34fc66640": 1, + "0xfb62b33800f564f9": 1, + "0x4559254ff4a00561": 1, + "0x8c92f907a913e7f3": 1, + "0x943c955143dfc3c7": 1, + "0xb3dbf06232a08a3e": 1, + "0x18bec2e87583ff9c": 1, + "0x4f3577bc00c69e30": 1, + "0xac5ed39cd90f2308": 1, + "0xe44bfac627ad0fd6": 1, + "0xc08718efb8f5d382": 1, + "0x585c7e1104d3590": 1, + "0xe3201f971462c28": 1, + "0x7e65ce06ae8838a7": 1, + "0x558884556a83c5b4": 1, + "0x2ae90d4fa722b2c3": 1, + "0xe1d4f2c31b83c508": 1, + "0x62d377498406c9e3": 1, + "0xe9bf5061a58de427": 1, + "0x45b77953b0f7ab38": 1, + "0x5ac3d5881dfa09c4": 2, + "0xb5629960c17ae1b": 1, + "0xf60f799d62498bc5": 1, + "0x229056fdaed47012": 1, + "0x2182cc23b34f2c47": 1, + "0x622bb975f880f919": 1, + "0x5ac238201c9dd5cb": 1, + "0xe721782bd837e7db": 1, + "0xc801cb00107d4cf8": 1, + "0x20b109153d8a0854": 1, + "0x229fa1a49da861b6": 1, + "0xed6d617ac9225aa8": 1, + "0x4cc7737bfd711d38": 1, + "0xa1b5530728d89500": 1, + "0xf93c0dc948c74b3f": 3, + "0x505928746b6cb903": 1, + "0xed81e2925bf6be6b": 1, + "0x949304df4d089880": 1, + "0xd19fbfbab127e18": 1, + "0xbe867932d1ddfe5e": 1, + "0xbbf45259456abbaf": 1, + "0x147daf31db257abb": 1, + "0x1b0f9ca8b4f67ac1": 1, + "0x9048d5331991a69f": 1, + "0x2391cbcd9172fe94": 1, + "0x42e0136fc5fd97da": 1, + "0xaf3fd2628553289": 1, + "0xf728bc3e20f482fc": 1, + "0x6d378101acf557a8": 1, + "0x1c8f25ccce20b6cc": 1, + "0xd1925a369a0ae5b1": 1, + "0x30c87daae9133b8a": 1, + "0xf094c8c222c06293": 1, + "0xc7a6ce90d5c279f8": 2, + "0x5cf7c95b7e526fcc": 1, + "0xe83b894740fd2b2c": 1, + "0x9af657241dc9732": 1, + "0xfe25dd0b98d392d2": 1, + "0x936e5ed3c0cc4cb6": 1, + "0x714028db04eaf31": 1, + "0x605fa708c20fdae2": 1, + "0xb0539d619279b0fb": 1, + "0x5c3a028250127c32": 1, + "0x4f186cbb3cdfe251": 1, + "0xcd8f8343dc349893": 1, + "0xf85077cc4522e007": 1, + "0xf28cdbd6bd539dca": 1, + "0xd9fd41c902031022": 1, + "0x5dea98e66435fc04": 1, + "0x7ffff26bb302618d": 1, + "0x57f297f4c43b94b1": 1, + "0x653b5682b057032d": 2, + "0xf5b37357d928114e": 1, + "0xbfca5222d3d4e43d": 1, + "0xb78cc0671af464bb": 1, + "0x12d72966409a78e4": 1, + "0x857b180ebc91909": 1, + "0x51bf5e4f080bdd73": 1, + "0xdc740ebf501ad20c": 1, + "0xaaf0a3a85f086e9f": 1, + "0xd8e8bed7f941f2ff": 1, + "0xf4f769cd9cc6e03f": 1, + "0xc6f3efcab9cf59f6": 2, + "0x5be878db5704b6f3": 1, + "0xafae1073fcbd829c": 1, + "0x23a544a02ac851d5": 1, + "0x3e0bcf205d92aeea": 1, + "0xa848b03882114966": 1, + "0xdf70f8ec96378790": 1, + "0x366ebe740866273f": 2, + "0x87efa7db36459d28": 1, + "0x7dc89bea2920882b": 1, + "0xab1ece2d23b76d2a": 1, + "0x993633c789547f89": 1, + "0x4173ca140cca9aa": 1, + "0xfbdf3938f3a13917": 1, + "0xf605dec92677cf11": 1, + "0xa0147dea23ad8d30": 1, + "0xf28477c7f1d2aecb": 1, + "0x9c260d308ea193a6": 1, + "0x2bc4eedcd43cf66c": 1, + "0xb4fe720b01f7323d": 1, + "0xf30593d935268571": 1, + "0x7ed41f381b15593e": 1, + "0xa7f77ae3274cd0e": 1 +} \ No newline at end of file diff --git a/models/LoRA/mg_7lambda_negative.safetensors b/models/LoRA/mg_7lambda_negative.safetensors new file mode 100644 index 0000000000000000000000000000000000000000..4b4fc2d5fac035175869f7db9ba77589348f33e2 --- /dev/null +++ b/models/LoRA/mg_7lambda_negative.safetensors @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:872ed1f75559f3d8aa8e3e7236625d616368af72d84a297539c011531f1185ab +size 228464628 diff --git a/pressets/mg_cade25.cfg b/pressets/mg_cade25.cfg new file mode 100644 index 0000000000000000000000000000000000000000..5b0c277ea4d7c5256dbf971156a0de75f52214bc --- /dev/null +++ b/pressets/mg_cade25.cfg @@ -0,0 +1,467 @@ +# MagicNodes CADE 2.5 presets + +[step1] +# core +seed: 0 +control_after_generate: randomize +steps: 30 +cfg: 8.0 +denoise: 1.0 +sampler_name: ddim +scheduler: MGHybrid +iterations: 2 +steps_delta: -1.04 +cfg_delta: 0.03 +denoise_delta: 0.28 + +# toggles +apply_sharpen: true +apply_upscale: true +apply_ids: true +clip_clean: true +latent_compare: true + +# detail controls +#ids_strength: 0.35 +ids_strength: 0.85 +upscale_method: lanczos +scale_by: 1.0 +scale_delta: -0.15 +noise_offset: 0.29 +threshold: 1.0 +#Sharpnes_strenght: 0.075 +Sharpnes_strenght: 0.025 +accumulation: fp32+fp32 + +# reference clean +reference_clean: true +ref_preview: 512 +ref_threshold: 0.02 +ref_cooldown: 2 + + +# guidance +guidance_mode: ZeResFDG +rescale_multiplier: 0.72 +momentum_beta: 0.12 +cfg_curve: 1.0 +perp_damp: 0.80 + +# NAG +use_nag: true +nag_scale: 4.0 +nag_tau: 2.5 +nag_alpha: 0.25 + +# zero init +use_zero_init: false +zero_init_steps: 0 + +# FDG / ZE thresholds +fdg_low: 0.25 +fdg_high: 0.7 +fdg_sigma: 1.20 +ze_res_zero_steps: 10 +ze_adaptive: true +ze_r_switch_hi: 0.85 +ze_r_switch_lo: 0.25 +fdg_low_adaptive: true +fdg_low_min: 0.45 +fdg_low_max: 0.85 +fdg_ema_beta: 0.45 + + +# Mahiro+/Muse blend +muse_blend: true +muse_blend_strength: 0.29 + +# eps scale +eps_scale_enable: true +eps_scale: 0.0025 + +# CLIPSeg +clipseg_enable: false +clipseg_text: hand, face +clipseg_preview: 512 +clipseg_threshold: 0.30 +clipseg_blur: 9.5 +clipseg_dilate: 5 +clipseg_gain: 0.6 +clipseg_blend: fuse +clipseg_ref_gate: true +clipseg_ref_threshold: 0.005 + +# polish +polish_enable: false +polish_keep_low: 0.40 +polish_edge_lock: 0.20 +polish_sigma: 1.0 +polish_start_after: 1 +polish_keep_low_ramp: 0.20 + +# mid-frequency stabilizer (hands/objects scale) +midfreq_enable: true +midfreq_gain: 0.10 +midfreq_sigma_lo: 0.90 +midfreq_sigma_hi: 2.00 + +# QSilk-AQClip-Lite (adaptive latent clipping) +aqclip_enable: true +aq_tile: 32 +aq_stride: 16 +aq_alpha: 2.0 + +aq_attn: true + +[step2] +# core +seed: 0 +control_after_generate: randomize +steps: 30 +cfg: 7 +denoise: 0.60 +sampler_name: ddim +scheduler: MGHybrid +iterations: 2 +steps_delta: 5.00 +cfg_delta: 0.03 +denoise_delta: 0.0500 + +# toggles +apply_sharpen: false +apply_upscale: true +apply_ids: true +clip_clean: true +latent_compare: true + +# detail controls +ids_strength: 0.60 +upscale_method: lanczos +scale_by: 1.20 +scale_delta: 0.15 +noise_offset: 0.09 +threshold: 1.000 +Sharpnes_strenght: 0.081 +accumulation: fp32+fp32 + +# reference clean +reference_clean: true +ref_preview: 512 +ref_threshold: 0.020 +ref_cooldown: 2 + + +# guidance +guidance_mode: ZeResFDG +#rescale_multiplier: 0.75 +rescale_multiplier: 0.95 +momentum_beta: 0.15 +cfg_curve: 0.85 +perp_damp: 0.80 + +# NAG +use_nag: true +nag_scale: 3.0 +nag_tau: 2.50 +nag_alpha: 0.25 + +# zero init +use_zero_init: false +zero_init_steps: 0 + +# FDG / ZE thresholds +fdg_low: 0.55 +fdg_high: 0.7 +fdg_sigma: 1.10 +ze_res_zero_steps: 12 +ze_adaptive: true +ze_r_switch_hi: 0.85 +ze_r_switch_lo: 0.25 +fdg_low_adaptive: true +fdg_low_min: 0.45 +fdg_low_max: 0.85 +fdg_ema_beta: 0.45 + + +# Mahiro+/Muse blend +muse_blend: true +muse_blend_strength: 0.24 + +# eps scale +eps_scale_enable: true +eps_scale: 0.0025 + +# CLIPSeg +clipseg_enable: true +clipseg_text: hand, feet, face +clipseg_preview: 512 +clipseg_threshold: 0.1 +clipseg_blur: 11.5 +clipseg_dilate: 5 +clipseg_gain: 0.35 +clipseg_blend: fuse +clipseg_ref_gate: true +clipseg_ref_threshold: 0.005 + +# polish +polish_enable: false +polish_keep_low: 0.40 +polish_edge_lock: 0.20 +polish_sigma: 1.0 +polish_start_after: 1 +polish_keep_low_ramp: 0.20 + +# mid-frequency stabilizer (hands/objects scale) +midfreq_enable: true +#midfreq_gain: 0.15 +midfreq_gain: 0.65 +midfreq_sigma_lo: 0.90 +midfreq_sigma_hi: 2.10 + +# QSilk-AQClip-Lite (adaptive latent clipping) +aqclip_enable: true +aq_tile: 32 +aq_stride: 16 +aq_alpha: 2.0 + +aq_attn: true + +[step3] +# core +seed: 0 +control_after_generate: randomize +steps: 25 +cfg: 7.0 +denoise: 0.60 +sampler_name: ddim +scheduler: MGHybrid +iterations: 2 +steps_delta: 5.00 +cfg_delta: 0.03 +denoise_delta: 0.0500 + +# toggles +apply_sharpen: false +apply_upscale: true +apply_ids: true +clip_clean: true +latent_compare: true + +# detail controls +ids_strength: 0.25 +upscale_method: lanczos +scale_by: 1.30 +scale_delta: 0.12 +noise_offset: 0.10 +threshold: 1.000 +#0.135 +Sharpnes_strenght: 0.185 +accumulation: fp32+fp32 + +# reference clean +reference_clean: true +ref_preview: 512 +ref_threshold: 0.020 +ref_cooldown: 2 + + +# guidance +guidance_mode: ZeResFDG +rescale_multiplier: 1.10 +momentum_beta: 0.37 +cfg_curve: 0.65 +perp_damp: 0.95 + +# NAG +use_nag: true +nag_scale: 4.0 +nag_tau: 2.50 +nag_alpha: 0.25 + +# zero init +use_zero_init: false +zero_init_steps: 0 + +# FDG / ZE thresholds +fdg_low: 0.55 +fdg_high: 0.7 +fdg_sigma: 1.10 +ze_res_zero_steps: 12 +ze_adaptive: true +ze_r_switch_hi: 0.85 +ze_r_switch_lo: 0.25 +fdg_low_adaptive: true +fdg_low_min: 0.45 +fdg_low_max: 0.85 +fdg_ema_beta: 0.45 + + +# Mahiro+/Muse blend +muse_blend: true +muse_blend_strength: 0.29 + +# eps scale +eps_scale_enable: true +eps_scale: 0.0025 + +# CLIPSeg +clipseg_enable: true +clipseg_text: hand, feet, face +clipseg_preview: 512 +clipseg_threshold: 0.1 +clipseg_blur: 11.5 +clipseg_dilate: 5 +clipseg_gain: 0.35 +clipseg_blend: fuse +clipseg_ref_gate: true +clipseg_ref_threshold: 0.005 + +# polish +polish_enable: false +polish_keep_low: 0.40 +polish_edge_lock: 0.10 +polish_sigma: 1.0 +polish_start_after: 1 +polish_keep_low_ramp: 0.10 + +# mid-frequency stabilizer (hands/objects scale) +midfreq_enable: true +#midfreq_gain: 0.20 +#midfreq_sigma_lo: 0.95 +#midfreq_sigma_hi: 2.20 + +midfreq_gain: 0.62 +midfreq_sigma_lo: 0.50 +midfreq_sigma_hi: 1.2 + +# QSilk-AQClip-Lite (adaptive latent clipping) +aqclip_enable: true +aq_tile: 48 +aq_stride: 32 +aq_alpha: 1.6 + +aq_attn: false +# KV pruning (self-attention speedup) +kv_prune_enable: true +kv_keep: 0.80 +kv_min_tokens: 256 + +#kv_keep: 0.85 +#kv_min_tokens: 128 +[step4] +# core +seed: 0 +control_after_generate: randomize +steps: 25 +cfg: 7.0 +#0.75 +denoise: 0.55 +sampler_name: ddim +scheduler: MGHybrid +iterations: 2 +steps_delta: 5.00 +cfg_delta: 0.03 +denoise_delta: 0.0500 + +# toggles +apply_sharpen: true +apply_upscale: true +apply_ids: true +clip_clean: true +latent_compare: true + +# detail controls +ids_strength: 0.30 +upscale_method: lanczos +scale_by: 1.58 +scale_delta: 0.04 +noise_offset: 0.02 +threshold: 1.000 +Sharpnes_strenght: 0.185 +accumulation: fp32+fp32 + +# reference clean +reference_clean: true +ref_preview: 512 +ref_threshold: 0.200 +ref_cooldown: 2 + + +# guidance +guidance_mode: ZeResFDG +rescale_multiplier: 0.95 +momentum_beta: 0.15 +cfg_curve: 0.80 +perp_damp: 0.75 + +# NAG +use_nag: true +nag_scale: 4.0 +nag_tau: 2.50 +nag_alpha: 0.25 + +# zero init +use_zero_init: false +zero_init_steps: 0 + +# FDG / ZE thresholds +fdg_low: 0.25 +fdg_high: 0.7 +fdg_sigma: 1.20 +ze_res_zero_steps: 12 +ze_adaptive: true +ze_r_switch_hi: 0.85 +ze_r_switch_lo: 0.25 +fdg_low_adaptive: true +fdg_low_min: 0.45 +fdg_low_max: 0.85 +fdg_ema_beta: 0.45 + + +# Mahiro+/Muse blend +muse_blend: true +muse_blend_strength: 0.25 + +# eps scale +eps_scale_enable: true +eps_scale: 0.0025 + +# CLIPSeg +clipseg_enable: true +clipseg_text: hand, feet, face +clipseg_preview: 512 +clipseg_threshold: 0.1 +clipseg_blur: 11.5 +clipseg_dilate: 5 +clipseg_gain: 0.35 +clipseg_blend: fuse +clipseg_ref_gate: true +clipseg_ref_threshold: 0.005 +#seg_use_cf_edges: false + +# polish +polish_enable: false +polish_keep_low: 0.40 +polish_edge_lock: 0.10 +polish_sigma: 1.0 +polish_start_after: 2 +polish_keep_low_ramp: 0.10 + +# mid-frequency stabilizer (hands/objects scale) +midfreq_enable: true +midfreq_gain: 0.72 +midfreq_sigma_lo: 0.50 +midfreq_sigma_hi: 1.2 + +# QSilk-AQClip-Lite (adaptive latent clipping) +aqclip_enable: true +aq_tile: 64 +aq_stride: 8 +aq_alpha: 2.0 + +aq_attn: true +# KV pruning (self-attention speedup) +kv_prune_enable: false +kv_keep: 0.95 +kv_min_tokens: 256 + diff --git a/pressets/mg_controlfusion.cfg b/pressets/mg_controlfusion.cfg new file mode 100644 index 0000000000000000000000000000000000000000..ea6c892ae29a04e690211c6ca8c99cb9adcc07b3 --- /dev/null +++ b/pressets/mg_controlfusion.cfg @@ -0,0 +1,161 @@ +# MagicNodes ControlFusion presets + +[step2] +# depth +enable_depth: true +depth_model_path: $(ROOT)/depth-anything/depth_anything_v2_vitl.pth +depth_resolution: 1024 + +# pyra (edges) +enable_pyra: true +pyra_low: 109 +pyra_high: 215 +pyra_resolution: 1024 +edge_thin_iter: 0 +edge_alpha: 0.75 +edge_boost: 0.10 +smart_tune: false +smart_boost: 0.62 + +# blend & strengths +blend_mode: normal +blend_factor: 0.35 +strength_pos: 0.47 +strength_neg: 0.91 + +# schedule window +start_percent: 0.000 +end_percent: 1.000 +preview_res: 1024 +mask_brightness: 1.00 +preview_show_strength: true +preview_strength_branch: max + +# misc toggles +hires_mask_auto: true +apply_to_uncond: false +stack_prev_control: false +split_apply: false + +# split timings +edge_start_percent: 0.000 +edge_end_percent: 0.600 +depth_start_percent: 0.000 +depth_end_percent: 1.000 + +# multipliers & shape +edge_strength_mul: 0.03 +depth_strength_mul: 0.03 +edge_width: 1.00 +edge_smooth: 0.60 +edge_single_line: true +edge_single_strength: 0.60 +edge_depth_gate: true +edge_depth_gamma: 0.20 + +[step3] +# depth +enable_depth: true +depth_model_path: $(ROOT)/depth-anything/depth_anything_v2_vitl.pth +depth_resolution: 1024 + +# pyra (edges) +enable_pyra: true +pyra_low: 123 +pyra_high: 255 +pyra_resolution: 2048 +edge_thin_iter: 0 +edge_alpha: 0.20 +edge_boost: 0.15 +smart_tune: false +smart_boost: 0.84 + +# blend & strengths +blend_mode: normal +blend_factor: 0.300 +strength_pos: 0.95 +strength_neg: 0.90 + +# schedule window +start_percent: 0.000 +end_percent: 0.600 +preview_res: 1024 +mask_brightness: 1.00 +preview_show_strength: true +preview_strength_branch: avg + +# misc toggles +hires_mask_auto: true +apply_to_uncond: false +stack_prev_control: false +split_apply: true + +# split timings +edge_start_percent: 0.000 +edge_end_percent: 0.400 +depth_start_percent: 0.000 +depth_end_percent: 0.800 + +# multipliers & shape +edge_strength_mul: 0.50 +depth_strength_mul: 0.45 +edge_width: 1.00 +edge_smooth: 0.60 +edge_single_line: true +edge_single_strength: 0.60 +edge_depth_gate: true +edge_depth_gamma: 0.20 + +[step4] +# depth +enable_depth: true +depth_model_path: $(ROOT)/depth-anything/depth_anything_v2_vitl.pth +depth_resolution: 1024 + +# pyra (edges) +enable_pyra: true +pyra_low: 115 +pyra_high: 255 +pyra_resolution: 2048 +edge_thin_iter: 0 +edge_alpha: 0.05 +edge_boost: 0.00 +smart_tune: false +smart_boost: 0.56 + +# blend & strengths +blend_mode: normal +blend_factor: 0.300 +strength_pos: 0.90 +strength_neg: 1.00 + +# schedule window +start_percent: 0.000 +end_percent: 0.450 +preview_res: 1024 +mask_brightness: 1.00 +preview_show_strength: true +preview_strength_branch: avg + +# misc toggles +hires_mask_auto: true +apply_to_uncond: false +stack_prev_control: false +split_apply: true + +# split timings +edge_start_percent: 0.000 +#0.350 +edge_end_percent: 0.450 +depth_start_percent: 0.000 +depth_end_percent: 1.000 + +# multipliers & shape +edge_strength_mul: 0.50 +depth_strength_mul: 0.45 +edge_width: 1.00 +edge_smooth: 0.60 +edge_single_line: true +edge_single_strength: 0.60 +edge_depth_gate: true +edge_depth_gamma: 0.90 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..ea354f98fedb251cd520c7f65b8c5817970e8ad7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,21 @@ +# Core dependencies for MagicNodes (beyond vanilla ComfyUI) + +# Text/image models (CLIPSeg in CADE) +transformers>=4.36,<5 + +# ONNX runtime removed (detectors disabled) + +# Computer vision / image IO +opencv-contrib-python>=4.8,<5 +Pillow>=9.5,<11 + +# Math/filters (IDS uses SciPy when available) +scipy>=1.10 + +# Old Attention acceleration (SageAttention); code falls back gracefully if missing +# sageattention + +# Optional extras (uncomment if needed) + +# Depth-Anything auxiliary fallback (not required when using the vendored implementation): +# controlnet-aux>=0.0.8 diff --git a/scripts/check_sageattention.bat b/scripts/check_sageattention.bat new file mode 100644 index 0000000000000000000000000000000000000000..67df3b53d97e0d228b9d979c6af3617f1486e499 --- /dev/null +++ b/scripts/check_sageattention.bat @@ -0,0 +1,7 @@ +@echo off +setlocal +set PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe +"%PS%" -ExecutionPolicy Bypass -File "%~dp0\check_sageattention.ps1" %* +echo. +pause +endlocal diff --git a/scripts/check_sageattention.ps1 b/scripts/check_sageattention.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..2746842bc3c3c36b57d473c7f9d27055c06dbc9a --- /dev/null +++ b/scripts/check_sageattention.ps1 @@ -0,0 +1,352 @@ +<# + SageAttention Environment Checker/Installer (Windows, PowerShell) + + - Detects Python, Torch, CUDA, GPU, and SageAttention version + - Warns about local shadow files (sageattention.py in Comfy root) + - Offers to install/upgrade SageAttention to 2.2.0 + - If no wheel is available, can build from source (needs MSVC + CUDA Toolkit) + + Usage: + powershell -ExecutionPolicy Bypass -File scripts\check_sageattention.ps1 + or run scripts\check_sageattention.bat +#> + +param( + [switch]$AutoYes, + [switch]$ForceSa2Source +) + +function Write-Section($t){ Write-Host "`n=== $t ===" -ForegroundColor Cyan } +function Ask-YesNo($q){ + if($AutoYes){ return $true } + $r = Read-Host "$q [y/N]"; return $r -match '^(?i:y|yes)$' +} + +function Get-Python(){ + $cands = @('python','py -3','py') + foreach($p in $cands){ try{ $v = & $p -c "import sys;print(sys.executable)" 2>$null; if($LASTEXITCODE -eq 0 -and $v){ return @{ exe=$p; path=$v.Trim() } } }catch{} } + return $null +} + +function Py-Exec($pyExe, $code){ + # Write code to a temporary .py to avoid complex quoting issues on Windows + $tmp = [System.IO.Path]::GetTempFileName() + $pyf = [System.IO.Path]::ChangeExtension($tmp, '.py') + Set-Content -Path $pyf -Value $code -Encoding UTF8 + try { $out = & $pyExe $pyf } finally { Remove-Item -ErrorAction SilentlyContinue $tmp, $pyf } + return $out +} + +function Invoke-Quiet($file, $argList, $label){ + $logOut = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetTempFileName(), '.out.log') + $logErr = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetTempFileName(), '.err.log') + try { + if([string]::IsNullOrWhiteSpace($argList)){ throw "ArgumentList is empty for $file" } + $p = Start-Process -FilePath $file -ArgumentList $argList -NoNewWindow -PassThru ` + -RedirectStandardOutput $logOut -RedirectStandardError $logErr + $spinner = @('|','/','-','\') + $i = 0 + while(-not $p.HasExited){ + Write-Host -NoNewline ("`r{0} {1}" -f $spinner[$i % $spinner.Count], $label) + Start-Sleep -Milliseconds 150 + $i++ + } + try { $p.Refresh() } catch {} + $exitCode = 1 + try { $exitCode = [int]$p.ExitCode } catch { $exitCode = 1 } + if($exitCode -eq 0){ + Write-Host ("`r{0} ... done " -f $label) -ForegroundColor Green + } else { + Write-Warning ("Failed: {0} (exit {1})" -f $label, $exitCode) + Write-Host "---- build tail ----" -ForegroundColor DarkYellow + if(Test-Path $logOut){ Get-Content $logOut -Tail 40 | ForEach-Object { Write-Host $_ -ForegroundColor DarkYellow } } + if(Test-Path $logErr){ Get-Content $logErr -Tail 40 | ForEach-Object { Write-Host $_ -ForegroundColor DarkYellow } } + Write-Host "--------------------" -ForegroundColor DarkYellow + } + return ($exitCode -eq 0) + } finally { + if(Test-Path $logOut){ Remove-Item -ErrorAction SilentlyContinue $logOut } + if(Test-Path $logErr){ Remove-Item -ErrorAction SilentlyContinue $logErr } + } +} + +function Get-TorchInfo($pyExe){ + $code = @' +try: + import torch + cuda = getattr(torch.version, "cuda", None) + is_cuda = torch.cuda.is_available() + name = torch.cuda.get_device_name(0) if is_cuda else "" + cc = ".".join(map(str, torch.cuda.get_device_capability(0))) if is_cuda else "" + print("|".join([torch.__version__, str(cuda or ""), "1" if is_cuda else "0", name.replace("|"," "), cc])) +except Exception: + print("") +'@ + $out = Py-Exec $pyExe $code + if(-not $out){ return @{ has_torch=$false } } + $p = $out -split '\|' + if($p.Length -lt 3){ return @{ has_torch=$false } } + return @{ has_torch=$true; torch=$p[0]; cuda=$p[1]; is_cuda=($p[2] -eq '1'); device_name=($p[3] | ForEach-Object { $_ }); cc=($p[4] | ForEach-Object { $_ }) } +} + +function Get-SageVersion($pyExe){ + $code = @' +try: + import importlib, importlib.util + mod = None + for name in ("SageAttention","sageattention"): + try: + if importlib.util.find_spec(name) is not None: + mod = name; break + except Exception: + pass + if not mod: + print("") + else: + try: + import importlib.metadata as md + ver = md.version(mod) + except Exception: + ver = "" + print(f"{mod}|{ver}") +except Exception: + print("") +'@ + $out = Py-Exec $pyExe $code + $p = $out -split '\|' + return @{ module=($p[0]); version=($p[1]) } +} + +function Test-ShadowFile(){ + # Look for a local sageattention.py that could shadow the package + $roots = @((Get-Location).Path) + # walk up to 3 parents + $d = Get-Item . + for($i=0;$i -lt 3;$i++){ $d = $d.PSParentPath; if(-not $d){ break }; $roots += $d } + foreach($r in $roots){ $f = Join-Path $r 'sageattention.py'; if(Test-Path $f){ return $f } } + return $null +} + +Write-Section "Python" +$py = Get-Python +if(-not $py){ Write-Error "Python not found on PATH."; exit 1 } +Write-Host ("Using Python: {0} ({1})" -f $py.exe, $py.path) + +Write-Section "Torch / CUDA / GPU" +$ti = Get-TorchInfo $py.path +if(-not $ti.has_torch){ Write-Warning "PyTorch not found: $($ti.err)" } else { + $ccdisp = if($ti.cc){ "sm_{0}" -f ($ti.cc -replace '\.','') } else { "-" } + Write-Host ("torch {0}, cuda {1}, cuda_available={2}, gpu='{3}', cc={4}" -f $ti.torch, $ti.cuda, $ti.is_cuda, $ti.device_name, $ccdisp) +} + +Write-Section "SageAttention" +$target = 'SA2 (Attn2++)' +Write-Host ("Build target: {0}" -f $target) -ForegroundColor Green +$sv = Get-SageVersion $py.path +if($sv.module){ + $svver = if($null -ne $sv.version -and $sv.version -ne ''){ $sv.version } else { 'unknown' } + Write-Host ("found module: {0} version: {1}" -f $sv.module, $svver) +} else { + Write-Host "not installed" +} + +$shadow = Test-ShadowFile +if($shadow){ Write-Warning "Local file shadows package: $shadow"; if(Ask-YesNo "Rename to sageattention.py.disabled now?"){ + Rename-Item -Path $shadow -NewName 'sageattention.py.disabled' -Force + Write-Host "Renamed." + } +} + +$needInstall = $false +$wantSA3 = $false # SA3 временно отключён +# Detect Windows and mark SA3 unsupported for now +$isWindows = ($env:OS -eq 'Windows_NT') +# Check if SA3 already present +$sa3check = @' +import importlib.util +print(importlib.util.find_spec("sageattn3") is not None) +'@ +$sa3present = $false +if($wantSA3){ + try { + $sa3present = ((Py-Exec $py.path $sa3check).Trim() -eq 'True') + } catch { $sa3present = $false } +} + +if($wantSA3){ + $needInstall = -not $sa3present +} else { + if(-not $sv.module){ $needInstall = $true } + else { try{ $ver=[Version]($sv.version -replace '[^0-9\.]',''); if($ver.Major -lt 2 -or ($ver.Major -eq 2 -and $ver.Minor -lt 2)){ $needInstall=$true } }catch{ $needInstall=$true } } +} + +if(-not $needInstall){ + Write-Host ("SageAttention present (target {0}) — nothing to do." -f $target) -ForegroundColor Green; exit 0 +} + +if($wantSA3){ + if($isWindows){ + Write-Warning "SageAttention 3 (Blackwell) is not supported on Windows currently. Falling back to SageAttention 2.2.x." + $wantSA3 = $false + } +} + +if($wantSA3){ + if(-not (Ask-YesNo "Install SageAttention SA3 (Blackwell) from source now?")){ Write-Host "Aborted by user."; exit 0 } + Write-Section "Installing (from source)" + $null = Invoke-Quiet $py.path "-m pip install -U pip setuptools wheel" "Installing SageAttention SA3, please wait a few minutes" + # Ensure toolchain for source build + Write-Section "Toolchain check" + $hasCL = ($null -ne (Get-Command cl.exe -ErrorAction SilentlyContinue)) + $hasNVCC = ($null -ne (Get-Command nvcc.exe -ErrorAction SilentlyContinue)) + if(-not $hasCL -or -not $hasNVCC){ + Write-Warning "MSVC cl or CUDA nvcc not found. Install MSVC Build Tools 2022 and CUDA Toolkit matching your torch (CUDA $($ti.cuda))." + exit 1 + } + # Set CUDA arch list (e.g., 12.0 for Blackwell) + if($ti.is_cuda -and $ti.cc){ $env:TORCH_CUDA_ARCH_LIST = $ti.cc } + Write-Section "Building SA3 from source" + $null = Invoke-Quiet $py.path "-m pip install -U packaging cmake ninja" "toolchain python deps" + $sa3built = $false + # Install SA3 specifically from subdirectory + $env:GIT_TERMINAL_PROMPT = "0" + if($sa3present){ $null = Invoke-Quiet $py.path "-m pip uninstall -y sageattn3" "uninstall SA3 (old)" } + if(Invoke-Quiet $py.path "-m pip install -U --force-reinstall --no-build-isolation --no-cache-dir git+https://github.com/thu-ml/SageAttention@main#subdirectory=sageattention3_blackwell" "install SA3 from git subdir (main)"){ $sa3built = $true } + if(-not $sa3built){ + # Try tags just in case + $tags = @('v2.2.1','v2.2.0','v2.2') + foreach($t in $tags){ + if(Invoke-Quiet $py.path ("-m pip install -U --force-reinstall --no-build-isolation --no-cache-dir git+https://github.com/thu-ml/SageAttention@{0}#subdirectory=sageattention3_blackwell" -f $t) ("install SA3 from git subdir: {0}" -f $t)){ $sa3built = $true; break } + } + } + try { $sa3present = ((Py-Exec $py.path $sa3check).Trim() -eq 'True') } catch { $sa3present = $false } + if(-not $sa3present){ + Write-Warning "SA3 package not importable after installation. Possible env mismatch or build skipped." + # Minimal diagnostics to understand where pip installed to + $null = Invoke-Quiet $py.path "-m pip show -f sageattn3" "pip show sageattn3" + $diag = @' +import sys, site, importlib.util +print("py=", sys.executable) +paths = [] +try: + paths += site.getsitepackages() +except Exception: + pass +try: + paths.append(site.getusersitepackages()) +except Exception: + pass +print("site=", ";".join(paths)) +spec = importlib.util.find_spec("sageattn3") +print("spec=", None if spec is None else (spec.origin or str(spec.submodule_search_locations))) +'@ + $dout = Py-Exec $py.path $diag + if($dout){ Write-Host $dout -ForegroundColor DarkYellow } + } + +} else { + if(-not (Ask-YesNo "Install/upgrade SageAttention to 2.2.x now?")){ Write-Host "Aborted by user."; exit 0 } + Write-Section "Installing (wheel if available)" + $null = Invoke-Quiet $py.path "-m pip install -U pip setuptools wheel" "Installing SageAttention 2.2.x, please wait a few minutes" + # Remove older v1 if present to avoid 'already satisfied' noise + $null = Invoke-Quiet $py.path "-m pip uninstall -y SageAttention" "uninstall legacy SageAttention (if any)" + $null = Invoke-Quiet $py.path "-m pip uninstall -y sageattention" "uninstall legacy sageattention (if any)" + $wheelOk = Invoke-Quiet $py.path "-m pip install -U --no-cache-dir sageattention>=2.2,<3" "pip install sageattention 2.2.x (wheel)" + # Verify actual installed version >= 2.2; otherwise treat as failure to trigger fallback + $sv2 = Get-SageVersion $py.path + $wheelHas22 = $false + if($sv2.module -and $sv2.version){ try{ $v=[Version]($sv2.version -replace '[^0-9\.]',''); if($v.Major -gt 2 -or ($v.Major -eq 2 -and $v.Minor -ge 2)){ $wheelHas22=$true } }catch{} + } + if($wheelOk -and -not $wheelHas22){ Write-Warning "Wheel installation did not provide SageAttention >= 2.2. Falling back to source build."; $wheelOk=$false } +} + +if(-not $wantSA3 -and -not $wheelOk){ + Write-Warning "Wheel install failed - will try source build." + # Try to infer arch list + $arch = '' + if($ti.is_cuda -and $ti.cc){ $arch = $ti.cc } + if($arch){ $env:TORCH_CUDA_ARCH_LIST = $arch } + # Ensure toolchain + Write-Section "Toolchain check" + $hasCL = ($null -ne (Get-Command cl.exe -ErrorAction SilentlyContinue)) + $hasNVCC = ($null -ne (Get-Command nvcc.exe -ErrorAction SilentlyContinue)) + if(-not $hasCL -or -not $hasNVCC){ + Write-Warning "MSVC cl or CUDA nvcc not found. Install MSVC Build Tools 2022 and CUDA Toolkit matching your torch (CUDA $($ti.cuda))." + exit 1 + } + if($isWindows -and -not $ForceSa2Source){ + Write-Warning "Attempting SageAttention 2.x source build on Windows (experimental upstream support)." + } + Write-Section "Building from source" + $null = Invoke-Quiet $py.path "-m pip install -U packaging cmake ninja" "toolchain python deps" + # Prefer upstream repo (thu-ml). First try main.zip to avoid git prompts + $built = $false + $urls = @( + 'https://github.com/thu-ml/SageAttention/archive/refs/heads/main.zip', + 'https://github.com/thu-ml/SageAttention/archive/refs/tags/v2.2.1.zip', + 'https://github.com/thu-ml/SageAttention/archive/refs/tags/v2.2.0.zip', + 'https://github.com/thu-ml/SageAttention/archive/refs/tags/v2.2.zip' + ) + foreach($u in $urls){ + if(Invoke-Quiet $py.path "-m pip install --no-build-isolation --no-cache-dir `"$u`"" ("build from archive: {0}" -f $u)) { $built = $true; break } + } + if(-not $built){ + Write-Warning "Tag archive not available; trying git main (noninteractive)." + $env:GIT_TERMINAL_PROMPT = "0" + $null = Invoke-Quiet $py.path "-m pip install --no-build-isolation --no-cache-dir git+https://github.com/thu-ml/SageAttention@main" "build from git main" + } + +} + +# If SA3 was requested but still not importable, try SA2 as a fallback +if($wantSA3 -and -not $sa3present){ + Write-Warning "Falling back to SageAttention 2.2.x (wheel/source)." + Write-Section "Installing SA2 (fallback)" + $null = Invoke-Quiet $py.path "-m pip install -U pip setuptools wheel" "Installing SageAttention 2.2.x, please wait a few minutes" + $wheelOk = Invoke-Quiet $py.path "-m pip install -U --no-cache-dir sageattention>=2.2,<3" "pip install sageattention 2.2.x (wheel)" + if(-not $wheelOk){ + Write-Section "Building SA2 from source" + $null = Invoke-Quiet $py.path "-m pip install -U packaging cmake ninja" "toolchain python deps" + $built = $false + $urls = @( + 'https://github.com/thu-ml/SageAttention/archive/refs/heads/main.zip', + 'https://github.com/thu-ml/SageAttention/archive/refs/tags/v2.2.1.zip', + 'https://github.com/thu-ml/SageAttention/archive/refs/tags/v2.2.0.zip', + 'https://github.com/thu-ml/SageAttention/archive/refs/tags/v2.2.zip' + ) + foreach($u in $urls){ if(Invoke-Quiet $py.path "-m pip install --no-build-isolation --no-cache-dir `"$u`"" ("build from archive: {0}" -f $u)) { $built = $true; break } } + if(-not $built){ + $env:GIT_TERMINAL_PROMPT = "0" + $null = Invoke-Quiet $py.path "-m pip install --no-build-isolation --no-cache-dir git+https://github.com/thu-ml/SageAttention@main" "build from git main" + } + } +} + +Write-Section "Validation" +$val = @' +try: + import importlib.util, torch + S = None + try: + import SageAttention as S + except Exception: + try: + import sageattention as S + except Exception: + S = None + sa_ok = False + if S is not None: + sa_ok = ( + hasattr(S, 'sageattn_qk_int8_pv_fp16_cuda') or + hasattr(S, 'sageattn_qk_int8_pv_fp16_cuda_fp16') + ) + if sa_ok: + print(f"OK: True torch {torch.__version__}") + else: + print(f"FAIL: torch {torch.__version__}") +except Exception as e: + print("ERR:", str(e)) +'@ +$out = Py-Exec $py.path $val +if($out -match '^OK:'){ Write-Host $out -ForegroundColor Green; Write-Host "Done." -ForegroundColor Green } +else { Write-Warning $out; Write-Warning "SageAttention not available. ComfyUI will fall back to stock attention (slower)." } diff --git a/vendor/__init__.py b/vendor/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..cefe37a586a1bc17bb3a8d1ff18ec7048ed239bb --- /dev/null +++ b/vendor/__init__.py @@ -0,0 +1,5 @@ +"""Vendored third-party modules used by MagicNodes. + +This package hosts lightweight copies of external libs for offline use. +""" + diff --git a/vendor/depth_anything_v2/LICENSE b/vendor/depth_anything_v2/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..57bc88a15a0ee8266c259b2667e64608d3f7e292 --- /dev/null +++ b/vendor/depth_anything_v2/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/vendor/depth_anything_v2/__init__.py b/vendor/depth_anything_v2/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1adf3e27da404f091500ec122a572043259972c1 --- /dev/null +++ b/vendor/depth_anything_v2/__init__.py @@ -0,0 +1,3 @@ +# Minimal vendor package for Depth Anything V2 (Apache-2.0) +from .dpt import DepthAnythingV2 + diff --git a/vendor/depth_anything_v2/dinov2.py b/vendor/depth_anything_v2/dinov2.py new file mode 100644 index 0000000000000000000000000000000000000000..83d250818c721c6df3b30d3f4352945527701615 --- /dev/null +++ b/vendor/depth_anything_v2/dinov2.py @@ -0,0 +1,415 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the Apache License, Version 2.0 +# found in the LICENSE file in the root directory of this source tree. + +# References: +# https://github.com/facebookresearch/dino/blob/main/vision_transformer.py +# https://github.com/rwightman/pytorch-image-models/tree/master/timm/models/vision_transformer.py + +from functools import partial +import math +import logging +from typing import Sequence, Tuple, Union, Callable + +import torch +import torch.nn as nn +import torch.utils.checkpoint +from torch.nn.init import trunc_normal_ + +from .dinov2_layers import Mlp, PatchEmbed, SwiGLUFFNFused, MemEffAttention, NestedTensorBlock as Block + + +logger = logging.getLogger("dinov2") + + +def named_apply(fn: Callable, module: nn.Module, name="", depth_first=True, include_root=False) -> nn.Module: + if not depth_first and include_root: + fn(module=module, name=name) + for child_name, child_module in module.named_children(): + child_name = ".".join((name, child_name)) if name else child_name + named_apply(fn=fn, module=child_module, name=child_name, depth_first=depth_first, include_root=True) + if depth_first and include_root: + fn(module=module, name=name) + return module + + +class BlockChunk(nn.ModuleList): + def forward(self, x): + for b in self: + x = b(x) + return x + + +class DinoVisionTransformer(nn.Module): + def __init__( + self, + img_size=224, + patch_size=16, + in_chans=3, + embed_dim=768, + depth=12, + num_heads=12, + mlp_ratio=4.0, + qkv_bias=True, + ffn_bias=True, + proj_bias=True, + drop_path_rate=0.0, + drop_path_uniform=False, + init_values=None, # for layerscale: None or 0 => no layerscale + embed_layer=PatchEmbed, + act_layer=nn.GELU, + block_fn=Block, + ffn_layer="mlp", + block_chunks=1, + num_register_tokens=0, + interpolate_antialias=False, + interpolate_offset=0.1, + ): + """ + Args: + img_size (int, tuple): input image size + patch_size (int, tuple): patch size + in_chans (int): number of input channels + embed_dim (int): embedding dimension + depth (int): depth of transformer + num_heads (int): number of attention heads + mlp_ratio (int): ratio of mlp hidden dim to embedding dim + qkv_bias (bool): enable bias for qkv if True + proj_bias (bool): enable bias for proj in attn if True + ffn_bias (bool): enable bias for ffn if True + drop_path_rate (float): stochastic depth rate + drop_path_uniform (bool): apply uniform drop rate across blocks + weight_init (str): weight init scheme + init_values (float): layer-scale init values + embed_layer (nn.Module): patch embedding layer + act_layer (nn.Module): MLP activation layer + block_fn (nn.Module): transformer block class + ffn_layer (str): "mlp", "swiglu", "swiglufused" or "identity" + block_chunks: (int) split block sequence into block_chunks units for FSDP wrap + num_register_tokens: (int) number of extra cls tokens (so-called "registers") + interpolate_antialias: (str) flag to apply anti-aliasing when interpolating positional embeddings + interpolate_offset: (float) work-around offset to apply when interpolating positional embeddings + """ + super().__init__() + norm_layer = partial(nn.LayerNorm, eps=1e-6) + + self.num_features = self.embed_dim = embed_dim # num_features for consistency with other models + self.num_tokens = 1 + self.n_blocks = depth + self.num_heads = num_heads + self.patch_size = patch_size + self.num_register_tokens = num_register_tokens + self.interpolate_antialias = interpolate_antialias + self.interpolate_offset = interpolate_offset + + self.patch_embed = embed_layer(img_size=img_size, patch_size=patch_size, in_chans=in_chans, embed_dim=embed_dim) + num_patches = self.patch_embed.num_patches + + self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim)) + self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + self.num_tokens, embed_dim)) + assert num_register_tokens >= 0 + self.register_tokens = ( + nn.Parameter(torch.zeros(1, num_register_tokens, embed_dim)) if num_register_tokens else None + ) + + if drop_path_uniform is True: + dpr = [drop_path_rate] * depth + else: + dpr = [x.item() for x in torch.linspace(0, drop_path_rate, depth)] # stochastic depth decay rule + + if ffn_layer == "mlp": + logger.info("using MLP layer as FFN") + ffn_layer = Mlp + elif ffn_layer == "swiglufused" or ffn_layer == "swiglu": + logger.info("using SwiGLU layer as FFN") + ffn_layer = SwiGLUFFNFused + elif ffn_layer == "identity": + logger.info("using Identity layer as FFN") + + def f(*args, **kwargs): + return nn.Identity() + + ffn_layer = f + else: + raise NotImplementedError + + blocks_list = [ + block_fn( + dim=embed_dim, + num_heads=num_heads, + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + proj_bias=proj_bias, + ffn_bias=ffn_bias, + drop_path=dpr[i], + norm_layer=norm_layer, + act_layer=act_layer, + ffn_layer=ffn_layer, + init_values=init_values, + ) + for i in range(depth) + ] + if block_chunks > 0: + self.chunked_blocks = True + chunked_blocks = [] + chunksize = depth // block_chunks + for i in range(0, depth, chunksize): + # this is to keep the block index consistent if we chunk the block list + chunked_blocks.append([nn.Identity()] * i + blocks_list[i : i + chunksize]) + self.blocks = nn.ModuleList([BlockChunk(p) for p in chunked_blocks]) + else: + self.chunked_blocks = False + self.blocks = nn.ModuleList(blocks_list) + + self.norm = norm_layer(embed_dim) + self.head = nn.Identity() + + self.mask_token = nn.Parameter(torch.zeros(1, embed_dim)) + + self.init_weights() + + def init_weights(self): + trunc_normal_(self.pos_embed, std=0.02) + nn.init.normal_(self.cls_token, std=1e-6) + if self.register_tokens is not None: + nn.init.normal_(self.register_tokens, std=1e-6) + named_apply(init_weights_vit_timm, self) + + def interpolate_pos_encoding(self, x, w, h): + previous_dtype = x.dtype + npatch = x.shape[1] - 1 + N = self.pos_embed.shape[1] - 1 + if npatch == N and w == h: + return self.pos_embed + pos_embed = self.pos_embed.float() + class_pos_embed = pos_embed[:, 0] + patch_pos_embed = pos_embed[:, 1:] + dim = x.shape[-1] + w0 = w // self.patch_size + h0 = h // self.patch_size + # we add a small number to avoid floating point error in the interpolation + # see discussion at https://github.com/facebookresearch/dino/issues/8 + # DINOv2 with register modify the interpolate_offset from 0.1 to 0.0 + w0, h0 = w0 + self.interpolate_offset, h0 + self.interpolate_offset + # w0, h0 = w0 + 0.1, h0 + 0.1 + + sqrt_N = math.sqrt(N) + sx, sy = float(w0) / sqrt_N, float(h0) / sqrt_N + patch_pos_embed = nn.functional.interpolate( + patch_pos_embed.reshape(1, int(sqrt_N), int(sqrt_N), dim).permute(0, 3, 1, 2), + scale_factor=(sx, sy), + # (int(w0), int(h0)), # to solve the upsampling shape issue + mode="bicubic", + antialias=self.interpolate_antialias + ) + + assert int(w0) == patch_pos_embed.shape[-2] + assert int(h0) == patch_pos_embed.shape[-1] + patch_pos_embed = patch_pos_embed.permute(0, 2, 3, 1).view(1, -1, dim) + return torch.cat((class_pos_embed.unsqueeze(0), patch_pos_embed), dim=1).to(previous_dtype) + + def prepare_tokens_with_masks(self, x, masks=None): + B, nc, w, h = x.shape + x = self.patch_embed(x) + if masks is not None: + x = torch.where(masks.unsqueeze(-1), self.mask_token.to(x.dtype).unsqueeze(0), x) + + x = torch.cat((self.cls_token.expand(x.shape[0], -1, -1), x), dim=1) + x = x + self.interpolate_pos_encoding(x, w, h) + + if self.register_tokens is not None: + x = torch.cat( + ( + x[:, :1], + self.register_tokens.expand(x.shape[0], -1, -1), + x[:, 1:], + ), + dim=1, + ) + + return x + + def forward_features_list(self, x_list, masks_list): + x = [self.prepare_tokens_with_masks(x, masks) for x, masks in zip(x_list, masks_list)] + for blk in self.blocks: + x = blk(x) + + all_x = x + output = [] + for x, masks in zip(all_x, masks_list): + x_norm = self.norm(x) + output.append( + { + "x_norm_clstoken": x_norm[:, 0], + "x_norm_regtokens": x_norm[:, 1 : self.num_register_tokens + 1], + "x_norm_patchtokens": x_norm[:, self.num_register_tokens + 1 :], + "x_prenorm": x, + "masks": masks, + } + ) + return output + + def forward_features(self, x, masks=None): + if isinstance(x, list): + return self.forward_features_list(x, masks) + + x = self.prepare_tokens_with_masks(x, masks) + + for blk in self.blocks: + x = blk(x) + + x_norm = self.norm(x) + return { + "x_norm_clstoken": x_norm[:, 0], + "x_norm_regtokens": x_norm[:, 1 : self.num_register_tokens + 1], + "x_norm_patchtokens": x_norm[:, self.num_register_tokens + 1 :], + "x_prenorm": x, + "masks": masks, + } + + def _get_intermediate_layers_not_chunked(self, x, n=1): + x = self.prepare_tokens_with_masks(x) + # If n is an int, take the n last blocks. If it's a list, take them + output, total_block_len = [], len(self.blocks) + blocks_to_take = range(total_block_len - n, total_block_len) if isinstance(n, int) else n + for i, blk in enumerate(self.blocks): + x = blk(x) + if i in blocks_to_take: + output.append(x) + assert len(output) == len(blocks_to_take), f"only {len(output)} / {len(blocks_to_take)} blocks found" + return output + + def _get_intermediate_layers_chunked(self, x, n=1): + x = self.prepare_tokens_with_masks(x) + output, i, total_block_len = [], 0, len(self.blocks[-1]) + # If n is an int, take the n last blocks. If it's a list, take them + blocks_to_take = range(total_block_len - n, total_block_len) if isinstance(n, int) else n + for block_chunk in self.blocks: + for blk in block_chunk[i:]: # Passing the nn.Identity() + x = blk(x) + if i in blocks_to_take: + output.append(x) + i += 1 + assert len(output) == len(blocks_to_take), f"only {len(output)} / {len(blocks_to_take)} blocks found" + return output + + def get_intermediate_layers( + self, + x: torch.Tensor, + n: Union[int, Sequence] = 1, # Layers or n last layers to take + reshape: bool = False, + return_class_token: bool = False, + norm=True + ) -> Tuple[Union[torch.Tensor, Tuple[torch.Tensor]]]: + if self.chunked_blocks: + outputs = self._get_intermediate_layers_chunked(x, n) + else: + outputs = self._get_intermediate_layers_not_chunked(x, n) + if norm: + outputs = [self.norm(out) for out in outputs] + class_tokens = [out[:, 0] for out in outputs] + outputs = [out[:, 1 + self.num_register_tokens:] for out in outputs] + if reshape: + B, _, w, h = x.shape + outputs = [ + out.reshape(B, w // self.patch_size, h // self.patch_size, -1).permute(0, 3, 1, 2).contiguous() + for out in outputs + ] + if return_class_token: + return tuple(zip(outputs, class_tokens)) + return tuple(outputs) + + def forward(self, *args, is_training=False, **kwargs): + ret = self.forward_features(*args, **kwargs) + if is_training: + return ret + else: + return self.head(ret["x_norm_clstoken"]) + + +def init_weights_vit_timm(module: nn.Module, name: str = ""): + """ViT weight initialization, original timm impl (for reproducibility)""" + if isinstance(module, nn.Linear): + trunc_normal_(module.weight, std=0.02) + if module.bias is not None: + nn.init.zeros_(module.bias) + + +def vit_small(patch_size=16, num_register_tokens=0, **kwargs): + model = DinoVisionTransformer( + patch_size=patch_size, + embed_dim=384, + depth=12, + num_heads=6, + mlp_ratio=4, + block_fn=partial(Block, attn_class=MemEffAttention), + num_register_tokens=num_register_tokens, + **kwargs, + ) + return model + + +def vit_base(patch_size=16, num_register_tokens=0, **kwargs): + model = DinoVisionTransformer( + patch_size=patch_size, + embed_dim=768, + depth=12, + num_heads=12, + mlp_ratio=4, + block_fn=partial(Block, attn_class=MemEffAttention), + num_register_tokens=num_register_tokens, + **kwargs, + ) + return model + + +def vit_large(patch_size=16, num_register_tokens=0, **kwargs): + model = DinoVisionTransformer( + patch_size=patch_size, + embed_dim=1024, + depth=24, + num_heads=16, + mlp_ratio=4, + block_fn=partial(Block, attn_class=MemEffAttention), + num_register_tokens=num_register_tokens, + **kwargs, + ) + return model + + +def vit_giant2(patch_size=16, num_register_tokens=0, **kwargs): + """ + Close to ViT-giant, with embed-dim 1536 and 24 heads => embed-dim per head 64 + """ + model = DinoVisionTransformer( + patch_size=patch_size, + embed_dim=1536, + depth=40, + num_heads=24, + mlp_ratio=4, + block_fn=partial(Block, attn_class=MemEffAttention), + num_register_tokens=num_register_tokens, + **kwargs, + ) + return model + + +def DINOv2(model_name): + model_zoo = { + "vits": vit_small, + "vitb": vit_base, + "vitl": vit_large, + "vitg": vit_giant2 + } + + return model_zoo[model_name]( + img_size=518, + patch_size=14, + init_values=1.0, + ffn_layer="mlp" if model_name != "vitg" else "swiglufused", + block_chunks=0, + num_register_tokens=0, + interpolate_antialias=False, + interpolate_offset=0.1 + ) diff --git a/vendor/depth_anything_v2/dinov2_layers/__init__.py b/vendor/depth_anything_v2/dinov2_layers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8120f4bc83066cb3f825ce32daa3b437f88486f1 --- /dev/null +++ b/vendor/depth_anything_v2/dinov2_layers/__init__.py @@ -0,0 +1,11 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from .mlp import Mlp +from .patch_embed import PatchEmbed +from .swiglu_ffn import SwiGLUFFN, SwiGLUFFNFused +from .block import NestedTensorBlock +from .attention import MemEffAttention diff --git a/vendor/depth_anything_v2/dinov2_layers/attention.py b/vendor/depth_anything_v2/dinov2_layers/attention.py new file mode 100644 index 0000000000000000000000000000000000000000..815a2bf53dbec496f6a184ed7d03bcecb7124262 --- /dev/null +++ b/vendor/depth_anything_v2/dinov2_layers/attention.py @@ -0,0 +1,83 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +# References: +# https://github.com/facebookresearch/dino/blob/master/vision_transformer.py +# https://github.com/rwightman/pytorch-image-models/tree/master/timm/models/vision_transformer.py + +import logging + +from torch import Tensor +from torch import nn + + +logger = logging.getLogger("dinov2") + + +try: + from xformers.ops import memory_efficient_attention, unbind, fmha + + XFORMERS_AVAILABLE = True +except ImportError: + logger.warning("xFormers not available") + XFORMERS_AVAILABLE = False + + +class Attention(nn.Module): + def __init__( + self, + dim: int, + num_heads: int = 8, + qkv_bias: bool = False, + proj_bias: bool = True, + attn_drop: float = 0.0, + proj_drop: float = 0.0, + ) -> None: + super().__init__() + self.num_heads = num_heads + head_dim = dim // num_heads + self.scale = head_dim**-0.5 + + self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias) + self.attn_drop = nn.Dropout(attn_drop) + self.proj = nn.Linear(dim, dim, bias=proj_bias) + self.proj_drop = nn.Dropout(proj_drop) + + def forward(self, x: Tensor) -> Tensor: + B, N, C = x.shape + qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4) + + q, k, v = qkv[0] * self.scale, qkv[1], qkv[2] + attn = q @ k.transpose(-2, -1) + + attn = attn.softmax(dim=-1) + attn = self.attn_drop(attn) + + x = (attn @ v).transpose(1, 2).reshape(B, N, C) + x = self.proj(x) + x = self.proj_drop(x) + return x + + +class MemEffAttention(Attention): + def forward(self, x: Tensor, attn_bias=None) -> Tensor: + if not XFORMERS_AVAILABLE: + assert attn_bias is None, "xFormers is required for nested tensors usage" + return super().forward(x) + + B, N, C = x.shape + qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads) + + q, k, v = unbind(qkv, 2) + + x = memory_efficient_attention(q, k, v, attn_bias=attn_bias) + x = x.reshape([B, N, C]) + + x = self.proj(x) + x = self.proj_drop(x) + return x + + \ No newline at end of file diff --git a/vendor/depth_anything_v2/dinov2_layers/block.py b/vendor/depth_anything_v2/dinov2_layers/block.py new file mode 100644 index 0000000000000000000000000000000000000000..25488f57cc0ad3c692f86b62555f6668e2a66db1 --- /dev/null +++ b/vendor/depth_anything_v2/dinov2_layers/block.py @@ -0,0 +1,252 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +# References: +# https://github.com/facebookresearch/dino/blob/master/vision_transformer.py +# https://github.com/rwightman/pytorch-image-models/tree/master/timm/layers/patch_embed.py + +import logging +from typing import Callable, List, Any, Tuple, Dict + +import torch +from torch import nn, Tensor + +from .attention import Attention, MemEffAttention +from .drop_path import DropPath +from .layer_scale import LayerScale +from .mlp import Mlp + + +logger = logging.getLogger("dinov2") + + +try: + from xformers.ops import fmha + from xformers.ops import scaled_index_add, index_select_cat + + XFORMERS_AVAILABLE = True +except ImportError: + logger.warning("xFormers not available") + XFORMERS_AVAILABLE = False + + +class Block(nn.Module): + def __init__( + self, + dim: int, + num_heads: int, + mlp_ratio: float = 4.0, + qkv_bias: bool = False, + proj_bias: bool = True, + ffn_bias: bool = True, + drop: float = 0.0, + attn_drop: float = 0.0, + init_values=None, + drop_path: float = 0.0, + act_layer: Callable[..., nn.Module] = nn.GELU, + norm_layer: Callable[..., nn.Module] = nn.LayerNorm, + attn_class: Callable[..., nn.Module] = Attention, + ffn_layer: Callable[..., nn.Module] = Mlp, + ) -> None: + super().__init__() + # print(f"biases: qkv: {qkv_bias}, proj: {proj_bias}, ffn: {ffn_bias}") + self.norm1 = norm_layer(dim) + self.attn = attn_class( + dim, + num_heads=num_heads, + qkv_bias=qkv_bias, + proj_bias=proj_bias, + attn_drop=attn_drop, + proj_drop=drop, + ) + self.ls1 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity() + self.drop_path1 = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() + + self.norm2 = norm_layer(dim) + mlp_hidden_dim = int(dim * mlp_ratio) + self.mlp = ffn_layer( + in_features=dim, + hidden_features=mlp_hidden_dim, + act_layer=act_layer, + drop=drop, + bias=ffn_bias, + ) + self.ls2 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity() + self.drop_path2 = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() + + self.sample_drop_ratio = drop_path + + def forward(self, x: Tensor) -> Tensor: + def attn_residual_func(x: Tensor) -> Tensor: + return self.ls1(self.attn(self.norm1(x))) + + def ffn_residual_func(x: Tensor) -> Tensor: + return self.ls2(self.mlp(self.norm2(x))) + + if self.training and self.sample_drop_ratio > 0.1: + # the overhead is compensated only for a drop path rate larger than 0.1 + x = drop_add_residual_stochastic_depth( + x, + residual_func=attn_residual_func, + sample_drop_ratio=self.sample_drop_ratio, + ) + x = drop_add_residual_stochastic_depth( + x, + residual_func=ffn_residual_func, + sample_drop_ratio=self.sample_drop_ratio, + ) + elif self.training and self.sample_drop_ratio > 0.0: + x = x + self.drop_path1(attn_residual_func(x)) + x = x + self.drop_path1(ffn_residual_func(x)) # FIXME: drop_path2 + else: + x = x + attn_residual_func(x) + x = x + ffn_residual_func(x) + return x + + +def drop_add_residual_stochastic_depth( + x: Tensor, + residual_func: Callable[[Tensor], Tensor], + sample_drop_ratio: float = 0.0, +) -> Tensor: + # 1) extract subset using permutation + b, n, d = x.shape + sample_subset_size = max(int(b * (1 - sample_drop_ratio)), 1) + brange = (torch.randperm(b, device=x.device))[:sample_subset_size] + x_subset = x[brange] + + # 2) apply residual_func to get residual + residual = residual_func(x_subset) + + x_flat = x.flatten(1) + residual = residual.flatten(1) + + residual_scale_factor = b / sample_subset_size + + # 3) add the residual + x_plus_residual = torch.index_add(x_flat, 0, brange, residual.to(dtype=x.dtype), alpha=residual_scale_factor) + return x_plus_residual.view_as(x) + + +def get_branges_scales(x, sample_drop_ratio=0.0): + b, n, d = x.shape + sample_subset_size = max(int(b * (1 - sample_drop_ratio)), 1) + brange = (torch.randperm(b, device=x.device))[:sample_subset_size] + residual_scale_factor = b / sample_subset_size + return brange, residual_scale_factor + + +def add_residual(x, brange, residual, residual_scale_factor, scaling_vector=None): + if scaling_vector is None: + x_flat = x.flatten(1) + residual = residual.flatten(1) + x_plus_residual = torch.index_add(x_flat, 0, brange, residual.to(dtype=x.dtype), alpha=residual_scale_factor) + else: + x_plus_residual = scaled_index_add( + x, brange, residual.to(dtype=x.dtype), scaling=scaling_vector, alpha=residual_scale_factor + ) + return x_plus_residual + + +attn_bias_cache: Dict[Tuple, Any] = {} + + +def get_attn_bias_and_cat(x_list, branges=None): + """ + this will perform the index select, cat the tensors, and provide the attn_bias from cache + """ + batch_sizes = [b.shape[0] for b in branges] if branges is not None else [x.shape[0] for x in x_list] + all_shapes = tuple((b, x.shape[1]) for b, x in zip(batch_sizes, x_list)) + if all_shapes not in attn_bias_cache.keys(): + seqlens = [] + for b, x in zip(batch_sizes, x_list): + for _ in range(b): + seqlens.append(x.shape[1]) + attn_bias = fmha.BlockDiagonalMask.from_seqlens(seqlens) + attn_bias._batch_sizes = batch_sizes + attn_bias_cache[all_shapes] = attn_bias + + if branges is not None: + cat_tensors = index_select_cat([x.flatten(1) for x in x_list], branges).view(1, -1, x_list[0].shape[-1]) + else: + tensors_bs1 = tuple(x.reshape([1, -1, *x.shape[2:]]) for x in x_list) + cat_tensors = torch.cat(tensors_bs1, dim=1) + + return attn_bias_cache[all_shapes], cat_tensors + + +def drop_add_residual_stochastic_depth_list( + x_list: List[Tensor], + residual_func: Callable[[Tensor, Any], Tensor], + sample_drop_ratio: float = 0.0, + scaling_vector=None, +) -> Tensor: + # 1) generate random set of indices for dropping samples in the batch + branges_scales = [get_branges_scales(x, sample_drop_ratio=sample_drop_ratio) for x in x_list] + branges = [s[0] for s in branges_scales] + residual_scale_factors = [s[1] for s in branges_scales] + + # 2) get attention bias and index+concat the tensors + attn_bias, x_cat = get_attn_bias_and_cat(x_list, branges) + + # 3) apply residual_func to get residual, and split the result + residual_list = attn_bias.split(residual_func(x_cat, attn_bias=attn_bias)) # type: ignore + + outputs = [] + for x, brange, residual, residual_scale_factor in zip(x_list, branges, residual_list, residual_scale_factors): + outputs.append(add_residual(x, brange, residual, residual_scale_factor, scaling_vector).view_as(x)) + return outputs + + +class NestedTensorBlock(Block): + def forward_nested(self, x_list: List[Tensor]) -> List[Tensor]: + """ + x_list contains a list of tensors to nest together and run + """ + assert isinstance(self.attn, MemEffAttention) + + if self.training and self.sample_drop_ratio > 0.0: + + def attn_residual_func(x: Tensor, attn_bias=None) -> Tensor: + return self.attn(self.norm1(x), attn_bias=attn_bias) + + def ffn_residual_func(x: Tensor, attn_bias=None) -> Tensor: + return self.mlp(self.norm2(x)) + + x_list = drop_add_residual_stochastic_depth_list( + x_list, + residual_func=attn_residual_func, + sample_drop_ratio=self.sample_drop_ratio, + scaling_vector=self.ls1.gamma if isinstance(self.ls1, LayerScale) else None, + ) + x_list = drop_add_residual_stochastic_depth_list( + x_list, + residual_func=ffn_residual_func, + sample_drop_ratio=self.sample_drop_ratio, + scaling_vector=self.ls2.gamma if isinstance(self.ls1, LayerScale) else None, + ) + return x_list + else: + + def attn_residual_func(x: Tensor, attn_bias=None) -> Tensor: + return self.ls1(self.attn(self.norm1(x), attn_bias=attn_bias)) + + def ffn_residual_func(x: Tensor, attn_bias=None) -> Tensor: + return self.ls2(self.mlp(self.norm2(x))) + + attn_bias, x = get_attn_bias_and_cat(x_list) + x = x + attn_residual_func(x, attn_bias=attn_bias) + x = x + ffn_residual_func(x) + return attn_bias.split(x) + + def forward(self, x_or_x_list): + if isinstance(x_or_x_list, Tensor): + return super().forward(x_or_x_list) + elif isinstance(x_or_x_list, list): + assert XFORMERS_AVAILABLE, "Please install xFormers for nested tensors usage" + return self.forward_nested(x_or_x_list) + else: + raise AssertionError diff --git a/vendor/depth_anything_v2/dinov2_layers/drop_path.py b/vendor/depth_anything_v2/dinov2_layers/drop_path.py new file mode 100644 index 0000000000000000000000000000000000000000..af05625984dd14682cc96a63bf0c97bab1f123b1 --- /dev/null +++ b/vendor/depth_anything_v2/dinov2_layers/drop_path.py @@ -0,0 +1,35 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +# References: +# https://github.com/facebookresearch/dino/blob/master/vision_transformer.py +# https://github.com/rwightman/pytorch-image-models/tree/master/timm/layers/drop.py + + +from torch import nn + + +def drop_path(x, drop_prob: float = 0.0, training: bool = False): + if drop_prob == 0.0 or not training: + return x + keep_prob = 1 - drop_prob + shape = (x.shape[0],) + (1,) * (x.ndim - 1) # work with diff dim tensors, not just 2D ConvNets + random_tensor = x.new_empty(shape).bernoulli_(keep_prob) + if keep_prob > 0.0: + random_tensor.div_(keep_prob) + output = x * random_tensor + return output + + +class DropPath(nn.Module): + """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).""" + + def __init__(self, drop_prob=None): + super(DropPath, self).__init__() + self.drop_prob = drop_prob + + def forward(self, x): + return drop_path(x, self.drop_prob, self.training) diff --git a/vendor/depth_anything_v2/dinov2_layers/layer_scale.py b/vendor/depth_anything_v2/dinov2_layers/layer_scale.py new file mode 100644 index 0000000000000000000000000000000000000000..ca5daa52bd81d3581adeb2198ea5b7dba2a3aea1 --- /dev/null +++ b/vendor/depth_anything_v2/dinov2_layers/layer_scale.py @@ -0,0 +1,28 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +# Modified from: https://github.com/huggingface/pytorch-image-models/blob/main/timm/models/vision_transformer.py#L103-L110 + +from typing import Union + +import torch +from torch import Tensor +from torch import nn + + +class LayerScale(nn.Module): + def __init__( + self, + dim: int, + init_values: Union[float, Tensor] = 1e-5, + inplace: bool = False, + ) -> None: + super().__init__() + self.inplace = inplace + self.gamma = nn.Parameter(init_values * torch.ones(dim)) + + def forward(self, x: Tensor) -> Tensor: + return x.mul_(self.gamma) if self.inplace else x * self.gamma diff --git a/vendor/depth_anything_v2/dinov2_layers/mlp.py b/vendor/depth_anything_v2/dinov2_layers/mlp.py new file mode 100644 index 0000000000000000000000000000000000000000..5e4b315f972f9a9f54aef1e4ef4e81b52976f018 --- /dev/null +++ b/vendor/depth_anything_v2/dinov2_layers/mlp.py @@ -0,0 +1,41 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +# References: +# https://github.com/facebookresearch/dino/blob/master/vision_transformer.py +# https://github.com/rwightman/pytorch-image-models/tree/master/timm/layers/mlp.py + + +from typing import Callable, Optional + +from torch import Tensor, nn + + +class Mlp(nn.Module): + def __init__( + self, + in_features: int, + hidden_features: Optional[int] = None, + out_features: Optional[int] = None, + act_layer: Callable[..., nn.Module] = nn.GELU, + drop: float = 0.0, + bias: bool = True, + ) -> None: + super().__init__() + out_features = out_features or in_features + hidden_features = hidden_features or in_features + self.fc1 = nn.Linear(in_features, hidden_features, bias=bias) + self.act = act_layer() + self.fc2 = nn.Linear(hidden_features, out_features, bias=bias) + self.drop = nn.Dropout(drop) + + def forward(self, x: Tensor) -> Tensor: + x = self.fc1(x) + x = self.act(x) + x = self.drop(x) + x = self.fc2(x) + x = self.drop(x) + return x diff --git a/vendor/depth_anything_v2/dinov2_layers/patch_embed.py b/vendor/depth_anything_v2/dinov2_layers/patch_embed.py new file mode 100644 index 0000000000000000000000000000000000000000..574abe41175568d700a389b8b96d1ba554914779 --- /dev/null +++ b/vendor/depth_anything_v2/dinov2_layers/patch_embed.py @@ -0,0 +1,89 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +# References: +# https://github.com/facebookresearch/dino/blob/master/vision_transformer.py +# https://github.com/rwightman/pytorch-image-models/tree/master/timm/layers/patch_embed.py + +from typing import Callable, Optional, Tuple, Union + +from torch import Tensor +import torch.nn as nn + + +def make_2tuple(x): + if isinstance(x, tuple): + assert len(x) == 2 + return x + + assert isinstance(x, int) + return (x, x) + + +class PatchEmbed(nn.Module): + """ + 2D image to patch embedding: (B,C,H,W) -> (B,N,D) + + Args: + img_size: Image size. + patch_size: Patch token size. + in_chans: Number of input image channels. + embed_dim: Number of linear projection output channels. + norm_layer: Normalization layer. + """ + + def __init__( + self, + img_size: Union[int, Tuple[int, int]] = 224, + patch_size: Union[int, Tuple[int, int]] = 16, + in_chans: int = 3, + embed_dim: int = 768, + norm_layer: Optional[Callable] = None, + flatten_embedding: bool = True, + ) -> None: + super().__init__() + + image_HW = make_2tuple(img_size) + patch_HW = make_2tuple(patch_size) + patch_grid_size = ( + image_HW[0] // patch_HW[0], + image_HW[1] // patch_HW[1], + ) + + self.img_size = image_HW + self.patch_size = patch_HW + self.patches_resolution = patch_grid_size + self.num_patches = patch_grid_size[0] * patch_grid_size[1] + + self.in_chans = in_chans + self.embed_dim = embed_dim + + self.flatten_embedding = flatten_embedding + + self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_HW, stride=patch_HW) + self.norm = norm_layer(embed_dim) if norm_layer else nn.Identity() + + def forward(self, x: Tensor) -> Tensor: + _, _, H, W = x.shape + patch_H, patch_W = self.patch_size + + assert H % patch_H == 0, f"Input image height {H} is not a multiple of patch height {patch_H}" + assert W % patch_W == 0, f"Input image width {W} is not a multiple of patch width: {patch_W}" + + x = self.proj(x) # B C H W + H, W = x.size(2), x.size(3) + x = x.flatten(2).transpose(1, 2) # B HW C + x = self.norm(x) + if not self.flatten_embedding: + x = x.reshape(-1, H, W, self.embed_dim) # B H W C + return x + + def flops(self) -> float: + Ho, Wo = self.patches_resolution + flops = Ho * Wo * self.embed_dim * self.in_chans * (self.patch_size[0] * self.patch_size[1]) + if self.norm is not None: + flops += Ho * Wo * self.embed_dim + return flops diff --git a/vendor/depth_anything_v2/dinov2_layers/swiglu_ffn.py b/vendor/depth_anything_v2/dinov2_layers/swiglu_ffn.py new file mode 100644 index 0000000000000000000000000000000000000000..b3324b266fb0a50ccf8c3a0ede2ae10ac4dfa03e --- /dev/null +++ b/vendor/depth_anything_v2/dinov2_layers/swiglu_ffn.py @@ -0,0 +1,63 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Callable, Optional + +from torch import Tensor, nn +import torch.nn.functional as F + + +class SwiGLUFFN(nn.Module): + def __init__( + self, + in_features: int, + hidden_features: Optional[int] = None, + out_features: Optional[int] = None, + act_layer: Callable[..., nn.Module] = None, + drop: float = 0.0, + bias: bool = True, + ) -> None: + super().__init__() + out_features = out_features or in_features + hidden_features = hidden_features or in_features + self.w12 = nn.Linear(in_features, 2 * hidden_features, bias=bias) + self.w3 = nn.Linear(hidden_features, out_features, bias=bias) + + def forward(self, x: Tensor) -> Tensor: + x12 = self.w12(x) + x1, x2 = x12.chunk(2, dim=-1) + hidden = F.silu(x1) * x2 + return self.w3(hidden) + + +try: + from xformers.ops import SwiGLU + + XFORMERS_AVAILABLE = True +except ImportError: + SwiGLU = SwiGLUFFN + XFORMERS_AVAILABLE = False + + +class SwiGLUFFNFused(SwiGLU): + def __init__( + self, + in_features: int, + hidden_features: Optional[int] = None, + out_features: Optional[int] = None, + act_layer: Callable[..., nn.Module] = None, + drop: float = 0.0, + bias: bool = True, + ) -> None: + out_features = out_features or in_features + hidden_features = hidden_features or in_features + hidden_features = (int(hidden_features * 2 / 3) + 7) // 8 * 8 + super().__init__( + in_features=in_features, + hidden_features=hidden_features, + out_features=out_features, + bias=bias, + ) diff --git a/vendor/depth_anything_v2/dpt.py b/vendor/depth_anything_v2/dpt.py new file mode 100644 index 0000000000000000000000000000000000000000..ba03ba36a9379ef0ade570d7b6aa976f500ca205 --- /dev/null +++ b/vendor/depth_anything_v2/dpt.py @@ -0,0 +1,220 @@ +import cv2 +import torch +import torch.nn as nn +import torch.nn.functional as F +from torchvision.transforms import Compose + +from .dinov2 import DINOv2 +from .util.blocks import FeatureFusionBlock, _make_scratch +from .util.transform import Resize, NormalizeImage, PrepareForNet + + +def _make_fusion_block(features, use_bn, size=None): + return FeatureFusionBlock( + features, + nn.ReLU(False), + deconv=False, + bn=use_bn, + expand=False, + align_corners=True, + size=size, + ) + + +class ConvBlock(nn.Module): + def __init__(self, in_feature, out_feature): + super().__init__() + + self.conv_block = nn.Sequential( + nn.Conv2d(in_feature, out_feature, kernel_size=3, stride=1, padding=1), + nn.BatchNorm2d(out_feature), + nn.ReLU(True) + ) + + def forward(self, x): + return self.conv_block(x) + + +class DPTHead(nn.Module): + def __init__( + self, + in_channels, + features=256, + use_bn=False, + out_channels=[256, 512, 1024, 1024], + use_clstoken=False + ): + super(DPTHead, self).__init__() + + self.use_clstoken = use_clstoken + + self.projects = nn.ModuleList([ + nn.Conv2d( + in_channels=in_channels, + out_channels=out_channel, + kernel_size=1, + stride=1, + padding=0, + ) for out_channel in out_channels + ]) + + self.resize_layers = nn.ModuleList([ + nn.ConvTranspose2d( + in_channels=out_channels[0], + out_channels=out_channels[0], + kernel_size=4, + stride=4, + padding=0), + nn.ConvTranspose2d( + in_channels=out_channels[1], + out_channels=out_channels[1], + kernel_size=2, + stride=2, + padding=0), + nn.Identity(), + nn.Conv2d( + in_channels=out_channels[3], + out_channels=out_channels[3], + kernel_size=3, + stride=2, + padding=1) + ]) + + if use_clstoken: + self.readout_projects = nn.ModuleList() + for _ in range(len(self.projects)): + self.readout_projects.append( + nn.Sequential( + nn.Linear(2 * in_channels, in_channels), + nn.GELU())) + + self.scratch = _make_scratch( + out_channels, + features, + groups=1, + expand=False, + ) + + self.scratch.stem_transpose = None + + self.scratch.refinenet1 = _make_fusion_block(features, use_bn) + self.scratch.refinenet2 = _make_fusion_block(features, use_bn) + self.scratch.refinenet3 = _make_fusion_block(features, use_bn) + self.scratch.refinenet4 = _make_fusion_block(features, use_bn) + + head_features_1 = features + head_features_2 = 32 + + self.scratch.output_conv1 = nn.Conv2d(head_features_1, head_features_1 // 2, kernel_size=3, stride=1, padding=1) + self.scratch.output_conv2 = nn.Sequential( + nn.Conv2d(head_features_1 // 2, head_features_2, kernel_size=3, stride=1, padding=1), + nn.ReLU(True), + nn.Conv2d(head_features_2, 1, kernel_size=1, stride=1, padding=0), + nn.ReLU(True), + nn.Identity(), + ) + + def forward(self, out_features, patch_h, patch_w): + out = [] + for i, x in enumerate(out_features): + if self.use_clstoken: + x, cls_token = x[0], x[1] + readout = cls_token.unsqueeze(1).expand_as(x) + x = self.readout_projects[i](torch.cat((x, readout), -1)) + else: + x = x[0] + + x = x.permute(0, 2, 1).reshape((x.shape[0], x.shape[-1], patch_h, patch_w)) + + x = self.projects[i](x) + x = self.resize_layers[i](x) + + out.append(x) + + layer_1, layer_2, layer_3, layer_4 = out + + layer_1_rn = self.scratch.layer1_rn(layer_1) + layer_2_rn = self.scratch.layer2_rn(layer_2) + layer_3_rn = self.scratch.layer3_rn(layer_3) + layer_4_rn = self.scratch.layer4_rn(layer_4) + + path_4 = self.scratch.refinenet4(layer_4_rn, size=layer_3_rn.shape[2:]) + path_3 = self.scratch.refinenet3(path_4, layer_3_rn, size=layer_2_rn.shape[2:]) + path_2 = self.scratch.refinenet2(path_3, layer_2_rn, size=layer_1_rn.shape[2:]) + path_1 = self.scratch.refinenet1(path_2, layer_1_rn) + + out = self.scratch.output_conv1(path_1) + out = F.interpolate(out, (int(patch_h * 14), int(patch_w * 14)), mode="bilinear", align_corners=True) + out = self.scratch.output_conv2(out) + + return out + + +class DepthAnythingV2(nn.Module): + def __init__( + self, + encoder='vitl', + features=256, + out_channels=[256, 512, 1024, 1024], + use_bn=False, + use_clstoken=False + ): + super(DepthAnythingV2, self).__init__() + + self.intermediate_layer_idx = { + 'vits': [2, 5, 8, 11], + 'vitb': [2, 5, 8, 11], + 'vitl': [4, 11, 17, 23], + 'vitg': [9, 19, 29, 39] + } + + self.encoder = encoder + self.pretrained = DINOv2(model_name=encoder) + + self.depth_head = DPTHead(self.pretrained.embed_dim, features, use_bn, out_channels=out_channels, use_clstoken=use_clstoken) + + def forward(self, x, max_depth): + patch_h, patch_w = x.shape[-2] // 14, x.shape[-1] // 14 + + features = self.pretrained.get_intermediate_layers(x, self.intermediate_layer_idx[self.encoder], return_class_token=True) + + depth = self.depth_head(features, patch_h, patch_w) * max_depth + + return depth.squeeze(1) + + @torch.no_grad() + def infer_image(self, raw_image, input_size=518, max_depth=20.0): + image, (h, w) = self.image2tensor(raw_image, input_size) + + depth = self.forward(image, max_depth) + + depth = F.interpolate(depth[:, None], (h, w), mode="bilinear", align_corners=True)[0, 0] + + return depth.cpu().numpy() + + def image2tensor(self, raw_image, input_size=518): + transform = Compose([ + Resize( + width=input_size, + height=input_size, + resize_target=False, + keep_aspect_ratio=True, + ensure_multiple_of=14, + resize_method='lower_bound', + image_interpolation_method=cv2.INTER_CUBIC, + ), + NormalizeImage(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), + PrepareForNet(), + ]) + + h, w = raw_image.shape[:2] + + image = cv2.cvtColor(raw_image, cv2.COLOR_BGR2RGB) / 255.0 + + image = transform({'image': image})['image'] + image = torch.from_numpy(image).unsqueeze(0) + + DEVICE = 'cuda' if torch.cuda.is_available() else 'mps' if torch.backends.mps.is_available() else 'cpu' + image = image.to(DEVICE) + + return image, (h, w) diff --git a/vendor/depth_anything_v2/util/__init__.py b/vendor/depth_anything_v2/util/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8ee2e8d1871d243621dc18f45ab42959197b580f --- /dev/null +++ b/vendor/depth_anything_v2/util/__init__.py @@ -0,0 +1,5 @@ +"""Utility modules for DepthAnythingV2 (vendored). + +Contains lightweight transforms and block helpers used during inference. +""" + diff --git a/vendor/depth_anything_v2/util/blocks.py b/vendor/depth_anything_v2/util/blocks.py new file mode 100644 index 0000000000000000000000000000000000000000..382ea183a40264056142afffc201c992a2b01d37 --- /dev/null +++ b/vendor/depth_anything_v2/util/blocks.py @@ -0,0 +1,148 @@ +import torch.nn as nn + + +def _make_scratch(in_shape, out_shape, groups=1, expand=False): + scratch = nn.Module() + + out_shape1 = out_shape + out_shape2 = out_shape + out_shape3 = out_shape + if len(in_shape) >= 4: + out_shape4 = out_shape + + if expand: + out_shape1 = out_shape + out_shape2 = out_shape * 2 + out_shape3 = out_shape * 4 + if len(in_shape) >= 4: + out_shape4 = out_shape * 8 + + scratch.layer1_rn = nn.Conv2d(in_shape[0], out_shape1, kernel_size=3, stride=1, padding=1, bias=False, groups=groups) + scratch.layer2_rn = nn.Conv2d(in_shape[1], out_shape2, kernel_size=3, stride=1, padding=1, bias=False, groups=groups) + scratch.layer3_rn = nn.Conv2d(in_shape[2], out_shape3, kernel_size=3, stride=1, padding=1, bias=False, groups=groups) + if len(in_shape) >= 4: + scratch.layer4_rn = nn.Conv2d(in_shape[3], out_shape4, kernel_size=3, stride=1, padding=1, bias=False, groups=groups) + + return scratch + + +class ResidualConvUnit(nn.Module): + """Residual convolution module. + """ + + def __init__(self, features, activation, bn): + """Init. + + Args: + features (int): number of features + """ + super().__init__() + + self.bn = bn + + self.groups=1 + + self.conv1 = nn.Conv2d(features, features, kernel_size=3, stride=1, padding=1, bias=True, groups=self.groups) + + self.conv2 = nn.Conv2d(features, features, kernel_size=3, stride=1, padding=1, bias=True, groups=self.groups) + + if self.bn == True: + self.bn1 = nn.BatchNorm2d(features) + self.bn2 = nn.BatchNorm2d(features) + + self.activation = activation + + self.skip_add = nn.quantized.FloatFunctional() + + def forward(self, x): + """Forward pass. + + Args: + x (tensor): input + + Returns: + tensor: output + """ + + out = self.activation(x) + out = self.conv1(out) + if self.bn == True: + out = self.bn1(out) + + out = self.activation(out) + out = self.conv2(out) + if self.bn == True: + out = self.bn2(out) + + if self.groups > 1: + out = self.conv_merge(out) + + return self.skip_add.add(out, x) + + +class FeatureFusionBlock(nn.Module): + """Feature fusion block. + """ + + def __init__( + self, + features, + activation, + deconv=False, + bn=False, + expand=False, + align_corners=True, + size=None + ): + """Init. + + Args: + features (int): number of features + """ + super(FeatureFusionBlock, self).__init__() + + self.deconv = deconv + self.align_corners = align_corners + + self.groups=1 + + self.expand = expand + out_features = features + if self.expand == True: + out_features = features // 2 + + self.out_conv = nn.Conv2d(features, out_features, kernel_size=1, stride=1, padding=0, bias=True, groups=1) + + self.resConfUnit1 = ResidualConvUnit(features, activation, bn) + self.resConfUnit2 = ResidualConvUnit(features, activation, bn) + + self.skip_add = nn.quantized.FloatFunctional() + + self.size=size + + def forward(self, *xs, size=None): + """Forward pass. + + Returns: + tensor: output + """ + output = xs[0] + + if len(xs) == 2: + res = self.resConfUnit1(xs[1]) + output = self.skip_add.add(output, res) + + output = self.resConfUnit2(output) + + if (size is None) and (self.size is None): + modifier = {"scale_factor": 2} + elif size is None: + modifier = {"size": self.size} + else: + modifier = {"size": size} + + output = nn.functional.interpolate(output, **modifier, mode="bilinear", align_corners=self.align_corners) + + output = self.out_conv(output) + + return output diff --git a/vendor/depth_anything_v2/util/transform.py b/vendor/depth_anything_v2/util/transform.py new file mode 100644 index 0000000000000000000000000000000000000000..b14aacd44ea086b01725a9ca68bb49eadcf37d73 --- /dev/null +++ b/vendor/depth_anything_v2/util/transform.py @@ -0,0 +1,158 @@ +import numpy as np +import cv2 + + +class Resize(object): + """Resize sample to given size (width, height). + """ + + def __init__( + self, + width, + height, + resize_target=True, + keep_aspect_ratio=False, + ensure_multiple_of=1, + resize_method="lower_bound", + image_interpolation_method=cv2.INTER_AREA, + ): + """Init. + + Args: + width (int): desired output width + height (int): desired output height + resize_target (bool, optional): + True: Resize the full sample (image, mask, target). + False: Resize image only. + Defaults to True. + keep_aspect_ratio (bool, optional): + True: Keep the aspect ratio of the input sample. + Output sample might not have the given width and height, and + resize behaviour depends on the parameter 'resize_method'. + Defaults to False. + ensure_multiple_of (int, optional): + Output width and height is constrained to be multiple of this parameter. + Defaults to 1. + resize_method (str, optional): + "lower_bound": Output will be at least as large as the given size. + "upper_bound": Output will be at max as large as the given size. (Output size might be smaller than given size.) + "minimal": Scale as least as possible. (Output size might be smaller than given size.) + Defaults to "lower_bound". + """ + self.__width = width + self.__height = height + + self.__resize_target = resize_target + self.__keep_aspect_ratio = keep_aspect_ratio + self.__multiple_of = ensure_multiple_of + self.__resize_method = resize_method + self.__image_interpolation_method = image_interpolation_method + + def constrain_to_multiple_of(self, x, min_val=0, max_val=None): + y = (np.round(x / self.__multiple_of) * self.__multiple_of).astype(int) + + if max_val is not None and y > max_val: + y = (np.floor(x / self.__multiple_of) * self.__multiple_of).astype(int) + + if y < min_val: + y = (np.ceil(x / self.__multiple_of) * self.__multiple_of).astype(int) + + return y + + def get_size(self, width, height): + # determine new height and width + scale_height = self.__height / height + scale_width = self.__width / width + + if self.__keep_aspect_ratio: + if self.__resize_method == "lower_bound": + # scale such that output size is lower bound + if scale_width > scale_height: + # fit width + scale_height = scale_width + else: + # fit height + scale_width = scale_height + elif self.__resize_method == "upper_bound": + # scale such that output size is upper bound + if scale_width < scale_height: + # fit width + scale_height = scale_width + else: + # fit height + scale_width = scale_height + elif self.__resize_method == "minimal": + # scale as least as possbile + if abs(1 - scale_width) < abs(1 - scale_height): + # fit width + scale_height = scale_width + else: + # fit height + scale_width = scale_height + else: + raise ValueError(f"resize_method {self.__resize_method} not implemented") + + if self.__resize_method == "lower_bound": + new_height = self.constrain_to_multiple_of(scale_height * height, min_val=self.__height) + new_width = self.constrain_to_multiple_of(scale_width * width, min_val=self.__width) + elif self.__resize_method == "upper_bound": + new_height = self.constrain_to_multiple_of(scale_height * height, max_val=self.__height) + new_width = self.constrain_to_multiple_of(scale_width * width, max_val=self.__width) + elif self.__resize_method == "minimal": + new_height = self.constrain_to_multiple_of(scale_height * height) + new_width = self.constrain_to_multiple_of(scale_width * width) + else: + raise ValueError(f"resize_method {self.__resize_method} not implemented") + + return (new_width, new_height) + + def __call__(self, sample): + width, height = self.get_size(sample["image"].shape[1], sample["image"].shape[0]) + + # resize sample + sample["image"] = cv2.resize(sample["image"], (width, height), interpolation=self.__image_interpolation_method) + + if self.__resize_target: + if "depth" in sample: + sample["depth"] = cv2.resize(sample["depth"], (width, height), interpolation=cv2.INTER_NEAREST) + + if "mask" in sample: + sample["mask"] = cv2.resize(sample["mask"].astype(np.float32), (width, height), interpolation=cv2.INTER_NEAREST) + + return sample + + +class NormalizeImage(object): + """Normlize image by given mean and std. + """ + + def __init__(self, mean, std): + self.__mean = mean + self.__std = std + + def __call__(self, sample): + sample["image"] = (sample["image"] - self.__mean) / self.__std + + return sample + + +class PrepareForNet(object): + """Prepare sample for usage as network input. + """ + + def __init__(self): + pass + + def __call__(self, sample): + image = np.transpose(sample["image"], (2, 0, 1)) + sample["image"] = np.ascontiguousarray(image).astype(np.float32) + + if "depth" in sample: + depth = sample["depth"].astype(np.float32) + sample["depth"] = np.ascontiguousarray(depth) + + if "mask" in sample: + sample["mask"] = sample["mask"].astype(np.float32) + sample["mask"] = np.ascontiguousarray(sample["mask"]) + + return sample \ No newline at end of file diff --git a/workflows/mg_Easy-Workflow.json b/workflows/mg_Easy-Workflow.json new file mode 100644 index 0000000000000000000000000000000000000000..54f4e8a2df5b64c27536070b3bdf58db7f662d2c --- /dev/null +++ b/workflows/mg_Easy-Workflow.json @@ -0,0 +1 @@ +{"id":"ab14c37a-6cec-41cf-899a-b499539f5b8b","revision":0,"last_node_id":58,"last_link_id":152,"nodes":[{"id":33,"type":"PreviewImage","pos":[934.524998846107,827.6679009139978],"size":[454.34515960057956,487.52469509807827],"flags":{},"order":24,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":142}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":16,"type":"PreviewImage","pos":[1500.7861350427,834.011023145272],"size":[454.34515960057956,487.52469509807827],"flags":{},"order":25,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":22}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":7,"type":"SaveImage","pos":[2796.435012229308,813.9383242288463],"size":[645.60324992196,715.7559184038435],"flags":{},"order":35,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":48},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null}],"outputs":[],"properties":{},"widgets_values":["ComfyUI"]},{"id":26,"type":"PreviewImage","pos":[2402.088511427638,-384.4257289383634],"size":[204.34144162678967,246],"flags":{},"order":32,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":41}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":45,"type":"PreviewImage","pos":[2134.6382392169603,-384.3467464741332],"size":[204.34144162678967,246],"flags":{},"order":29,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":123}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":44,"type":"MG_ControlFusion_Easy","pos":[2230.0043967143683,-89.74152781984677],"size":[270,258],"flags":{},"order":26,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"IMAGE","link":110},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":119},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":120},{"localized_name":"control_net","name":"control_net","type":"CONTROL_NET","link":146},{"localized_name":"vae","name":"vae","type":"VAE","link":134},{"localized_name":"preset_step","name":"preset_step","type":"COMBO","widget":{"name":"preset_step"},"link":null},{"localized_name":"custom","name":"custom","type":"BOOLEAN","widget":{"name":"custom"},"link":null},{"localized_name":"enable_depth","name":"enable_depth","type":"BOOLEAN","widget":{"name":"enable_depth"},"link":null},{"localized_name":"enable_pyra","name":"enable_pyra","type":"BOOLEAN","widget":{"name":"enable_pyra"},"link":null},{"localized_name":"edge_alpha","name":"edge_alpha","type":"FLOAT","widget":{"name":"edge_alpha"},"link":null},{"localized_name":"blend_factor","name":"blend_factor","type":"FLOAT","widget":{"name":"blend_factor"},"link":null}],"outputs":[{"localized_name":"positive","name":"positive","type":"CONDITIONING","links":[115,122]},{"localized_name":"negative","name":"negative","type":"CONDITIONING","links":[116,121]},{"localized_name":"Mask_Preview","name":"Mask_Preview","type":"IMAGE","links":[123]}],"properties":{"Node name for S&R":"MG_ControlFusion_Easy"},"widgets_values":["Step 3",false,true,true,1,0.02]},{"id":42,"type":"PreviewImage","pos":[2869.9561925844105,-392.07761053265784],"size":[204.34144162678967,246],"flags":{},"order":34,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":90}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":31,"type":"PreviewImage","pos":[3124.9260576007664,-392.6557805266826],"size":[204.34144162678967,246],"flags":{},"order":36,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":46}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":41,"type":"MG_ControlFusion_Easy","pos":[2963.5735553194004,-88.49280600871076],"size":[270,258],"flags":{},"order":31,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"IMAGE","link":85},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":122},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":121},{"localized_name":"control_net","name":"control_net","type":"CONTROL_NET","link":148},{"localized_name":"vae","name":"vae","type":"VAE","link":133},{"localized_name":"preset_step","name":"preset_step","type":"COMBO","widget":{"name":"preset_step"},"link":null},{"localized_name":"custom","name":"custom","type":"BOOLEAN","widget":{"name":"custom"},"link":null},{"localized_name":"enable_depth","name":"enable_depth","type":"BOOLEAN","widget":{"name":"enable_depth"},"link":null},{"localized_name":"enable_pyra","name":"enable_pyra","type":"BOOLEAN","widget":{"name":"enable_pyra"},"link":null},{"localized_name":"edge_alpha","name":"edge_alpha","type":"FLOAT","widget":{"name":"edge_alpha"},"link":null},{"localized_name":"blend_factor","name":"blend_factor","type":"FLOAT","widget":{"name":"blend_factor"},"link":null}],"outputs":[{"localized_name":"positive","name":"positive","type":"CONDITIONING","links":[86]},{"localized_name":"negative","name":"negative","type":"CONDITIONING","links":[87]},{"localized_name":"Mask_Preview","name":"Mask_Preview","type":"IMAGE","links":[90]}],"properties":{"Node name for S&R":"MG_ControlFusion_Easy"},"widgets_values":["Step 4",false,true,true,1,0.02]},{"id":43,"type":"MG_ControlFusion_Easy","pos":[1591.642692427568,-97.88472358998305],"size":[270,258],"flags":{},"order":19,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"IMAGE","link":97},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":102},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":103},{"localized_name":"control_net","name":"control_net","type":"CONTROL_NET","link":144},{"localized_name":"vae","name":"vae","type":"VAE","link":135},{"localized_name":"preset_step","name":"preset_step","type":"COMBO","widget":{"name":"preset_step"},"link":null},{"localized_name":"custom","name":"custom","type":"BOOLEAN","widget":{"name":"custom"},"link":null},{"localized_name":"enable_depth","name":"enable_depth","type":"BOOLEAN","widget":{"name":"enable_depth"},"link":null},{"localized_name":"enable_pyra","name":"enable_pyra","type":"BOOLEAN","widget":{"name":"enable_pyra"},"link":null},{"localized_name":"edge_alpha","name":"edge_alpha","type":"FLOAT","widget":{"name":"edge_alpha"},"link":null},{"localized_name":"blend_factor","name":"blend_factor","type":"FLOAT","widget":{"name":"blend_factor"},"link":null}],"outputs":[{"localized_name":"positive","name":"positive","type":"CONDITIONING","links":[107,119]},{"localized_name":"negative","name":"negative","type":"CONDITIONING","links":[106,120]},{"localized_name":"Mask_Preview","name":"Mask_Preview","type":"IMAGE","links":[124]}],"properties":{"Node name for S&R":"MG_ControlFusion_Easy"},"widgets_values":["Step 2",false,true,true,1,0.02]},{"id":15,"type":"PreviewImage","pos":[1735.1930557379187,-393.0297689400224],"size":[204.34144162678967,246],"flags":{},"order":27,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":21}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":46,"type":"PreviewImage","pos":[1507.7533042139721,-396.74243519648866],"size":[204.34144162678967,246],"flags":{},"order":23,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":124}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":10,"type":"Note","pos":[-144.73815950276332,524.3438860412525],"size":[210,135.28179391870685],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[],"title":"Positive","properties":{},"widgets_values":["---->"],"color":"#232","bgcolor":"#353"},{"id":8,"type":"Note","pos":[-136.6498868216612,934.3499360344714],"size":[213.5817209730206,155.86666460906906],"flags":{},"order":1,"mode":0,"inputs":[],"outputs":[],"title":"Negative","properties":{},"widgets_values":["---->\n\n(super-wrong:1) - it's a trigger negative prompt for 'mg_7lambda_negative'"],"color":"#322","bgcolor":"#533"},{"id":53,"type":"Reroute","pos":[990.3715999390612,503.0075133219898],"size":[75,26],"flags":{},"order":20,"mode":0,"inputs":[{"name":"","type":"*","link":141}],"outputs":[{"name":"","type":"IMAGE","links":[142]}],"properties":{"showOutputText":false,"horizontal":false}},{"id":3,"type":"MagicSeedLatent","pos":[629.902126183393,-182.7453558545654],"size":[270,198],"flags":{},"order":9,"mode":0,"inputs":[{"localized_name":"vae","name":"vae","shape":7,"type":"VAE","link":2},{"localized_name":"image","name":"image","shape":7,"type":"IMAGE","link":null},{"localized_name":"width","name":"width","type":"INT","widget":{"name":"width"},"link":null},{"localized_name":"height","name":"height","type":"INT","widget":{"name":"height"},"link":null},{"localized_name":"batch_size","name":"batch_size","type":"INT","widget":{"name":"batch_size"},"link":null},{"localized_name":"sigma","name":"sigma","type":"FLOAT","widget":{"name":"sigma"},"link":null},{"localized_name":"bias","name":"bias","type":"FLOAT","widget":{"name":"bias"},"link":null},{"localized_name":"mix_image","name":"mix_image","type":"BOOLEAN","widget":{"name":"mix_image"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":[19]}],"properties":{"Node name for S&R":"MagicSeedLatent"},"widgets_values":[672,944,1,0.8,0.5,false],"color":"#323","bgcolor":"#535"},{"id":4,"type":"ControlNetLoader","pos":[1106.8272213312625,465.13839701649334],"size":[271.9963341032716,58],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"control_net_name","name":"control_net_name","type":"COMBO","widget":{"name":"control_net_name"},"link":null}],"outputs":[{"label":"CONTROL_NET","localized_name":"CONTROL_NET","name":"CONTROL_NET","type":"CONTROL_NET","slot_index":0,"links":[143]}],"properties":{"Node name for S&R":"ControlNetLoader","models":[{"name":"control_v11p_sd15_scribble_fp16.safetensors","url":"https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_scribble_fp16.safetensors?download=true","directory":"controlnet"}]},"widgets_values":["diffusers_xl_depth_full.safetensors"],"color":"#233","bgcolor":"#355"},{"id":5,"type":"CLIPVisionLoader","pos":[1107.383961736022,356.1351441545039],"size":[267.557917518395,58],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"clip_name","name":"clip_name","type":"COMBO","widget":{"name":"clip_name"},"link":null}],"outputs":[{"localized_name":"CLIP_VISION","name":"CLIP_VISION","type":"CLIP_VISION","links":[17,92]}],"properties":{"Node name for S&R":"CLIPVisionLoader"},"widgets_values":["open_clip_model(NS).safetensors"],"color":"#2a363b","bgcolor":"#3f5159"},{"id":49,"type":"Reroute","pos":[2127.6689254267244,589.457535723742],"size":[75,26],"flags":{},"order":15,"mode":0,"inputs":[{"name":"","type":"*","link":129}],"outputs":[{"name":"","type":"VAE","links":[130,131,134]}],"properties":{"showOutputText":false,"horizontal":false},"color":"#432","bgcolor":"#653"},{"id":47,"type":"Reroute","pos":[2126.2268260656597,621.1837216671687],"size":[75,26],"flags":{},"order":12,"mode":0,"inputs":[{"name":"","type":"*","link":125}],"outputs":[{"name":"","type":"CLIP_VISION","links":[126,127]}],"properties":{"showOutputText":false,"horizontal":false},"color":"#2a363b","bgcolor":"#3f5159"},{"id":55,"type":"Reroute","pos":[2125.520836752072,649.1034792254129],"size":[75,26],"flags":{},"order":11,"mode":0,"inputs":[{"name":"","type":"*","link":145}],"outputs":[{"name":"","type":"CONTROL_NET","links":[146,147]}],"properties":{"showOutputText":false,"horizontal":false},"color":"#233","bgcolor":"#355"},{"id":52,"type":"Reroute","pos":[2847.2765065980852,559.1734491413802],"size":[75,26],"flags":{},"order":18,"mode":0,"inputs":[{"name":"","type":"*","link":139}],"outputs":[{"name":"","type":"MODEL","links":[140]}],"properties":{"showOutputText":false,"horizontal":false},"color":"#223","bgcolor":"#335"},{"id":50,"type":"Reroute","pos":[2847.276506598083,593.7838338069366],"size":[75,26],"flags":{},"order":21,"mode":0,"inputs":[{"name":"","type":"*","link":131}],"outputs":[{"name":"","type":"VAE","links":[132,133]}],"properties":{"showOutputText":false,"horizontal":false},"color":"#432","bgcolor":"#653"},{"id":48,"type":"Reroute","pos":[2847.2765065980825,624.0679203892983],"size":[75,26],"flags":{},"order":17,"mode":0,"inputs":[{"name":"","type":"*","link":127}],"outputs":[{"name":"","type":"CLIP_VISION","links":[128]}],"properties":{"showOutputText":false,"horizontal":false},"color":"#2a363b","bgcolor":"#3f5159"},{"id":56,"type":"Reroute","pos":[2843.686318562366,656.3139760307369],"size":[75,26],"flags":{},"order":16,"mode":0,"inputs":[{"name":"","type":"*","link":147}],"outputs":[{"name":"","type":"CONTROL_NET","links":[148]}],"properties":{"showOutputText":false,"horizontal":false},"color":"#233","bgcolor":"#355"},{"id":19,"type":"Reroute","pos":[1498.7055053565248,550.5312952244669],"size":[75,26],"flags":{},"order":7,"mode":0,"inputs":[{"name":"","type":"*","link":150}],"outputs":[{"name":"","type":"MODEL","links":[136,137]}],"properties":{"showOutputText":false,"horizontal":false},"color":"#223","bgcolor":"#335"},{"id":20,"type":"Reroute","pos":[1500.2829216438683,587.3239702140152],"size":[75,26],"flags":{},"order":10,"mode":0,"inputs":[{"name":"","type":"*","link":31}],"outputs":[{"name":"","type":"VAE","links":[32,129,135]}],"properties":{"showOutputText":false,"horizontal":false},"color":"#432","bgcolor":"#653"},{"id":17,"type":"Reroute","pos":[1500.0465475793615,619.3098903755488],"size":[75,26],"flags":{},"order":6,"mode":0,"inputs":[{"name":"","type":"*","link":92}],"outputs":[{"name":"","type":"CLIP_VISION","links":[28,125]}],"properties":{"showOutputText":false,"horizontal":false},"color":"#2a363b","bgcolor":"#3f5159"},{"id":54,"type":"Reroute","pos":[1500.2152857001722,653.5359574268514],"size":[75,26],"flags":{},"order":5,"mode":0,"inputs":[{"name":"","type":"*","link":143}],"outputs":[{"name":"","type":"CONTROL_NET","links":[144,145]}],"properties":{"showOutputText":false,"horizontal":false},"color":"#233","bgcolor":"#355"},{"id":51,"type":"Reroute","pos":[2129.1110247877914,554.8471510581859],"size":[75,26],"flags":{},"order":13,"mode":0,"inputs":[{"name":"","type":"*","link":137}],"outputs":[{"name":"","type":"MODEL","links":[138,139]}],"properties":{"showOutputText":false,"horizontal":false},"color":"#223","bgcolor":"#335"},{"id":24,"type":"ComfyAdaptiveDetailEnhancer25_Easy","pos":[2216.167965338495,236.13994164208663],"size":[304.44140625,394],"flags":{},"order":28,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":138},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":115},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":116},{"localized_name":"vae","name":"vae","type":"VAE","link":130},{"localized_name":"latent","name":"latent","type":"LATENT","link":62},{"localized_name":"reference_image","name":"reference_image","shape":7,"type":"IMAGE","link":66},{"localized_name":"clip_vision","name":"clip_vision","shape":7,"type":"CLIP_VISION","link":126},{"localized_name":"preset_step","name":"preset_step","type":"COMBO","widget":{"name":"preset_step"},"link":null},{"localized_name":"custom","name":"custom","type":"BOOLEAN","widget":{"name":"custom"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"cfg","name":"cfg","type":"FLOAT","widget":{"name":"cfg"},"link":null},{"localized_name":"denoise","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null},{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null},{"localized_name":"clipseg_text","name":"clipseg_text","shape":7,"type":"STRING","widget":{"name":"clipseg_text"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":[61]},{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[40,65,85]},{"localized_name":"mask_preview","name":"mask_preview","type":"IMAGE","links":[41]}],"properties":{"Node name for S&R":"ComfyAdaptiveDetailEnhancer25_Easy"},"widgets_values":["Step 3",false,0,"fixed",30,7,0.55,"ddim","MGHybrid","hand, feet, face"]},{"id":25,"type":"PreviewImage","pos":[2076.6050781706476,818.6164042016213],"size":[607.2076918734667,715.3763941463438],"flags":{},"order":30,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":40}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":29,"type":"ComfyAdaptiveDetailEnhancer25_Easy","pos":[2947.1571822581677,229.68250150174998],"size":[304.44140625,394],"flags":{},"order":33,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":140},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":86},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":87},{"localized_name":"vae","name":"vae","type":"VAE","link":132},{"localized_name":"latent","name":"latent","type":"LATENT","link":61},{"localized_name":"reference_image","name":"reference_image","shape":7,"type":"IMAGE","link":65},{"localized_name":"clip_vision","name":"clip_vision","shape":7,"type":"CLIP_VISION","link":128},{"localized_name":"preset_step","name":"preset_step","type":"COMBO","widget":{"name":"preset_step"},"link":null},{"localized_name":"custom","name":"custom","type":"BOOLEAN","widget":{"name":"custom"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"cfg","name":"cfg","type":"FLOAT","widget":{"name":"cfg"},"link":null},{"localized_name":"denoise","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null},{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null},{"localized_name":"clipseg_text","name":"clipseg_text","shape":7,"type":"STRING","widget":{"name":"clipseg_text"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":null},{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[48]},{"localized_name":"mask_preview","name":"mask_preview","type":"IMAGE","links":[46]}],"properties":{"Node name for S&R":"ComfyAdaptiveDetailEnhancer25_Easy"},"widgets_values":["Step 4",false,0,"fixed",30,7,0.45,"ddim","MGHybrid","hand, feet, face"]},{"id":11,"type":"ComfyAdaptiveDetailEnhancer25_Easy","pos":[1572.9731847035082,222.56018167321056],"size":[304.44140625,394],"flags":{},"order":22,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":136},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":107},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":106},{"localized_name":"vae","name":"vae","type":"VAE","link":32},{"localized_name":"latent","name":"latent","type":"LATENT","link":37},{"localized_name":"reference_image","name":"reference_image","shape":7,"type":"IMAGE","link":24},{"localized_name":"clip_vision","name":"clip_vision","shape":7,"type":"CLIP_VISION","link":28},{"localized_name":"preset_step","name":"preset_step","type":"COMBO","widget":{"name":"preset_step"},"link":null},{"localized_name":"custom","name":"custom","type":"BOOLEAN","widget":{"name":"custom"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"cfg","name":"cfg","type":"FLOAT","widget":{"name":"cfg"},"link":null},{"localized_name":"denoise","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null},{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null},{"localized_name":"clipseg_text","name":"clipseg_text","shape":7,"type":"STRING","widget":{"name":"clipseg_text"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":[62]},{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[22,66,110]},{"localized_name":"mask_preview","name":"mask_preview","type":"IMAGE","links":[21]}],"properties":{"Node name for S&R":"ComfyAdaptiveDetailEnhancer25_Easy"},"widgets_values":["Step 2",false,0,"fixed",30,7,0.65,"ddim","MGHybrid","hand, feet, face"]},{"id":14,"type":"ComfyAdaptiveDetailEnhancer25_Easy","pos":[1028.7352577781799,-254.68484251639404],"size":[304.44140625,394],"flags":{},"order":14,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":152},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":13},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":14},{"localized_name":"vae","name":"vae","type":"VAE","link":15},{"localized_name":"latent","name":"latent","type":"LATENT","link":19},{"localized_name":"reference_image","name":"reference_image","shape":7,"type":"IMAGE","link":null},{"localized_name":"clip_vision","name":"clip_vision","shape":7,"type":"CLIP_VISION","link":17},{"localized_name":"preset_step","name":"preset_step","type":"COMBO","widget":{"name":"preset_step"},"link":null},{"localized_name":"custom","name":"custom","type":"BOOLEAN","widget":{"name":"custom"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"cfg","name":"cfg","type":"FLOAT","widget":{"name":"cfg"},"link":null},{"localized_name":"denoise","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null},{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null},{"localized_name":"clipseg_text","name":"clipseg_text","shape":7,"type":"STRING","widget":{"name":"clipseg_text"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":[37]},{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[24,97,141]},{"localized_name":"mask_preview","name":"mask_preview","type":"IMAGE","links":[]}],"properties":{"Node name for S&R":"ComfyAdaptiveDetailEnhancer25_Easy"},"widgets_values":["Step 1",false,0,"fixed",30,7,0.9999,"ddim","MGHybrid","hand, feet, face"]},{"id":58,"type":"PatchSageAttention","pos":[627.8244030121156,-297.19892309893845],"size":[270,58],"flags":{},"order":8,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":151},{"localized_name":"sage_attention","name":"sage_attention","type":"COMBO","widget":{"name":"sage_attention"},"link":null}],"outputs":[{"localized_name":"MODEL","name":"MODEL","type":"MODEL","links":[152]}],"properties":{"Node name for S&R":"PatchSageAttention"},"widgets_values":["auto_quality"],"color":"#223","bgcolor":"#335"},{"id":2,"type":"MagicNodesCombiNode","pos":[93.09648271877055,-350.9065523480804],"size":[439.3682369626521,1874.7812342789503],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"model_in","name":"model_in","shape":7,"type":"MODEL","link":null},{"localized_name":"clip_in","name":"clip_in","shape":7,"type":"CLIP","link":null},{"localized_name":"vae_in","name":"vae_in","shape":7,"type":"VAE","link":null},{"localized_name":"positive_in","name":"positive_in","shape":7,"type":"CONDITIONING","link":null},{"localized_name":"negative_in","name":"negative_in","shape":7,"type":"CONDITIONING","link":null},{"localized_name":"use_checkpoint","name":"use_checkpoint","type":"BOOLEAN","widget":{"name":"use_checkpoint"},"link":null},{"localized_name":"checkpoint","name":"checkpoint","type":"COMBO","widget":{"name":"checkpoint"},"link":null},{"localized_name":"clear_cache","name":"clear_cache","type":"BOOLEAN","widget":{"name":"clear_cache"},"link":null},{"localized_name":"use_lora_1","name":"use_lora_1","type":"BOOLEAN","widget":{"name":"use_lora_1"},"link":null},{"localized_name":"lora_1","name":"lora_1","type":"COMBO","widget":{"name":"lora_1"},"link":null},{"localized_name":"strength_model_1","name":"strength_model_1","type":"FLOAT","widget":{"name":"strength_model_1"},"link":null},{"localized_name":"strength_clip_1","name":"strength_clip_1","type":"FLOAT","widget":{"name":"strength_clip_1"},"link":null},{"localized_name":"use_lora_2","name":"use_lora_2","type":"BOOLEAN","widget":{"name":"use_lora_2"},"link":null},{"localized_name":"lora_2","name":"lora_2","type":"COMBO","widget":{"name":"lora_2"},"link":null},{"localized_name":"strength_model_2","name":"strength_model_2","type":"FLOAT","widget":{"name":"strength_model_2"},"link":null},{"localized_name":"strength_clip_2","name":"strength_clip_2","type":"FLOAT","widget":{"name":"strength_clip_2"},"link":null},{"localized_name":"use_lora_3","name":"use_lora_3","type":"BOOLEAN","widget":{"name":"use_lora_3"},"link":null},{"localized_name":"lora_3","name":"lora_3","type":"COMBO","widget":{"name":"lora_3"},"link":null},{"localized_name":"strength_model_3","name":"strength_model_3","type":"FLOAT","widget":{"name":"strength_model_3"},"link":null},{"localized_name":"strength_clip_3","name":"strength_clip_3","type":"FLOAT","widget":{"name":"strength_clip_3"},"link":null},{"localized_name":"use_lora_4","name":"use_lora_4","type":"BOOLEAN","widget":{"name":"use_lora_4"},"link":null},{"localized_name":"lora_4","name":"lora_4","type":"COMBO","widget":{"name":"lora_4"},"link":null},{"localized_name":"strength_model_4","name":"strength_model_4","type":"FLOAT","widget":{"name":"strength_model_4"},"link":null},{"localized_name":"strength_clip_4","name":"strength_clip_4","type":"FLOAT","widget":{"name":"strength_clip_4"},"link":null},{"localized_name":"use_lora_5","name":"use_lora_5","type":"BOOLEAN","widget":{"name":"use_lora_5"},"link":null},{"localized_name":"lora_5","name":"lora_5","type":"COMBO","widget":{"name":"lora_5"},"link":null},{"localized_name":"strength_model_5","name":"strength_model_5","type":"FLOAT","widget":{"name":"strength_model_5"},"link":null},{"localized_name":"strength_clip_5","name":"strength_clip_5","type":"FLOAT","widget":{"name":"strength_clip_5"},"link":null},{"localized_name":"use_lora_6","name":"use_lora_6","type":"BOOLEAN","widget":{"name":"use_lora_6"},"link":null},{"localized_name":"lora_6","name":"lora_6","type":"COMBO","widget":{"name":"lora_6"},"link":null},{"localized_name":"strength_model_6","name":"strength_model_6","type":"FLOAT","widget":{"name":"strength_model_6"},"link":null},{"localized_name":"strength_clip_6","name":"strength_clip_6","type":"FLOAT","widget":{"name":"strength_clip_6"},"link":null},{"localized_name":"positive_prompt","name":"positive_prompt","shape":7,"type":"STRING","widget":{"name":"positive_prompt"},"link":null},{"localized_name":"negative_prompt","name":"negative_prompt","shape":7,"type":"STRING","widget":{"name":"negative_prompt"},"link":null},{"localized_name":"clip_set_last_layer_positive","name":"clip_set_last_layer_positive","shape":7,"type":"INT","widget":{"name":"clip_set_last_layer_positive"},"link":null},{"localized_name":"clip_set_last_layer_negative","name":"clip_set_last_layer_negative","shape":7,"type":"INT","widget":{"name":"clip_set_last_layer_negative"},"link":null},{"localized_name":"recipe_slot","name":"recipe_slot","shape":7,"type":"COMBO","widget":{"name":"recipe_slot"},"link":null},{"localized_name":"recipe_save","name":"recipe_save","shape":7,"type":"BOOLEAN","widget":{"name":"recipe_save"},"link":null},{"localized_name":"recipe_use","name":"recipe_use","shape":7,"type":"BOOLEAN","widget":{"name":"recipe_use"},"link":null},{"localized_name":"standard_pipeline","name":"standard_pipeline","shape":7,"type":"BOOLEAN","widget":{"name":"standard_pipeline"},"link":null},{"localized_name":"clip_lora_pos_gain","name":"clip_lora_pos_gain","shape":7,"type":"FLOAT","widget":{"name":"clip_lora_pos_gain"},"link":null},{"localized_name":"clip_lora_neg_gain","name":"clip_lora_neg_gain","shape":7,"type":"FLOAT","widget":{"name":"clip_lora_neg_gain"},"link":null},{"localized_name":"dynamic_pos","name":"dynamic_pos","shape":7,"type":"BOOLEAN","widget":{"name":"dynamic_pos"},"link":null},{"localized_name":"dynamic_neg","name":"dynamic_neg","shape":7,"type":"BOOLEAN","widget":{"name":"dynamic_neg"},"link":null},{"localized_name":"dyn_seed","name":"dyn_seed","shape":7,"type":"INT","widget":{"name":"dyn_seed"},"link":null},{"localized_name":"dynamic_break_freeze","name":"dynamic_break_freeze","shape":7,"type":"BOOLEAN","widget":{"name":"dynamic_break_freeze"},"link":null},{"localized_name":"show_expanded_prompts","name":"show_expanded_prompts","shape":7,"type":"BOOLEAN","widget":{"name":"show_expanded_prompts"},"link":null},{"localized_name":"save_expanded_prompts","name":"save_expanded_prompts","shape":7,"type":"BOOLEAN","widget":{"name":"save_expanded_prompts"},"link":null}],"outputs":[{"localized_name":"MODEL","name":"MODEL","type":"MODEL","links":[150,151]},{"localized_name":"CLIP","name":"CLIP","type":"CLIP","links":null},{"localized_name":"Positive","name":"Positive","type":"CONDITIONING","links":[13,102]},{"localized_name":"Negative","name":"Negative","type":"CONDITIONING","links":[14,103]},{"localized_name":"VAE","name":"VAE","type":"VAE","links":[2,15,31]}],"properties":{"Node name for S&R":"MagicNodesCombiNode"},"widgets_values":[true,"7Lambda_1.3_2D_Universal.safetensors",true,true,"mg_7lambda_negative.safetensors",-1,0.2,false,"mg_7lambda_negative.safetensors",0,0,false,"mg_7lambda_negative.safetensors",0,0,false,"mg_7lambda_negative.safetensors",0,0,false,"mg_7lambda_negative.safetensors",0,0,false,"mg_7lambda_negative.safetensors",false,false,"(correct human anatomy:1).\n(masterwork:1), very aesthetic, super detailed, newest, masterpiece, amazing quality, highres, sharpen image, best quality.\n25yrs 1woman, necklace, earnings, jewelry, wrist jewelry, ponytail hair, blue hair, blue eyes, yellow kimono with floral print, holds a large pillow, purple pillow, smile, 2 hands.\nFront view, Bedroom","(super-wrong:1), wrong, worst, mistake, (anatomically incorrect human:1), wrong geometry\n(six fingers:1), pixelated,\n(bad hands:1), deformed nails, (fused fingers), (incorrecting hold in hand:1), bad fingers\nugly, (bad anatomy), junior artist, beginner's drawing, bad composition, loose, underpainting, muddy colors, broken symmetry, unclear focal point, blurry details, incorrect perspective, shaky outlines, uneven lines,\n(unsharpen eyes:1), imperfect eyes, skewed eyes, unnatural face, unnatural body, extra limb, missing limbs, distorted eyelashes, misplaced pupils, noisly eyes, long neck,\nobject clipping, clothing clipping, object intersection, unrealistic overlap, geometry intersection,\ntext, typo, signature, watermarks on image, error",-2,-2,"Off",false,false,false,0.92,1,false,false,0,false,false,false]}],"links":[[2,2,4,3,0,"VAE"],[13,2,2,14,1,"CONDITIONING"],[14,2,3,14,2,"CONDITIONING"],[15,2,4,14,3,"VAE"],[17,5,0,14,6,"CLIP_VISION"],[19,3,0,14,4,"LATENT"],[21,11,2,15,0,"IMAGE"],[22,11,1,16,0,"IMAGE"],[24,14,1,11,5,"IMAGE"],[28,17,0,11,6,"CLIP_VISION"],[31,2,4,20,0,"*"],[32,20,0,11,3,"VAE"],[37,14,0,11,4,"LATENT"],[40,24,1,25,0,"IMAGE"],[41,24,2,26,0,"IMAGE"],[46,29,2,31,0,"IMAGE"],[48,29,1,7,0,"IMAGE"],[61,24,0,29,4,"LATENT"],[62,11,0,24,4,"LATENT"],[65,24,1,29,5,"IMAGE"],[66,11,1,24,5,"IMAGE"],[85,24,1,41,0,"IMAGE"],[86,41,0,29,1,"CONDITIONING"],[87,41,1,29,2,"CONDITIONING"],[90,41,2,42,0,"IMAGE"],[92,5,0,17,0,"*"],[97,14,1,43,0,"IMAGE"],[102,2,2,43,1,"CONDITIONING"],[103,2,3,43,2,"CONDITIONING"],[106,43,1,11,2,"CONDITIONING"],[107,43,0,11,1,"CONDITIONING"],[110,11,1,44,0,"IMAGE"],[115,44,0,24,1,"CONDITIONING"],[116,44,1,24,2,"CONDITIONING"],[119,43,0,44,1,"CONDITIONING"],[120,43,1,44,2,"CONDITIONING"],[121,44,1,41,2,"CONDITIONING"],[122,44,0,41,1,"CONDITIONING"],[123,44,2,45,0,"IMAGE"],[124,43,2,46,0,"IMAGE"],[125,17,0,47,0,"*"],[126,47,0,24,6,"CLIP_VISION"],[127,47,0,48,0,"*"],[128,48,0,29,6,"CLIP_VISION"],[129,20,0,49,0,"*"],[130,49,0,24,3,"VAE"],[131,49,0,50,0,"*"],[132,50,0,29,3,"VAE"],[133,50,0,41,4,"VAE"],[134,49,0,44,4,"VAE"],[135,20,0,43,4,"VAE"],[136,19,0,11,0,"MODEL"],[137,19,0,51,0,"*"],[138,51,0,24,0,"MODEL"],[139,51,0,52,0,"*"],[140,52,0,29,0,"MODEL"],[141,14,1,53,0,"*"],[142,53,0,33,0,"IMAGE"],[143,4,0,54,0,"*"],[144,54,0,43,3,"CONTROL_NET"],[145,54,0,55,0,"*"],[146,55,0,44,3,"CONTROL_NET"],[147,55,0,56,0,"*"],[148,56,0,41,3,"CONTROL_NET"],[150,2,0,19,0,"*"],[151,2,0,58,0,"MODEL"],[152,58,0,14,0,"MODEL"]],"groups":[{"id":1,"title":"Step1 - Pre-warm","bounding":[985.2202161884722,-368.00948371274734,405.87327906278074,908.4829257340471],"color":"#a1309b","font_size":22,"flags":{}},{"id":2,"title":"Step2 - warming","bounding":[1491.7975585478357,-546.9292924524468,464.03429546870484,1238.2310938486526],"color":"#b58b2a","font_size":22,"flags":{}},{"id":3,"title":"Step3 - Pre-ready","bounding":[2110.501998988056,-553.0885338063888,518.639493048679,1245.9769642733752],"color":"#3f789e","font_size":22,"flags":{}},{"id":4,"title":"Step4 - High-res","bounding":[2836.675702344997,-548.5808647096898,526.8246054854121,1235.0376260654407],"color":"#8A8","font_size":22,"flags":{}},{"id":5,"title":"1 - Pre-warm","bounding":[912.8922171272666,734.9178623674503,505.331679024805,647.6692243308507],"color":"#a1309b","font_size":22,"flags":{}},{"id":6,"title":"2 - warming","bounding":[1478.1593316885921,734.6063030539617,505.331679024805,647.6692243308507],"color":"#b58b2a","font_size":22,"flags":{}},{"id":7,"title":"3 - Pre-ready","bounding":[2040.803574403982,730.9381266791784,683.3155821674318,849.0584001035625],"color":"#3f789e","font_size":22,"flags":{}},{"id":8,"title":"Step4 - High-res","bounding":[2771.933529470263,732.683066906069,700.3585746163794,850.3693995227123],"color":"#8A8","font_size":22,"flags":{}}],"config":{},"extra":{"ds":{"scale":1.1167815779424883,"offset":[314.5473164115067,582.999969688245]}},"version":0.4} \ No newline at end of file diff --git a/workflows/mg_SuperSimple-Workflow.json b/workflows/mg_SuperSimple-Workflow.json new file mode 100644 index 0000000000000000000000000000000000000000..dbc01bfc1d436f1698106a7b0c7f848acc4937d9 --- /dev/null +++ b/workflows/mg_SuperSimple-Workflow.json @@ -0,0 +1 @@ +{"id":"56753873-517d-40af-ae8d-5268147c2215","revision":0,"last_node_id":11,"last_link_id":13,"nodes":[{"id":3,"type":"MagicSeedLatent","pos":[512.301972673182,-160.53141723865224],"size":[270,198],"flags":{},"order":7,"mode":0,"inputs":[{"localized_name":"vae","name":"vae","shape":7,"type":"VAE","link":2},{"localized_name":"image","name":"image","shape":7,"type":"IMAGE","link":null},{"localized_name":"width","name":"width","type":"INT","widget":{"name":"width"},"link":null},{"localized_name":"height","name":"height","type":"INT","widget":{"name":"height"},"link":null},{"localized_name":"batch_size","name":"batch_size","type":"INT","widget":{"name":"batch_size"},"link":null},{"localized_name":"sigma","name":"sigma","type":"FLOAT","widget":{"name":"sigma"},"link":null},{"localized_name":"bias","name":"bias","type":"FLOAT","widget":{"name":"bias"},"link":null},{"localized_name":"mix_image","name":"mix_image","type":"BOOLEAN","widget":{"name":"mix_image"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":[1]}],"properties":{"Node name for S&R":"MagicSeedLatent"},"widgets_values":[672,944,1,0.8,0.5,false]},{"id":5,"type":"CLIPVisionLoader","pos":[506.9319719576752,211.9373321706582],"size":[270,58],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"clip_name","name":"clip_name","type":"COMBO","widget":{"name":"clip_name"},"link":null}],"outputs":[{"localized_name":"CLIP_VISION","name":"CLIP_VISION","type":"CLIP_VISION","links":[6]}],"properties":{"Node name for S&R":"CLIPVisionLoader"},"widgets_values":["open_clip_model(NS).safetensors"]},{"id":4,"type":"ControlNetLoader","pos":[511.7792692396367,92.03226884939174],"size":[250.8316192626953,70.76512908935547],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"control_net_name","name":"control_net_name","type":"COMBO","widget":{"name":"control_net_name"},"link":null}],"outputs":[{"label":"CONTROL_NET","localized_name":"CONTROL_NET","name":"CONTROL_NET","type":"CONTROL_NET","slot_index":0,"links":[5]}],"properties":{"Node name for S&R":"ControlNetLoader","models":[{"name":"control_v11p_sd15_scribble_fp16.safetensors","url":"https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_scribble_fp16.safetensors?download=true","directory":"controlnet"}]},"widgets_values":["diffusers_xl_depth_full.safetensors"]},{"id":7,"type":"SaveImage","pos":[1313.88318732331,-284.27461870524576],"size":[649.1787028832787,738.4004538255251],"flags":{},"order":9,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":9},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null}],"outputs":[],"properties":{},"widgets_values":["ComfyUI"]},{"id":9,"type":"Note","pos":[910.5361930926384,-655.0060405255866],"size":[392.52185101243265,274.0344197725986],"flags":{},"order":2,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["Choose `step_count` (2/3/4) and Run.\n\n- When \"Custom\" is off, presets fully drive parameters\n- When \"Custom\" is on, the visible CADE controls override the Step presets across all steps; Step 1 still enforces `denoise=1.0`\n "],"color":"#432","bgcolor":"#653"},{"id":8,"type":"Note","pos":[-185.13480893968315,807.6288513313478],"size":[210,139.74892023047641],"flags":{},"order":3,"mode":0,"inputs":[],"outputs":[],"title":"Negative","properties":{},"widgets_values":["(super-wrong:1) - it's a trigger negative prompt for 'mg_7lambda_negative'"],"color":"#322","bgcolor":"#533"},{"id":10,"type":"Note","pos":[-187.5189731363135,484.2103704484261],"size":[210,135.28179391870685],"flags":{},"order":4,"mode":0,"inputs":[],"outputs":[],"title":"Positive","properties":{},"widgets_values":["->"],"color":"#232","bgcolor":"#353"},{"id":2,"type":"MagicNodesCombiNode","pos":[43.7826785907816,-342.50895107491186],"size":[438.90744171577774,1665.2680405789415],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"model_in","name":"model_in","shape":7,"type":"MODEL","link":null},{"localized_name":"clip_in","name":"clip_in","shape":7,"type":"CLIP","link":null},{"localized_name":"vae_in","name":"vae_in","shape":7,"type":"VAE","link":null},{"localized_name":"positive_in","name":"positive_in","shape":7,"type":"CONDITIONING","link":null},{"localized_name":"negative_in","name":"negative_in","shape":7,"type":"CONDITIONING","link":null},{"localized_name":"use_checkpoint","name":"use_checkpoint","type":"BOOLEAN","widget":{"name":"use_checkpoint"},"link":null},{"localized_name":"checkpoint","name":"checkpoint","type":"COMBO","widget":{"name":"checkpoint"},"link":null},{"localized_name":"clear_cache","name":"clear_cache","type":"BOOLEAN","widget":{"name":"clear_cache"},"link":null},{"localized_name":"use_lora_1","name":"use_lora_1","type":"BOOLEAN","widget":{"name":"use_lora_1"},"link":null},{"localized_name":"lora_1","name":"lora_1","type":"COMBO","widget":{"name":"lora_1"},"link":null},{"localized_name":"strength_model_1","name":"strength_model_1","type":"FLOAT","widget":{"name":"strength_model_1"},"link":null},{"localized_name":"strength_clip_1","name":"strength_clip_1","type":"FLOAT","widget":{"name":"strength_clip_1"},"link":null},{"localized_name":"use_lora_2","name":"use_lora_2","type":"BOOLEAN","widget":{"name":"use_lora_2"},"link":null},{"localized_name":"lora_2","name":"lora_2","type":"COMBO","widget":{"name":"lora_2"},"link":null},{"localized_name":"strength_model_2","name":"strength_model_2","type":"FLOAT","widget":{"name":"strength_model_2"},"link":null},{"localized_name":"strength_clip_2","name":"strength_clip_2","type":"FLOAT","widget":{"name":"strength_clip_2"},"link":null},{"localized_name":"use_lora_3","name":"use_lora_3","type":"BOOLEAN","widget":{"name":"use_lora_3"},"link":null},{"localized_name":"lora_3","name":"lora_3","type":"COMBO","widget":{"name":"lora_3"},"link":null},{"localized_name":"strength_model_3","name":"strength_model_3","type":"FLOAT","widget":{"name":"strength_model_3"},"link":null},{"localized_name":"strength_clip_3","name":"strength_clip_3","type":"FLOAT","widget":{"name":"strength_clip_3"},"link":null},{"localized_name":"use_lora_4","name":"use_lora_4","type":"BOOLEAN","widget":{"name":"use_lora_4"},"link":null},{"localized_name":"lora_4","name":"lora_4","type":"COMBO","widget":{"name":"lora_4"},"link":null},{"localized_name":"strength_model_4","name":"strength_model_4","type":"FLOAT","widget":{"name":"strength_model_4"},"link":null},{"localized_name":"strength_clip_4","name":"strength_clip_4","type":"FLOAT","widget":{"name":"strength_clip_4"},"link":null},{"localized_name":"use_lora_5","name":"use_lora_5","type":"BOOLEAN","widget":{"name":"use_lora_5"},"link":null},{"localized_name":"lora_5","name":"lora_5","type":"COMBO","widget":{"name":"lora_5"},"link":null},{"localized_name":"strength_model_5","name":"strength_model_5","type":"FLOAT","widget":{"name":"strength_model_5"},"link":null},{"localized_name":"strength_clip_5","name":"strength_clip_5","type":"FLOAT","widget":{"name":"strength_clip_5"},"link":null},{"localized_name":"use_lora_6","name":"use_lora_6","type":"BOOLEAN","widget":{"name":"use_lora_6"},"link":null},{"localized_name":"lora_6","name":"lora_6","type":"COMBO","widget":{"name":"lora_6"},"link":null},{"localized_name":"strength_model_6","name":"strength_model_6","type":"FLOAT","widget":{"name":"strength_model_6"},"link":null},{"localized_name":"strength_clip_6","name":"strength_clip_6","type":"FLOAT","widget":{"name":"strength_clip_6"},"link":null},{"localized_name":"positive_prompt","name":"positive_prompt","shape":7,"type":"STRING","widget":{"name":"positive_prompt"},"link":null},{"localized_name":"negative_prompt","name":"negative_prompt","shape":7,"type":"STRING","widget":{"name":"negative_prompt"},"link":null},{"localized_name":"clip_set_last_layer_positive","name":"clip_set_last_layer_positive","shape":7,"type":"INT","widget":{"name":"clip_set_last_layer_positive"},"link":null},{"localized_name":"clip_set_last_layer_negative","name":"clip_set_last_layer_negative","shape":7,"type":"INT","widget":{"name":"clip_set_last_layer_negative"},"link":null},{"localized_name":"recipe_slot","name":"recipe_slot","shape":7,"type":"COMBO","widget":{"name":"recipe_slot"},"link":null},{"localized_name":"recipe_save","name":"recipe_save","shape":7,"type":"BOOLEAN","widget":{"name":"recipe_save"},"link":null},{"localized_name":"recipe_use","name":"recipe_use","shape":7,"type":"BOOLEAN","widget":{"name":"recipe_use"},"link":null},{"localized_name":"standard_pipeline","name":"standard_pipeline","shape":7,"type":"BOOLEAN","widget":{"name":"standard_pipeline"},"link":null},{"localized_name":"clip_lora_pos_gain","name":"clip_lora_pos_gain","shape":7,"type":"FLOAT","widget":{"name":"clip_lora_pos_gain"},"link":null},{"localized_name":"clip_lora_neg_gain","name":"clip_lora_neg_gain","shape":7,"type":"FLOAT","widget":{"name":"clip_lora_neg_gain"},"link":null},{"localized_name":"dynamic_pos","name":"dynamic_pos","shape":7,"type":"BOOLEAN","widget":{"name":"dynamic_pos"},"link":null},{"localized_name":"dynamic_neg","name":"dynamic_neg","shape":7,"type":"BOOLEAN","widget":{"name":"dynamic_neg"},"link":null},{"localized_name":"dyn_seed","name":"dyn_seed","shape":7,"type":"INT","widget":{"name":"dyn_seed"},"link":null},{"localized_name":"dynamic_break_freeze","name":"dynamic_break_freeze","shape":7,"type":"BOOLEAN","widget":{"name":"dynamic_break_freeze"},"link":null},{"localized_name":"show_expanded_prompts","name":"show_expanded_prompts","shape":7,"type":"BOOLEAN","widget":{"name":"show_expanded_prompts"},"link":null},{"localized_name":"save_expanded_prompts","name":"save_expanded_prompts","shape":7,"type":"BOOLEAN","widget":{"name":"save_expanded_prompts"},"link":null}],"outputs":[{"localized_name":"MODEL","name":"MODEL","type":"MODEL","links":[12]},{"localized_name":"CLIP","name":"CLIP","type":"CLIP","links":null},{"localized_name":"Positive","name":"Positive","type":"CONDITIONING","links":[3]},{"localized_name":"Negative","name":"Negative","type":"CONDITIONING","links":[4]},{"localized_name":"VAE","name":"VAE","type":"VAE","links":[2,10]}],"properties":{"Node name for S&R":"MagicNodesCombiNode"},"widgets_values":[true,"7Lambda_1.3_2D_Universal.safetensors",false,true,"mg_7lambda_negative.safetensors",-1,0.2,false,"mg_7lambda_negative.safetensors",0,0,false,"mg_7lambda_negative.safetensors",0,0,false,"mg_7lambda_negative.safetensors",0,0,false,"mg_7lambda_negative.safetensors",0,0,false,"mg_7lambda_negative.safetensors",false,false,"(correct human anatomy:1).\n(masterwork:1), very aesthetic, super detailed, newest, masterpiece, amazing quality, highres, sharpen image, best quality.\n25yrs 1woman, necklace, earnings, jewelry, wrist jewelry, ponytail hair, blue hair, blue eyes, yellow kimono with floral print, holds a large pillow, purple pillow, smile, 2 hands.\nFront view, Bedroom","(super-wrong:1), wrong, worst, mistake, (anatomically incorrect human:1), wrong geometry\n(six fingers:1), pixelated,\n(bad hands:1), deformed nails, (fused fingers), (incorrecting hold in hand:1), bad fingers\nugly, (bad anatomy), junior artist, beginner's drawing, bad composition, loose, underpainting, muddy colors, broken symmetry, unclear focal point, blurry details, incorrect perspective, shaky outlines, uneven lines,\n(unsharpen eyes:1), imperfect eyes, skewed eyes, unnatural face, unnatural body, extra limb, missing limbs, distorted eyelashes, misplaced pupils, noisly eyes, long neck,\nobject clipping, clothing clipping, object intersection, unrealistic overlap, geometry intersection,\ntext, typo, signature, watermarks on image, error",-2,-2,"Off",false,false,false,0.92,0.2,false,false,0,false,false,false]},{"id":11,"type":"PatchSageAttention","pos":[513.1090242917288,-276.13824745203897],"size":[270,58],"flags":{},"order":6,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":12},{"localized_name":"sage_attention","name":"sage_attention","type":"COMBO","widget":{"name":"sage_attention"},"link":null}],"outputs":[{"localized_name":"MODEL","name":"MODEL","type":"MODEL","links":[13]}],"properties":{"Node name for S&R":"PatchSageAttention"},"widgets_values":["auto_quality"]},{"id":1,"type":"MG_SuperSimple","pos":[967.5636043818711,-308.67868061536035],"size":[270,414],"flags":{},"order":8,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":13},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":3},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":4},{"localized_name":"vae","name":"vae","type":"VAE","link":10},{"localized_name":"latent","name":"latent","type":"LATENT","link":1},{"localized_name":"control_net","name":"control_net","type":"CONTROL_NET","link":5},{"localized_name":"reference_image","name":"reference_image","shape":7,"type":"IMAGE","link":null},{"localized_name":"clip_vision","name":"clip_vision","shape":7,"type":"CLIP_VISION","link":6},{"localized_name":"step_count","name":"step_count","type":"INT","widget":{"name":"step_count"},"link":null},{"localized_name":"custom","name":"custom","type":"BOOLEAN","widget":{"name":"custom"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"cfg","name":"cfg","type":"FLOAT","widget":{"name":"cfg"},"link":null},{"localized_name":"denoise","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null},{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null},{"localized_name":"clipseg_text","name":"clipseg_text","type":"STRING","widget":{"name":"clipseg_text"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":null},{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[9]}],"properties":{"Node name for S&R":"MG_SuperSimple"},"widgets_values":[4,false,824025253278145,"randomize",30,4.5,0.65,"ddim","MGHybrid","hand, feet, face"]}],"links":[[1,3,0,1,4,"LATENT"],[2,2,4,3,0,"VAE"],[3,2,2,1,1,"CONDITIONING"],[4,2,3,1,2,"CONDITIONING"],[5,4,0,1,5,"CONTROL_NET"],[6,5,0,1,7,"CLIP_VISION"],[9,1,1,7,0,"IMAGE"],[10,2,4,1,3,"VAE"],[12,2,0,11,0,"MODEL"],[13,11,0,1,0,"MODEL"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.7400249944258689,"offset":[289.8997630357029,579.3737880920758]}},"version":0.4} \ No newline at end of file