DZRobo
commited on
Initial project structure and core files added (#1)
Browse filesAdd MagicNodes repository with documentation, licensing, assets, workflows, presets, and main Python modules for Easy/Hard nodes, CADE 2.5, ControlFusion, QSilk, and supporting utilities. Includes sample images, negative LoRA, Depth Anything v2 vendor code, and setup scripts for ComfyUI integration.
This view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +8 -0
- .github/CODEOWNERS +9 -0
- .github/FUNDING.yml +5 -0
- .github/ISSUE_TEMPLATE/bug_report.yml +67 -0
- .github/ISSUE_TEMPLATE/config.yml +8 -0
- .github/ISSUE_TEMPLATE/feature_request.yml +39 -0
- .github/ISSUE_TEMPLATE/question.yml +20 -0
- .github/PULL_REQUEST_TEMPLATE.md +28 -0
- .github/workflows/hf-mirror.yml +41 -0
- .gitignore +35 -0
- CITATION.cff +35 -0
- CREDITS.md +14 -0
- LICENSE +19 -0
- NOTICE +6 -0
- README.md +322 -0
- __init__.py +97 -0
- assets/Anime1.jpg +3 -0
- assets/Anime1_crop.jpg +3 -0
- assets/Dog1_crop_ours_CADE25_QSilk.jpg +3 -0
- assets/Dog1_ours_CADE25_QSilk.jpg +3 -0
- assets/MagicNodes.png +3 -0
- assets/PhotoCup1.jpg +3 -0
- assets/PhotoCup1_crop.jpg +3 -0
- assets/PhotoPortrait1.jpg +3 -0
- assets/PhotoPortrait1_crop1.jpg +3 -0
- assets/PhotoPortrait1_crop2.jpg +3 -0
- assets/PhotoPortrait1_crop3.jpg +3 -0
- depth-anything/place depth model here +0 -0
- docs/EasyNodes.md +54 -0
- docs/HardNodes.md +11 -0
- docs/hard/CADE25.md +72 -0
- docs/hard/ControlFusion.md +70 -0
- docs/hard/IDS.md +20 -0
- docs/hard/UpscaleModule.md +23 -0
- docs/hard/ZeSmartSampler.md +22 -0
- init +0 -1
- mod/__init__.py +8 -0
- mod/easy/__init__.py +8 -0
- mod/easy/mg_cade25_easy.py +0 -0
- mod/easy/mg_controlfusion_easy.py +611 -0
- mod/easy/mg_supersimple_easy.py +148 -0
- mod/easy/preset_loader.py +115 -0
- mod/hard/__init__.py +9 -0
- mod/hard/mg_adaptive.py +39 -0
- mod/hard/mg_cade25.py +1864 -0
- mod/hard/mg_controlfusion.py +519 -0
- mod/hard/mg_ids.py +67 -0
- mod/hard/mg_upscale_module.py +72 -0
- mod/hard/mg_zesmart_sampler_v1_1.py +210 -0
- mod/mg_combinode.py +448 -0
.gitattributes
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 2 |
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 3 |
*.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
* text=auto
|
| 2 |
+
|
| 3 |
+
# Track large model files with Git LFS
|
| 4 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 9 |
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 10 |
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 11 |
*.png filter=lfs diff=lfs merge=lfs -text
|
.github/CODEOWNERS
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Code owners for MagicNodes
|
| 2 |
+
# Docs: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
| 3 |
+
|
| 4 |
+
# Default owner for the whole repository
|
| 5 |
+
* @1dZb1
|
| 6 |
+
|
| 7 |
+
# (Optional)
|
| 8 |
+
# /docs/ @1dZb1
|
| 9 |
+
# /mod/ @1dZb1
|
.github/FUNDING.yml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
github: 1dZb1
|
| 2 |
+
buy_me_a_coffee: dzrobo
|
| 3 |
+
custom:
|
| 4 |
+
- https://buymeacoffee.com/dzrobo
|
| 5 |
+
- https://github.com/sponsors/1dZb1
|
.github/ISSUE_TEMPLATE/bug_report.yml
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "Bug report"
|
| 2 |
+
description: "Something is broken or not working as expected"
|
| 3 |
+
title: "[Bug] <short summary>"
|
| 4 |
+
labels: [bug]
|
| 5 |
+
body:
|
| 6 |
+
- type: checkboxes
|
| 7 |
+
id: preflight
|
| 8 |
+
attributes:
|
| 9 |
+
label: Pre‑flight
|
| 10 |
+
description: Please confirm you checked these first
|
| 11 |
+
options:
|
| 12 |
+
- label: I searched existing issues and discussions
|
| 13 |
+
required: true
|
| 14 |
+
- label: I’m on the latest MagicNodes commit and preset files
|
| 15 |
+
required: false
|
| 16 |
+
- type: textarea
|
| 17 |
+
id: summary
|
| 18 |
+
attributes:
|
| 19 |
+
label: Summary
|
| 20 |
+
description: What happened? What did you expect instead?
|
| 21 |
+
placeholder: A clear and concise description of the issue
|
| 22 |
+
validations:
|
| 23 |
+
required: true
|
| 24 |
+
- type: textarea
|
| 25 |
+
id: repro
|
| 26 |
+
attributes:
|
| 27 |
+
label: Steps to reproduce
|
| 28 |
+
description: Minimal steps / workflow to reproduce the problem
|
| 29 |
+
placeholder: |
|
| 30 |
+
1. Preset/step/config used
|
| 31 |
+
2. Node settings (seed/steps/cfg/denoise/sampler)
|
| 32 |
+
3. What you observed
|
| 33 |
+
validations:
|
| 34 |
+
required: true
|
| 35 |
+
- type: textarea
|
| 36 |
+
id: env
|
| 37 |
+
attributes:
|
| 38 |
+
label: Environment
|
| 39 |
+
description: OS/GPU/driver and versions
|
| 40 |
+
placeholder: |
|
| 41 |
+
OS: Windows 11 / Linux
|
| 42 |
+
GPU: RTX 4090 (driver 560.xx)
|
| 43 |
+
Python: 3.10.x | PyTorch: 2.8.0+cu129
|
| 44 |
+
ComfyUI: <commit/date>
|
| 45 |
+
MagicNodes: <commit>
|
| 46 |
+
validations:
|
| 47 |
+
required: false
|
| 48 |
+
- type: textarea
|
| 49 |
+
id: logs
|
| 50 |
+
attributes:
|
| 51 |
+
label: Logs / Screenshots
|
| 52 |
+
description: Paste relevant logs, stack traces, or attach screenshots
|
| 53 |
+
render: shell
|
| 54 |
+
validations:
|
| 55 |
+
required: false
|
| 56 |
+
- type: dropdown
|
| 57 |
+
id: severity
|
| 58 |
+
attributes:
|
| 59 |
+
label: Impact
|
| 60 |
+
options:
|
| 61 |
+
- Crash/blocks generation
|
| 62 |
+
- Wrong output/quality regression
|
| 63 |
+
- UI/Docs glitch
|
| 64 |
+
- Minor inconvenience
|
| 65 |
+
validations:
|
| 66 |
+
required: false
|
| 67 |
+
|
.github/ISSUE_TEMPLATE/config.yml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
blank_issues_enabled: false
|
| 2 |
+
contact_links:
|
| 3 |
+
- name: Q&A and ideas (Discussions)
|
| 4 |
+
url: https://github.com/1dZb1/MagicNodes/discussions
|
| 5 |
+
about: Ask questions and share ideas in Discussions
|
| 6 |
+
- name: Hugging Face page
|
| 7 |
+
url: https://huggingface.co/DD32/MagicNodes
|
| 8 |
+
about: Releases and mirrors on HF
|
.github/ISSUE_TEMPLATE/feature_request.yml
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "Feature request"
|
| 2 |
+
description: "Request an enhancement or new capability"
|
| 3 |
+
title: "[Feat] <short summary>"
|
| 4 |
+
labels: [enhancement]
|
| 5 |
+
body:
|
| 6 |
+
- type: textarea
|
| 7 |
+
id: problem
|
| 8 |
+
attributes:
|
| 9 |
+
label: Problem / motivation
|
| 10 |
+
description: What use‑case does this solve? Why is it valuable?
|
| 11 |
+
validations:
|
| 12 |
+
required: true
|
| 13 |
+
- type: textarea
|
| 14 |
+
id: proposal
|
| 15 |
+
attributes:
|
| 16 |
+
label: Proposed solution
|
| 17 |
+
description: API/UI/UX draft, presets, examples
|
| 18 |
+
placeholder: Describe the change and how it would work
|
| 19 |
+
validations:
|
| 20 |
+
required: true
|
| 21 |
+
- type: textarea
|
| 22 |
+
id: alternatives
|
| 23 |
+
attributes:
|
| 24 |
+
label: Alternatives considered
|
| 25 |
+
description: Any workarounds or different approaches
|
| 26 |
+
validations:
|
| 27 |
+
required: false
|
| 28 |
+
- type: checkboxes
|
| 29 |
+
id: scope
|
| 30 |
+
attributes:
|
| 31 |
+
label: Scope
|
| 32 |
+
options:
|
| 33 |
+
- label: Easy nodes / presets
|
| 34 |
+
- label: Hard nodes / advanced params
|
| 35 |
+
- label: Docs / examples / workflows
|
| 36 |
+
- label: Performance / memory
|
| 37 |
+
validations:
|
| 38 |
+
required: false
|
| 39 |
+
|
.github/ISSUE_TEMPLATE/question.yml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "Question / Help"
|
| 2 |
+
description: "Ask a question about usage, presets, or behavior"
|
| 3 |
+
title: "[Q] <short summary>"
|
| 4 |
+
labels: [question]
|
| 5 |
+
body:
|
| 6 |
+
- type: textarea
|
| 7 |
+
id: question
|
| 8 |
+
attributes:
|
| 9 |
+
label: Your question
|
| 10 |
+
description: What do you want to understand or achieve?
|
| 11 |
+
validations:
|
| 12 |
+
required: true
|
| 13 |
+
- type: textarea
|
| 14 |
+
id: context
|
| 15 |
+
attributes:
|
| 16 |
+
label: Context
|
| 17 |
+
description: Share your preset, node settings, or screenshot if helpful
|
| 18 |
+
validations:
|
| 19 |
+
required: false
|
| 20 |
+
|
.github/PULL_REQUEST_TEMPLATE.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!--
|
| 2 |
+
Thank you for your contribution to MagicNodes!
|
| 3 |
+
Please provide a clear summary and the minimal context to review safely.
|
| 4 |
+
-->
|
| 5 |
+
|
| 6 |
+
Title: <short, action‑oriented summary>
|
| 7 |
+
|
| 8 |
+
What
|
| 9 |
+
- Briefly describe the change (user‑facing behavior, docs, presets, or internals).
|
| 10 |
+
|
| 11 |
+
Why
|
| 12 |
+
- What problem does it solve? Link issues/discussions if applicable.
|
| 13 |
+
|
| 14 |
+
How (high level)
|
| 15 |
+
- Outline the approach. Call out any trade‑offs.
|
| 16 |
+
|
| 17 |
+
Test plan
|
| 18 |
+
- Steps or screenshots proving it works (minimal workflow, seed/steps/cfg if relevant).
|
| 19 |
+
|
| 20 |
+
Checklist
|
| 21 |
+
- [ ] Builds/runs locally with default presets
|
| 22 |
+
- [ ] Docs/README updated if behavior changed
|
| 23 |
+
- [ ] No large binaries added (weights go to HF or Releases)
|
| 24 |
+
- [ ] Passes lint/format (if configured)
|
| 25 |
+
|
| 26 |
+
Notes for reviewers
|
| 27 |
+
- Anything sensitive or risky to double‑check (paths, presets, defaults).
|
| 28 |
+
|
.github/workflows/hf-mirror.yml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Mirror to Hugging Face
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [main]
|
| 6 |
+
workflow_dispatch:
|
| 7 |
+
|
| 8 |
+
concurrency:
|
| 9 |
+
group: hf-mirror-${{ github.ref }}
|
| 10 |
+
cancel-in-progress: true
|
| 11 |
+
|
| 12 |
+
jobs:
|
| 13 |
+
mirror:
|
| 14 |
+
runs-on: ubuntu-latest
|
| 15 |
+
steps:
|
| 16 |
+
- name: Checkout (full history)
|
| 17 |
+
uses: actions/checkout@v4
|
| 18 |
+
with:
|
| 19 |
+
fetch-depth: 0
|
| 20 |
+
lfs: false
|
| 21 |
+
|
| 22 |
+
- name: Configure git identity
|
| 23 |
+
run: |
|
| 24 |
+
git config user.name "github-actions[bot]"
|
| 25 |
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
| 26 |
+
|
| 27 |
+
- name: Push to HF mirror
|
| 28 |
+
env:
|
| 29 |
+
HF_USERNAME: ${{ secrets.HF_USERNAME }}
|
| 30 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 31 |
+
HF_REPO: MagicNodes
|
| 32 |
+
run: |
|
| 33 |
+
if [ -z "${HF_USERNAME}" ] || [ -z "${HF_TOKEN}" ]; then
|
| 34 |
+
echo "HF secrets are missing. Set HF_USERNAME and HF_TOKEN in repo secrets." >&2
|
| 35 |
+
exit 1
|
| 36 |
+
fi
|
| 37 |
+
git lfs install || true
|
| 38 |
+
git remote add hf "https://${HF_USERNAME}:${HF_TOKEN}@huggingface.co/${HF_USERNAME}/${HF_REPO}.git" 2>/dev/null || true
|
| 39 |
+
# Mirror current branch to HF main
|
| 40 |
+
git push --force --prune hf HEAD:main
|
| 41 |
+
|
.gitignore
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / cache
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*.pyo
|
| 5 |
+
|
| 6 |
+
# Virtual envs / tooling
|
| 7 |
+
.venv/
|
| 8 |
+
venv/
|
| 9 |
+
.env
|
| 10 |
+
.env.*
|
| 11 |
+
.idea/
|
| 12 |
+
.vscode/
|
| 13 |
+
|
| 14 |
+
# OS junk
|
| 15 |
+
.DS_Store
|
| 16 |
+
Thumbs.db
|
| 17 |
+
|
| 18 |
+
# Build/output/temp
|
| 19 |
+
dist/
|
| 20 |
+
build/
|
| 21 |
+
out/
|
| 22 |
+
*.log
|
| 23 |
+
*.tmp
|
| 24 |
+
temp/
|
| 25 |
+
temp_patch.diff
|
| 26 |
+
|
| 27 |
+
# NOTE: We intentionally keep model weights in repo via Git LFS.
|
| 28 |
+
# If you prefer not to ship them, re-add ignores for models/** and weight extensions.
|
| 29 |
+
|
| 30 |
+
# ComfyUI caches
|
| 31 |
+
**/web/tmp/
|
| 32 |
+
**/web/dist/
|
| 33 |
+
|
| 34 |
+
# Node / front-end (if any submodules appear later)
|
| 35 |
+
node_modules/
|
CITATION.cff
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
cff-version: 1.2.0
|
| 2 |
+
message: If you use CADE 2.5 / MG_SuperSimple, please cite our preprints (ZeResFDG and QSilk).
|
| 3 |
+
title: "MagicNodes: CADE 2.5 (ZeResFDG and QSilk)"
|
| 4 |
+
authors:
|
| 5 |
+
- family-names: Rychkovskiy
|
| 6 |
+
given-names: Denis
|
| 7 |
+
alias: DZRobo
|
| 8 |
+
version: preprint
|
| 9 |
+
date-released: 2025-10-11
|
| 10 |
+
repository-code: https://github.com/1dZb1/MagicNodes
|
| 11 |
+
url: https://huggingface.co/DD32/MagicNodes
|
| 12 |
+
|
| 13 |
+
preferred-citation:
|
| 14 |
+
type: article
|
| 15 |
+
title: CADE 2.5: ZeResFDG - Frequency-Decoupled, Rescaled and Zero-Projected Guidance for SD/SDXL Latent Diffusion Models
|
| 16 |
+
authors:
|
| 17 |
+
- family-names: Rychkovskiy
|
| 18 |
+
given-names: Denis
|
| 19 |
+
year: 2025
|
| 20 |
+
journal: arXiv
|
| 21 |
+
identifiers:
|
| 22 |
+
- type: url
|
| 23 |
+
value: https://arxiv.org/abs/2510.12954
|
| 24 |
+
|
| 25 |
+
references:
|
| 26 |
+
- type: article
|
| 27 |
+
title: QSilk: Micrograin Stabilization and Adaptive Quantile Clipping for Detail-Friendly Latent Diffusion
|
| 28 |
+
authors:
|
| 29 |
+
- family-names: Rychkovskiy
|
| 30 |
+
given-names: Denis
|
| 31 |
+
year: 2025
|
| 32 |
+
journal: arXiv
|
| 33 |
+
identifiers:
|
| 34 |
+
- type: url
|
| 35 |
+
value: https://arxiv.org/abs/2510.15761
|
CREDITS.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Credits and Attributions
|
| 2 |
+
|
| 3 |
+
This project includes adapted code and ideas from:
|
| 4 |
+
|
| 5 |
+
- KJ-Nodes — ComfyUI-KJNodes (GPL-3.0) by KJ (kijai)
|
| 6 |
+
Repository: https://github.com/kijai/ComfyUI-KJNodes
|
| 7 |
+
Usage: SageAttention integration and attention override approach in mod/mg_sagpu_attention.py.
|
| 8 |
+
|
| 9 |
+
- ComfyUI (GPL-3.0+)
|
| 10 |
+
Source idea: early beta node "Mahiro is so cute that she deserves a better guidance function!! (。・ω・。)"
|
| 11 |
+
Repository: https://github.com/comfyanonymous/ComfyUI
|
| 12 |
+
Usage: inspiration for directional post‑mix ("Muse Blend"); implementation rewritten and expanded in MagicNodes.
|
| 13 |
+
|
| 14 |
+
Licensing note: Under GPLv3 §13, the combined work is distributed under AGPL-3.0-or-later. See LICENSE.
|
LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MagicNodes
|
| 2 |
+
SPDX-License-Identifier: AGPL-3.0-or-later
|
| 3 |
+
|
| 4 |
+
This project is free software: you can redistribute it and/or modify
|
| 5 |
+
it under the terms of the GNU Affero General Public License as
|
| 6 |
+
published by the Free Software Foundation, either version 3 of the
|
| 7 |
+
License, or (at your option) any later version.
|
| 8 |
+
|
| 9 |
+
The full text of the GNU Affero General Public License v3.0 is available at:
|
| 10 |
+
https://www.gnu.org/licenses/agpl-3.0.txt
|
| 11 |
+
|
| 12 |
+
You should have received a copy of the GNU Affero General Public License
|
| 13 |
+
along with this program. If not, see https://www.gnu.org/licenses/.
|
| 14 |
+
|
| 15 |
+
Copyright (C) 2025 MagicNodes contributors
|
| 16 |
+
|
| 17 |
+
Third-party notices: Portions of this project are derived from third-party code.
|
| 18 |
+
See CREDITS.md for attribution and links to original repositories.
|
| 19 |
+
|
NOTICE
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Attribution (kind request)
|
| 2 |
+
|
| 3 |
+
Includes CADE 2.5 (ZeResFDG) by Denis Rychkovskiy (“DZRobo”).
|
| 4 |
+
|
| 5 |
+
If you use this work or parts of it, please consider preserving this notice
|
| 6 |
+
in your README/About or documentation.
|
README.md
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MagicNodes — ComfyUI Render Pipeline (SD/SDXL)
|
| 2 |
+
Simple start. Expert-grade results. Reliable detail.
|
| 3 |
+
[](https://arxiv.org/abs/2510.12954) / [](https://arxiv.org/pdf/2510.15761)
|
| 4 |
+
|
| 5 |
+
<table>
|
| 6 |
+
<tr>
|
| 7 |
+
<td width="140" valign="top">
|
| 8 |
+
<img src="assets/MagicNodes.png" alt="MagicNodes" width="120" />
|
| 9 |
+
</td>
|
| 10 |
+
<td>
|
| 11 |
+
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.
|
| 12 |
+
|
| 13 |
+
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.
|
| 14 |
+
</tr>
|
| 15 |
+
</table>
|
| 16 |
+
|
| 17 |
+
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.
|
| 18 |
+
|
| 19 |
+
Draw
|
| 20 |
+
<div align="center">
|
| 21 |
+
<img src="assets/Anime1.jpg" alt="Anime full" width="39%" />
|
| 22 |
+
<img src="assets/Anime1_crop.jpg" alt="Anime crop" width="39%" />
|
| 23 |
+
|
| 24 |
+
</div>
|
| 25 |
+
|
| 26 |
+
Photo Portrait
|
| 27 |
+
<div align="center">
|
| 28 |
+
<img src="assets/PhotoPortrait1.jpg" alt="Photo A" width="39%" />
|
| 29 |
+
<img src="assets/PhotoPortrait1_crop1.jpg" alt="Photo B" width="39%" />
|
| 30 |
+
</div>
|
| 31 |
+
<div align="center">
|
| 32 |
+
<img src="assets/PhotoPortrait1_crop2.jpg" alt="Photo C" width="39%" />
|
| 33 |
+
<img src="assets/PhotoPortrait1_crop3.jpg" alt="Photo D" width="39%" />
|
| 34 |
+
</div>
|
| 35 |
+
|
| 36 |
+
Photo Cup
|
| 37 |
+
<div align="center">
|
| 38 |
+
<img src="assets/PhotoCup1.jpg" alt="Photo A" width="39%" />
|
| 39 |
+
<img src="assets/PhotoCup1_crop.jpg" alt="Photo B" width="39%" />
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
Photo Dog
|
| 43 |
+
<div align="center">
|
| 44 |
+
<img src="assets/Dog1_crop_ours_CADE25_QSilk.jpg" alt="Photo A" width="39%" />
|
| 45 |
+
<img src="assets/Dog1_ours_CADE25_QSilk.jpg" alt="Photo B" width="39%" />
|
| 46 |
+
</div>
|
| 47 |
+
|
| 48 |
+
---
|
| 49 |
+
|
| 50 |
+
## Features
|
| 51 |
+
- ZeResFDG: LF/HF split, energy rescale, and zero-projection (stable early, sharp late)
|
| 52 |
+
- NAG (Normalized Attention Guidance): small attention variance normalization (positive branch)
|
| 53 |
+
- Local spatial gating: optional CLIPSeg masks for faces/hands/pose
|
| 54 |
+
- EPS scale: small early-step exposure bias
|
| 55 |
+
- 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.
|
| 56 |
+
- 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.
|
| 57 |
+
- 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
|
| 58 |
+
- 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
|
| 59 |
+
- Muse Blend and Polish: directional post-mix and final low-frequency-preserving clean-up
|
| 60 |
+
- 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`.
|
| 61 |
+
<b>I highly recommend working with SmartSeed.</b>
|
| 62 |
+
- CADE2.5 pipeline does not just upscale the image, it iterates and adds small details, doing it carefully, at every stage.
|
| 63 |
+
|
| 64 |
+
## Hardware Requirements
|
| 65 |
+
- 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.
|
| 66 |
+
- System RAM: ~12-20 GB during generation (depends on start latent and whether Depth/ControlFusion are enabled). 16+ GB recommended.
|
| 67 |
+
- Notes
|
| 68 |
+
- Lowering the starting latent (e.g., 512x768) reduces both VRAM and RAM.
|
| 69 |
+
- Disabling hi-res depth/edges (ControlFusion) reduces peaks. (not recommended!)
|
| 70 |
+
- Depth weights add a bit of RAM on load; models live under `depth-anything/`.
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
## Install (ComfyUI 0.3.60, tested on this version)
|
| 74 |
+
Preparing:
|
| 75 |
+
I recomend update pytorch version: 2.8.0+cu129.
|
| 76 |
+
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`
|
| 77 |
+
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
|
| 78 |
+
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.
|
| 79 |
+
|
| 80 |
+
Next:
|
| 81 |
+
1. Clone or download this repo into `ComfyUI/custom_nodes/`
|
| 82 |
+
2. Install helpers: `pip install -r requirements.txt`
|
| 83 |
+
3. Take my negative LoRA `models/LoRA/mg_7lambda_negative.safetensors` and place the file in ComfyUI, to `ComfyUI/models/loras`
|
| 84 |
+
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.
|
| 85 |
+
5. Workflows
|
| 86 |
+
Folder `workflows/` contains ready-to-use graphs:
|
| 87 |
+
- `mg_SuperSimple-Workflow.json` — one-node pipeline (2/3/4 steps) with presets
|
| 88 |
+
- `mg_Easy-Workflow.json` — the same logic built from individual Easy nodes
|
| 89 |
+
You can save this workflow to ComfyUI `ComfyUI\user\default\workflows`
|
| 90 |
+
6. Restart ComfyUI. Nodes appear under the "MagicNodes" categories.
|
| 91 |
+
|
| 92 |
+
💥 I strongly recommend use `mg_Easy-Workflow` workflow + default settings + your model and my negative LoRA `mg_7lambda_negative.safetensors`, for best result.
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
## 🚀 "One-Node" Quickstart (MG_SuperSimple)
|
| 96 |
+
Start with `MG_SuperSimple` for the easiest path:
|
| 97 |
+
1. Drop `MG_SuperSimple` into the graph
|
| 98 |
+
2. Connect `model / positive / negative / vae / latent` and a `Load ControlNet Model` module
|
| 99 |
+
3. Choose `step_count` (2/3/4) and Run
|
| 100 |
+
|
| 101 |
+
or load `mg_SuperSimple-Workflow` in panel ComfyUI
|
| 102 |
+
|
| 103 |
+
Notes:
|
| 104 |
+
- When "Custom" is off, presets fully drive parameters
|
| 105 |
+
- When "Custom" is on, the visible CADE controls override the Step presets across all steps; Step 1 still enforces `denoise=1.0`
|
| 106 |
+
- CLIP Vision (if connected) is applied from Step 2 onward; if no reference image is provided, SuperSimple uses the previous step image as reference
|
| 107 |
+
|
| 108 |
+
## ❗Tips
|
| 109 |
+
(!) 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.
|
| 110 |
+
|
| 111 |
+
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.
|
| 112 |
+
|
| 113 |
+
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.
|
| 114 |
+
|
| 115 |
+
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.
|
| 116 |
+
|
| 117 |
+
3) Samplers: i recomend use `ddim` for many cases (Draw and Realism style). Scheduler: use `MGHybrid` in this pipeline.
|
| 118 |
+
|
| 119 |
+
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.
|
| 120 |
+
|
| 121 |
+
5) If you see unwanted artifacts on the final (4th) step, slightly lower denoise to ~0.5–0.6 or simply change the seed.
|
| 122 |
+
|
| 123 |
+
6) You can get interesting results by repeating steps (in Easy/Hard workflows), e.g., `1 -> 2 -> 3 -> 3`. Just experiment with it!
|
| 124 |
+
|
| 125 |
+
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.
|
| 126 |
+
|
| 127 |
+
8) Unlucky seeds happen — just try another. (We may later add stabilization to this process.)
|
| 128 |
+
|
| 129 |
+
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.
|
| 130 |
+
|
| 131 |
+
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.
|
| 132 |
+
|
| 133 |
+
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.
|
| 134 |
+
|
| 135 |
+
12) Try connecting reference images in either workflow — you can get unusual and interesting outcomes.
|
| 136 |
+
|
| 137 |
+
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`.
|
| 138 |
+
|
| 139 |
+
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: <number>`. Use any non‑zero seed for fully deterministic runs.
|
| 140 |
+
|
| 141 |
+
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.
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
## Repository Layout
|
| 146 |
+
```
|
| 147 |
+
MagicNodes/
|
| 148 |
+
├─ README.md
|
| 149 |
+
├─ LICENSE # AGPL-3.0-or-later
|
| 150 |
+
├─ assets/
|
| 151 |
+
├─ docs/
|
| 152 |
+
│ ├─ EasyNodes.md
|
| 153 |
+
│ ├─ HardNodes.md
|
| 154 |
+
│ └─ hard/
|
| 155 |
+
│ ├─ CADE25.md
|
| 156 |
+
│ ├─ ControlFusion.md
|
| 157 |
+
│ ├─ UpscaleModule.md
|
| 158 |
+
│ ├─ IDS.md
|
| 159 |
+
│ └─ ZeSmartSampler.md
|
| 160 |
+
│
|
| 161 |
+
├─ mod/
|
| 162 |
+
│ ├─ easy/
|
| 163 |
+
│ │ ├─ mg_cade25_easy.py
|
| 164 |
+
│ │ ├─ mg_controlfusion_easy.py
|
| 165 |
+
│ │ └─ mg_supersimple_easy.py
|
| 166 |
+
│ │ └─ preset_loader.py
|
| 167 |
+
│ └─ hard/
|
| 168 |
+
│ ├─ mg_cade25.py
|
| 169 |
+
│ ├─ mg_controlfusion.py
|
| 170 |
+
│ ├─ mg_tde2.py
|
| 171 |
+
│ ├─ mg_upscale_module.py
|
| 172 |
+
│ ├─ mg_ids.py
|
| 173 |
+
│ └─ mg_zesmart_sampler_v1_1.py
|
| 174 |
+
│
|
| 175 |
+
├─ pressets/
|
| 176 |
+
│ ├─ mg_cade25.cfg
|
| 177 |
+
│ └─ mg_controlfusion.cfg
|
| 178 |
+
│
|
| 179 |
+
├─ scripts/
|
| 180 |
+
│ ├─ check_sageattention.bat
|
| 181 |
+
│ └─ check_sageattention.ps1
|
| 182 |
+
│
|
| 183 |
+
├─ depth-anything/ # place Depth Anything v2 weights (.pth), e.g., depth_anything_v2_vitl.pth
|
| 184 |
+
│ └─depth_anything_v2_vitl.pth
|
| 185 |
+
│
|
| 186 |
+
├─ vendor/
|
| 187 |
+
│ └─ depth_anything_v2/ # vendored Depth Anything v2 code (Apache-2.0)
|
| 188 |
+
│
|
| 189 |
+
├─ models/
|
| 190 |
+
│ └─ LoRA/
|
| 191 |
+
│ └─ mg_7lambda_negative.safetensors
|
| 192 |
+
│
|
| 193 |
+
├─ workflows/
|
| 194 |
+
│ ├─ mg_SuperSimple-Workflow.json
|
| 195 |
+
│ └─ mg_Easy-Workflow.json
|
| 196 |
+
|
|
| 197 |
+
└─ requirements.txt
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
Models folder
|
| 201 |
+
- The repo includes a sample negative LoRA at `models/LoRA/mg_7lambda_negative.safetensors`.
|
| 202 |
+
- To use it in ComfyUI, copy or move the file to `ComfyUI/models/loras` — it will then appear in LoRA selectors.
|
| 203 |
+
- Keeping a copy under `models/` here is fine as a backup.
|
| 204 |
+
|
| 205 |
+
Depth models (Depth Anything v2)
|
| 206 |
+
- Place DA v2 weights (`.pth`) in `depth-anything/`. Recommended: `depth_anything_v2_vitl.pth` (ViT-L). Supported names include:
|
| 207 |
+
`depth_anything_v2_vits.pth`, `depth_anything_v2_vitb.pth`, `depth_anything_v2_vitl.pth`, `depth_anything_v2_vitg.pth`,
|
| 208 |
+
and the metric variants `depth_anything_v2_metric_vkitti_vitl.pth`, `depth_anything_v2_metric_hypersim_vitl.pth`.
|
| 209 |
+
- ControlFusion auto-detects the correct config from the filename and uses this path by default. You can override via the
|
| 210 |
+
`depth_model_path` parameter (preset) if needed.
|
| 211 |
+
- If no weights are found, ControlFusion falls back gracefully (luminance pseudo-depth), but results are better with DA v2.
|
| 212 |
+
- Where to get weights: see the official Depth Anything v2 repository (https://github.com/DepthAnything/Depth-Anything-V2)
|
| 213 |
+
and its Hugging Face models page (https://huggingface.co/Depth-Anything) for pre-trained `.pth` files.
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
## Documentation
|
| 217 |
+
- Easy nodes overview and `MG_SuperSimple`: `docs/EasyNodes.md`
|
| 218 |
+
- Hard nodes documentation index: `docs/HardNodes.md`
|
| 219 |
+
|
| 220 |
+
## Control Fusion (mg_controlfusion.py, mg_controlfusion_easy.py,)
|
| 221 |
+
- Builds depth + edge masks with preserved aspect ratio; hires-friendly mask mode
|
| 222 |
+
- Key surface knobs: `edge_alpha`, `edge_smooth`, `edge_width`, `edge_single_line`/`edge_single_strength`, `edge_depth_gate`/`edge_depth_gamma`
|
| 223 |
+
- Preview can optionally reflect ControlNet strength via `preview_show_strength` and `preview_strength_branch`
|
| 224 |
+
|
| 225 |
+
## CADE 2.5 (mg_cade25.py, mg_cade25_easy.py)
|
| 226 |
+
- Deterministic preflight: CLIPSeg pinned to CPU; preview mask reset; noise tied to `iter_seed`
|
| 227 |
+
- Encode/Decode: stride-aligned, with larger overlap for >2K to avoid artifacts
|
| 228 |
+
- Polish mode (final hi-res refinement):
|
| 229 |
+
- `polish_enable`, `polish_keep_low` (global form from reference), `polish_edge_lock`, `polish_sigma`
|
| 230 |
+
- Smooth start via `polish_start_after` and `polish_keep_low_ramp`
|
| 231 |
+
- `eps_scale` supported for gentle exposure shaping
|
| 232 |
+
|
| 233 |
+
## Depth Anything v2 (vendor)
|
| 234 |
+
- Lives under `vendor/depth_anything_v2`; Apache-2.0 license
|
| 235 |
+
|
| 236 |
+
## MG_ZeSmartSampler (Experimental)
|
| 237 |
+
- Custom sampler that builds hybrid sigma schedules (Karras/Beta blend) with tail smoothing
|
| 238 |
+
- Inputs/Outputs match KSampler: `MODEL/SEED/STEPS/CFG/base_sampler/schedule/CONDITIONING/LATENT` -> `LATENT`
|
| 239 |
+
- Key params: `hybrid_mix`, `jitter_sigma`, `tail_smooth`, optional PC2-like shaping (`smart_strength`, `target_error`, `curv_sensitivity`)
|
| 240 |
+
|
| 241 |
+
## Seed Latent (mg_seed_latent.py)
|
| 242 |
+
- 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.
|
| 243 |
+
- Inputs
|
| 244 |
+
- `width`, `height`, `batch_size`
|
| 245 |
+
- `sigma` (noise amplitude) and `bias` (additive offset)
|
| 246 |
+
- Optional `vae` and `image` when `mix_image` is enabled
|
| 247 |
+
- Output: `LATENT` dict `{ "samples": tensor }` ready to feed into CADE/SuperSimple.
|
| 248 |
+
- Usage notes
|
| 249 |
+
- Keep dimensions multiples of 8; recommended starting sizes around ~672x944 (other aspect ratios work). With SuperSimple’s default scale, step 4 lands near ~3688x5192.
|
| 250 |
+
- `mix_image=True` encodes the provided image via VAE and adds noise: a soft way to keep global structure while allowing refinement downstream.
|
| 251 |
+
- 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.
|
| 252 |
+
- Batch friendly: `batch_size>1` produces independent latents of the chosen size.
|
| 253 |
+
|
| 254 |
+
## Dependencies (Why These Packages)
|
| 255 |
+
- 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.
|
| 256 |
+
|
| 257 |
+
- opencv-contrib-python — ControlFusion edge stack (Pyramid Canny, thinning via ximgproc), morphological ops, light smoothing.
|
| 258 |
+
- Pillow — image I/O and small conversions in preview/CLIPSeg pipelines.
|
| 259 |
+
- scipy — preferred Gaussian filtering path for IDS (quality). If not installed, IDS falls back to a PyTorch implementation.
|
| 260 |
+
- sageattention — accelerated attention kernels (auto-picks a kernel per GPU arch); CADE/attention patch falls back to stock attention if not present.
|
| 261 |
+
|
| 262 |
+
Optional extras
|
| 263 |
+
- controlnet-aux — alternative loader for Depth Anything v2 if you don’t use the vendored implementation (not required by default).
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
## Preprint
|
| 267 |
+
- CADE 2.5 - ZeResFDG
|
| 268 |
+
- PDF: https://arxiv.org/pdf/2510.12954.pdf
|
| 269 |
+
- arXiv: https://arxiv.org/abs/2510.12954
|
| 270 |
+
|
| 271 |
+
- CADE 2.5 - QSilk
|
| 272 |
+
- PDF: https://arxiv.org/pdf/2510.15761
|
| 273 |
+
- arXiv: https://arxiv.org/abs/2510.15761
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
### How to Cite
|
| 277 |
+
```
|
| 278 |
+
@misc{rychkovskiy2025cade25zeresfdg,
|
| 279 |
+
title={CADE 2.5 - ZeResFDG: Frequency-Decoupled, Rescaled and Zero-Projected Guidance for SD/SDXL Latent Diffusion Models},
|
| 280 |
+
author={Denis Rychkovskiy},
|
| 281 |
+
year={2025},
|
| 282 |
+
eprint={2510.12954},
|
| 283 |
+
archivePrefix={arXiv},
|
| 284 |
+
primaryClass={cs.CV},
|
| 285 |
+
url={https://arxiv.org/abs/2510.12954},
|
| 286 |
+
}
|
| 287 |
+
```
|
| 288 |
+
```
|
| 289 |
+
@misc{rychkovskiy2025qsilkmicrograinstabilizationadaptive,
|
| 290 |
+
title={QSilk: Micrograin Stabilization and Adaptive Quantile Clipping for Detail-Friendly Latent Diffusion},
|
| 291 |
+
author={Denis Rychkovskiy},
|
| 292 |
+
year={2025},
|
| 293 |
+
eprint={2510.15761},
|
| 294 |
+
archivePrefix={arXiv},
|
| 295 |
+
primaryClass={cs.CV},
|
| 296 |
+
url={https://arxiv.org/abs/2510.15761},
|
| 297 |
+
}
|
| 298 |
+
```
|
| 299 |
+
|
| 300 |
+
## Attribution (kind request)
|
| 301 |
+
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”)"
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
## License and Credits
|
| 305 |
+
- License: AGPL-3.0-or-later (see `LICENSE`)
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
## Support
|
| 309 |
+
If this project saved you time, you can leave a tip:
|
| 310 |
+
- GitHub Sponsors: https://github.com/sponsors/1dZb1
|
| 311 |
+
- Bymeacoffee: https://buymeacoffee.com/dzrobo
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
|
__init__.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os, sys, importlib.util
|
| 2 |
+
|
| 3 |
+
# Normalize package name so relative imports work even if loaded by absolute path
|
| 4 |
+
if __name__ != 'MagicNodes':
|
| 5 |
+
sys.modules['MagicNodes'] = sys.modules[__name__]
|
| 6 |
+
__package__ = 'MagicNodes'
|
| 7 |
+
# Precreate subpackage alias MagicNodes.mod
|
| 8 |
+
_mod_pkg_name = 'MagicNodes.mod'
|
| 9 |
+
_mod_pkg_dir = os.path.join(os.path.dirname(__file__), 'mod')
|
| 10 |
+
_mod_pkg_file = os.path.join(_mod_pkg_dir, '__init__.py')
|
| 11 |
+
if _mod_pkg_name not in sys.modules and os.path.isfile(_mod_pkg_file):
|
| 12 |
+
_spec = importlib.util.spec_from_file_location(
|
| 13 |
+
_mod_pkg_name, _mod_pkg_file, submodule_search_locations=[_mod_pkg_dir]
|
| 14 |
+
)
|
| 15 |
+
_mod = importlib.util.module_from_spec(_spec)
|
| 16 |
+
sys.modules[_mod_pkg_name] = _mod
|
| 17 |
+
assert _spec.loader is not None
|
| 18 |
+
_spec.loader.exec_module(_mod)
|
| 19 |
+
|
| 20 |
+
# Imports of active nodes
|
| 21 |
+
from .mod.mg_combinode import MagicNodesCombiNode
|
| 22 |
+
from .mod.hard.mg_upscale_module import MagicUpscaleModule
|
| 23 |
+
from .mod.hard.mg_adaptive import AdaptiveSamplerHelper
|
| 24 |
+
from .mod.hard.mg_cade25 import ComfyAdaptiveDetailEnhancer25
|
| 25 |
+
from .mod.hard.mg_ids import IntelligentDetailStabilizer
|
| 26 |
+
from .mod.mg_seed_latent import MagicSeedLatent
|
| 27 |
+
from .mod.mg_sagpu_attention import PatchSageAttention
|
| 28 |
+
from .mod.hard.mg_controlfusion import MG_ControlFusion
|
| 29 |
+
from .mod.hard.mg_zesmart_sampler_v1_1 import MG_ZeSmartSampler
|
| 30 |
+
from .mod.easy.mg_cade25_easy import CADEEasyUI as ComfyAdaptiveDetailEnhancer25_Easy
|
| 31 |
+
from .mod.easy.mg_controlfusion_easy import MG_ControlFusionEasyUI as MG_ControlFusion_Easy
|
| 32 |
+
from .mod.easy.mg_supersimple_easy import MG_SuperSimple
|
| 33 |
+
|
| 34 |
+
# Place Easy/Hard variants under dedicated UI categories
|
| 35 |
+
try:
|
| 36 |
+
ComfyAdaptiveDetailEnhancer25_Easy.CATEGORY = "MagicNodes/Easy"
|
| 37 |
+
except Exception:
|
| 38 |
+
pass
|
| 39 |
+
try:
|
| 40 |
+
MG_ControlFusion_Easy.CATEGORY = "MagicNodes/Easy"
|
| 41 |
+
except Exception:
|
| 42 |
+
pass
|
| 43 |
+
try:
|
| 44 |
+
MG_SuperSimple.CATEGORY = "MagicNodes/Easy"
|
| 45 |
+
except Exception:
|
| 46 |
+
pass
|
| 47 |
+
try:
|
| 48 |
+
ComfyAdaptiveDetailEnhancer25.CATEGORY = "MagicNodes/Hard"
|
| 49 |
+
IntelligentDetailStabilizer.CATEGORY = "MagicNodes/Hard"
|
| 50 |
+
MagicUpscaleModule.CATEGORY = "MagicNodes/Hard"
|
| 51 |
+
AdaptiveSamplerHelper.CATEGORY = "MagicNodes/Hard"
|
| 52 |
+
PatchSageAttention.CATEGORY = "MagicNodes"
|
| 53 |
+
MG_ControlFusion.CATEGORY = "MagicNodes/Hard"
|
| 54 |
+
MG_ZeSmartSampler.CATEGORY = "MagicNodes/Hard"
|
| 55 |
+
except Exception:
|
| 56 |
+
pass
|
| 57 |
+
|
| 58 |
+
NODE_CLASS_MAPPINGS = {
|
| 59 |
+
"MagicNodesCombiNode": MagicNodesCombiNode,
|
| 60 |
+
"MagicSeedLatent": MagicSeedLatent,
|
| 61 |
+
"PatchSageAttention": PatchSageAttention,
|
| 62 |
+
"MagicUpscaleModule": MagicUpscaleModule,
|
| 63 |
+
"ComfyAdaptiveDetailEnhancer25": ComfyAdaptiveDetailEnhancer25,
|
| 64 |
+
"IntelligentDetailStabilizer": IntelligentDetailStabilizer,
|
| 65 |
+
"MG_ControlFusion": MG_ControlFusion,
|
| 66 |
+
"MG_ZeSmartSampler": MG_ZeSmartSampler,
|
| 67 |
+
# Easy variants (limited-surface controls)
|
| 68 |
+
"ComfyAdaptiveDetailEnhancer25_Easy": ComfyAdaptiveDetailEnhancer25_Easy,
|
| 69 |
+
"MG_ControlFusion_Easy": MG_ControlFusion_Easy,
|
| 70 |
+
"MG_SuperSimple": MG_SuperSimple,
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 74 |
+
"MagicNodesCombiNode": "MG_CombiNode",
|
| 75 |
+
"MagicSeedLatent": "MG_SeedLatent",
|
| 76 |
+
# TDE removed from this build
|
| 77 |
+
"PatchSageAttention": "MG_AccelAttention",
|
| 78 |
+
"ComfyAdaptiveDetailEnhancer25": "MG_CADE 2.5",
|
| 79 |
+
"MG_ControlFusion": "MG_ControlFusion",
|
| 80 |
+
"MG_ZeSmartSampler": "MG_ZeSmartSampler",
|
| 81 |
+
"IntelligentDetailStabilizer": "MG_IDS",
|
| 82 |
+
"MagicUpscaleModule": "MG_UpscaleModule",
|
| 83 |
+
# Easy variants (grouped under MagicNodes/Easy)
|
| 84 |
+
"ComfyAdaptiveDetailEnhancer25_Easy": "MG_CADE 2.5 (Easy)",
|
| 85 |
+
"MG_ControlFusion_Easy": "MG_ControlFusion (Easy)",
|
| 86 |
+
"MG_SuperSimple": "MG_SuperSimple",
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
__all__ = [
|
| 90 |
+
'NODE_CLASS_MAPPINGS',
|
| 91 |
+
'NODE_DISPLAY_NAME_MAPPINGS',
|
| 92 |
+
]
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
|
assets/Anime1.jpg
ADDED
|
Git LFS Details
|
assets/Anime1_crop.jpg
ADDED
|
Git LFS Details
|
assets/Dog1_crop_ours_CADE25_QSilk.jpg
ADDED
|
Git LFS Details
|
assets/Dog1_ours_CADE25_QSilk.jpg
ADDED
|
Git LFS Details
|
assets/MagicNodes.png
ADDED
|
Git LFS Details
|
assets/PhotoCup1.jpg
ADDED
|
Git LFS Details
|
assets/PhotoCup1_crop.jpg
ADDED
|
Git LFS Details
|
assets/PhotoPortrait1.jpg
ADDED
|
Git LFS Details
|
assets/PhotoPortrait1_crop1.jpg
ADDED
|
Git LFS Details
|
assets/PhotoPortrait1_crop2.jpg
ADDED
|
Git LFS Details
|
assets/PhotoPortrait1_crop3.jpg
ADDED
|
Git LFS Details
|
depth-anything/place depth model here
ADDED
|
File without changes
|
docs/EasyNodes.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Easy Nodes and MG_SuperSimple
|
| 2 |
+
|
| 3 |
+
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`.
|
| 4 |
+
|
| 5 |
+
- 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.
|
| 6 |
+
- Loader: `mod/easy/preset_loader.py` caches by mtime and does light type parsing.
|
| 7 |
+
- 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.
|
| 8 |
+
|
| 9 |
+
## MG_SuperSimple (Easy)
|
| 10 |
+
|
| 11 |
+
Single node that reproduces the 2/3/4‑step CADE+ControlFusion pipeline with minimal surface.
|
| 12 |
+
|
| 13 |
+
Category: `MagicNodes/Easy`
|
| 14 |
+
|
| 15 |
+
Inputs
|
| 16 |
+
- `model` (MODEL)
|
| 17 |
+
- `positive` (CONDITIONING), `negative` (CONDITIONING)
|
| 18 |
+
- `vae` (VAE)
|
| 19 |
+
- `latent` (LATENT)
|
| 20 |
+
- `control_net` (CONTROL_NET) — required by ControlFusion
|
| 21 |
+
- `reference_image` (IMAGE, optional) — forwarded to CADE
|
| 22 |
+
- `clip_vision` (CLIP_VISION, optional) — forwarded to CADE
|
| 23 |
+
|
| 24 |
+
Controls
|
| 25 |
+
- `step_count` int (1..4): how many steps to run
|
| 26 |
+
- `custom` toggle: when On, the visible CADE controls below override the Step presets across all steps; when Off, all CADE values come from presets
|
| 27 |
+
- `seed` int with `control_after_generate`
|
| 28 |
+
- `steps` int (default 25) — applies to steps 2..4
|
| 29 |
+
- `cfg` float (default 4.5)
|
| 30 |
+
- `denoise` float (default 0.65, clamped 0.45..0.9) — applies to steps 2..4
|
| 31 |
+
- `sampler_name` (default `ddim`)
|
| 32 |
+
- `scheduler` (default `MGHybrid`)
|
| 33 |
+
- `clipseg_text` string (default `hand, feet, face`)
|
| 34 |
+
|
| 35 |
+
Behavior
|
| 36 |
+
- 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.
|
| 37 |
+
- 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.
|
| 38 |
+
- Initial `positive/negative` come from the node inputs; subsequent steps use the latest CF outputs. `latent` is always taken from the previous CADE.
|
| 39 |
+
- When `custom` is Off, UI values are ignored entirely; presets define all CADE parameters.
|
| 40 |
+
- ControlFusion inside this node always relies on presets (no additional CF UI here) to keep the surface minimal.
|
| 41 |
+
|
| 42 |
+
Outputs
|
| 43 |
+
- `(LATENT, IMAGE)` from the final executed step (e.g., step 2 if `step_count=2`). No preview outputs.
|
| 44 |
+
|
| 45 |
+
Quickstart
|
| 46 |
+
1) Drop `MG_SuperSimple` into your graph under `MagicNodes/Easy`.
|
| 47 |
+
2) Connect `model/positive/negative/vae/latent`, and a `control_net` module; optionally connect `reference_image` and `clip_vision`.
|
| 48 |
+
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`).
|
| 49 |
+
4) Run. The node returns the final `(LATENT, IMAGE)` for the chosen depth.
|
| 50 |
+
|
| 51 |
+
Notes
|
| 52 |
+
- Presets are read from `pressets/mg_cade25.cfg` and `pressets/mg_controlfusion.cfg`. Keep them in UTF‑8 and prefer `$(ROOT)` over absolute paths.
|
| 53 |
+
- `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.
|
| 54 |
+
|
docs/HardNodes.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hard Nodes (Overview)
|
| 2 |
+
|
| 3 |
+
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.
|
| 4 |
+
|
| 5 |
+
Available docs
|
| 6 |
+
- CADE 2.5: see `docs/hard/CADE25.md`
|
| 7 |
+
- ControlFusion: see `docs/hard/ControlFusion.md`
|
| 8 |
+
- Upscale Module: see `docs/hard/UpscaleModule.md`
|
| 9 |
+
- Intelligent Detail Stabilizer (IDS): see `docs/hard/IDS.md`
|
| 10 |
+
- ZeSmart Sampler: see `docs/hard/ZeSmartSampler.md`
|
| 11 |
+
|
docs/hard/CADE25.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CADE 2.5 (ComfyAdaptiveDetailEnhancer25)
|
| 2 |
+
|
| 3 |
+
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).
|
| 4 |
+
|
| 5 |
+
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.
|
| 6 |
+
|
| 7 |
+
## Overview
|
| 8 |
+
- Iterative latent refinement with configurable steps/CFG/denoise
|
| 9 |
+
- Optional guidance override (Rescale/CFGZero‑style, FDG/NAG ideas, epsilon scaling)
|
| 10 |
+
- Hybrid schedule path (`MGHybrid`) that builds ZeSmart‑style sigma stacks
|
| 11 |
+
- Local spatial guidance via CLIPSeg prompts
|
| 12 |
+
- Reference polishing with CLIP‑Vision (preserves low‑frequency structure)
|
| 13 |
+
- Optional upscaling mid‑run, detail stabilization, and gentle sharpening
|
| 14 |
+
- Determinism helpers: CLIPSeg pinned to CPU, mask state cleared per run
|
| 15 |
+
|
| 16 |
+
## Inputs
|
| 17 |
+
- `model` (MODEL)
|
| 18 |
+
- `positive` (CONDITIONING), `negative` (CONDITIONING)
|
| 19 |
+
- `vae` (VAE)
|
| 20 |
+
- `latent` (LATENT)
|
| 21 |
+
- `reference_image` (IMAGE, optional)
|
| 22 |
+
- `clip_vision` (CLIP_VISION, optional)
|
| 23 |
+
|
| 24 |
+
## Outputs
|
| 25 |
+
- `LATENT`: refined latent
|
| 26 |
+
- `IMAGE`: decoded image after the last internal iteration
|
| 27 |
+
- `mask_preview` (IMAGE): last fused mask preview (RGB 0..1)
|
| 28 |
+
- Internal values like effective `steps/cfg/denoise` are tracked across the loop (the Easy wrapper surfaces them if needed).
|
| 29 |
+
|
| 30 |
+
## Core Controls (essentials)
|
| 31 |
+
- `seed` (with control_after_generate)
|
| 32 |
+
- `steps`, `cfg`, `denoise`
|
| 33 |
+
- `sampler_name` (e.g., `ddim`)
|
| 34 |
+
- `scheduler` (`MGHybrid` recommended for smooth tails)
|
| 35 |
+
|
| 36 |
+
Typical starting points
|
| 37 |
+
- General: steps≈25, cfg≈7.0, denoise≈0.7, sampler=`euler_ancestral`, scheduler=`MGHybrid`
|
| 38 |
+
- As the first pass of a multi‑step pipeline: denoise=1.0 (full rewrite pass)
|
| 39 |
+
|
| 40 |
+
## MGHybrid schedule
|
| 41 |
+
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`.
|
| 42 |
+
|
| 43 |
+
## Local guidance (CLIPSeg)
|
| 44 |
+
- CLIPSeg prompts (comma‑separated) produce a soft mask that can attenuate denoise/CFG.
|
| 45 |
+
- CLIPSeg inference is pinned to CPU by default for reproducibility.
|
| 46 |
+
|
| 47 |
+
## Reference polish (CLIP‑Vision)
|
| 48 |
+
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.
|
| 49 |
+
|
| 50 |
+
## Advanced features (high‑level)
|
| 51 |
+
- Guidance override wrapper (rescale curves, momentum, perpendicular dampers)
|
| 52 |
+
- FDG/ZeRes‑inspired options with adaptive thresholds
|
| 53 |
+
- Mid‑run upscale support via `MagicUpscaleModule` with post‑adjusted CFG/denoise
|
| 54 |
+
- Post passes: `IntelligentDetailStabilizer`, optional mild sharpen
|
| 55 |
+
|
| 56 |
+
## Related
|
| 57 |
+
- 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.
|
| 58 |
+
|
| 59 |
+
## Tips
|
| 60 |
+
- Keep `vae` consistent across passes; CADE re‑encodes when scale changes.
|
| 61 |
+
- 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`.
|
| 62 |
+
- If you rely on presets, consider the Easy wrapper or `MG_SuperSimple` to avoid UI/preset drift.
|
| 63 |
+
|
| 64 |
+
## Quickstart (Hard)
|
| 65 |
+
1) Connect `MODEL / VAE / CONDITIONING / LATENT`.
|
| 66 |
+
2) Set `seed`, `steps≈25`, `cfg≈7.0`, `denoise≈0.7`, `sampler=euler_ancestral`, `scheduler=MGHybrid`.
|
| 67 |
+
3) (Optional) Add `reference_image` and `clip_vision`, and a CLIPSeg prompt.
|
| 68 |
+
4) Run and fine‑tune denoise/CFG first; only then adjust sampler/schedule.
|
| 69 |
+
|
| 70 |
+
Notes
|
| 71 |
+
- The node clears internal masks and patches at the end of a run even on errors.
|
| 72 |
+
- Some experimental toggles are intentionally conservative in default configs to avoid destabilizing results.
|
docs/hard/ControlFusion.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ControlFusion (Hard)
|
| 2 |
+
|
| 3 |
+
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.
|
| 4 |
+
|
| 5 |
+
For minimal usage, see the Easy wrapper documented in `docs/EasyNodes.md`.
|
| 6 |
+
|
| 7 |
+
## Overview
|
| 8 |
+
- Depth: Depth Anything v2 if available (vendored/local/aux fallbacks), otherwise pseudo‑depth from luminance + blur.
|
| 9 |
+
- Edges: multi‑scale Pyramid Canny with optional thinning, width adjust, smoothing, single‑line collapse, and depth‑based gating.
|
| 10 |
+
- Blending: `normal` (weighted mix), `max`, or `edge_over_depth` prior to ControlNet.
|
| 11 |
+
- Application: single fused hint or `split_apply` (Depth first, then Edges) with independent strengths and schedules.
|
| 12 |
+
- Preview: aspect‑kept visualization with optional strength reflection (display‑only).
|
| 13 |
+
|
| 14 |
+
## Inputs
|
| 15 |
+
- `image` (IMAGE, BHWC 0..1)
|
| 16 |
+
- `positive` (CONDITIONING), `negative` (CONDITIONING)
|
| 17 |
+
- `control_net` (CONTROL_NET)
|
| 18 |
+
- `vae` (VAE)
|
| 19 |
+
|
| 20 |
+
## Outputs
|
| 21 |
+
- `positive` (CONDITIONING), `negative` (CONDITIONING) — updated with ControlNet hint
|
| 22 |
+
- `Mask_Preview` (IMAGE) — fused mask preview (RGB 0..1)
|
| 23 |
+
|
| 24 |
+
## Core Controls
|
| 25 |
+
Depth
|
| 26 |
+
- `enable_depth` (bool)
|
| 27 |
+
- `depth_model_path` (pth for Depth Anything v2)
|
| 28 |
+
- `depth_resolution` (min‑side target; hires mode keeps aspect)
|
| 29 |
+
|
| 30 |
+
Edges (PyraCanny)
|
| 31 |
+
- `enable_pyra` (bool), `pyra_low`, `pyra_high`, `pyra_resolution`
|
| 32 |
+
- `edge_thin_iter` (thinning passes, auto‑tuned in smart mode)
|
| 33 |
+
- `edge_alpha` (pre‑blend opacity), `edge_boost` (micro‑contrast), `smart_tune`, `smart_boost`
|
| 34 |
+
|
| 35 |
+
Blend and Strength
|
| 36 |
+
- `blend_mode`: `normal` | `max` | `edge_over_depth`
|
| 37 |
+
- `blend_factor` (for `normal`)
|
| 38 |
+
- `strength_pos`, `strength_neg` (global)
|
| 39 |
+
- `start_percent`, `end_percent` (schedule window 0..1)
|
| 40 |
+
|
| 41 |
+
Preview and Quality
|
| 42 |
+
- `preview_res` (min‑side), `mask_brightness`
|
| 43 |
+
- `preview_show_strength` with `preview_strength_branch` = `positive` | `negative` | `max` | `avg`
|
| 44 |
+
- `hires_mask_auto` (keep aspect and higher caps)
|
| 45 |
+
|
| 46 |
+
Application Options
|
| 47 |
+
- `apply_to_uncond` (mirror ControlNet hint to uncond)
|
| 48 |
+
- `stack_prev_control` (stack with previous ControlNet in the cond dict)
|
| 49 |
+
- `split_apply` (Depth first, Edges second)
|
| 50 |
+
- Separate schedules and multipliers when split:
|
| 51 |
+
- Depth: `depth_start_percent`, `depth_end_percent`, `depth_strength_mul`
|
| 52 |
+
- Edges: `edge_start_percent`, `edge_end_percent`, `edge_strength_mul`
|
| 53 |
+
|
| 54 |
+
Extra Edge Controls
|
| 55 |
+
- `edge_width` (thin/thicken), `edge_smooth` (reduce pixelation)
|
| 56 |
+
- `edge_single_line`, `edge_single_strength` (collapse double outlines)
|
| 57 |
+
- `edge_depth_gate`, `edge_depth_gamma` (weigh edges by depth)
|
| 58 |
+
|
| 59 |
+
## Behavior Notes
|
| 60 |
+
- Depth min‑side is capped (default 1024) and aspect is preserved to avoid distortions.
|
| 61 |
+
- In `split_apply`, the order is deterministic: Depth → Edges.
|
| 62 |
+
- Preview image reflects strength only if `preview_show_strength` is enabled; it does not affect the hint itself.
|
| 63 |
+
- When both Depth and Edges are disabled, the node passes inputs through and returns a zero preview.
|
| 64 |
+
|
| 65 |
+
## Quickstart
|
| 66 |
+
1) Connect `image/positive/negative/control_net/vae`.
|
| 67 |
+
2) Enable Depth and/or PyraCanny. Start with `edge_alpha≈1.0`, `blend_mode=normal`, `blend_factor≈0.02`.
|
| 68 |
+
3) Schedule the apply window (`start_percent/end_percent`) and tune `strength_pos/neg`.
|
| 69 |
+
4) Use `split_apply` if you want Depth to anchor structure and Edges to refine contours separately.
|
| 70 |
+
|
docs/hard/IDS.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# IntelligentDetailStabilizer (IDS)
|
| 2 |
+
|
| 3 |
+
Gentle, fast post‑pass for stabilizing micro‑detail and suppressing noise while preserving sharpness.
|
| 4 |
+
|
| 5 |
+
## Overview
|
| 6 |
+
- Two‑stage blur/sharpen split with strength‑controlled recombination.
|
| 7 |
+
- Uses SciPy Gaussian if available; otherwise a portable PyTorch separable blur.
|
| 8 |
+
- Operates on images (BHWC, 0..1) and returns a single stabilized `IMAGE`.
|
| 9 |
+
|
| 10 |
+
## Inputs
|
| 11 |
+
- `image` (IMAGE)
|
| 12 |
+
- `ids_strength` (float, default 0.5, range −1.0..1.0)
|
| 13 |
+
|
| 14 |
+
## Outputs
|
| 15 |
+
- `IMAGE` — stabilized image
|
| 16 |
+
|
| 17 |
+
## Tips
|
| 18 |
+
- Start around `ids_strength≈0.5` for gentle cleanup.
|
| 19 |
+
- Negative values bias toward more smoothing; positive increases sharpening of denoised base.
|
| 20 |
+
|
docs/hard/UpscaleModule.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MagicUpscaleModule
|
| 2 |
+
|
| 3 |
+
Lightweight latent‑space upscaler that keeps shapes aligned to the VAE stride to avoid border artifacts.
|
| 4 |
+
|
| 5 |
+
## Overview
|
| 6 |
+
- Decodes latent to image, resamples with selected filter, and re‑encodes.
|
| 7 |
+
- Aligns target size up to the VAE spatial compression stride to keep shapes consistent.
|
| 8 |
+
- Clears GPU/RAM caches to minimize fragmentation before heavy resizes.
|
| 9 |
+
|
| 10 |
+
## Inputs
|
| 11 |
+
- `samples` (LATENT)
|
| 12 |
+
- `vae` (VAE)
|
| 13 |
+
- `upscale_method` in `nearest-exact | bilinear | area | bicubic | lanczos`
|
| 14 |
+
- `scale_by` (float)
|
| 15 |
+
|
| 16 |
+
## Outputs
|
| 17 |
+
- `LATENT` — upscaled latent
|
| 18 |
+
- `Upscaled Image` — convenience decoded image
|
| 19 |
+
|
| 20 |
+
## Tips
|
| 21 |
+
- Use modest `scale_by` first (e.g., 1.2–1.5) and chain passes if needed.
|
| 22 |
+
- Keep the same `vae` before and after upscale in a larger pipeline.
|
| 23 |
+
|
docs/hard/ZeSmartSampler.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MG_ZeSmartSampler (v1.1)
|
| 2 |
+
|
| 3 |
+
Custom sampler that builds hybrid sigma schedules (Karras/Beta blend), adds tiny schedule jitter, and optionally applies a PC2‑like predictor‑corrector shaping.
|
| 4 |
+
|
| 5 |
+
## Overview
|
| 6 |
+
- Inputs/Outputs match a standard KSampler: `MODEL / SEED / STEPS / CFG / base_sampler / schedule / CONDITIONING / LATENT` → `LATENT`.
|
| 7 |
+
- `hybrid_mix` blends the tail toward Beta; `tail_smooth` softens tail jumps adaptively.
|
| 8 |
+
- `jitter_sigma` introduces a tiny monotonic noise to schedules for de‑ringing; remains deterministic with fixed seed.
|
| 9 |
+
- PC2‑style shaping is available via `smart_strength/target_error/curv_sensitivity` (kept conservative by default).
|
| 10 |
+
|
| 11 |
+
## Controls (high‑level)
|
| 12 |
+
- `base_sampler` and `schedule` (karras/beta/hybrid)
|
| 13 |
+
- `hybrid_mix` ∈ [0..1]
|
| 14 |
+
- `jitter_sigma` ∈ [0..0.1]
|
| 15 |
+
- `tail_smooth` ∈ [0..1]
|
| 16 |
+
- `smart_strength`, `target_error`, `curv_sensitivity`
|
| 17 |
+
|
| 18 |
+
## Tips
|
| 19 |
+
- Start hybrid at `hybrid_mix≈0.3` for 2D work; 0.5–0.7 for photo‑like.
|
| 20 |
+
- Keep `jitter_sigma` very small (≈0.005–0.01) to avoid destabilizing steps.
|
| 21 |
+
- If using inside CADE (`scheduler=MGHybrid`), CADE will construct the schedule and run the custom path automatically.
|
| 22 |
+
|
init
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
Init: MagicNodes (CADE 2.5, QSilk)
|
|
|
|
|
|
mod/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MagicNodes.mod package
|
| 2 |
+
|
| 3 |
+
Holds the primary node implementations after repo cleanup. Keeping this as a
|
| 4 |
+
package ensures stable relative imports from the project root.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
# No runtime side effects; modules are imported from MagicNodes.__init__.
|
| 8 |
+
|
mod/easy/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MagicNodes Easy variants
|
| 2 |
+
|
| 3 |
+
Holds simplified, user‑friendly node variants that expose only
|
| 4 |
+
high‑level parameters. Registered under category "MagicNodes/Easy".
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
# Modules are imported from MagicNodes.__init__ to control registration.
|
| 8 |
+
|
mod/easy/mg_cade25_easy.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
mod/easy/mg_controlfusion_easy.py
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import math
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
import comfy.model_management as model_management
|
| 9 |
+
from .preset_loader import get as load_preset
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
_DEPTH_INIT = False
|
| 13 |
+
_DEPTH_MODEL = None
|
| 14 |
+
_DEPTH_PROC = None
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _insert_aux_path():
|
| 18 |
+
try:
|
| 19 |
+
base = os.path.dirname(os.path.dirname(__file__)) # .../custom_nodes
|
| 20 |
+
aux_root = os.path.join(base, 'comfyui_controlnet_aux')
|
| 21 |
+
aux_src = os.path.join(aux_root, 'src')
|
| 22 |
+
for p in (aux_src, aux_root):
|
| 23 |
+
if os.path.isdir(p) and p not in sys.path:
|
| 24 |
+
sys.path.insert(0, p)
|
| 25 |
+
except Exception:
|
| 26 |
+
pass
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _try_init_depth_anything(model_path: str):
|
| 30 |
+
global _DEPTH_INIT, _DEPTH_MODEL, _DEPTH_PROC
|
| 31 |
+
if _DEPTH_INIT:
|
| 32 |
+
return _DEPTH_MODEL is not None
|
| 33 |
+
_DEPTH_INIT = True
|
| 34 |
+
# Prefer our vendored implementation first
|
| 35 |
+
try:
|
| 36 |
+
from ...vendor.depth_anything_v2.dpt import DepthAnythingV2 # type: ignore
|
| 37 |
+
# Guess config from filename
|
| 38 |
+
fname = os.path.basename(model_path or '')
|
| 39 |
+
cfgs = {
|
| 40 |
+
'depth_anything_v2_vits.pth': dict(encoder='vits', features=64, out_channels=[48,96,192,384]),
|
| 41 |
+
'depth_anything_v2_vitb.pth': dict(encoder='vitb', features=128, out_channels=[96,192,384,768]),
|
| 42 |
+
'depth_anything_v2_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]),
|
| 43 |
+
'depth_anything_v2_vitg.pth': dict(encoder='vitg', features=384, out_channels=[1536,1536,1536,1536]),
|
| 44 |
+
'depth_anything_v2_metric_vkitti_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]),
|
| 45 |
+
'depth_anything_v2_metric_hypersim_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]),
|
| 46 |
+
}
|
| 47 |
+
# fallback to vitl if unknown
|
| 48 |
+
cfg = cfgs.get(fname, cfgs['depth_anything_v2_vitl.pth'])
|
| 49 |
+
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 50 |
+
m = DepthAnythingV2(**cfg)
|
| 51 |
+
sd = torch.load(model_path, map_location='cpu')
|
| 52 |
+
m.load_state_dict(sd)
|
| 53 |
+
_DEPTH_MODEL = m.to(device).eval()
|
| 54 |
+
_DEPTH_PROC = True
|
| 55 |
+
return True
|
| 56 |
+
except Exception:
|
| 57 |
+
# Try local checkout of comfyui_controlnet_aux (if present)
|
| 58 |
+
_insert_aux_path()
|
| 59 |
+
try:
|
| 60 |
+
from custom_controlnet_aux.depth_anything_v2.dpt import DepthAnythingV2 # type: ignore
|
| 61 |
+
fname = os.path.basename(model_path or '')
|
| 62 |
+
cfgs = {
|
| 63 |
+
'depth_anything_v2_vits.pth': dict(encoder='vits', features=64, out_channels=[48,96,192,384]),
|
| 64 |
+
'depth_anything_v2_vitb.pth': dict(encoder='vitb', features=128, out_channels=[96,192,384,768]),
|
| 65 |
+
'depth_anything_v2_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]),
|
| 66 |
+
'depth_anything_v2_vitg.pth': dict(encoder='vitg', features=384, out_channels=[1536,1536,1536,1536]),
|
| 67 |
+
'depth_anything_v2_metric_vkitti_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]),
|
| 68 |
+
'depth_anything_v2_metric_hypersim_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]),
|
| 69 |
+
}
|
| 70 |
+
cfg = cfgs.get(fname, cfgs['depth_anything_v2_vitl.pth'])
|
| 71 |
+
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 72 |
+
m = DepthAnythingV2(**cfg)
|
| 73 |
+
sd = torch.load(model_path, map_location='cpu')
|
| 74 |
+
m.load_state_dict(sd)
|
| 75 |
+
_DEPTH_MODEL = m.to(device).eval()
|
| 76 |
+
_DEPTH_PROC = True
|
| 77 |
+
return True
|
| 78 |
+
except Exception:
|
| 79 |
+
# Fallback: packaged auxiliary API
|
| 80 |
+
try:
|
| 81 |
+
from controlnet_aux.depth_anything import DepthAnythingDetector, DepthAnythingV2 # type: ignore
|
| 82 |
+
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 83 |
+
_DEPTH_MODEL = DepthAnythingV2(model_path=model_path, device=device)
|
| 84 |
+
_DEPTH_PROC = True
|
| 85 |
+
return True
|
| 86 |
+
except Exception:
|
| 87 |
+
_DEPTH_MODEL = None
|
| 88 |
+
_DEPTH_PROC = False
|
| 89 |
+
return False
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _build_depth_map(image_bhwc: torch.Tensor, res: int, model_path: str, hires_mode: bool = True) -> torch.Tensor:
|
| 93 |
+
B, H, W, C = image_bhwc.shape
|
| 94 |
+
dev = image_bhwc.device
|
| 95 |
+
dtype = image_bhwc.dtype
|
| 96 |
+
# Choose target min-side for processing. In hires mode we allow higher caps and keep aspect.
|
| 97 |
+
# DepthAnything v2 can be memory-hungry on large inputs; cap min-side at 1024
|
| 98 |
+
cap = 1024
|
| 99 |
+
target = int(max(16, min(cap, res)))
|
| 100 |
+
if _try_init_depth_anything(model_path):
|
| 101 |
+
try:
|
| 102 |
+
# to CPU uint8
|
| 103 |
+
img = image_bhwc.detach().to('cpu')
|
| 104 |
+
x = img[0].movedim(-1, 0).unsqueeze(0)
|
| 105 |
+
# keep aspect ratio: scale so that min(H,W) == target
|
| 106 |
+
_, Cc, Ht, Wt = x.shape
|
| 107 |
+
min_side = max(1, min(Ht, Wt))
|
| 108 |
+
scale = float(target) / float(min_side)
|
| 109 |
+
out_h = max(1, int(round(Ht * scale)))
|
| 110 |
+
out_w = max(1, int(round(Wt * scale)))
|
| 111 |
+
x = F.interpolate(x, size=(out_h, out_w), mode='bilinear', align_corners=False)
|
| 112 |
+
# make channels-last and ensure contiguous layout for OpenCV
|
| 113 |
+
arr = (x[0].movedim(0, -1).contiguous().numpy() * 255.0).astype('uint8')
|
| 114 |
+
# Prefer direct DepthAnythingV2 inference if model has infer_image
|
| 115 |
+
if hasattr(_DEPTH_MODEL, 'infer_image'):
|
| 116 |
+
import cv2
|
| 117 |
+
# Drive input_size from desired depth resolution (min side), let DA keep aspect
|
| 118 |
+
input_sz = int(max(224, min(cap, res)))
|
| 119 |
+
depth = _DEPTH_MODEL.infer_image(cv2.cvtColor(arr, cv2.COLOR_RGB2BGR), input_size=input_sz, max_depth=20.0)
|
| 120 |
+
d = np.asarray(depth, dtype=np.float32)
|
| 121 |
+
# Normalize DepthAnythingV2 output (0..max_depth) to 0..1
|
| 122 |
+
d = d / 20.0
|
| 123 |
+
else:
|
| 124 |
+
depth = _DEPTH_MODEL(arr)
|
| 125 |
+
d = np.asarray(depth, dtype=np.float32)
|
| 126 |
+
if d.max() > 1.0:
|
| 127 |
+
d = d / 255.0
|
| 128 |
+
d = torch.from_numpy(d)[None, None] # 1,1,h,w
|
| 129 |
+
d = F.interpolate(d, size=(H, W), mode='bilinear', align_corners=False)
|
| 130 |
+
d = d[0, 0].to(device=dev, dtype=dtype)
|
| 131 |
+
d = d.clamp(0, 1)
|
| 132 |
+
return d
|
| 133 |
+
except Exception:
|
| 134 |
+
pass
|
| 135 |
+
# Fallback pseudo-depth: luminance + gentle blur
|
| 136 |
+
lum = (0.2126 * image_bhwc[..., 0] + 0.7152 * image_bhwc[..., 1] + 0.0722 * image_bhwc[..., 2]).to(dtype=dtype)
|
| 137 |
+
x = lum.movedim(-1, 0).unsqueeze(0) if lum.ndim == 3 else lum.unsqueeze(0).unsqueeze(0)
|
| 138 |
+
x = F.interpolate(x, size=(H, W), mode='bilinear', align_corners=False)
|
| 139 |
+
x = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1)
|
| 140 |
+
return x[0, 0].clamp(0, 1)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def _pyracanny(image_bhwc: torch.Tensor,
|
| 144 |
+
low: int,
|
| 145 |
+
high: int,
|
| 146 |
+
res: int,
|
| 147 |
+
thin_iter: int = 0,
|
| 148 |
+
edge_boost: float = 0.0,
|
| 149 |
+
smart_tune: bool = False,
|
| 150 |
+
smart_boost: float = 0.2,
|
| 151 |
+
preserve_aspect: bool = True) -> torch.Tensor:
|
| 152 |
+
try:
|
| 153 |
+
import cv2
|
| 154 |
+
except Exception:
|
| 155 |
+
# Fallback: simple Sobel magnitude
|
| 156 |
+
x = image_bhwc.movedim(-1, 1)
|
| 157 |
+
xg = x.mean(dim=1, keepdim=True)
|
| 158 |
+
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)
|
| 159 |
+
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)
|
| 160 |
+
mag = torch.sqrt(gx*gx + gy*gy)
|
| 161 |
+
mag = (mag - mag.amin())/(mag.amax()-mag.amin()+1e-6)
|
| 162 |
+
return mag[0,0].clamp(0,1)
|
| 163 |
+
B,H,W,C = image_bhwc.shape
|
| 164 |
+
img = (image_bhwc.detach().to('cpu')[0].contiguous().numpy()*255.0).astype('uint8')
|
| 165 |
+
cap = 4096
|
| 166 |
+
target = int(max(64, min(cap, res)))
|
| 167 |
+
if preserve_aspect:
|
| 168 |
+
scale = float(target) / float(max(1, min(H, W)))
|
| 169 |
+
out_h = max(8, int(round(H * scale)))
|
| 170 |
+
out_w = max(8, int(round(W * scale)))
|
| 171 |
+
img_res = cv2.resize(img, (out_w, out_h), interpolation=cv2.INTER_LINEAR)
|
| 172 |
+
else:
|
| 173 |
+
img_res = cv2.resize(img, (target, target), interpolation=cv2.INTER_LINEAR)
|
| 174 |
+
gray = cv2.cvtColor(img_res, cv2.COLOR_RGB2GRAY)
|
| 175 |
+
pyr_scales = [1.0, 0.5, 0.25]
|
| 176 |
+
acc = None
|
| 177 |
+
for s in pyr_scales:
|
| 178 |
+
if preserve_aspect:
|
| 179 |
+
sz = (max(8, int(round(img_res.shape[1]*s))), max(8, int(round(img_res.shape[0]*s))))
|
| 180 |
+
else:
|
| 181 |
+
sz = (max(8, int(target*s)), max(8, int(target*s)))
|
| 182 |
+
g = cv2.resize(gray, sz, interpolation=cv2.INTER_AREA)
|
| 183 |
+
g = cv2.GaussianBlur(g, (5,5), 0)
|
| 184 |
+
e = cv2.Canny(g, threshold1=int(low*s), threshold2=int(high*s))
|
| 185 |
+
e = cv2.resize(e, (W, H), interpolation=cv2.INTER_LINEAR)
|
| 186 |
+
e = (e.astype(np.float32)/255.0)
|
| 187 |
+
acc = e if acc is None else np.maximum(acc, e)
|
| 188 |
+
# Estimate density and sharpness for smart tuning
|
| 189 |
+
edensity_pre = None
|
| 190 |
+
try:
|
| 191 |
+
edensity_pre = float(np.mean(acc)) if acc is not None else None
|
| 192 |
+
except Exception:
|
| 193 |
+
edensity_pre = None
|
| 194 |
+
lap_var = None
|
| 195 |
+
try:
|
| 196 |
+
g32 = gray.astype(np.float32) / 255.0
|
| 197 |
+
lap = cv2.Laplacian(g32, cv2.CV_32F)
|
| 198 |
+
lap_var = float(lap.var())
|
| 199 |
+
except Exception:
|
| 200 |
+
lap_var = None
|
| 201 |
+
|
| 202 |
+
# optional thinning
|
| 203 |
+
try:
|
| 204 |
+
thin_iter_eff = int(thin_iter)
|
| 205 |
+
if smart_tune:
|
| 206 |
+
# simple heuristic: more thinning on high res and dense edges
|
| 207 |
+
auto = 0
|
| 208 |
+
if target >= 1024:
|
| 209 |
+
auto += 1
|
| 210 |
+
if target >= 1400:
|
| 211 |
+
auto += 1
|
| 212 |
+
if edensity_pre is not None and edensity_pre > 0.12:
|
| 213 |
+
auto += 1
|
| 214 |
+
if edensity_pre is not None and edensity_pre < 0.05:
|
| 215 |
+
auto = max(0, auto - 1)
|
| 216 |
+
thin_iter_eff = max(thin_iter_eff, min(3, auto))
|
| 217 |
+
if thin_iter_eff > 0:
|
| 218 |
+
import cv2
|
| 219 |
+
if hasattr(cv2, 'ximgproc') and hasattr(cv2.ximgproc, 'thinning'):
|
| 220 |
+
th = acc.copy()
|
| 221 |
+
th = (th*255).astype('uint8')
|
| 222 |
+
th = cv2.ximgproc.thinning(th)
|
| 223 |
+
acc = th.astype(np.float32)/255.0
|
| 224 |
+
else:
|
| 225 |
+
# simple erosion-based thinning approximation
|
| 226 |
+
kernel = np.ones((3,3), np.uint8)
|
| 227 |
+
t = (acc*255).astype('uint8')
|
| 228 |
+
for _ in range(int(thin_iter_eff)):
|
| 229 |
+
t = cv2.erode(t, kernel, iterations=1)
|
| 230 |
+
acc = t.astype(np.float32)/255.0
|
| 231 |
+
except Exception:
|
| 232 |
+
pass
|
| 233 |
+
# optional edge boost (unsharp on edge map)
|
| 234 |
+
# We fix a gentle boost for micro‑contrast; smart_tune may nudge it slightly
|
| 235 |
+
boost_eff = 0.10
|
| 236 |
+
if smart_tune:
|
| 237 |
+
try:
|
| 238 |
+
lv = 0.0 if lap_var is None else max(0.0, min(1.0, lap_var / 2.0))
|
| 239 |
+
dens = 0.0 if edensity_pre is None else float(max(0.0, min(1.0, edensity_pre)))
|
| 240 |
+
boost_eff = max(0.05, min(0.20, boost_eff + (1.0 - dens) * 0.05 + (1.0 - lv) * 0.02))
|
| 241 |
+
except Exception:
|
| 242 |
+
pass
|
| 243 |
+
if boost_eff and boost_eff != 0.0:
|
| 244 |
+
try:
|
| 245 |
+
import cv2
|
| 246 |
+
blur = cv2.GaussianBlur(acc, (0,0), sigmaX=1.0)
|
| 247 |
+
acc = np.clip(acc + float(boost_eff)*(acc - blur), 0.0, 1.0)
|
| 248 |
+
except Exception:
|
| 249 |
+
pass
|
| 250 |
+
ed = torch.from_numpy(acc).to(device=image_bhwc.device, dtype=image_bhwc.dtype)
|
| 251 |
+
return ed.clamp(0,1)
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def _blend(depth: torch.Tensor, edges: torch.Tensor, mode: str, factor: float) -> torch.Tensor:
|
| 255 |
+
depth = depth.clamp(0,1)
|
| 256 |
+
edges = edges.clamp(0,1)
|
| 257 |
+
if mode == 'max':
|
| 258 |
+
return torch.maximum(depth, edges)
|
| 259 |
+
if mode == 'edge_over_depth':
|
| 260 |
+
# edges override depth (edge=1) while preserving depth elsewhere
|
| 261 |
+
return (depth * (1.0 - edges) + edges).clamp(0,1)
|
| 262 |
+
# normal
|
| 263 |
+
f = float(max(0.0, min(1.0, factor)))
|
| 264 |
+
return (depth*(1.0-f) + edges*f).clamp(0,1)
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def _apply_controlnet_separate(positive, negative, control_net, image_bhwc: torch.Tensor,
|
| 268 |
+
strength_pos: float, strength_neg: float,
|
| 269 |
+
start_percent: float, end_percent: float, vae=None,
|
| 270 |
+
apply_to_uncond: bool = False,
|
| 271 |
+
stack_prev_control: bool = False):
|
| 272 |
+
control_hint = image_bhwc.movedim(-1,1)
|
| 273 |
+
out_pos = []
|
| 274 |
+
out_neg = []
|
| 275 |
+
# POS
|
| 276 |
+
for t in positive:
|
| 277 |
+
d = t[1].copy()
|
| 278 |
+
prev = d.get('control', None) if stack_prev_control else None
|
| 279 |
+
c_net = control_net.copy().set_cond_hint(control_hint, float(strength_pos), (start_percent, end_percent), vae=vae, extra_concat=[])
|
| 280 |
+
c_net.set_previous_controlnet(prev)
|
| 281 |
+
d['control'] = c_net
|
| 282 |
+
d['control_apply_to_uncond'] = bool(apply_to_uncond)
|
| 283 |
+
out_pos.append([t[0], d])
|
| 284 |
+
# NEG
|
| 285 |
+
for t in negative:
|
| 286 |
+
d = t[1].copy()
|
| 287 |
+
prev = d.get('control', None) if stack_prev_control else None
|
| 288 |
+
c_net = control_net.copy().set_cond_hint(control_hint, float(strength_neg), (start_percent, end_percent), vae=vae, extra_concat=[])
|
| 289 |
+
c_net.set_previous_controlnet(prev)
|
| 290 |
+
d['control'] = c_net
|
| 291 |
+
d['control_apply_to_uncond'] = bool(apply_to_uncond)
|
| 292 |
+
out_neg.append([t[0], d])
|
| 293 |
+
return out_pos, out_neg
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
class MG_ControlFusion:
|
| 297 |
+
@classmethod
|
| 298 |
+
def INPUT_TYPES(cls):
|
| 299 |
+
return {
|
| 300 |
+
"required": {
|
| 301 |
+
"preset_step": (["Custom", "Step 2", "Step 3", "Step 4"], {"default": "Custom", "tooltip": "Apply preset values from pressets/mg_controlfusion.cfg. UI values override."}),
|
| 302 |
+
"image": ("IMAGE", {"tooltip": "Input RGB image (B,H,W,3) in 0..1."}),
|
| 303 |
+
"positive": ("CONDITIONING", {"tooltip": "Positive conditioning to apply ControlNet to."}),
|
| 304 |
+
"negative": ("CONDITIONING", {"tooltip": "Negative conditioning to apply ControlNet to."}),
|
| 305 |
+
"control_net": ("CONTROL_NET", {"tooltip": "ControlNet module receiving the fused mask as hint."}),
|
| 306 |
+
"vae": ("VAE", {"tooltip": "VAE used by ControlNet when encoding the hint."}),
|
| 307 |
+
},
|
| 308 |
+
"optional": {
|
| 309 |
+
"enable_depth": ("BOOLEAN", {"default": True, "tooltip": "Enable depth map fusion (Depth Anything v2 if available)."}),
|
| 310 |
+
"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)."}),
|
| 311 |
+
"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."}),
|
| 312 |
+
"enable_pyra": ("BOOLEAN", {"default": True, "tooltip": "Enable PyraCanny edge detector."}),
|
| 313 |
+
"pyra_low": ("INT", {"default": 109, "min": 0, "max": 255, "tooltip": "Canny low threshold (0..255)."}),
|
| 314 |
+
"pyra_high": ("INT", {"default": 147, "min": 0, "max": 255, "tooltip": "Canny high threshold (0..255)."}),
|
| 315 |
+
"pyra_resolution": ("INT", {"default": 1024, "min": 64, "max": 4096, "step": 64, "tooltip": "Working resolution for edges (min side, keeps aspect)."}),
|
| 316 |
+
"edge_thin_iter": ("INT", {"default": 0, "min": 0, "max": 10, "step": 1, "tooltip": "Thinning iterations for edges (skeletonize). 0 = off."}),
|
| 317 |
+
"edge_alpha": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Opacity for edges before blending (0..1)."}),
|
| 318 |
+
"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."}),
|
| 319 |
+
"smart_tune": ("BOOLEAN", {"default": False, "tooltip": "Auto-adjust thinning/boost from image edge density and sharpness."}),
|
| 320 |
+
"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."}),
|
| 321 |
+
"blend_mode": (["normal","max","edge_over_depth"], {"default": "normal", "tooltip": "Depth+edges merge: normal (mix), max (strongest), edge_over_depth (edges overlay)."}),
|
| 322 |
+
"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)."}),
|
| 323 |
+
"strength_pos": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01, "tooltip": "ControlNet strength for positive branch."}),
|
| 324 |
+
"strength_neg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01, "tooltip": "ControlNet strength for negative branch."}),
|
| 325 |
+
"start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Start percentage along the sampling schedule."}),
|
| 326 |
+
"end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "End percentage along the sampling schedule."}),
|
| 327 |
+
"preview_res": ("INT", {"default": 1024, "min": 256, "max": 2048, "step": 64, "tooltip": "Preview minimum side (keeps aspect ratio)."}),
|
| 328 |
+
"mask_brightness": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Preview brightness multiplier (visualization only)."}),
|
| 329 |
+
"preview_show_strength": ("BOOLEAN", {"default": True, "tooltip": "Multiply preview by ControlNet strength for visualization."}),
|
| 330 |
+
"preview_strength_branch": (["positive","negative","max","avg"], {"default": "max", "tooltip": "Which strength to reflect in preview (display only)."}),
|
| 331 |
+
"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)."}),
|
| 332 |
+
"apply_to_uncond": ("BOOLEAN", {"default": False, "tooltip": "Apply ControlNet hint to the unconditional branch as well (stronger global hold on very large images)."}),
|
| 333 |
+
"stack_prev_control": ("BOOLEAN", {"default": False, "tooltip": "Chain with any previously attached ControlNet in the conditioning (advanced). Off = replace to avoid memory bloat."}),
|
| 334 |
+
# Split apply: chain Depth and Edges with separate schedules/strengths (fixed order: depth -> edges)
|
| 335 |
+
"split_apply": ("BOOLEAN", {"default": False, "tooltip": "Apply Depth and Edges as two chained ControlNets (fixed order: depth then edges)."}),
|
| 336 |
+
"edge_start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Edges start percent (when split is enabled)."}),
|
| 337 |
+
"edge_end_percent": ("FLOAT", {"default": 0.6, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Edges end percent (when split is enabled)."}),
|
| 338 |
+
"depth_start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Depth start percent (when split is enabled)."}),
|
| 339 |
+
"depth_end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Depth end percent (when split is enabled)."}),
|
| 340 |
+
"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."}),
|
| 341 |
+
"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."}),
|
| 342 |
+
# Extra edge controls (bottom)
|
| 343 |
+
"edge_width": ("FLOAT", {"default": 0.0, "min": -0.5, "max": 1.5, "step": 0.05, "tooltip": "Edge thickness adjust: negative thins, positive thickens."}),
|
| 344 |
+
"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)."}),
|
| 345 |
+
"edge_single_line": ("BOOLEAN", {"default": False, "tooltip": "Try to collapse double outlines into a single centerline."}),
|
| 346 |
+
"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."}),
|
| 347 |
+
"edge_depth_gate": ("BOOLEAN", {"default": False, "tooltip": "Weigh edges by depth so distant lines are fainter."}),
|
| 348 |
+
"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."}),
|
| 349 |
+
}
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
RETURN_TYPES = ("CONDITIONING","CONDITIONING","IMAGE")
|
| 353 |
+
RETURN_NAMES = ("positive","negative","Mask_Preview")
|
| 354 |
+
FUNCTION = "apply"
|
| 355 |
+
CATEGORY = "MagicNodes"
|
| 356 |
+
|
| 357 |
+
def apply(self, image, positive, negative, control_net, vae,
|
| 358 |
+
enable_depth=True, depth_model_path="", depth_resolution=1024,
|
| 359 |
+
enable_pyra=True, pyra_low=109, pyra_high=147, pyra_resolution=1024,
|
| 360 |
+
edge_thin_iter=0, edge_alpha=1.0, edge_boost=0.0,
|
| 361 |
+
smart_tune=False, smart_boost=0.2,
|
| 362 |
+
blend_mode="normal", blend_factor=0.02,
|
| 363 |
+
strength_pos=1.0, strength_neg=1.0, start_percent=0.0, end_percent=1.0,
|
| 364 |
+
preview_res=1024, mask_brightness=1.0,
|
| 365 |
+
preview_show_strength=True, preview_strength_branch="max",
|
| 366 |
+
hires_mask_auto=True, apply_to_uncond=False, stack_prev_control=False,
|
| 367 |
+
edge_width=0.0, edge_smooth=0.0, edge_single_line=False, edge_single_strength=0.0,
|
| 368 |
+
edge_depth_gate=False, edge_depth_gamma=1.5,
|
| 369 |
+
split_apply=False, edge_start_percent=0.0, edge_end_percent=0.6,
|
| 370 |
+
depth_start_percent=0.0, depth_end_percent=1.0,
|
| 371 |
+
edge_strength_mul=1.0, depth_strength_mul=1.0,
|
| 372 |
+
preset_step="Step 2", custom_override=False):
|
| 373 |
+
|
| 374 |
+
# Merge preset values (if selected) with UI values; UI overrides preset
|
| 375 |
+
try:
|
| 376 |
+
if isinstance(preset_step, str) and preset_step.lower() != "custom":
|
| 377 |
+
p = load_preset("mg_controlfusion", preset_step)
|
| 378 |
+
else:
|
| 379 |
+
p = {}
|
| 380 |
+
except Exception:
|
| 381 |
+
p = {}
|
| 382 |
+
def pv(name, cur):
|
| 383 |
+
return p.get(name, cur)
|
| 384 |
+
enable_depth = bool(pv('enable_depth', enable_depth))
|
| 385 |
+
depth_model_path = str(pv('depth_model_path', depth_model_path))
|
| 386 |
+
depth_resolution = int(pv('depth_resolution', depth_resolution))
|
| 387 |
+
enable_pyra = bool(pv('enable_pyra', enable_pyra))
|
| 388 |
+
pyra_low = int(pv('pyra_low', pyra_low))
|
| 389 |
+
pyra_high = int(pv('pyra_high', pyra_high))
|
| 390 |
+
pyra_resolution = int(pv('pyra_resolution', pyra_resolution))
|
| 391 |
+
edge_thin_iter = int(pv('edge_thin_iter', edge_thin_iter))
|
| 392 |
+
edge_alpha = float(pv('edge_alpha', edge_alpha))
|
| 393 |
+
edge_boost = float(pv('edge_boost', edge_boost))
|
| 394 |
+
smart_tune = bool(pv('smart_tune', smart_tune))
|
| 395 |
+
smart_boost = float(pv('smart_boost', smart_boost))
|
| 396 |
+
blend_mode = str(pv('blend_mode', blend_mode))
|
| 397 |
+
blend_factor = float(pv('blend_factor', blend_factor))
|
| 398 |
+
strength_pos = float(pv('strength_pos', strength_pos))
|
| 399 |
+
strength_neg = float(pv('strength_neg', strength_neg))
|
| 400 |
+
start_percent = float(pv('start_percent', start_percent))
|
| 401 |
+
end_percent = float(pv('end_percent', end_percent))
|
| 402 |
+
preview_res = int(pv('preview_res', preview_res))
|
| 403 |
+
mask_brightness = float(pv('mask_brightness', mask_brightness))
|
| 404 |
+
preview_show_strength = bool(pv('preview_show_strength', preview_show_strength))
|
| 405 |
+
preview_strength_branch = str(pv('preview_strength_branch', preview_strength_branch))
|
| 406 |
+
hires_mask_auto = bool(pv('hires_mask_auto', hires_mask_auto))
|
| 407 |
+
apply_to_uncond = bool(pv('apply_to_uncond', apply_to_uncond))
|
| 408 |
+
stack_prev_control = bool(pv('stack_prev_control', stack_prev_control))
|
| 409 |
+
split_apply = bool(pv('split_apply', split_apply))
|
| 410 |
+
edge_start_percent = float(pv('edge_start_percent', edge_start_percent))
|
| 411 |
+
edge_end_percent = float(pv('edge_end_percent', edge_end_percent))
|
| 412 |
+
depth_start_percent = float(pv('depth_start_percent', depth_start_percent))
|
| 413 |
+
depth_end_percent = float(pv('depth_end_percent', depth_end_percent))
|
| 414 |
+
edge_strength_mul = float(pv('edge_strength_mul', edge_strength_mul))
|
| 415 |
+
depth_strength_mul = float(pv('depth_strength_mul', depth_strength_mul))
|
| 416 |
+
edge_width = float(pv('edge_width', edge_width))
|
| 417 |
+
edge_smooth = float(pv('edge_smooth', edge_smooth))
|
| 418 |
+
edge_single_line = bool(pv('edge_single_line', edge_single_line))
|
| 419 |
+
edge_single_strength = float(pv('edge_single_strength', edge_single_strength))
|
| 420 |
+
edge_depth_gate = bool(pv('edge_depth_gate', edge_depth_gate))
|
| 421 |
+
edge_depth_gamma = float(pv('edge_depth_gamma', edge_depth_gamma))
|
| 422 |
+
|
| 423 |
+
dev = image.device
|
| 424 |
+
dtype = image.dtype
|
| 425 |
+
B,H,W,C = image.shape
|
| 426 |
+
# Build depth/edges
|
| 427 |
+
depth = None
|
| 428 |
+
edges = None
|
| 429 |
+
if enable_depth:
|
| 430 |
+
model_path = depth_model_path or os.path.join(os.path.dirname(__file__), '..','depth-anything','depth_anything_v2_vitl.pth')
|
| 431 |
+
depth = _build_depth_map(image, int(depth_resolution), model_path, bool(hires_mask_auto))
|
| 432 |
+
if enable_pyra:
|
| 433 |
+
edges = _pyracanny(image,
|
| 434 |
+
int(pyra_low), int(pyra_high), int(pyra_resolution),
|
| 435 |
+
int(edge_thin_iter), float(edge_boost),
|
| 436 |
+
bool(smart_tune), float(smart_boost), bool(hires_mask_auto))
|
| 437 |
+
if depth is None and edges is None:
|
| 438 |
+
# Nothing to do: return inputs and zero preview
|
| 439 |
+
prev = torch.zeros((B, max(H,1), max(W,1), 3), device=dev, dtype=dtype)
|
| 440 |
+
return positive, negative, prev
|
| 441 |
+
|
| 442 |
+
if depth is None:
|
| 443 |
+
depth = torch.zeros_like(edges)
|
| 444 |
+
if edges is None:
|
| 445 |
+
edges = torch.zeros_like(depth)
|
| 446 |
+
|
| 447 |
+
# Edge post-process: width/single-line/smooth
|
| 448 |
+
def _edges_post(acc_t: torch.Tensor) -> torch.Tensor:
|
| 449 |
+
try:
|
| 450 |
+
import cv2, numpy as _np
|
| 451 |
+
acc = acc_t.detach().to('cpu').numpy()
|
| 452 |
+
img = (acc*255.0).astype(_np.uint8)
|
| 453 |
+
k = _np.ones((3,3), _np.uint8)
|
| 454 |
+
# Adjust thickness
|
| 455 |
+
w = float(edge_width)
|
| 456 |
+
if abs(w) > 1e-6:
|
| 457 |
+
it = int(abs(w))
|
| 458 |
+
frac = abs(w) - it
|
| 459 |
+
op = cv2.dilate if w > 0 else cv2.erode
|
| 460 |
+
y = img.copy()
|
| 461 |
+
for _ in range(max(0, it)):
|
| 462 |
+
y = op(y, k, iterations=1)
|
| 463 |
+
if frac > 1e-6:
|
| 464 |
+
y2 = op(y, k, iterations=1)
|
| 465 |
+
y = ((1.0-frac)*y.astype(_np.float32) + frac*y2.astype(_np.float32)).astype(_np.uint8)
|
| 466 |
+
img = y
|
| 467 |
+
# Collapse double lines to single centerline
|
| 468 |
+
if bool(edge_single_line) and float(edge_single_strength) > 1e-6:
|
| 469 |
+
try:
|
| 470 |
+
s = float(edge_single_strength)
|
| 471 |
+
close = cv2.morphologyEx(img, cv2.MORPH_CLOSE, k, iterations=1)
|
| 472 |
+
if hasattr(cv2, 'ximgproc') and hasattr(cv2.ximgproc, 'thinning'):
|
| 473 |
+
sk = cv2.ximgproc.thinning(close)
|
| 474 |
+
else:
|
| 475 |
+
# limited-iteration morphological skeletonization
|
| 476 |
+
iters = max(1, int(round(2 + 6*s)))
|
| 477 |
+
sk = _np.zeros_like(close)
|
| 478 |
+
src = close.copy()
|
| 479 |
+
elem = cv2.getStructuringElement(cv2.MORPH_CROSS, (3,3))
|
| 480 |
+
for _ in range(iters):
|
| 481 |
+
er = cv2.erode(src, elem, iterations=1)
|
| 482 |
+
op = cv2.morphologyEx(er, cv2.MORPH_OPEN, elem)
|
| 483 |
+
tmp = cv2.subtract(er, op)
|
| 484 |
+
sk = cv2.bitwise_or(sk, tmp)
|
| 485 |
+
src = er
|
| 486 |
+
if not _np.any(src):
|
| 487 |
+
break
|
| 488 |
+
# Blend skeleton back with original according to strength
|
| 489 |
+
img = ((_np.float32(1.0 - s) * img.astype(_np.float32)) + (_np.float32(s) * sk.astype(_np.float32))).astype(_np.uint8)
|
| 490 |
+
except Exception:
|
| 491 |
+
pass
|
| 492 |
+
# Smooth
|
| 493 |
+
if float(edge_smooth) > 1e-6:
|
| 494 |
+
sigma = max(0.1, min(2.0, float(edge_smooth) * 1.2))
|
| 495 |
+
img = cv2.GaussianBlur(img, (0,0), sigmaX=sigma)
|
| 496 |
+
out = torch.from_numpy((img.astype(_np.float32)/255.0)).to(device=acc_t.device, dtype=acc_t.dtype)
|
| 497 |
+
return out.clamp(0,1)
|
| 498 |
+
except Exception:
|
| 499 |
+
# Torch fallback: light blur-only
|
| 500 |
+
if float(edge_smooth) > 1e-6:
|
| 501 |
+
s = max(1, int(round(float(edge_smooth)*2)))
|
| 502 |
+
return F.avg_pool2d(acc_t.unsqueeze(0).unsqueeze(0), kernel_size=2*s+1, stride=1, padding=s)[0,0].clamp(0,1)
|
| 503 |
+
return acc_t
|
| 504 |
+
|
| 505 |
+
edges = _edges_post(edges)
|
| 506 |
+
|
| 507 |
+
# Depth gating of edges
|
| 508 |
+
if bool(edge_depth_gate):
|
| 509 |
+
# Inverted gating per feedback: use depth^gamma (nearer = stronger if depth is larger)
|
| 510 |
+
g = (depth.clamp(0,1)) ** float(edge_depth_gamma)
|
| 511 |
+
edges = (edges * g).clamp(0,1)
|
| 512 |
+
|
| 513 |
+
# Apply edge alpha before blending
|
| 514 |
+
edges = (edges * float(edge_alpha)).clamp(0,1)
|
| 515 |
+
|
| 516 |
+
fused = _blend(depth, edges, str(blend_mode), float(blend_factor))
|
| 517 |
+
# Apply as split (Edges then Depth) or single fused hint
|
| 518 |
+
if bool(split_apply):
|
| 519 |
+
# Fixed order for determinism: Depth first, then Edges
|
| 520 |
+
hint_edges = edges.unsqueeze(-1).repeat(1,1,1,3)
|
| 521 |
+
hint_depth = depth.unsqueeze(-1).repeat(1,1,1,3)
|
| 522 |
+
# Depth first
|
| 523 |
+
pos_mid, neg_mid = _apply_controlnet_separate(
|
| 524 |
+
positive, negative, control_net, hint_depth,
|
| 525 |
+
float(strength_pos) * float(depth_strength_mul),
|
| 526 |
+
float(strength_neg) * float(depth_strength_mul),
|
| 527 |
+
float(depth_start_percent), float(depth_end_percent), vae,
|
| 528 |
+
bool(apply_to_uncond), True
|
| 529 |
+
)
|
| 530 |
+
# Then edges
|
| 531 |
+
pos_out, neg_out = _apply_controlnet_separate(
|
| 532 |
+
pos_mid, neg_mid, control_net, hint_edges,
|
| 533 |
+
float(strength_pos) * float(edge_strength_mul),
|
| 534 |
+
float(strength_neg) * float(edge_strength_mul),
|
| 535 |
+
float(edge_start_percent), float(edge_end_percent), vae,
|
| 536 |
+
bool(apply_to_uncond), True
|
| 537 |
+
)
|
| 538 |
+
else:
|
| 539 |
+
hint = fused.unsqueeze(-1).repeat(1,1,1,3)
|
| 540 |
+
pos_out, neg_out = _apply_controlnet_separate(
|
| 541 |
+
positive, negative, control_net, hint,
|
| 542 |
+
float(strength_pos), float(strength_neg),
|
| 543 |
+
float(start_percent), float(end_percent), vae,
|
| 544 |
+
bool(apply_to_uncond), bool(stack_prev_control)
|
| 545 |
+
)
|
| 546 |
+
# Build preview: keep aspect ratio, set minimal side
|
| 547 |
+
prev_res = int(max(256, min(2048, preview_res)))
|
| 548 |
+
scale = prev_res / float(min(H, W))
|
| 549 |
+
out_h = max(1, int(round(H * scale)))
|
| 550 |
+
out_w = max(1, int(round(W * scale)))
|
| 551 |
+
prev = F.interpolate(fused.unsqueeze(0).unsqueeze(0), size=(out_h, out_w), mode='bilinear', align_corners=False)[0,0]
|
| 552 |
+
# Optionally reflect ControlNet strength in preview (display only)
|
| 553 |
+
if bool(preview_show_strength):
|
| 554 |
+
br = str(preview_strength_branch)
|
| 555 |
+
sp = float(strength_pos)
|
| 556 |
+
sn = float(strength_neg)
|
| 557 |
+
if br == 'negative':
|
| 558 |
+
s_vis = sn
|
| 559 |
+
elif br == 'max':
|
| 560 |
+
s_vis = max(sp, sn)
|
| 561 |
+
elif br == 'avg':
|
| 562 |
+
s_vis = 0.5 * (sp + sn)
|
| 563 |
+
else:
|
| 564 |
+
s_vis = sp
|
| 565 |
+
# clamp for display range
|
| 566 |
+
s_vis = max(0.0, min(1.0, s_vis))
|
| 567 |
+
prev = prev * s_vis
|
| 568 |
+
# Apply visualization brightness only for preview
|
| 569 |
+
prev = (prev * float(mask_brightness)).clamp(0.0, 1.0)
|
| 570 |
+
prev = prev.unsqueeze(-1).repeat(1,1,3).to(device=dev, dtype=dtype).unsqueeze(0)
|
| 571 |
+
return (pos_out, neg_out, prev)
|
| 572 |
+
|
| 573 |
+
|
| 574 |
+
# === Easy UI wrapper: simplified controls + Step/Custom preset logic ===
|
| 575 |
+
class MG_ControlFusionEasyUI(MG_ControlFusion):
|
| 576 |
+
@classmethod
|
| 577 |
+
def INPUT_TYPES(cls):
|
| 578 |
+
return {
|
| 579 |
+
"required": {
|
| 580 |
+
# Step preset first for emphasis
|
| 581 |
+
"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."}),
|
| 582 |
+
# Custom toggle: when enabled, UI values override the Step for visible controls
|
| 583 |
+
"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."}),
|
| 584 |
+
# Connectors
|
| 585 |
+
"image": ("IMAGE", {"tooltip": "Input RGB image (B,H,W,3) in 0..1."}),
|
| 586 |
+
"positive": ("CONDITIONING", {"tooltip": "Positive conditioning to apply ControlNet to."}),
|
| 587 |
+
"negative": ("CONDITIONING", {"tooltip": "Negative conditioning to apply ControlNet to."}),
|
| 588 |
+
"control_net": ("CONTROL_NET", {"tooltip": "ControlNet module receiving the fused mask as hint."}),
|
| 589 |
+
"vae": ("VAE", {"tooltip": "VAE used by ControlNet when encoding the hint."}),
|
| 590 |
+
# Minimal surface controls
|
| 591 |
+
"enable_depth": ("BOOLEAN", {"default": True, "tooltip": "Enable depth map fusion (Depth Anything v2 if available)."}),
|
| 592 |
+
"enable_pyra": ("BOOLEAN", {"default": True, "tooltip": "Enable PyraCanny edge detector."}),
|
| 593 |
+
"edge_alpha": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Opacity for edges before blending (0..1)."}),
|
| 594 |
+
"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)."}),
|
| 595 |
+
},
|
| 596 |
+
"optional": {}
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
RETURN_TYPES = ("CONDITIONING","CONDITIONING","IMAGE")
|
| 600 |
+
RETURN_NAMES = ("positive","negative","Mask_Preview")
|
| 601 |
+
FUNCTION = "apply_easy"
|
| 602 |
+
|
| 603 |
+
def apply_easy(self, preset_step, custom, image, positive, negative, control_net, vae,
|
| 604 |
+
enable_depth=True, enable_pyra=True, edge_alpha=1.0, blend_factor=0.02):
|
| 605 |
+
# Use Step preset; if custom is True, allow visible UI values to override inside base impl via custom_override
|
| 606 |
+
return super().apply(
|
| 607 |
+
image=image, positive=positive, negative=negative, control_net=control_net, vae=vae,
|
| 608 |
+
enable_depth=bool(enable_depth), enable_pyra=bool(enable_pyra), edge_alpha=float(edge_alpha), blend_factor=float(blend_factor),
|
| 609 |
+
preset_step=str(preset_step) if not bool(custom) else "Custom",
|
| 610 |
+
custom_override=bool(custom),
|
| 611 |
+
)
|
mod/easy/mg_supersimple_easy.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
"""MG_SuperSimple: Orchestrates a 1–4 step pipeline over CF→CADE pairs.
|
| 4 |
+
|
| 5 |
+
- Step 1: CADE with Step 1 preset. Exception: forces denoise=1.0.
|
| 6 |
+
- Steps 2..N: ControlFusion (CF) with Step N preset → CADE with Step N preset.
|
| 7 |
+
- When custom is True: visible CADE controls (seed/steps/cfg/denoise/sampler/scheduler/clipseg_text)
|
| 8 |
+
override corresponding Step presets across all steps (except step 1 denoise is always 1.0).
|
| 9 |
+
- When custom is False: all CADE values come from Step presets; node UI values are ignored.
|
| 10 |
+
- CF always uses its Step presets (no extra UI here) to keep the node minimal.
|
| 11 |
+
|
| 12 |
+
Inputs
|
| 13 |
+
- model/vae/latent/positive/negative: standard Comfy connectors
|
| 14 |
+
- control_net: ControlNet module for CF (required)
|
| 15 |
+
- reference_image/clip_vision: forwarded into CADE (optional)
|
| 16 |
+
|
| 17 |
+
Outputs
|
| 18 |
+
- (LATENT, IMAGE) from the final executed step
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import torch
|
| 22 |
+
|
| 23 |
+
from .mg_cade25_easy import ComfyAdaptiveDetailEnhancer25 as _CADE
|
| 24 |
+
from .mg_controlfusion_easy import MG_ControlFusion as _CF
|
| 25 |
+
from .mg_cade25_easy import _sampler_names as _sampler_names
|
| 26 |
+
from .mg_cade25_easy import _scheduler_names as _scheduler_names
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class MG_SuperSimple:
|
| 30 |
+
CATEGORY = "MagicNodes/Easy"
|
| 31 |
+
|
| 32 |
+
@classmethod
|
| 33 |
+
def INPUT_TYPES(cls):
|
| 34 |
+
return {
|
| 35 |
+
"required": {
|
| 36 |
+
# High-level pipeline control
|
| 37 |
+
"step_count": ("INT", {"default": 4, "min": 1, "max": 4, "tooltip": "Number of steps to run (1..4)."}),
|
| 38 |
+
"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)."}),
|
| 39 |
+
|
| 40 |
+
# Connectors
|
| 41 |
+
"model": ("MODEL", {}),
|
| 42 |
+
"positive": ("CONDITIONING", {}),
|
| 43 |
+
"negative": ("CONDITIONING", {}),
|
| 44 |
+
"vae": ("VAE", {}),
|
| 45 |
+
"latent": ("LATENT", {}),
|
| 46 |
+
"control_net": ("CONTROL_NET", {"tooltip": "ControlNet module used by ControlFusion."}),
|
| 47 |
+
|
| 48 |
+
# Shared CADE surface controls
|
| 49 |
+
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF, "control_after_generate": True, "tooltip": "Seed 0 = SmartSeed (Sobol + light probe). Non-zero = fixed seed (deterministic)."}),
|
| 50 |
+
"steps": ("INT", {"default": 25, "min": 1, "max": 10000, "tooltip": "KSampler steps for CADE (applies to all steps)."}),
|
| 51 |
+
"cfg": ("FLOAT", {"default": 4.5, "min": 0.0, "max": 100.0, "step": 0.1}),
|
| 52 |
+
# Denoise is clamped; Step 1 uses 1.0 regardless
|
| 53 |
+
"denoise": ("FLOAT", {"default": 0.65, "min": 0.35, "max": 0.9, "step": 0.0001}),
|
| 54 |
+
"sampler_name": (_sampler_names(), {"default": _sampler_names()[0]}),
|
| 55 |
+
"scheduler": (_scheduler_names(), {"default": "MGHybrid"}),
|
| 56 |
+
"clipseg_text": ("STRING", {"default": "hand, feet, face", "multiline": False, "tooltip": "Focus terms for CLIPSeg (comma-separated)."}),
|
| 57 |
+
},
|
| 58 |
+
"optional": {
|
| 59 |
+
"reference_image": ("IMAGE", {}),
|
| 60 |
+
"clip_vision": ("CLIP_VISION", {}),
|
| 61 |
+
},
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
RETURN_TYPES = ("LATENT", "IMAGE")
|
| 65 |
+
RETURN_NAMES = ("LATENT", "IMAGE")
|
| 66 |
+
FUNCTION = "run"
|
| 67 |
+
|
| 68 |
+
def _cade(self,
|
| 69 |
+
preset_step: str,
|
| 70 |
+
custom_override: bool,
|
| 71 |
+
model, vae, positive, negative, latent,
|
| 72 |
+
seed: int, steps: int, cfg: float, denoise: float,
|
| 73 |
+
sampler_name: str, scheduler: str,
|
| 74 |
+
clipseg_text: str,
|
| 75 |
+
reference_image=None, clip_vision=None):
|
| 76 |
+
# CADE core call mirrors CADEEasyUI -> apply_cade2
|
| 77 |
+
lat, img, _s, _c, _d, _mask = _CADE().apply_cade2(
|
| 78 |
+
model, vae, positive, negative, latent,
|
| 79 |
+
int(seed), int(steps), float(cfg), float(denoise),
|
| 80 |
+
str(sampler_name), str(scheduler), 0.0,
|
| 81 |
+
preset_step=str(preset_step), custom_override=bool(custom_override),
|
| 82 |
+
clipseg_text=str(clipseg_text),
|
| 83 |
+
reference_image=reference_image, clip_vision=clip_vision,
|
| 84 |
+
)
|
| 85 |
+
return lat, img
|
| 86 |
+
|
| 87 |
+
def _cf(self,
|
| 88 |
+
preset_step: str,
|
| 89 |
+
image, positive, negative, control_net, vae):
|
| 90 |
+
# Keep CF strictly on presets for SuperSimple (no extra UI),
|
| 91 |
+
# so pass custom_override=False intentionally.
|
| 92 |
+
pos, neg, _prev = _CF().apply(
|
| 93 |
+
image=image, positive=positive, negative=negative,
|
| 94 |
+
control_net=control_net, vae=vae,
|
| 95 |
+
preset_step=str(preset_step), custom_override=False,
|
| 96 |
+
)
|
| 97 |
+
return pos, neg
|
| 98 |
+
|
| 99 |
+
def run(self,
|
| 100 |
+
step_count, custom,
|
| 101 |
+
model, positive, negative, vae, latent, control_net,
|
| 102 |
+
seed, steps, cfg, denoise, sampler_name, scheduler, clipseg_text,
|
| 103 |
+
reference_image=None, clip_vision=None):
|
| 104 |
+
# Clamp step_count to 1..4
|
| 105 |
+
n = int(max(1, min(4, step_count)))
|
| 106 |
+
|
| 107 |
+
cur_latent = latent
|
| 108 |
+
cur_image = None
|
| 109 |
+
cur_pos = positive
|
| 110 |
+
cur_neg = negative
|
| 111 |
+
|
| 112 |
+
# Step 1: CADE with Step 1 preset, denoise forced to 1.0
|
| 113 |
+
denoise_step1 = 1.0
|
| 114 |
+
lat1, img1 = self._cade(
|
| 115 |
+
preset_step="Step 1",
|
| 116 |
+
custom_override=bool(custom),
|
| 117 |
+
model=model, vae=vae, positive=cur_pos, negative=cur_neg, latent=cur_latent,
|
| 118 |
+
seed=seed, steps=steps, cfg=cfg, denoise=denoise_step1,
|
| 119 |
+
sampler_name=sampler_name, scheduler=scheduler,
|
| 120 |
+
clipseg_text=clipseg_text,
|
| 121 |
+
reference_image=reference_image, clip_vision=clip_vision,
|
| 122 |
+
)
|
| 123 |
+
cur_latent, cur_image = lat1, img1
|
| 124 |
+
|
| 125 |
+
# Steps 2..n: CF -> CADE per step
|
| 126 |
+
for i in range(2, n + 1):
|
| 127 |
+
# ControlFusion on current image/conds
|
| 128 |
+
cur_pos, cur_neg = self._cf(
|
| 129 |
+
preset_step=f"Step {i}",
|
| 130 |
+
image=cur_image, positive=cur_pos, negative=cur_neg,
|
| 131 |
+
control_net=control_net, vae=vae,
|
| 132 |
+
)
|
| 133 |
+
# CADE with shared controls
|
| 134 |
+
# If no external reference_image is provided, use the previous step image
|
| 135 |
+
# so that reference_clean / CLIP-Vision gating can take effect.
|
| 136 |
+
ref_img = reference_image if (reference_image is not None) else cur_image
|
| 137 |
+
lat_i, img_i = self._cade(
|
| 138 |
+
preset_step=f"Step {i}",
|
| 139 |
+
custom_override=bool(custom),
|
| 140 |
+
model=model, vae=vae, positive=cur_pos, negative=cur_neg, latent=cur_latent,
|
| 141 |
+
seed=seed, steps=steps, cfg=cfg, denoise=denoise,
|
| 142 |
+
sampler_name=sampler_name, scheduler=scheduler,
|
| 143 |
+
clipseg_text=clipseg_text,
|
| 144 |
+
reference_image=ref_img, clip_vision=clip_vision,
|
| 145 |
+
)
|
| 146 |
+
cur_latent, cur_image = lat_i, img_i
|
| 147 |
+
|
| 148 |
+
return (cur_latent, cur_image)
|
mod/easy/preset_loader.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import Dict, Tuple
|
| 3 |
+
|
| 4 |
+
_CACHE: Dict[str, Tuple[float, Dict[str, Dict[str, object]]]] = {}
|
| 5 |
+
|
| 6 |
+
_MSG_PREFIX = "[MagicNodes][Presets]"
|
| 7 |
+
|
| 8 |
+
def _root_dir() -> str:
|
| 9 |
+
# .../MagicNodes/mod/easy -> .../MagicNodes
|
| 10 |
+
return os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
| 11 |
+
|
| 12 |
+
def _pressets_dir() -> str:
|
| 13 |
+
return os.path.join(_root_dir(), "pressets")
|
| 14 |
+
|
| 15 |
+
def _cfg_path(kind: str) -> str:
|
| 16 |
+
# kind examples: "mg_cade25", "mg_controlfusion"
|
| 17 |
+
return os.path.join(_pressets_dir(), f"{kind}.cfg")
|
| 18 |
+
|
| 19 |
+
def _parse_value(raw: str):
|
| 20 |
+
s = raw.strip()
|
| 21 |
+
if not s:
|
| 22 |
+
return ""
|
| 23 |
+
low = s.lower()
|
| 24 |
+
if low in ("true", "false"):
|
| 25 |
+
return low == "true"
|
| 26 |
+
try:
|
| 27 |
+
if "." in s or "e" in low:
|
| 28 |
+
return float(s)
|
| 29 |
+
return int(s)
|
| 30 |
+
except Exception:
|
| 31 |
+
pass
|
| 32 |
+
# variable substitution
|
| 33 |
+
s = s.replace("$(ROOT)", _root_dir())
|
| 34 |
+
if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")):
|
| 35 |
+
s = s[1:-1]
|
| 36 |
+
return s
|
| 37 |
+
|
| 38 |
+
def _load_kind(kind: str) -> Dict[str, Dict[str, object]]:
|
| 39 |
+
path = _cfg_path(kind)
|
| 40 |
+
if not os.path.isfile(path):
|
| 41 |
+
print(f"{_MSG_PREFIX} No configuration file for '{kind}' found; loaded defaults — results may be unpredictable!")
|
| 42 |
+
return {}
|
| 43 |
+
try:
|
| 44 |
+
mtime = os.path.getmtime(path)
|
| 45 |
+
cached = _CACHE.get(path)
|
| 46 |
+
if cached and cached[0] == mtime:
|
| 47 |
+
return cached[1]
|
| 48 |
+
|
| 49 |
+
data: Dict[str, Dict[str, object]] = {}
|
| 50 |
+
cur_section = None
|
| 51 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 52 |
+
for ln, line in enumerate(f, start=1):
|
| 53 |
+
line = line.strip()
|
| 54 |
+
if not line or line.startswith("#") or line.startswith(";"):
|
| 55 |
+
continue
|
| 56 |
+
if line.startswith("[") and line.endswith("]"):
|
| 57 |
+
cur_section = line[1:-1].strip().lower()
|
| 58 |
+
data.setdefault(cur_section, {})
|
| 59 |
+
continue
|
| 60 |
+
if ":" in line:
|
| 61 |
+
if cur_section is None:
|
| 62 |
+
print(f"{_MSG_PREFIX} Parse warning at line {ln}: key outside of any [section]; ignored")
|
| 63 |
+
continue
|
| 64 |
+
k, v = line.split(":", 1)
|
| 65 |
+
key = k.strip()
|
| 66 |
+
try:
|
| 67 |
+
val = _parse_value(v)
|
| 68 |
+
except Exception:
|
| 69 |
+
print(f"{_MSG_PREFIX} Missing or invalid parameter '{key}'; this may affect results!")
|
| 70 |
+
continue
|
| 71 |
+
data[cur_section][key] = val
|
| 72 |
+
else:
|
| 73 |
+
print(f"{_MSG_PREFIX} Unknown line at {ln}: '{line}'; ignored")
|
| 74 |
+
|
| 75 |
+
_CACHE[path] = (mtime, data)
|
| 76 |
+
return data
|
| 77 |
+
except Exception as e:
|
| 78 |
+
print(f"{_MSG_PREFIX} Failed to read '{path}': {e}. Loaded defaults — results may be unpredictable!")
|
| 79 |
+
return {}
|
| 80 |
+
|
| 81 |
+
def get(kind: str, step: str) -> Dict[str, object]:
|
| 82 |
+
"""Return dict of parameters for a given kind and step.
|
| 83 |
+
step accepts 'Step 1', '1', 'step1', case-insensitive.
|
| 84 |
+
"""
|
| 85 |
+
data = _load_kind(kind)
|
| 86 |
+
if not data:
|
| 87 |
+
return {}
|
| 88 |
+
label = step.strip().lower().replace(" ", "")
|
| 89 |
+
if label.startswith("step"):
|
| 90 |
+
key = label
|
| 91 |
+
elif label.isdigit():
|
| 92 |
+
key = f"step{label}"
|
| 93 |
+
else:
|
| 94 |
+
key = f"step{label}"
|
| 95 |
+
|
| 96 |
+
if key not in data:
|
| 97 |
+
# Special case: CF is intentionally not applied on Step 1 in this pipeline.
|
| 98 |
+
# Suppress noisy log for missing 'Step 1' in mg_controlfusion.
|
| 99 |
+
if kind == "mg_controlfusion" and key in ("step1", "1"):
|
| 100 |
+
return {}
|
| 101 |
+
print(f"{_MSG_PREFIX} Preset step '{step}' not found for '{kind}'; using defaults")
|
| 102 |
+
return {}
|
| 103 |
+
res = dict(data[key])
|
| 104 |
+
# Side-effect: when CADE presets are loaded, optionally enable KV pruning in attention
|
| 105 |
+
try:
|
| 106 |
+
if kind == "mg_cade25":
|
| 107 |
+
from .. import mg_sagpu_attention as sa_patch # local import to avoid cycles
|
| 108 |
+
kv_enable = bool(res.get("kv_prune_enable", False))
|
| 109 |
+
kv_keep = float(res.get("kv_keep", 0.85))
|
| 110 |
+
kv_min = int(res.get("kv_min_tokens", 128)) if "kv_min_tokens" in res else 128
|
| 111 |
+
if hasattr(sa_patch, "set_kv_prune"):
|
| 112 |
+
sa_patch.set_kv_prune(kv_enable, kv_keep, kv_min)
|
| 113 |
+
except Exception:
|
| 114 |
+
pass
|
| 115 |
+
return res
|
mod/hard/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MagicNodes Hard variants
|
| 2 |
+
|
| 3 |
+
Complex, full‑control node implementations. Imported and registered
|
| 4 |
+
from the package root to expose them under the UI category
|
| 5 |
+
"MagicNodes/Hard".
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
# No side effects on import.
|
| 9 |
+
|
mod/hard/mg_adaptive.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Adaptive sampler helper node (moved to mod/).
|
| 2 |
+
|
| 3 |
+
Keeps class/key name AdaptiveSamplerHelper for backward compatibility.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
from scipy.ndimage import laplace
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class AdaptiveSamplerHelper:
|
| 11 |
+
@classmethod
|
| 12 |
+
def INPUT_TYPES(cls):
|
| 13 |
+
return {
|
| 14 |
+
"required": {
|
| 15 |
+
"image": ("IMAGE", {}),
|
| 16 |
+
"steps": ("INT", {"default": 20, "min": 1, "max": 200}),
|
| 17 |
+
"cfg": ("FLOAT", {"default": 7.0, "min": 0.1, "max": 20.0, "step": 0.1}),
|
| 18 |
+
"denoise": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
RETURN_TYPES = ("INT", "FLOAT", "FLOAT")
|
| 23 |
+
RETURN_NAMES = ("steps", "cfg", "denoise")
|
| 24 |
+
FUNCTION = "tune"
|
| 25 |
+
CATEGORY = "MagicNodes"
|
| 26 |
+
|
| 27 |
+
def tune(self, image, steps, cfg, denoise):
|
| 28 |
+
img = image[0].cpu().numpy()
|
| 29 |
+
gray = img.mean(axis=2)
|
| 30 |
+
brightness = float(gray.mean())
|
| 31 |
+
contrast = float(gray.std())
|
| 32 |
+
sharpness = float(np.var(laplace(gray)))
|
| 33 |
+
|
| 34 |
+
tuned_steps = int(max(1, round(steps + sharpness * 10)))
|
| 35 |
+
tuned_cfg = float(cfg + contrast * 2.0)
|
| 36 |
+
tuned_denoise = float(np.clip(denoise + (0.5 - brightness), 0.0, 1.0))
|
| 37 |
+
|
| 38 |
+
return (tuned_steps, tuned_cfg, tuned_denoise)
|
| 39 |
+
|
mod/hard/mg_cade25.py
ADDED
|
@@ -0,0 +1,1864 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CADE 2.5: refined adaptive enhancer with reference clean and accumulation override.
|
| 2 |
+
|
| 3 |
+
Builds on the CADE2 Beta: single clean iteration loop, optional latent-based
|
| 4 |
+
parameter damping, CLIP-based reference clean, and per-run SageAttention
|
| 5 |
+
accumulation override.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations # moved/renamed module: mg_cade25
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
import os
|
| 12 |
+
import numpy as np
|
| 13 |
+
import torch.nn.functional as F
|
| 14 |
+
|
| 15 |
+
import nodes
|
| 16 |
+
import comfy.model_management as model_management
|
| 17 |
+
|
| 18 |
+
from .mg_adaptive import AdaptiveSamplerHelper
|
| 19 |
+
from .mg_zesmart_sampler_v1_1 import _build_hybrid_sigmas
|
| 20 |
+
import comfy.sample as _sample
|
| 21 |
+
import comfy.samplers as _samplers
|
| 22 |
+
import comfy.utils as _utils
|
| 23 |
+
from .mg_upscale_module import MagicUpscaleModule, clear_gpu_and_ram_cache
|
| 24 |
+
from .mg_controlfusion import _build_depth_map as _cf_build_depth_map
|
| 25 |
+
from .mg_ids import IntelligentDetailStabilizer
|
| 26 |
+
from .. import mg_sagpu_attention as sa_patch
|
| 27 |
+
# FDG/NAG experimental paths removed for now; keeping code lean
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# Lazy CLIPSeg cache
|
| 31 |
+
_CLIPSEG_MODEL = None
|
| 32 |
+
_CLIPSEG_PROC = None
|
| 33 |
+
_CLIPSEG_DEV = "cpu"
|
| 34 |
+
_CLIPSEG_FORCE_CPU = True # pin CLIPSeg to CPU to avoid device drift
|
| 35 |
+
|
| 36 |
+
# Per-iteration spatial guidance mask (B,1,H,W) in [0,1]; used by cfg_func when enabled
|
| 37 |
+
# Kept for potential future use with non-ONNX masks (e.g., CLIPSeg/ControlFusion),
|
| 38 |
+
# but not set by this node since ONNX paths are removed.
|
| 39 |
+
CURRENT_ONNX_MASK_BCHW = None
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# ONNX runtime initialization removed
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _try_init_clipseg():
|
| 46 |
+
"""Lazy-load CLIPSeg processor + model and choose device.
|
| 47 |
+
Returns True on success.
|
| 48 |
+
"""
|
| 49 |
+
global _CLIPSEG_MODEL, _CLIPSEG_PROC, _CLIPSEG_DEV
|
| 50 |
+
if (_CLIPSEG_MODEL is not None) and (_CLIPSEG_PROC is not None):
|
| 51 |
+
return True
|
| 52 |
+
try:
|
| 53 |
+
from transformers import CLIPSegProcessor, CLIPSegForImageSegmentation # type: ignore
|
| 54 |
+
except Exception:
|
| 55 |
+
if not globals().get("_CLIPSEG_WARNED", False):
|
| 56 |
+
print("[CADE2.5][CLIPSeg] transformers not available; CLIPSeg disabled.")
|
| 57 |
+
globals()["_CLIPSEG_WARNED"] = True
|
| 58 |
+
return False
|
| 59 |
+
try:
|
| 60 |
+
_CLIPSEG_PROC = CLIPSegProcessor.from_pretrained("CIDAS/clipseg-rd64-refined")
|
| 61 |
+
_CLIPSEG_MODEL = CLIPSegForImageSegmentation.from_pretrained("CIDAS/clipseg-rd64-refined")
|
| 62 |
+
if _CLIPSEG_FORCE_CPU:
|
| 63 |
+
_CLIPSEG_DEV = "cpu"
|
| 64 |
+
else:
|
| 65 |
+
_CLIPSEG_DEV = "cuda" if torch.cuda.is_available() else "cpu"
|
| 66 |
+
_CLIPSEG_MODEL = _CLIPSEG_MODEL.to(_CLIPSEG_DEV)
|
| 67 |
+
_CLIPSEG_MODEL.eval()
|
| 68 |
+
return True
|
| 69 |
+
except Exception as e:
|
| 70 |
+
print(f"[CADE2.5][CLIPSeg] failed to load model: {e}")
|
| 71 |
+
return False
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _clipseg_build_mask(image_bhwc: torch.Tensor,
|
| 75 |
+
text: str,
|
| 76 |
+
preview: int = 224,
|
| 77 |
+
threshold: float = 0.4,
|
| 78 |
+
blur: float = 7.0,
|
| 79 |
+
dilate: int = 4,
|
| 80 |
+
gain: float = 1.0,
|
| 81 |
+
ref_embed: torch.Tensor | None = None,
|
| 82 |
+
clip_vision=None,
|
| 83 |
+
ref_threshold: float = 0.03) -> torch.Tensor | None:
|
| 84 |
+
"""Return BHWC single-channel mask [0,1] from CLIPSeg.
|
| 85 |
+
- Uses cached CLIPSeg model; gracefully returns None on failure.
|
| 86 |
+
- Applies optional threshold/blur/dilate and scaling gain.
|
| 87 |
+
- If clip_vision + ref_embed provided, gates mask by CLIP-Vision distance.
|
| 88 |
+
"""
|
| 89 |
+
if not text or not isinstance(text, str):
|
| 90 |
+
return None
|
| 91 |
+
if not _try_init_clipseg():
|
| 92 |
+
return None
|
| 93 |
+
try:
|
| 94 |
+
# Prepare preview image (CPU PIL)
|
| 95 |
+
target = int(max(16, min(1024, preview)))
|
| 96 |
+
img = image_bhwc.detach().to('cpu')
|
| 97 |
+
B, H, W, C = img.shape
|
| 98 |
+
x = img[0].movedim(-1, 0).unsqueeze(0) # 1,C,H,W
|
| 99 |
+
x = F.interpolate(x, size=(target, target), mode='bilinear', align_corners=False)
|
| 100 |
+
x = x.clamp(0, 1)
|
| 101 |
+
arr = (x[0].movedim(0, -1).numpy() * 255.0).astype('uint8')
|
| 102 |
+
from PIL import Image # lazy import
|
| 103 |
+
pil_img = Image.fromarray(arr)
|
| 104 |
+
|
| 105 |
+
# Run CLIPSeg
|
| 106 |
+
import re
|
| 107 |
+
prompts = [t.strip() for t in re.split(r"[\|,;\n]+", text) if t.strip()]
|
| 108 |
+
if not prompts:
|
| 109 |
+
prompts = [text.strip()]
|
| 110 |
+
prompts = prompts[:8]
|
| 111 |
+
inputs = _CLIPSEG_PROC(text=prompts, images=[pil_img] * len(prompts), return_tensors="pt")
|
| 112 |
+
inputs = {k: v.to(_CLIPSEG_DEV) for k, v in inputs.items()}
|
| 113 |
+
with torch.inference_mode():
|
| 114 |
+
outputs = _CLIPSEG_MODEL(**inputs) # type: ignore
|
| 115 |
+
# logits: [N, H', W'] for N prompts
|
| 116 |
+
logits = outputs.logits # [N,h,w]
|
| 117 |
+
if logits.ndim == 2:
|
| 118 |
+
logits = logits.unsqueeze(0)
|
| 119 |
+
prob = torch.sigmoid(logits) # [N,h,w]
|
| 120 |
+
# Soft-OR fuse across prompts
|
| 121 |
+
prob = 1.0 - torch.prod(1.0 - prob.clamp(0, 1), dim=0, keepdim=True) # [1,h,w]
|
| 122 |
+
prob = prob.unsqueeze(1) # [1,1,h,w]
|
| 123 |
+
# Resize to original image size
|
| 124 |
+
prob = F.interpolate(prob, size=(H, W), mode='bilinear', align_corners=False)
|
| 125 |
+
m = prob[0, 0].to(dtype=image_bhwc.dtype, device=image_bhwc.device)
|
| 126 |
+
# Threshold + blur (approx)
|
| 127 |
+
if threshold > 0.0:
|
| 128 |
+
m = torch.where(m > float(threshold), m, torch.zeros_like(m))
|
| 129 |
+
# Gaussian blur via our depthwise helper
|
| 130 |
+
if blur > 0.0:
|
| 131 |
+
rad = int(max(1, min(7, round(blur))))
|
| 132 |
+
m = _gaussian_blur_nchw(m.unsqueeze(0).unsqueeze(0), sigma=float(max(0.5, blur)), radius=rad)[0, 0]
|
| 133 |
+
# Dilation via max-pool
|
| 134 |
+
if int(dilate) > 0:
|
| 135 |
+
k = int(dilate) * 2 + 1
|
| 136 |
+
p = int(dilate)
|
| 137 |
+
m = F.max_pool2d(m.unsqueeze(0).unsqueeze(0), kernel_size=k, stride=1, padding=p)[0, 0]
|
| 138 |
+
# Optional CLIP-Vision gating by reference distance
|
| 139 |
+
if (clip_vision is not None) and (ref_embed is not None):
|
| 140 |
+
try:
|
| 141 |
+
cur = _encode_clip_image(image_bhwc, clip_vision, target_res=224)
|
| 142 |
+
dist = _clip_cosine_distance(cur, ref_embed)
|
| 143 |
+
if dist > float(ref_threshold):
|
| 144 |
+
# up to +50% gain if сильно уехали
|
| 145 |
+
gate = 1.0 + min(0.5, (dist - float(ref_threshold)) * 4.0)
|
| 146 |
+
m = m * gate
|
| 147 |
+
except Exception:
|
| 148 |
+
pass
|
| 149 |
+
m = (m * float(max(0.0, gain))).clamp(0, 1)
|
| 150 |
+
return m.unsqueeze(0).unsqueeze(-1) # BHWC with B=1,C=1
|
| 151 |
+
except Exception as e:
|
| 152 |
+
if not globals().get("_CLIPSEG_WARNED", False):
|
| 153 |
+
print(f"[CADE2.5][CLIPSeg] mask failed: {e}")
|
| 154 |
+
globals()["_CLIPSEG_WARNED"] = True
|
| 155 |
+
return None
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def _np_to_mask_tensor(np_map: np.ndarray, out_h: int, out_w: int, device, dtype):
|
| 159 |
+
"""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."""
|
| 160 |
+
if np_map.ndim == 3:
|
| 161 |
+
np_map = np_map.reshape(np_map.shape[-2], np_map.shape[-1]) if (np_map.shape[0] == 1) else np_map.squeeze()
|
| 162 |
+
if np_map.ndim != 2:
|
| 163 |
+
return None
|
| 164 |
+
t = torch.from_numpy(np_map.astype(np.float32))
|
| 165 |
+
t = t.clamp_min(0.0)
|
| 166 |
+
t = t.unsqueeze(0).unsqueeze(0) # B=1,C=1,H,W
|
| 167 |
+
t = F.interpolate(t, size=(out_h, out_w), mode="bilinear", align_corners=False)
|
| 168 |
+
t = t.permute(0, 2, 3, 1).to(device=device, dtype=dtype) # B,H,W,C
|
| 169 |
+
return t.clamp(0, 1)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
# --- Firefly/Hot-pixel remover (image space, BHWC in 0..1) ---
|
| 173 |
+
def _median_pool3x3_bhwc(img_bhwc: torch.Tensor) -> torch.Tensor:
|
| 174 |
+
B, H, W, C = img_bhwc.shape
|
| 175 |
+
x = img_bhwc.permute(0, 3, 1, 2) # B,C,H,W
|
| 176 |
+
unfold = F.unfold(x, kernel_size=3, padding=1) # B, 9*C, H*W
|
| 177 |
+
unfold = unfold.view(B, x.shape[1], 9, H, W) # B,C,9,H,W
|
| 178 |
+
med, _ = torch.median(unfold, dim=2) # B,C,H,W
|
| 179 |
+
return med.permute(0, 2, 3, 1) # B,H,W,C
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def _despeckle_fireflies(img_bhwc: torch.Tensor,
|
| 183 |
+
thr: float = 0.985,
|
| 184 |
+
max_iso: float | None = None,
|
| 185 |
+
grad_gate: float = 0.25) -> torch.Tensor:
|
| 186 |
+
try:
|
| 187 |
+
dev, dt = img_bhwc.device, img_bhwc.dtype
|
| 188 |
+
B, H, W, C = img_bhwc.shape
|
| 189 |
+
s = max(H, W) / 1024.0
|
| 190 |
+
k = 3 if s <= 1.1 else (5 if s <= 2.0 else 7)
|
| 191 |
+
pad = k // 2
|
| 192 |
+
lum = (0.2126 * img_bhwc[..., 0] + 0.7152 * img_bhwc[..., 1] + 0.0722 * img_bhwc[..., 2]).to(device=dev, dtype=dt)
|
| 193 |
+
try:
|
| 194 |
+
q = float(torch.quantile(lum.reshape(-1), 0.9995).item())
|
| 195 |
+
thr_eff = max(float(thr), min(0.997, q))
|
| 196 |
+
except Exception:
|
| 197 |
+
thr_eff = float(thr)
|
| 198 |
+
# S/V based candidate: white, low saturation
|
| 199 |
+
R, G, Bc = img_bhwc[..., 0], img_bhwc[..., 1], img_bhwc[..., 2]
|
| 200 |
+
V = torch.maximum(R, torch.maximum(G, Bc))
|
| 201 |
+
mi = torch.minimum(R, torch.minimum(G, Bc))
|
| 202 |
+
S = 1.0 - (mi / (V + 1e-6))
|
| 203 |
+
v_thr = max(0.985, thr_eff)
|
| 204 |
+
s_thr = 0.06
|
| 205 |
+
cand = (V > v_thr) & (S < s_thr)
|
| 206 |
+
# gradient gate
|
| 207 |
+
kx = torch.tensor([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], device=dev, dtype=dt).view(1, 1, 3, 3)
|
| 208 |
+
ky = torch.tensor([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], device=dev, dtype=dt).view(1, 1, 3, 3)
|
| 209 |
+
gx = F.conv2d(lum.unsqueeze(1), kx, padding=1)
|
| 210 |
+
gy = F.conv2d(lum.unsqueeze(1), ky, padding=1)
|
| 211 |
+
grad = torch.sqrt(gx * gx + gy * gy).squeeze(1)
|
| 212 |
+
safe_gate = float(grad_gate) * (k / 3.0) ** 0.5
|
| 213 |
+
cand = cand & (grad < safe_gate)
|
| 214 |
+
if cand.any():
|
| 215 |
+
try:
|
| 216 |
+
import cv2, numpy as _np
|
| 217 |
+
masks = []
|
| 218 |
+
for b in range(cand.shape[0]):
|
| 219 |
+
msk = cand[b].detach().to('cpu').numpy().astype('uint8') * 255
|
| 220 |
+
num, labels, stats, _ = cv2.connectedComponentsWithStats(msk, connectivity=8)
|
| 221 |
+
rem = _np.zeros_like(msk, dtype='uint8')
|
| 222 |
+
area_max = int(max(3, round((k * k) * 0.6)))
|
| 223 |
+
for lbl in range(1, num):
|
| 224 |
+
area = stats[lbl, cv2.CC_STAT_AREA]
|
| 225 |
+
if area <= area_max:
|
| 226 |
+
rem[labels == lbl] = 255
|
| 227 |
+
masks.append(torch.from_numpy(rem > 0))
|
| 228 |
+
rm = torch.stack(masks, dim=0).to(device=dev)
|
| 229 |
+
rm = rm.unsqueeze(-1)
|
| 230 |
+
if rm.any():
|
| 231 |
+
med = _median_pool3x3_bhwc(img_bhwc)
|
| 232 |
+
return torch.where(rm, med, img_bhwc)
|
| 233 |
+
except Exception:
|
| 234 |
+
pass
|
| 235 |
+
# Fallback: density isolation
|
| 236 |
+
bright = (img_bhwc.min(dim=-1).values > v_thr)
|
| 237 |
+
dens = F.avg_pool2d(bright.float().unsqueeze(1), k, 1, pad).squeeze(1)
|
| 238 |
+
max_iso_eff = (2.0 / (k * k)) if (max_iso is None) else float(max_iso)
|
| 239 |
+
iso = bright & (dens < max_iso_eff) & (grad < safe_gate)
|
| 240 |
+
if not iso.any():
|
| 241 |
+
return img_bhwc
|
| 242 |
+
med = _median_pool3x3_bhwc(img_bhwc)
|
| 243 |
+
return torch.where(iso.unsqueeze(-1), med, img_bhwc)
|
| 244 |
+
except Exception:
|
| 245 |
+
return img_bhwc
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def _try_heatmap_from_outputs(outputs: list, preview_hw: tuple[int, int]):
|
| 249 |
+
"""Return [H,W] heatmap from model outputs if possible.
|
| 250 |
+
Supports:
|
| 251 |
+
- Segmentation logits/probabilities (NCHW / NHWC)
|
| 252 |
+
- Keypoints arrays -> gaussian disks on points
|
| 253 |
+
- Bounding boxes -> soft rectangles
|
| 254 |
+
"""
|
| 255 |
+
if not outputs:
|
| 256 |
+
return None
|
| 257 |
+
|
| 258 |
+
Ht, Wt = int(preview_hw[0]), int(preview_hw[1])
|
| 259 |
+
|
| 260 |
+
def to_float(arr):
|
| 261 |
+
if arr.dtype not in (np.float32, np.float64):
|
| 262 |
+
try:
|
| 263 |
+
arr = arr.astype(np.float32)
|
| 264 |
+
except Exception:
|
| 265 |
+
return None
|
| 266 |
+
return arr
|
| 267 |
+
|
| 268 |
+
def sigmoid(x):
|
| 269 |
+
return 1.0 / (1.0 + np.exp(-x))
|
| 270 |
+
|
| 271 |
+
# 1) Prefer any spatial heatmap first
|
| 272 |
+
for out in outputs:
|
| 273 |
+
try:
|
| 274 |
+
arr = np.asarray(out)
|
| 275 |
+
except Exception:
|
| 276 |
+
continue
|
| 277 |
+
arr = to_float(arr)
|
| 278 |
+
if arr is None:
|
| 279 |
+
continue
|
| 280 |
+
if arr.ndim == 4:
|
| 281 |
+
n, a, b, c = arr.shape
|
| 282 |
+
if c <= 4 and a >= 8 and b >= 8:
|
| 283 |
+
if c == 1:
|
| 284 |
+
hm = sigmoid(arr[0, :, :, 0]) if np.max(np.abs(arr)) > 1.5 else arr[0, :, :, 0]
|
| 285 |
+
else:
|
| 286 |
+
ex = np.exp(arr[0] - np.max(arr[0], axis=-1, keepdims=True))
|
| 287 |
+
prob = ex / np.clip(ex.sum(axis=-1, keepdims=True), 1e-6, None)
|
| 288 |
+
hm = 1.0 - prob[..., 0] if prob.shape[-1] > 1 else prob[..., 0]
|
| 289 |
+
return hm.astype(np.float32)
|
| 290 |
+
else:
|
| 291 |
+
if a == 1:
|
| 292 |
+
ch = arr[0, 0]
|
| 293 |
+
hm = sigmoid(ch) if np.max(np.abs(ch)) > 1.5 else ch
|
| 294 |
+
return hm.astype(np.float32)
|
| 295 |
+
else:
|
| 296 |
+
x = arr[0]
|
| 297 |
+
x = x - np.max(x, axis=0, keepdims=True)
|
| 298 |
+
ex = np.exp(x)
|
| 299 |
+
prob = ex / np.clip(np.sum(ex, axis=0, keepdims=True), 1e-6, None)
|
| 300 |
+
bg = prob[0] if prob.shape[0] > 1 else prob[0]
|
| 301 |
+
hm = 1.0 - bg
|
| 302 |
+
return hm.astype(np.float32)
|
| 303 |
+
if arr.ndim == 3:
|
| 304 |
+
if arr.shape[0] == 1 and arr.shape[1] >= 8 and arr.shape[2] >= 8:
|
| 305 |
+
return arr[0].astype(np.float32)
|
| 306 |
+
if arr.ndim == 2 and arr.shape[0] >= 8 and arr.shape[1] >= 8:
|
| 307 |
+
return arr.astype(np.float32)
|
| 308 |
+
|
| 309 |
+
# 2) Try keypoints and boxes
|
| 310 |
+
heat = np.zeros((Ht, Wt), dtype=np.float32)
|
| 311 |
+
|
| 312 |
+
def draw_gaussian(hm, cx, cy, sigma=2.5, amp=1.0):
|
| 313 |
+
r = max(1, int(3 * sigma))
|
| 314 |
+
xs = np.arange(-r, r + 1, dtype=np.float32)
|
| 315 |
+
ys = np.arange(-r, r + 1, dtype=np.float32)
|
| 316 |
+
gx = np.exp(-(xs**2) / (2 * sigma * sigma))
|
| 317 |
+
gy = np.exp(-(ys**2) / (2 * sigma * sigma))
|
| 318 |
+
g = np.outer(gy, gx) * float(amp)
|
| 319 |
+
x0 = int(round(cx)) - r
|
| 320 |
+
y0 = int(round(cy)) - r
|
| 321 |
+
x1 = x0 + g.shape[1]
|
| 322 |
+
y1 = y0 + g.shape[0]
|
| 323 |
+
if x1 < 0 or y1 < 0 or x0 >= Wt or y0 >= Ht:
|
| 324 |
+
return
|
| 325 |
+
xs0 = max(0, x0)
|
| 326 |
+
ys0 = max(0, y0)
|
| 327 |
+
xs1 = min(Wt, x1)
|
| 328 |
+
ys1 = min(Ht, y1)
|
| 329 |
+
gx0 = xs0 - x0
|
| 330 |
+
gy0 = ys0 - y0
|
| 331 |
+
gx1 = gx0 + (xs1 - xs0)
|
| 332 |
+
gy1 = gy0 + (ys1 - ys0)
|
| 333 |
+
hm[ys0:ys1, xs0:xs1] = np.maximum(hm[ys0:ys1, xs0:xs1], g[gy0:gy1, gx0:gx1])
|
| 334 |
+
|
| 335 |
+
def draw_soft_rect(hm, x0, y0, x1, y1, edge=3.0):
|
| 336 |
+
x0, y0, x1, y1 = int(x0), int(y0), int(x1), int(y1)
|
| 337 |
+
if x1 <= 0 or y1 <= 0 or x0 >= Wt or y0 >= Ht:
|
| 338 |
+
return
|
| 339 |
+
xs0 = max(0, min(x0, x1))
|
| 340 |
+
ys0 = max(0, min(y0, y1))
|
| 341 |
+
xs1 = min(Wt, max(x0, x1))
|
| 342 |
+
ys1 = min(Ht, max(y0, y1))
|
| 343 |
+
if xs1 - xs0 <= 0 or ys1 - ys0 <= 0:
|
| 344 |
+
return
|
| 345 |
+
hm[ys0:ys1, xs0:xs1] = np.maximum(hm[ys0:ys1, xs0:xs1], 1.0)
|
| 346 |
+
# feather edges with simple blur-like falloff
|
| 347 |
+
if edge > 0:
|
| 348 |
+
rad = int(edge)
|
| 349 |
+
if rad > 0:
|
| 350 |
+
# quick separable triangle filter
|
| 351 |
+
line = np.linspace(0, 1, rad + 1, dtype=np.float32)[1:]
|
| 352 |
+
for d in range(1, rad + 1):
|
| 353 |
+
w = line[d - 1]
|
| 354 |
+
if ys0 - d >= 0:
|
| 355 |
+
hm[ys0 - d:ys0, xs0:xs1] = np.maximum(hm[ys0 - d:ys0, xs0:xs1], w)
|
| 356 |
+
if ys1 + d <= Ht:
|
| 357 |
+
hm[ys1:ys1 + d, xs0:xs1] = np.maximum(hm[ys1:ys1 + d, xs0:xs1], w)
|
| 358 |
+
if xs0 - d >= 0:
|
| 359 |
+
hm[max(0, ys0 - d):min(Ht, ys1 + d), xs0 - d:xs0] = np.maximum(
|
| 360 |
+
hm[max(0, ys0 - d):min(Ht, ys1 + d), xs0 - d:xs0], w)
|
| 361 |
+
if xs1 + d <= Wt:
|
| 362 |
+
hm[max(0, ys0 - d):min(Ht, ys1 + d), xs1:xs1 + d] = np.maximum(
|
| 363 |
+
hm[max(0, ys0 - d):min(Ht, ys1 + d), xs1:xs1 + d], w)
|
| 364 |
+
|
| 365 |
+
# Inspect outputs to find plausible keypoints/boxes
|
| 366 |
+
for out in outputs:
|
| 367 |
+
try:
|
| 368 |
+
arr = np.asarray(out)
|
| 369 |
+
except Exception:
|
| 370 |
+
continue
|
| 371 |
+
arr = to_float(arr)
|
| 372 |
+
if arr is None:
|
| 373 |
+
continue
|
| 374 |
+
a = arr
|
| 375 |
+
# Squeeze batch dims like [1,N,4] -> [N,4]
|
| 376 |
+
while a.ndim > 2 and a.shape[0] == 1:
|
| 377 |
+
a = np.squeeze(a, axis=0)
|
| 378 |
+
# Keypoints: [N,2] or [N,3] or [K, N, 2/3] (relax N limit; subsample if huge)
|
| 379 |
+
if a.ndim == 2 and a.shape[-1] in (2, 3):
|
| 380 |
+
pts = a
|
| 381 |
+
elif a.ndim == 3 and a.shape[-1] in (2, 3):
|
| 382 |
+
pts = a.reshape(-1, a.shape[-1])
|
| 383 |
+
else:
|
| 384 |
+
pts = None
|
| 385 |
+
if pts is not None:
|
| 386 |
+
# Coordinates range guess: if max>1.2 -> absolute; else normalized
|
| 387 |
+
maxv = float(np.nanmax(np.abs(pts[:, :2]))) if pts.size else 0.0
|
| 388 |
+
for px, py, *rest in pts:
|
| 389 |
+
if np.isnan(px) or np.isnan(py):
|
| 390 |
+
continue
|
| 391 |
+
if maxv <= 1.2:
|
| 392 |
+
cx = float(px) * (Wt - 1)
|
| 393 |
+
cy = float(py) * (Ht - 1)
|
| 394 |
+
else:
|
| 395 |
+
cx = float(px)
|
| 396 |
+
cy = float(py)
|
| 397 |
+
base_sig = max(1.5, min(Ht, Wt) / 128.0)
|
| 398 |
+
if _ONNX_KPTS_ENABLE:
|
| 399 |
+
draw_gaussian(heat, cx, cy, sigma=base_sig * float(_ONNX_KPTS_SIGMA), amp=float(_ONNX_KPTS_GAIN))
|
| 400 |
+
else:
|
| 401 |
+
draw_gaussian(heat, cx, cy, sigma=base_sig)
|
| 402 |
+
continue
|
| 403 |
+
|
| 404 |
+
# Wholebody-style packed keypoints: [N, K*3] with triples (x,y,conf)
|
| 405 |
+
if _ONNX_KPTS_ENABLE and a.ndim == 2 and a.shape[-1] >= 6 and (a.shape[-1] % 3) == 0:
|
| 406 |
+
K = a.shape[-1] // 3
|
| 407 |
+
if K >= 5 and K <= 256:
|
| 408 |
+
# Guess coordinate range once
|
| 409 |
+
with np.errstate(invalid='ignore'):
|
| 410 |
+
maxv = float(np.nanmax(np.abs(a[:, :2]))) if a.size else 0.0
|
| 411 |
+
for i in range(a.shape[0]):
|
| 412 |
+
row = a[i]
|
| 413 |
+
kp = row.reshape(K, 3)
|
| 414 |
+
for (px, py, pc) in kp:
|
| 415 |
+
if np.isnan(px) or np.isnan(py):
|
| 416 |
+
continue
|
| 417 |
+
if np.isfinite(pc) and pc < float(_ONNX_KPTS_CONF):
|
| 418 |
+
continue
|
| 419 |
+
if maxv <= 1.2:
|
| 420 |
+
cx = float(px) * (Wt - 1)
|
| 421 |
+
cy = float(py) * (Ht - 1)
|
| 422 |
+
else:
|
| 423 |
+
cx = float(px)
|
| 424 |
+
cy = float(py)
|
| 425 |
+
base_sig = max(1.0, min(Ht, Wt) / 128.0)
|
| 426 |
+
draw_gaussian(heat, cx, cy, sigma=base_sig * float(_ONNX_KPTS_SIGMA), amp=float(_ONNX_KPTS_GAIN))
|
| 427 |
+
continue
|
| 428 |
+
# Boxes: [N,4+] (x0,y0,x1,y1) or [N, (x,y,w,h, [conf, ...])]; relax N limit (handle YOLO-style outputs)
|
| 429 |
+
if a.ndim == 2 and a.shape[-1] >= 4:
|
| 430 |
+
boxes = a
|
| 431 |
+
elif a.ndim == 3 and a.shape[-1] >= 4:
|
| 432 |
+
# choose the smallest first two dims as N
|
| 433 |
+
if a.shape[0] == 1:
|
| 434 |
+
boxes = a.reshape(-1, a.shape[-1])
|
| 435 |
+
else:
|
| 436 |
+
boxes = a.reshape(-1, a.shape[-1])
|
| 437 |
+
else:
|
| 438 |
+
boxes = None
|
| 439 |
+
if boxes is not None:
|
| 440 |
+
# Optional score gating (try to find a confidence column)
|
| 441 |
+
score = None
|
| 442 |
+
if boxes.shape[-1] >= 6:
|
| 443 |
+
score = boxes[:, 4]
|
| 444 |
+
# if classes follow, mix in best class prob
|
| 445 |
+
try:
|
| 446 |
+
score = score * np.max(boxes[:, 5:], axis=-1)
|
| 447 |
+
except Exception:
|
| 448 |
+
pass
|
| 449 |
+
elif boxes.shape[-1] == 5:
|
| 450 |
+
score = boxes[:, 4]
|
| 451 |
+
# Keep top-K by score if available
|
| 452 |
+
if score is not None:
|
| 453 |
+
try:
|
| 454 |
+
order = np.argsort(-score)
|
| 455 |
+
keep = order[: min(64, order.shape[0])]
|
| 456 |
+
boxes = boxes[keep]
|
| 457 |
+
score = score[keep]
|
| 458 |
+
except Exception:
|
| 459 |
+
score = None
|
| 460 |
+
|
| 461 |
+
xy = boxes[:, :4]
|
| 462 |
+
maxv = float(np.nanmax(np.abs(xy))) if xy.size else 0.0
|
| 463 |
+
if maxv <= 1.2:
|
| 464 |
+
x0 = xy[:, 0] * (Wt - 1)
|
| 465 |
+
y0 = xy[:, 1] * (Ht - 1)
|
| 466 |
+
x1 = xy[:, 2] * (Wt - 1)
|
| 467 |
+
y1 = xy[:, 3] * (Ht - 1)
|
| 468 |
+
else:
|
| 469 |
+
x0, y0, x1, y1 = xy[:, 0], xy[:, 1], xy[:, 2], xy[:, 3]
|
| 470 |
+
# Heuristic: if many boxes are inverted, treat as [x,y,w,h]
|
| 471 |
+
invalid = np.sum((x1 <= x0) | (y1 <= y0))
|
| 472 |
+
if invalid > 0.5 * x0.shape[0]:
|
| 473 |
+
x, y, w, h = x0, y0, x1, y1
|
| 474 |
+
x0 = x - w * 0.5
|
| 475 |
+
y0 = y - h * 0.5
|
| 476 |
+
x1 = x + w * 0.5
|
| 477 |
+
y1 = y + h * 0.5
|
| 478 |
+
for i in range(x0.shape[0]):
|
| 479 |
+
if score is not None and np.isfinite(score[i]) and score[i] < 0.2:
|
| 480 |
+
continue
|
| 481 |
+
draw_soft_rect(heat, x0[i], y0[i], x1[i], y1[i], edge=3.0)
|
| 482 |
+
|
| 483 |
+
# Embedded keypoints in YOLO-style rows: try to parse trailing triples (x,y,conf)
|
| 484 |
+
if _ONNX_KPTS_ENABLE and boxes.shape[-1] > 6:
|
| 485 |
+
D = boxes.shape[-1]
|
| 486 |
+
for i in range(boxes.shape[0]):
|
| 487 |
+
row = boxes[i]
|
| 488 |
+
parsed = False
|
| 489 |
+
# try [xyxy, conf, cls, kpts] or [xyxy, conf, kpts] or [xyxy, kpts]
|
| 490 |
+
for offset in (6, 5, 4):
|
| 491 |
+
t = D - offset
|
| 492 |
+
if t >= 6 and t % 3 == 0:
|
| 493 |
+
k = t // 3
|
| 494 |
+
kp = row[offset:offset + 3 * k].reshape(k, 3)
|
| 495 |
+
parsed = True
|
| 496 |
+
break
|
| 497 |
+
if not parsed:
|
| 498 |
+
continue
|
| 499 |
+
for (px, py, pc) in kp:
|
| 500 |
+
if np.isnan(px) or np.isnan(py):
|
| 501 |
+
continue
|
| 502 |
+
if pc < float(_ONNX_KPTS_CONF):
|
| 503 |
+
continue
|
| 504 |
+
if maxv <= 1.2:
|
| 505 |
+
cx = float(px) * (Wt - 1)
|
| 506 |
+
cy = float(py) * (Ht - 1)
|
| 507 |
+
else:
|
| 508 |
+
cx = float(px)
|
| 509 |
+
cy = float(py)
|
| 510 |
+
base_sig = max(1.0, min(Ht, Wt) / 128.0)
|
| 511 |
+
draw_gaussian(heat, cx, cy, sigma=base_sig * float(_ONNX_KPTS_SIGMA), amp=float(_ONNX_KPTS_GAIN))
|
| 512 |
+
|
| 513 |
+
if heat.max() > 0:
|
| 514 |
+
heat = np.clip(heat, 0.0, 1.0)
|
| 515 |
+
return heat
|
| 516 |
+
return None
|
| 517 |
+
|
| 518 |
+
|
| 519 |
+
def _onnx_build_mask(image_bhwc: torch.Tensor, preview: int, sensitivity: float, models_dir: str, anomaly_gain: float = 1.0) -> torch.Tensor:
|
| 520 |
+
"""Deprecated: ONNX path removed. Returns zero mask of input size."""
|
| 521 |
+
B, H, W, C = image_bhwc.shape
|
| 522 |
+
return torch.zeros((B, H, W, 1), device=image_bhwc.device, dtype=image_bhwc.dtype)
|
| 523 |
+
if not _try_init_onnx(models_dir):
|
| 524 |
+
return torch.zeros((image_bhwc.shape[0], image_bhwc.shape[1], image_bhwc.shape[2], 1), device=image_bhwc.device, dtype=image_bhwc.dtype)
|
| 525 |
+
|
| 526 |
+
if not _ONNX_SESS:
|
| 527 |
+
return torch.zeros((image_bhwc.shape[0], image_bhwc.shape[1], image_bhwc.shape[2], 1), device=image_bhwc.device, dtype=image_bhwc.dtype)
|
| 528 |
+
|
| 529 |
+
B, H, W, C = image_bhwc.shape
|
| 530 |
+
device = image_bhwc.device
|
| 531 |
+
dtype = image_bhwc.dtype
|
| 532 |
+
|
| 533 |
+
# Process per-batch image
|
| 534 |
+
masks = []
|
| 535 |
+
img_cpu = image_bhwc.detach().to('cpu')
|
| 536 |
+
for b in range(B):
|
| 537 |
+
masks_b = []
|
| 538 |
+
# Prepare input resized square preview
|
| 539 |
+
target = int(max(16, min(1024, preview)))
|
| 540 |
+
xb = img_cpu[b].movedim(-1, 0).unsqueeze(0) # 1,C,H,W
|
| 541 |
+
x_stretch = F.interpolate(xb, size=(target, target), mode='bilinear', align_corners=False).clamp(0, 1)
|
| 542 |
+
x_letter = _letterbox_nchw(xb, target).clamp(0, 1)
|
| 543 |
+
# Try four variants: stretch RGB, letterbox RGB, stretch BGR, letterbox BGR
|
| 544 |
+
variants = [
|
| 545 |
+
("stretch-RGB", x_stretch),
|
| 546 |
+
("letterbox-RGB", x_letter),
|
| 547 |
+
("stretch-BGR", x_stretch[:, [2, 1, 0], :, :]),
|
| 548 |
+
("letterbox-BGR", x_letter[:, [2, 1, 0], :, :]),
|
| 549 |
+
]
|
| 550 |
+
if _ONNX_DEBUG:
|
| 551 |
+
try:
|
| 552 |
+
print(f"[CADE2.5][ONNX] Build mask for image[{b}] -> preview {target}x{target}")
|
| 553 |
+
except Exception:
|
| 554 |
+
pass
|
| 555 |
+
|
| 556 |
+
for name, sess in list(_ONNX_SESS.items()):
|
| 557 |
+
try:
|
| 558 |
+
inputs = sess.get_inputs()
|
| 559 |
+
if not inputs:
|
| 560 |
+
continue
|
| 561 |
+
in_name = inputs[0].name
|
| 562 |
+
in_shape = inputs[0].shape if hasattr(inputs[0], 'shape') else None
|
| 563 |
+
# Choose layout automatically based on the presence of channel dim=3
|
| 564 |
+
if isinstance(in_shape, (list, tuple)) and len(in_shape) == 4:
|
| 565 |
+
dim_vals = []
|
| 566 |
+
for d in in_shape:
|
| 567 |
+
try:
|
| 568 |
+
dim_vals.append(int(d))
|
| 569 |
+
except Exception:
|
| 570 |
+
dim_vals.append(-1)
|
| 571 |
+
if dim_vals[-1] == 3:
|
| 572 |
+
layout = "NHWC"
|
| 573 |
+
else:
|
| 574 |
+
layout = "NCHW"
|
| 575 |
+
else:
|
| 576 |
+
layout = "NCHW?"
|
| 577 |
+
if _ONNX_DEBUG:
|
| 578 |
+
try:
|
| 579 |
+
print(f"[CADE2.5][ONNX] Model '{name}' in_shape={in_shape} layout={layout}")
|
| 580 |
+
except Exception:
|
| 581 |
+
pass
|
| 582 |
+
# Try multiple input variants and scales
|
| 583 |
+
hm = None
|
| 584 |
+
chosen = None
|
| 585 |
+
for vname, vx in variants:
|
| 586 |
+
if layout.startswith("NHWC"):
|
| 587 |
+
xin = vx.permute(0, 2, 3, 1)
|
| 588 |
+
else:
|
| 589 |
+
xin = vx
|
| 590 |
+
for scale in (1.0, 255.0):
|
| 591 |
+
inp = (xin * float(scale)).numpy().astype(np.float32)
|
| 592 |
+
feed = {in_name: inp}
|
| 593 |
+
outs = sess.run(None, feed)
|
| 594 |
+
if _ONNX_DEBUG:
|
| 595 |
+
try:
|
| 596 |
+
shapes = []
|
| 597 |
+
for o in outs:
|
| 598 |
+
try:
|
| 599 |
+
shapes.append(tuple(np.asarray(o).shape))
|
| 600 |
+
except Exception:
|
| 601 |
+
shapes.append("?")
|
| 602 |
+
print(f"[CADE2.5][ONNX] '{name}' {vname} scale={scale} -> outs shapes {shapes}")
|
| 603 |
+
except Exception:
|
| 604 |
+
pass
|
| 605 |
+
hm = _try_heatmap_from_outputs(outs, (target, target))
|
| 606 |
+
if _ONNX_DEBUG:
|
| 607 |
+
try:
|
| 608 |
+
if hm is None:
|
| 609 |
+
print(f"[CADE2.5][ONNX] '{name}' {vname} scale={scale}: no spatial heatmap detected")
|
| 610 |
+
else:
|
| 611 |
+
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}")
|
| 612 |
+
except Exception:
|
| 613 |
+
pass
|
| 614 |
+
if hm is not None and np.max(hm) > 0:
|
| 615 |
+
chosen = (vname, scale)
|
| 616 |
+
break
|
| 617 |
+
if hm is not None and np.max(hm) > 0:
|
| 618 |
+
break
|
| 619 |
+
if hm is None:
|
| 620 |
+
continue
|
| 621 |
+
# Scale by sensitivity and optional anomaly gain
|
| 622 |
+
gain = float(max(0.0, sensitivity))
|
| 623 |
+
if 'anomaly' in name.lower():
|
| 624 |
+
gain *= float(max(0.0, anomaly_gain))
|
| 625 |
+
hm = np.clip(hm * gain, 0.0, 1.0)
|
| 626 |
+
tmask = _np_to_mask_tensor(hm, H, W, device, dtype)
|
| 627 |
+
if tmask is not None:
|
| 628 |
+
masks_b.append(tmask)
|
| 629 |
+
if _ONNX_DEBUG:
|
| 630 |
+
try:
|
| 631 |
+
area = float(tmask.movedim(-1,1).mean().item())
|
| 632 |
+
if chosen is not None:
|
| 633 |
+
vname, scale = chosen
|
| 634 |
+
print(f"[CADE2.5][ONNX] '{name}' via {vname} x{scale} area={area:.4f}")
|
| 635 |
+
else:
|
| 636 |
+
print(f"[CADE2.5][ONNX] '{name}' contribution area={area:.4f}")
|
| 637 |
+
except Exception:
|
| 638 |
+
pass
|
| 639 |
+
except Exception:
|
| 640 |
+
# Ignore failing models
|
| 641 |
+
continue
|
| 642 |
+
if not masks_b:
|
| 643 |
+
masks.append(torch.zeros((1, H, W, 1), device=device, dtype=dtype))
|
| 644 |
+
else:
|
| 645 |
+
# Soft-OR fusion: 1 - prod(1 - m)
|
| 646 |
+
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
|
| 647 |
+
fused = 1.0 - torch.prod(1.0 - stack.clamp(0, 1), dim=0)
|
| 648 |
+
# Light smoothing via bilinear down/up (anti alias)
|
| 649 |
+
ch = fused.permute(0, 3, 1, 2) # B=1,C=1,H,W
|
| 650 |
+
dd = F.interpolate(ch, scale_factor=0.5, mode='bilinear', align_corners=False, recompute_scale_factor=False)
|
| 651 |
+
uu = F.interpolate(dd, size=(H, W), mode='bilinear', align_corners=False)
|
| 652 |
+
fused = uu.permute(0, 2, 3, 1).clamp(0, 1)
|
| 653 |
+
if _ONNX_DEBUG:
|
| 654 |
+
try:
|
| 655 |
+
area = float(fused.movedim(-1,1).mean().item())
|
| 656 |
+
print(f"[CADE2.5][ONNX] Fused area (image[{b}])={area:.4f}")
|
| 657 |
+
except Exception:
|
| 658 |
+
pass
|
| 659 |
+
masks.append(fused)
|
| 660 |
+
|
| 661 |
+
return torch.cat(masks, dim=0)
|
| 662 |
+
|
| 663 |
+
def _sampler_names():
|
| 664 |
+
try:
|
| 665 |
+
import comfy.samplers
|
| 666 |
+
return comfy.samplers.KSampler.SAMPLERS
|
| 667 |
+
except Exception:
|
| 668 |
+
return ["euler"]
|
| 669 |
+
|
| 670 |
+
|
| 671 |
+
def _scheduler_names():
|
| 672 |
+
try:
|
| 673 |
+
import comfy.samplers
|
| 674 |
+
scheds = list(comfy.samplers.KSampler.SCHEDULERS)
|
| 675 |
+
if "MGHybrid" not in scheds:
|
| 676 |
+
scheds.append("MGHybrid")
|
| 677 |
+
return scheds
|
| 678 |
+
except Exception:
|
| 679 |
+
return ["normal", "MGHybrid"]
|
| 680 |
+
|
| 681 |
+
|
| 682 |
+
def safe_decode(vae, lat, tile=512, ovlp=64):
|
| 683 |
+
h, w = lat["samples"].shape[-2:]
|
| 684 |
+
if min(h, w) > 1024:
|
| 685 |
+
# Increase overlap for ultra-hires to reduce seam artifacts
|
| 686 |
+
ov = 128 if max(h, w) > 2048 else ovlp
|
| 687 |
+
return vae.decode_tiled(lat["samples"], tile_x=tile, tile_y=tile, overlap=ov)
|
| 688 |
+
return vae.decode(lat["samples"])
|
| 689 |
+
|
| 690 |
+
|
| 691 |
+
def safe_encode(vae, img, tile=512, ovlp=64):
|
| 692 |
+
import math, torch.nn.functional as F
|
| 693 |
+
h, w = img.shape[1:3]
|
| 694 |
+
try:
|
| 695 |
+
stride = int(vae.spacial_compression_decode())
|
| 696 |
+
except Exception:
|
| 697 |
+
stride = 8
|
| 698 |
+
if stride <= 0:
|
| 699 |
+
stride = 8
|
| 700 |
+
def _align_up(x, s):
|
| 701 |
+
return int(((x + s - 1) // s) * s)
|
| 702 |
+
Ht = _align_up(h, stride)
|
| 703 |
+
Wt = _align_up(w, stride)
|
| 704 |
+
x = img
|
| 705 |
+
if (Ht != h) or (Wt != w):
|
| 706 |
+
# pad on bottom/right using replicate to avoid black borders
|
| 707 |
+
pad_h = Ht - h
|
| 708 |
+
pad_w = Wt - w
|
| 709 |
+
x_nchw = img.movedim(-1, 1)
|
| 710 |
+
x_nchw = F.pad(x_nchw, (0, pad_w, 0, pad_h), mode='replicate')
|
| 711 |
+
x = x_nchw.movedim(1, -1)
|
| 712 |
+
if min(Ht, Wt) > 1024:
|
| 713 |
+
ov = 128 if max(Ht, Wt) > 2048 else ovlp
|
| 714 |
+
return vae.encode_tiled(x[:, :, :, :3], tile_x=tile, tile_y=tile, overlap=ov)
|
| 715 |
+
return vae.encode(x[:, :, :, :3])
|
| 716 |
+
|
| 717 |
+
|
| 718 |
+
|
| 719 |
+
def _gaussian_kernel(kernel_size: int, sigma: float, device=None):
|
| 720 |
+
x, y = torch.meshgrid(
|
| 721 |
+
torch.linspace(-1, 1, kernel_size, device=device),
|
| 722 |
+
torch.linspace(-1, 1, kernel_size, device=device),
|
| 723 |
+
indexing="ij",
|
| 724 |
+
)
|
| 725 |
+
d = torch.sqrt(x * x + y * y)
|
| 726 |
+
g = torch.exp(-(d * d) / (2.0 * sigma * sigma))
|
| 727 |
+
return g / g.sum()
|
| 728 |
+
|
| 729 |
+
|
| 730 |
+
def _sharpen_image(image: torch.Tensor, sharpen_radius: int, sigma: float, alpha: float):
|
| 731 |
+
if sharpen_radius == 0:
|
| 732 |
+
return (image,)
|
| 733 |
+
|
| 734 |
+
image = image.to(model_management.get_torch_device())
|
| 735 |
+
batch_size, height, width, channels = image.shape
|
| 736 |
+
|
| 737 |
+
kernel_size = sharpen_radius * 2 + 1
|
| 738 |
+
kernel = _gaussian_kernel(kernel_size, sigma, device=image.device) * -(alpha * 10)
|
| 739 |
+
kernel = kernel.to(dtype=image.dtype)
|
| 740 |
+
center = kernel_size // 2
|
| 741 |
+
kernel[center, center] = kernel[center, center] - kernel.sum() + 1.0
|
| 742 |
+
kernel = kernel.repeat(channels, 1, 1).unsqueeze(1)
|
| 743 |
+
|
| 744 |
+
tensor_image = image.permute(0, 3, 1, 2)
|
| 745 |
+
tensor_image = F.pad(tensor_image, (sharpen_radius, sharpen_radius, sharpen_radius, sharpen_radius), 'reflect')
|
| 746 |
+
sharpened = F.conv2d(tensor_image, kernel, padding=center, groups=channels)[:, :, sharpen_radius:-sharpen_radius, sharpen_radius:-sharpen_radius]
|
| 747 |
+
sharpened = sharpened.permute(0, 2, 3, 1)
|
| 748 |
+
|
| 749 |
+
result = torch.clamp(sharpened, 0, 1)
|
| 750 |
+
return (result.to(model_management.intermediate_device()),)
|
| 751 |
+
|
| 752 |
+
|
| 753 |
+
def _encode_clip_image(image: torch.Tensor, clip_vision, target_res: int) -> torch.Tensor:
|
| 754 |
+
# image: BHWC in [0,1]
|
| 755 |
+
img = image.movedim(-1, 1) # BCHW
|
| 756 |
+
img = F.interpolate(img, size=(target_res, target_res), mode="bilinear", align_corners=False)
|
| 757 |
+
img = (img * 2.0) - 1.0
|
| 758 |
+
embeds = clip_vision.encode_image(img)["image_embeds"]
|
| 759 |
+
embeds = F.normalize(embeds, dim=-1)
|
| 760 |
+
return embeds
|
| 761 |
+
|
| 762 |
+
|
| 763 |
+
def _clip_cosine_distance(a: torch.Tensor, b: torch.Tensor) -> float:
|
| 764 |
+
if a.shape != b.shape:
|
| 765 |
+
m = min(a.shape[0], b.shape[0])
|
| 766 |
+
a = a[:m]
|
| 767 |
+
b = b[:m]
|
| 768 |
+
sim = (a * b).sum(dim=-1).mean().clamp(-1.0, 1.0).item()
|
| 769 |
+
return 1.0 - sim
|
| 770 |
+
|
| 771 |
+
|
| 772 |
+
def _gaussian_blur_nchw(x: torch.Tensor, sigma: float = 1.0, radius: int = 1) -> torch.Tensor:
|
| 773 |
+
"""Lightweight depthwise Gaussian blur for NCHW tensors.
|
| 774 |
+
Uses reflect padding and a normalized kernel built by _gaussian_kernel.
|
| 775 |
+
"""
|
| 776 |
+
if radius <= 0:
|
| 777 |
+
return x
|
| 778 |
+
ksz = radius * 2 + 1
|
| 779 |
+
kernel = _gaussian_kernel(ksz, sigma, device=x.device).to(dtype=x.dtype)
|
| 780 |
+
kernel = kernel.repeat(x.shape[1], 1, 1).unsqueeze(1) # [C,1,K,K]
|
| 781 |
+
x_pad = F.pad(x, (radius, radius, radius, radius), mode='reflect')
|
| 782 |
+
y = F.conv2d(x_pad, kernel, padding=0, groups=x.shape[1])
|
| 783 |
+
return y
|
| 784 |
+
|
| 785 |
+
|
| 786 |
+
def _letterbox_nchw(x: torch.Tensor, target: int, pad_val: float = 114.0 / 255.0) -> torch.Tensor:
|
| 787 |
+
"""Letterbox a BCHW tensor to target x target with constant padding (YOLO-style).
|
| 788 |
+
Preserves aspect ratio, centers content, pads with pad_val.
|
| 789 |
+
"""
|
| 790 |
+
if x.ndim != 4:
|
| 791 |
+
return F.interpolate(x, size=(target, target), mode='bilinear', align_corners=False)
|
| 792 |
+
b, c, h, w = x.shape
|
| 793 |
+
if h == 0 or w == 0:
|
| 794 |
+
return F.interpolate(x, size=(target, target), mode='bilinear', align_corners=False)
|
| 795 |
+
r = float(min(target / max(1, h), target / max(1, w)))
|
| 796 |
+
nh = max(1, int(round(h * r)))
|
| 797 |
+
nw = max(1, int(round(w * r)))
|
| 798 |
+
y = F.interpolate(x, size=(nh, nw), mode='bilinear', align_corners=False)
|
| 799 |
+
pt = (target - nh) // 2
|
| 800 |
+
pb = target - nh - pt
|
| 801 |
+
pl = (target - nw) // 2
|
| 802 |
+
pr = target - nw - pl
|
| 803 |
+
if pt < 0 or pb < 0 or pl < 0 or pr < 0:
|
| 804 |
+
# Fallback stretch if rounding went weird
|
| 805 |
+
return F.interpolate(x, size=(target, target), mode='bilinear', align_corners=False)
|
| 806 |
+
return F.pad(y, (pl, pr, pt, pb), mode='constant', value=float(pad_val))
|
| 807 |
+
|
| 808 |
+
|
| 809 |
+
def _fdg_filter(delta: torch.Tensor, low_gain: float, high_gain: float, sigma: float = 1.0, radius: int = 1) -> torch.Tensor:
|
| 810 |
+
"""Frequency-Decoupled Guidance: split delta into low/high bands and reweight.
|
| 811 |
+
delta: [B,C,H,W]
|
| 812 |
+
"""
|
| 813 |
+
low = _gaussian_blur_nchw(delta, sigma=sigma, radius=radius)
|
| 814 |
+
high = delta - low
|
| 815 |
+
return low * float(low_gain) + high * float(high_gain)
|
| 816 |
+
|
| 817 |
+
|
| 818 |
+
def _fdg_split_three(delta: torch.Tensor,
|
| 819 |
+
sigma_lo: float = 0.8,
|
| 820 |
+
sigma_hi: float = 2.0,
|
| 821 |
+
radius: int = 1) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 822 |
+
"""Tri-band split: returns (low, mid, high) for NCHW delta.
|
| 823 |
+
low = G(sigma_hi)
|
| 824 |
+
mid = G(sigma_lo) - G(sigma_hi)
|
| 825 |
+
high = delta - G(sigma_lo)
|
| 826 |
+
"""
|
| 827 |
+
sig_lo = float(max(0.05, sigma_lo))
|
| 828 |
+
sig_hi = float(max(sig_lo + 1e-3, sigma_hi))
|
| 829 |
+
blur_lo = _gaussian_blur_nchw(delta, sigma=sig_lo, radius=radius)
|
| 830 |
+
blur_hi = _gaussian_blur_nchw(delta, sigma=sig_hi, radius=radius)
|
| 831 |
+
low = blur_hi
|
| 832 |
+
mid = blur_lo - blur_hi
|
| 833 |
+
high = delta - blur_lo
|
| 834 |
+
return low, mid, high
|
| 835 |
+
|
| 836 |
+
|
| 837 |
+
def _fdg_energy_fraction(delta: torch.Tensor, sigma: float = 1.0, radius: int = 1) -> torch.Tensor:
|
| 838 |
+
"""Return fraction of high-frequency energy: E_high / (E_low + E_high)."""
|
| 839 |
+
low = _gaussian_blur_nchw(delta, sigma=sigma, radius=radius)
|
| 840 |
+
high = delta - low
|
| 841 |
+
e_low = (low * low).mean(dim=(1, 2, 3), keepdim=True)
|
| 842 |
+
e_high = (high * high).mean(dim=(1, 2, 3), keepdim=True)
|
| 843 |
+
frac = e_high / (e_low + e_high + 1e-8)
|
| 844 |
+
return frac
|
| 845 |
+
|
| 846 |
+
|
| 847 |
+
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,
|
| 848 |
+
midfreq_enable: bool = False, midfreq_gain: float = 0.0, midfreq_sigma_lo: float = 0.8, midfreq_sigma_hi: float = 2.0,
|
| 849 |
+
mahiro_plus_enable: bool = False, mahiro_plus_strength: float = 0.5,
|
| 850 |
+
eps_scale_enable: bool = False, eps_scale: float = 0.0):
|
| 851 |
+
|
| 852 |
+
"""Clone model and attach a cfg mixing function implementing RescaleCFG/FDG, CFGZero*/FD, or hybrid ZeResFDG.
|
| 853 |
+
guidance_mode: 'default' | 'RescaleCFG' | 'RescaleFDG' | 'CFGZero*' | 'CFGZeroFD' | 'ZeResFDG'
|
| 854 |
+
"""
|
| 855 |
+
if guidance_mode == "default":
|
| 856 |
+
return model
|
| 857 |
+
m = model.clone()
|
| 858 |
+
|
| 859 |
+
# State for momentum and sigma normalization across steps
|
| 860 |
+
prev_delta = {"t": None}
|
| 861 |
+
sigma_seen = {"max": None, "min": None}
|
| 862 |
+
# Spectral switching/adaptive low state
|
| 863 |
+
spec_state = {"ema": None, "mode": "CFGZeroFD"}
|
| 864 |
+
|
| 865 |
+
def cfg_func(args):
|
| 866 |
+
cond = args["cond"]
|
| 867 |
+
uncond = args["uncond"]
|
| 868 |
+
cond_scale = args["cond_scale"]
|
| 869 |
+
sigma = args.get("sigma", None)
|
| 870 |
+
x_orig = args.get("input", None)
|
| 871 |
+
|
| 872 |
+
# Local spatial gain from CURRENT_ONNX_MASK_BCHW, resized to cond spatial size
|
| 873 |
+
def _local_gain_for(hw):
|
| 874 |
+
if not bool(use_local_mask):
|
| 875 |
+
return None
|
| 876 |
+
m = globals().get("CURRENT_ONNX_MASK_BCHW", None)
|
| 877 |
+
if m is None:
|
| 878 |
+
return None
|
| 879 |
+
try:
|
| 880 |
+
Ht, Wt = int(hw[0]), int(hw[1])
|
| 881 |
+
g = m.to(device=cond.device, dtype=cond.dtype)
|
| 882 |
+
if g.shape[-2] != Ht or g.shape[-1] != Wt:
|
| 883 |
+
g = F.interpolate(g, size=(Ht, Wt), mode='bilinear', align_corners=False)
|
| 884 |
+
gi = float(mask_inside)
|
| 885 |
+
go = float(mask_outside)
|
| 886 |
+
gain = g * gi + (1.0 - g) * go # [B,1,H,W]
|
| 887 |
+
return gain
|
| 888 |
+
except Exception:
|
| 889 |
+
return None
|
| 890 |
+
|
| 891 |
+
# Allow hybrid switch per-step
|
| 892 |
+
mode = guidance_mode
|
| 893 |
+
if guidance_mode == "ZeResFDG":
|
| 894 |
+
if bool(ze_adaptive):
|
| 895 |
+
try:
|
| 896 |
+
delta_raw = args["cond"] - args["uncond"]
|
| 897 |
+
frac_b = _fdg_energy_fraction(delta_raw, sigma=float(fdg_sigma), radius=1) # [B,1,1,1]
|
| 898 |
+
frac = float(frac_b.mean().clamp(0.0, 1.0).item())
|
| 899 |
+
except Exception:
|
| 900 |
+
frac = 0.0
|
| 901 |
+
if spec_state["ema"] is None:
|
| 902 |
+
spec_state["ema"] = frac
|
| 903 |
+
else:
|
| 904 |
+
beta = float(max(0.0, min(0.99, fdg_ema_beta)))
|
| 905 |
+
spec_state["ema"] = beta * float(spec_state["ema"]) + (1.0 - beta) * frac
|
| 906 |
+
r = float(spec_state["ema"])
|
| 907 |
+
# Hysteresis: switch up/down with two thresholds
|
| 908 |
+
if spec_state["mode"] == "CFGZeroFD" and r >= float(ze_r_switch_hi):
|
| 909 |
+
spec_state["mode"] = "RescaleFDG"
|
| 910 |
+
elif spec_state["mode"] == "RescaleFDG" and r <= float(ze_r_switch_lo):
|
| 911 |
+
spec_state["mode"] = "CFGZeroFD"
|
| 912 |
+
mode = spec_state["mode"]
|
| 913 |
+
else:
|
| 914 |
+
try:
|
| 915 |
+
sigmas = args["model_options"]["transformer_options"]["sample_sigmas"]
|
| 916 |
+
matched_idx = (sigmas == args["timestep"][0]).nonzero()
|
| 917 |
+
if len(matched_idx) > 0:
|
| 918 |
+
current_idx = matched_idx.item()
|
| 919 |
+
else:
|
| 920 |
+
current_idx = 0
|
| 921 |
+
except Exception:
|
| 922 |
+
current_idx = 0
|
| 923 |
+
mode = "CFGZeroFD" if current_idx <= int(ze_zero_steps) else "RescaleFDG"
|
| 924 |
+
|
| 925 |
+
if mode in ("CFGZero*", "CFGZeroFD"):
|
| 926 |
+
# Optional zero-init for the first N steps
|
| 927 |
+
if use_zero_init and "model_options" in args and args.get("timestep") is not None:
|
| 928 |
+
try:
|
| 929 |
+
sigmas = args["model_options"]["transformer_options"]["sample_sigmas"]
|
| 930 |
+
matched_idx = (sigmas == args["timestep"][0]).nonzero()
|
| 931 |
+
if len(matched_idx) > 0:
|
| 932 |
+
current_idx = matched_idx.item()
|
| 933 |
+
else:
|
| 934 |
+
# fallback lookup
|
| 935 |
+
current_idx = 0
|
| 936 |
+
if current_idx <= int(zero_init_steps):
|
| 937 |
+
return cond * 0.0
|
| 938 |
+
except Exception:
|
| 939 |
+
pass
|
| 940 |
+
# Project cond onto uncond subspace (batch-wise alpha)
|
| 941 |
+
bsz = cond.shape[0]
|
| 942 |
+
pos_flat = cond.view(bsz, -1)
|
| 943 |
+
neg_flat = uncond.view(bsz, -1)
|
| 944 |
+
dot = torch.sum(pos_flat * neg_flat, dim=1, keepdim=True)
|
| 945 |
+
denom = torch.sum(neg_flat * neg_flat, dim=1, keepdim=True).clamp_min(1e-8)
|
| 946 |
+
alpha = (dot / denom).view(bsz, *([1] * (cond.dim() - 1)))
|
| 947 |
+
resid = cond - uncond * alpha
|
| 948 |
+
# Adaptive low gain if enabled
|
| 949 |
+
low_gain_eff = float(fdg_low)
|
| 950 |
+
if bool(fdg_low_adaptive) and spec_state["ema"] is not None:
|
| 951 |
+
s = float(spec_state["ema"]) # 0..1 fraction of high-frequency energy
|
| 952 |
+
lmin = float(fdg_low_min)
|
| 953 |
+
lmax = float(fdg_low_max)
|
| 954 |
+
low_gain_eff = max(0.0, min(2.0, lmin + (lmax - lmin) * s))
|
| 955 |
+
if mode == "CFGZeroFD":
|
| 956 |
+
resid = _fdg_filter(resid, low_gain=low_gain_eff, high_gain=fdg_high, sigma=float(fdg_sigma), radius=1)
|
| 957 |
+
# Apply local spatial gain to residual guidance
|
| 958 |
+
lg = _local_gain_for((cond.shape[-2], cond.shape[-1]))
|
| 959 |
+
if lg is not None:
|
| 960 |
+
resid = resid * lg.expand(-1, resid.shape[1], -1, -1)
|
| 961 |
+
noise_pred = uncond * alpha + cond_scale * resid
|
| 962 |
+
return noise_pred
|
| 963 |
+
|
| 964 |
+
# RescaleCFG/FDG path (with optional momentum/perp damping and S-curve shaping)
|
| 965 |
+
delta = cond - uncond
|
| 966 |
+
pd = float(max(0.0, min(1.0, perp_damp)))
|
| 967 |
+
if pd > 0.0 and (prev_delta["t"] is not None) and (prev_delta["t"].shape == delta.shape):
|
| 968 |
+
prev = prev_delta["t"]
|
| 969 |
+
denom = (prev * prev).sum(dim=(1,2,3), keepdim=True).clamp_min(1e-6)
|
| 970 |
+
coeff = ((delta * prev).sum(dim=(1,2,3), keepdim=True) / denom)
|
| 971 |
+
parallel = coeff * prev
|
| 972 |
+
delta = delta - pd * parallel
|
| 973 |
+
beta = float(max(0.0, min(0.95, momentum_beta)))
|
| 974 |
+
if beta > 0.0:
|
| 975 |
+
if prev_delta["t"] is None or prev_delta["t"].shape != delta.shape:
|
| 976 |
+
prev_delta["t"] = delta.detach()
|
| 977 |
+
delta = (1.0 - beta) * delta + beta * prev_delta["t"]
|
| 978 |
+
prev_delta["t"] = delta.detach()
|
| 979 |
+
cond = uncond + delta
|
| 980 |
+
else:
|
| 981 |
+
prev_delta["t"] = delta.detach()
|
| 982 |
+
# After momentum: optionally apply FDG and rebuild cond
|
| 983 |
+
if mode == "RescaleFDG":
|
| 984 |
+
# Adaptive low gain if enabled
|
| 985 |
+
low_gain_eff = float(fdg_low)
|
| 986 |
+
if bool(fdg_low_adaptive) and spec_state["ema"] is not None:
|
| 987 |
+
s = float(spec_state["ema"]) # 0..1
|
| 988 |
+
lmin = float(fdg_low_min)
|
| 989 |
+
lmax = float(fdg_low_max)
|
| 990 |
+
low_gain_eff = max(0.0, min(2.0, lmin + (lmax - lmin) * s))
|
| 991 |
+
delta_fdg = _fdg_filter(delta, low_gain=low_gain_eff, high_gain=fdg_high, sigma=float(fdg_sigma), radius=1)
|
| 992 |
+
# Optional mid-frequency emphasis (band-pass) blended on top
|
| 993 |
+
if bool(midfreq_enable) and abs(float(midfreq_gain)) > 1e-6:
|
| 994 |
+
lo, mid, hi = _fdg_split_three(delta, sigma_lo=float(midfreq_sigma_lo), sigma_hi=float(midfreq_sigma_hi), radius=1)
|
| 995 |
+
# Respect local mask gain if present
|
| 996 |
+
lg = _local_gain_for((cond.shape[-2], cond.shape[-1]))
|
| 997 |
+
if lg is not None:
|
| 998 |
+
mid = mid * lg.expand(-1, mid.shape[1], -1, -1)
|
| 999 |
+
delta_fdg = delta_fdg + float(midfreq_gain) * mid
|
| 1000 |
+
lg = _local_gain_for((cond.shape[-2], cond.shape[-1]))
|
| 1001 |
+
if lg is not None:
|
| 1002 |
+
delta_fdg = delta_fdg * lg.expand(-1, delta_fdg.shape[1], -1, -1)
|
| 1003 |
+
cond = uncond + delta_fdg
|
| 1004 |
+
else:
|
| 1005 |
+
lg = _local_gain_for((cond.shape[-2], cond.shape[-1]))
|
| 1006 |
+
if lg is not None:
|
| 1007 |
+
delta = delta * lg.expand(-1, delta.shape[1], -1, -1)
|
| 1008 |
+
cond = uncond + delta
|
| 1009 |
+
|
| 1010 |
+
cond_scale_eff = cond_scale
|
| 1011 |
+
if cfg_curve > 0.0 and (sigma is not None):
|
| 1012 |
+
s = sigma
|
| 1013 |
+
if s.ndim > 1:
|
| 1014 |
+
s = s.flatten()
|
| 1015 |
+
s_max = float(torch.max(s).item())
|
| 1016 |
+
s_min = float(torch.min(s).item())
|
| 1017 |
+
if sigma_seen["max"] is None:
|
| 1018 |
+
sigma_seen["max"] = s_max
|
| 1019 |
+
sigma_seen["min"] = s_min
|
| 1020 |
+
else:
|
| 1021 |
+
sigma_seen["max"] = max(sigma_seen["max"], s_max)
|
| 1022 |
+
sigma_seen["min"] = min(sigma_seen["min"], s_min)
|
| 1023 |
+
lo = max(1e-6, sigma_seen["min"])
|
| 1024 |
+
hi = max(lo * (1.0 + 1e-6), sigma_seen["max"])
|
| 1025 |
+
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)
|
| 1026 |
+
t = t.clamp(0.0, 1.0)
|
| 1027 |
+
k = 6.0 * float(cfg_curve)
|
| 1028 |
+
s_curve = torch.tanh((t - 0.5) * k)
|
| 1029 |
+
gain = 1.0 + 0.15 * float(cfg_curve) * s_curve
|
| 1030 |
+
if gain.ndim > 0:
|
| 1031 |
+
gain = gain.mean().item()
|
| 1032 |
+
cond_scale_eff = cond_scale * float(gain)
|
| 1033 |
+
|
| 1034 |
+
# Epsilon scaling (exposure bias correction): early steps get multiplier closer to (1 + eps_scale)
|
| 1035 |
+
eps_mult = 1.0
|
| 1036 |
+
if bool(eps_scale_enable) and (sigma is not None):
|
| 1037 |
+
try:
|
| 1038 |
+
s = sigma
|
| 1039 |
+
if s.ndim > 1:
|
| 1040 |
+
s = s.flatten()
|
| 1041 |
+
s_max = float(torch.max(s).item())
|
| 1042 |
+
s_min = float(torch.min(s).item())
|
| 1043 |
+
if sigma_seen["max"] is None:
|
| 1044 |
+
sigma_seen["max"] = s_max
|
| 1045 |
+
sigma_seen["min"] = s_min
|
| 1046 |
+
else:
|
| 1047 |
+
sigma_seen["max"] = max(sigma_seen["max"], s_max)
|
| 1048 |
+
sigma_seen["min"] = min(sigma_seen["min"], s_min)
|
| 1049 |
+
lo = max(1e-6, sigma_seen["min"])
|
| 1050 |
+
hi = max(lo * (1.0 + 1e-6), sigma_seen["max"])
|
| 1051 |
+
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)
|
| 1052 |
+
t_lin = t_lin.clamp(0.0, 1.0)
|
| 1053 |
+
w_early = (1.0 - t_lin).mean().item()
|
| 1054 |
+
eps_mult = float(1.0 + eps_scale * w_early)
|
| 1055 |
+
except Exception:
|
| 1056 |
+
eps_mult = float(1.0 + eps_scale)
|
| 1057 |
+
|
| 1058 |
+
if sigma is None or x_orig is None:
|
| 1059 |
+
return uncond + cond_scale * (cond - uncond)
|
| 1060 |
+
sigma_ = sigma.view(sigma.shape[:1] + (1,) * (cond.ndim - 1))
|
| 1061 |
+
x = x_orig / (sigma_ * sigma_ + 1.0)
|
| 1062 |
+
v_cond = ((x - (x_orig - cond)) * (sigma_ ** 2 + 1.0) ** 0.5) / (sigma_)
|
| 1063 |
+
v_uncond = ((x - (x_orig - uncond)) * (sigma_ ** 2 + 1.0) ** 0.5) / (sigma_)
|
| 1064 |
+
v_cfg = v_uncond + cond_scale_eff * (v_cond - v_uncond)
|
| 1065 |
+
ro_pos = torch.std(v_cond, dim=(1, 2, 3), keepdim=True)
|
| 1066 |
+
ro_cfg = torch.std(v_cfg, dim=(1, 2, 3), keepdim=True).clamp_min(1e-6)
|
| 1067 |
+
v_rescaled = v_cfg * (ro_pos / ro_cfg)
|
| 1068 |
+
v_final = float(rescale_multiplier) * v_rescaled + (1.0 - float(rescale_multiplier)) * v_cfg
|
| 1069 |
+
eps = x_orig - (x - (v_final * eps_mult) * sigma_ / (sigma_ * sigma_ + 1.0) ** 0.5)
|
| 1070 |
+
return eps
|
| 1071 |
+
|
| 1072 |
+
m.set_model_sampler_cfg_function(cfg_func, disable_cfg1_optimization=True)
|
| 1073 |
+
|
| 1074 |
+
# Optional directional post-mix inspired by Mahiro (global, no ONNX)
|
| 1075 |
+
if bool(mahiro_plus_enable):
|
| 1076 |
+
s_clamp = float(max(0.0, min(1.0, mahiro_plus_strength)))
|
| 1077 |
+
mb_state = {"ema": None}
|
| 1078 |
+
|
| 1079 |
+
def _sqrt_sign(x: torch.Tensor) -> torch.Tensor:
|
| 1080 |
+
return x.sign() * torch.sqrt(x.abs().clamp_min(1e-12))
|
| 1081 |
+
|
| 1082 |
+
def _hp_split(x: torch.Tensor, radius: int = 1, sigma: float = 1.0):
|
| 1083 |
+
low = _gaussian_blur_nchw(x, sigma=sigma, radius=radius)
|
| 1084 |
+
high = x - low
|
| 1085 |
+
return low, high
|
| 1086 |
+
|
| 1087 |
+
def _sched_gain(args) -> float:
|
| 1088 |
+
# Gentle mid-steps boost: triangle peak at the middle of schedule
|
| 1089 |
+
try:
|
| 1090 |
+
sigmas = args["model_options"]["transformer_options"]["sample_sigmas"]
|
| 1091 |
+
idx_t = args.get("timestep", None)
|
| 1092 |
+
if idx_t is None:
|
| 1093 |
+
return 1.0
|
| 1094 |
+
matched = (sigmas == idx_t[0]).nonzero()
|
| 1095 |
+
if len(matched) == 0:
|
| 1096 |
+
return 1.0
|
| 1097 |
+
i = float(matched.item())
|
| 1098 |
+
n = float(sigmas.shape[0])
|
| 1099 |
+
if n <= 1:
|
| 1100 |
+
return 1.0
|
| 1101 |
+
phase = i / (n - 1.0)
|
| 1102 |
+
tri = 1.0 - abs(2.0 * phase - 1.0)
|
| 1103 |
+
return float(0.6 + 0.4 * tri) # 0.6 at edges -> 1.0 mid
|
| 1104 |
+
except Exception:
|
| 1105 |
+
return 1.0
|
| 1106 |
+
|
| 1107 |
+
def mahiro_plus_post(args):
|
| 1108 |
+
try:
|
| 1109 |
+
scale = args.get('cond_scale', 1.0)
|
| 1110 |
+
cond_p = args['cond_denoised']
|
| 1111 |
+
uncond_p = args['uncond_denoised']
|
| 1112 |
+
cfg = args['denoised']
|
| 1113 |
+
|
| 1114 |
+
# Orthogonalize positive to negative direction (batch-wise)
|
| 1115 |
+
bsz = cond_p.shape[0]
|
| 1116 |
+
pos_flat = cond_p.view(bsz, -1)
|
| 1117 |
+
neg_flat = uncond_p.view(bsz, -1)
|
| 1118 |
+
dot = torch.sum(pos_flat * neg_flat, dim=1, keepdim=True)
|
| 1119 |
+
denom = torch.sum(neg_flat * neg_flat, dim=1, keepdim=True).clamp_min(1e-8)
|
| 1120 |
+
alpha = (dot / denom).view(bsz, *([1] * (cond_p.dim() - 1)))
|
| 1121 |
+
c_orth = cond_p - uncond_p * alpha
|
| 1122 |
+
|
| 1123 |
+
leap_raw = float(scale) * c_orth
|
| 1124 |
+
# Light high-pass emphasis for detail, protect low-frequency tone
|
| 1125 |
+
low, high = _hp_split(leap_raw, radius=1, sigma=1.0)
|
| 1126 |
+
leap = 0.35 * low + 1.00 * high
|
| 1127 |
+
|
| 1128 |
+
# Directional agreement (global cosine over flattened dims)
|
| 1129 |
+
u_leap = float(scale) * uncond_p
|
| 1130 |
+
merge = 0.5 * (leap + cfg)
|
| 1131 |
+
nu = _sqrt_sign(u_leap).flatten(1)
|
| 1132 |
+
nm = _sqrt_sign(merge).flatten(1)
|
| 1133 |
+
sim = F.cosine_similarity(nu, nm, dim=1).mean()
|
| 1134 |
+
a = torch.clamp((sim + 1.0) * 0.5, 0.0, 1.0)
|
| 1135 |
+
# Small EMA for temporal smoothness
|
| 1136 |
+
if mb_state["ema"] is None:
|
| 1137 |
+
mb_state["ema"] = float(a)
|
| 1138 |
+
else:
|
| 1139 |
+
mb_state["ema"] = 0.8 * float(mb_state["ema"]) + 0.2 * float(a)
|
| 1140 |
+
a_eff = float(mb_state["ema"])
|
| 1141 |
+
w = a_eff * cfg + (1.0 - a_eff) * leap
|
| 1142 |
+
|
| 1143 |
+
# Gentle energy match to CFG
|
| 1144 |
+
dims = tuple(range(1, w.dim()))
|
| 1145 |
+
ro_w = torch.std(w, dim=dims, keepdim=True).clamp_min(1e-6)
|
| 1146 |
+
ro_cfg = torch.std(cfg, dim=dims, keepdim=True).clamp_min(1e-6)
|
| 1147 |
+
w_res = w * (ro_cfg / ro_w)
|
| 1148 |
+
|
| 1149 |
+
# Schedule gain over steps (mid stronger)
|
| 1150 |
+
s_eff = s_clamp * _sched_gain(args)
|
| 1151 |
+
out = (1.0 - s_eff) * cfg + s_eff * w_res
|
| 1152 |
+
return out
|
| 1153 |
+
except Exception:
|
| 1154 |
+
return args['denoised']
|
| 1155 |
+
|
| 1156 |
+
try:
|
| 1157 |
+
m.set_model_sampler_post_cfg_function(mahiro_plus_post)
|
| 1158 |
+
except Exception:
|
| 1159 |
+
pass
|
| 1160 |
+
|
| 1161 |
+
# Quantile clamp stabilizer (per-sample): soft range limit for denoised tensor
|
| 1162 |
+
# Always on, under the hood. Helps prevent rare exploding values.
|
| 1163 |
+
def _qclamp_post(args):
|
| 1164 |
+
try:
|
| 1165 |
+
x = args.get("denoised", None)
|
| 1166 |
+
if x is None:
|
| 1167 |
+
return args["denoised"]
|
| 1168 |
+
dt = x.dtype
|
| 1169 |
+
xf = x.to(dtype=torch.float32)
|
| 1170 |
+
B = xf.shape[0]
|
| 1171 |
+
lo_q, hi_q = 0.001, 0.999
|
| 1172 |
+
out = []
|
| 1173 |
+
for i in range(B):
|
| 1174 |
+
t = xf[i].reshape(-1)
|
| 1175 |
+
try:
|
| 1176 |
+
lo = torch.quantile(t, lo_q)
|
| 1177 |
+
hi = torch.quantile(t, hi_q)
|
| 1178 |
+
except Exception:
|
| 1179 |
+
n = t.numel()
|
| 1180 |
+
k_lo = max(1, int(n * lo_q))
|
| 1181 |
+
k_hi = max(1, int(n * hi_q))
|
| 1182 |
+
lo = torch.kthvalue(t, k_lo).values
|
| 1183 |
+
hi = torch.kthvalue(t, k_hi).values
|
| 1184 |
+
out.append(xf[i].clamp(min=lo, max=hi))
|
| 1185 |
+
y = torch.stack(out, dim=0).to(dtype=dt)
|
| 1186 |
+
return y
|
| 1187 |
+
except Exception:
|
| 1188 |
+
return args["denoised"]
|
| 1189 |
+
|
| 1190 |
+
try:
|
| 1191 |
+
m.set_model_sampler_post_cfg_function(_qclamp_post)
|
| 1192 |
+
except Exception:
|
| 1193 |
+
pass
|
| 1194 |
+
|
| 1195 |
+
return m
|
| 1196 |
+
|
| 1197 |
+
|
| 1198 |
+
# --- AQClip-Lite: adaptive soft quantile clipping in latent space (tile overlap) ---
|
| 1199 |
+
@torch.no_grad()
|
| 1200 |
+
def _aqclip_lite(latent_bchw: torch.Tensor,
|
| 1201 |
+
tile: int = 32,
|
| 1202 |
+
stride: int = 16,
|
| 1203 |
+
alpha: float = 2.0,
|
| 1204 |
+
ema_state: dict | None = None,
|
| 1205 |
+
ema_beta: float = 0.8,
|
| 1206 |
+
H_override: torch.Tensor | None = None) -> tuple[torch.Tensor, dict]:
|
| 1207 |
+
try:
|
| 1208 |
+
z = latent_bchw
|
| 1209 |
+
B, C, H, W = z.shape
|
| 1210 |
+
dev, dt = z.device, z.dtype
|
| 1211 |
+
ksize = max(8, min(int(tile), min(H, W)))
|
| 1212 |
+
kstride = max(1, min(int(stride), ksize))
|
| 1213 |
+
|
| 1214 |
+
# Confidence map: attention entropy override or gradient proxy
|
| 1215 |
+
if (H_override is not None) and isinstance(H_override, torch.Tensor):
|
| 1216 |
+
hsrc = H_override.to(device=dev, dtype=dt)
|
| 1217 |
+
if hsrc.dim() == 3:
|
| 1218 |
+
hsrc = hsrc.unsqueeze(1)
|
| 1219 |
+
gpool = F.avg_pool2d(hsrc, kernel_size=ksize, stride=kstride)
|
| 1220 |
+
else:
|
| 1221 |
+
zm = z.mean(dim=1, keepdim=True)
|
| 1222 |
+
kx = torch.tensor([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], device=dev, dtype=dt).view(1, 1, 3, 3)
|
| 1223 |
+
ky = torch.tensor([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], device=dev, dtype=dt).view(1, 1, 3, 3)
|
| 1224 |
+
gx = F.conv2d(zm, kx, padding=1)
|
| 1225 |
+
gy = F.conv2d(zm, ky, padding=1)
|
| 1226 |
+
gmag = torch.sqrt(gx * gx + gy * gy)
|
| 1227 |
+
gpool = F.avg_pool2d(gmag, kernel_size=ksize, stride=kstride)
|
| 1228 |
+
gmax = gpool.amax(dim=(2, 3), keepdim=True).clamp_min(1e-6)
|
| 1229 |
+
Hn = (gpool / gmax).squeeze(1) # B,h',w'
|
| 1230 |
+
L = Hn.shape[1] * Hn.shape[2]
|
| 1231 |
+
Hn = Hn.reshape(B, L)
|
| 1232 |
+
|
| 1233 |
+
# Map confidence -> quantiles
|
| 1234 |
+
ql = 0.5 * (Hn ** 2)
|
| 1235 |
+
qh = 1.0 - 0.5 * ((1.0 - Hn) ** 2)
|
| 1236 |
+
|
| 1237 |
+
# Per-tile mean/std
|
| 1238 |
+
unf = F.unfold(z, kernel_size=ksize, stride=kstride) # B, C*ksize*ksize, L
|
| 1239 |
+
M = unf.shape[1]
|
| 1240 |
+
mu = unf.mean(dim=1).to(torch.float32) # B,L
|
| 1241 |
+
var = (unf.to(torch.float32) - mu.unsqueeze(1)).pow(2).mean(dim=1)
|
| 1242 |
+
sigma = (var + 1e-12).sqrt()
|
| 1243 |
+
|
| 1244 |
+
# Normal inverse approximation: ndtri(q) = sqrt(2)*erfinv(2q-1)
|
| 1245 |
+
def _ndtri(q: torch.Tensor) -> torch.Tensor:
|
| 1246 |
+
return (2.0 ** 0.5) * torch.special.erfinv(q.mul(2.0).sub(1.0).clamp(-0.999999, 0.999999))
|
| 1247 |
+
k_neg = _ndtri(ql).abs()
|
| 1248 |
+
k_pos = _ndtri(qh).abs()
|
| 1249 |
+
lo = mu - k_neg * sigma
|
| 1250 |
+
hi = mu + k_pos * sigma
|
| 1251 |
+
|
| 1252 |
+
# EMA smooth
|
| 1253 |
+
if ema_state is None:
|
| 1254 |
+
ema_state = {}
|
| 1255 |
+
b = float(max(0.0, min(0.999, ema_beta)))
|
| 1256 |
+
if 'lo' in ema_state and 'hi' in ema_state and ema_state['lo'].shape == lo.shape:
|
| 1257 |
+
lo = b * ema_state['lo'] + (1.0 - b) * lo
|
| 1258 |
+
hi = b * ema_state['hi'] + (1.0 - b) * hi
|
| 1259 |
+
ema_state['lo'] = lo.detach()
|
| 1260 |
+
ema_state['hi'] = hi.detach()
|
| 1261 |
+
|
| 1262 |
+
# Soft tanh clip (vectorized in unfold domain)
|
| 1263 |
+
mid = (lo + hi) * 0.5
|
| 1264 |
+
half = (hi - lo) * 0.5
|
| 1265 |
+
half = half.clamp_min(1e-6)
|
| 1266 |
+
y = (unf.to(torch.float32) - mid.unsqueeze(1)) / half.unsqueeze(1)
|
| 1267 |
+
y = torch.tanh(float(alpha) * y)
|
| 1268 |
+
unf_clipped = mid.unsqueeze(1) + half.unsqueeze(1) * y
|
| 1269 |
+
unf_clipped = unf_clipped.to(dt)
|
| 1270 |
+
|
| 1271 |
+
out = F.fold(unf_clipped, output_size=(H, W), kernel_size=ksize, stride=kstride)
|
| 1272 |
+
ones = torch.ones((B, M, L), device=dev, dtype=dt)
|
| 1273 |
+
w = F.fold(ones, output_size=(H, W), kernel_size=ksize, stride=kstride).clamp_min(1e-6)
|
| 1274 |
+
out = out / w
|
| 1275 |
+
return out, ema_state
|
| 1276 |
+
except Exception:
|
| 1277 |
+
return latent_bchw, (ema_state or {})
|
| 1278 |
+
|
| 1279 |
+
class ComfyAdaptiveDetailEnhancer25:
|
| 1280 |
+
@classmethod
|
| 1281 |
+
def INPUT_TYPES(cls):
|
| 1282 |
+
return {
|
| 1283 |
+
"required": {
|
| 1284 |
+
"model": ("MODEL", {}),
|
| 1285 |
+
"positive": ("CONDITIONING", {}),
|
| 1286 |
+
"negative": ("CONDITIONING", {}),
|
| 1287 |
+
"vae": ("VAE", {}),
|
| 1288 |
+
"latent": ("LATENT", {}),
|
| 1289 |
+
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}),
|
| 1290 |
+
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
|
| 1291 |
+
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step": 0.1}),
|
| 1292 |
+
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.0001}),
|
| 1293 |
+
"sampler_name": (_sampler_names(), {"default": _sampler_names()[0]}),
|
| 1294 |
+
"scheduler": (_scheduler_names(), {"default": _scheduler_names()[0]}),
|
| 1295 |
+
"iterations": ("INT", {"default": 1, "min": 1, "max": 1000}),
|
| 1296 |
+
"steps_delta": ("FLOAT", {"default": 0.0, "min": -1000.0, "max": 1000.0, "step": 0.01}),
|
| 1297 |
+
"cfg_delta": ("FLOAT", {"default": 0.0, "min": -100.0, "max": 100.0, "step": 0.01}),
|
| 1298 |
+
"denoise_delta": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.0001}),
|
| 1299 |
+
"apply_sharpen": ("BOOLEAN", {"default": False}),
|
| 1300 |
+
"apply_upscale": ("BOOLEAN", {"default": False}),
|
| 1301 |
+
"apply_ids": ("BOOLEAN", {"default": False}),
|
| 1302 |
+
"clip_clean": ("BOOLEAN", {"default": False}),
|
| 1303 |
+
"ids_strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
| 1304 |
+
"upscale_method": (MagicUpscaleModule.upscale_methods, {"default": "lanczos"}),
|
| 1305 |
+
"scale_by": ("FLOAT", {"default": 1.2, "min": 1.0, "max": 8.0, "step": 0.01}),
|
| 1306 |
+
"scale_delta": ("FLOAT", {"default": 0.0, "min": -8.0, "max": 8.0, "step": 0.01}),
|
| 1307 |
+
"noise_offset": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 0.5, "step": 0.01}),
|
| 1308 |
+
"threshold": ("FLOAT", {"default": 0.03, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "RMS latent drift threshold (smaller = more damping)."}),
|
| 1309 |
+
},
|
| 1310 |
+
"optional": {
|
| 1311 |
+
"Sharpnes_strenght": ("FLOAT", {"default": 0.300, "min": 0.0, "max": 1.0, "step": 0.001}),
|
| 1312 |
+
"latent_compare": ("BOOLEAN", {"default": False, "tooltip": "Use latent drift to gently damp params (safer than overwriting latents)."}),
|
| 1313 |
+
"accumulation": (["default", "fp32+fp16", "fp32+fp32"], {"default": "default", "tooltip": "Override SageAttention PV accumulation mode for this node run."}),
|
| 1314 |
+
"reference_clean": ("BOOLEAN", {"default": False, "tooltip": "Use CLIP-Vision similarity to a reference image to stabilize output."}),
|
| 1315 |
+
"reference_image": ("IMAGE", {}),
|
| 1316 |
+
"clip_vision": ("CLIP_VISION", {}),
|
| 1317 |
+
"ref_preview": ("INT", {"default": 224, "min": 64, "max": 512, "step": 16}),
|
| 1318 |
+
"ref_threshold": ("FLOAT", {"default": 0.03, "min": 0.0, "max": 0.2, "step": 0.001}),
|
| 1319 |
+
"ref_cooldown": ("INT", {"default": 1, "min": 1, "max": 8}),
|
| 1320 |
+
|
| 1321 |
+
# ONNX detectors removed
|
| 1322 |
+
|
| 1323 |
+
# Guidance controls
|
| 1324 |
+
"guidance_mode": (["default", "RescaleCFG", "RescaleFDG", "CFGZero*", "CFGZeroFD", "ZeResFDG"], {"default": "RescaleCFG", "tooltip": "Rescale (stable), RescaleFDG (spectral), CFGZero*, CFGZeroFD, or hybrid ZeResFDG."}),
|
| 1325 |
+
"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)."}),
|
| 1326 |
+
"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."}),
|
| 1327 |
+
"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)."}),
|
| 1328 |
+
"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)."}),
|
| 1329 |
+
|
| 1330 |
+
# NAG (Normalized Attention Guidance) toggles
|
| 1331 |
+
"use_nag": ("BOOLEAN", {"default": False, "tooltip": "Apply NAG inside CrossAttention (positive branch) during this node."}),
|
| 1332 |
+
"nag_scale": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 50.0, "step": 0.1}),
|
| 1333 |
+
"nag_tau": ("FLOAT", {"default": 2.5, "min": 0.0, "max": 10.0, "step": 0.01}),
|
| 1334 |
+
"nag_alpha": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01}),
|
| 1335 |
+
|
| 1336 |
+
# AQClip-Lite (adaptive latent clipping)
|
| 1337 |
+
"aqclip_enable": ("BOOLEAN", {"default": False, "tooltip": "Adaptive soft tile clipping with overlap (reduces spikes on uncertain regions)."}),
|
| 1338 |
+
"aq_tile": ("INT", {"default": 32, "min": 8, "max": 128, "step": 1}),
|
| 1339 |
+
"aq_stride": ("INT", {"default": 16, "min": 4, "max": 128, "step": 1}),
|
| 1340 |
+
"aq_alpha": ("FLOAT", {"default": 2.0, "min": 0.5, "max": 4.0, "step": 0.1}),
|
| 1341 |
+
"aq_ema_beta": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 0.99, "step": 0.01}),
|
| 1342 |
+
"aq_attn": ("BOOLEAN", {"default": False, "tooltip": "Use attention entropy as confidence (requires patched attention)."}),
|
| 1343 |
+
|
| 1344 |
+
# CFGZero* extras
|
| 1345 |
+
"use_zero_init": ("BOOLEAN", {"default": False, "tooltip": "For CFGZero*, zero out first few steps."}),
|
| 1346 |
+
"zero_init_steps": ("INT", {"default": 0, "min": 0, "max": 20, "step": 1}),
|
| 1347 |
+
|
| 1348 |
+
# FDG controls (placed last to avoid reordering existing fields)
|
| 1349 |
+
"fdg_low": ("FLOAT", {"default": 0.6, "min": 0.0, "max": 2.0, "step": 0.01, "tooltip": "Low-frequency gain (<1 to restrain masses)."}),
|
| 1350 |
+
"fdg_high": ("FLOAT", {"default": 1.3, "min": 0.5, "max": 2.5, "step": 0.01, "tooltip": "High-frequency gain (>1 to boost details)."}),
|
| 1351 |
+
"fdg_sigma": ("FLOAT", {"default": 1.0, "min": 0.5, "max": 2.5, "step": 0.05, "tooltip": "Gaussian sigma for FDG low-pass split."}),
|
| 1352 |
+
"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."}),
|
| 1353 |
+
|
| 1354 |
+
# Adaptive spectral switch (ZeRes) and adaptive low gain
|
| 1355 |
+
"ze_adaptive": ("BOOLEAN", {"default": False, "tooltip": "Enable spectral switch: CFGZeroFD, RescaleFDG by HF/LF ratio (EMA)."}),
|
| 1356 |
+
"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."}),
|
| 1357 |
+
"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)."}),
|
| 1358 |
+
"fdg_low_adaptive": ("BOOLEAN", {"default": False, "tooltip": "Adapt fdg_low by HF fraction (EMA)."}),
|
| 1359 |
+
"fdg_low_min": ("FLOAT", {"default": 0.45, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Lower bound for adaptive fdg_low."}),
|
| 1360 |
+
"fdg_low_max": ("FLOAT", {"default": 0.70, "min": 0.0, "max": 2.0, "step": 0.01, "tooltip": "Upper bound for adaptive fdg_low."}),
|
| 1361 |
+
"fdg_ema_beta": ("FLOAT", {"default": 0.80, "min": 0.0, "max": 0.99, "step": 0.01, "tooltip": "EMA smoothing for spectral ratio (higher = smoother)."}),
|
| 1362 |
+
|
| 1363 |
+
# ONNX local guidance and keypoints removed
|
| 1364 |
+
|
| 1365 |
+
# Muse Blend global directional post-mix
|
| 1366 |
+
"muse_blend": ("BOOLEAN", {"default": False, "tooltip": "Enable Muse Blend (Mahiro+): gentle directional positive blend (global)."}),
|
| 1367 |
+
"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)."}),
|
| 1368 |
+
# Exposure Bias Correction (epsilon scaling)
|
| 1369 |
+
"eps_scale_enable": ("BOOLEAN", {"default": False, "tooltip": "Exposure Bias Correction: scale predicted noise early in schedule."}),
|
| 1370 |
+
"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)."}),
|
| 1371 |
+
# KV pruning (self-attention speedup)
|
| 1372 |
+
"kv_prune_enable": ("BOOLEAN", {"default": False, "tooltip": "Speed: prune K/V tokens in self-attention by energy (safe on hi-res blocks)."}),
|
| 1373 |
+
"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."}),
|
| 1374 |
+
"kv_min_tokens": ("INT", {"default": 128, "min": 1, "max": 16384, "step": 1, "tooltip": "Minimum sequence length to apply KV pruning."}),
|
| 1375 |
+
"clipseg_enable": ("BOOLEAN", {"default": False, "tooltip": "Use CLIPSeg to build a text-driven mask (e.g., 'eyes | hands | face')."}),
|
| 1376 |
+
"clipseg_text": ("STRING", {"default": "", "multiline": False}),
|
| 1377 |
+
"clipseg_preview": ("INT", {"default": 224, "min": 64, "max": 512, "step": 16}),
|
| 1378 |
+
"clipseg_threshold": ("FLOAT", {"default": 0.40, "min": 0.0, "max": 1.0, "step": 0.05}),
|
| 1379 |
+
"clipseg_blur": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 15.0, "step": 0.1}),
|
| 1380 |
+
"clipseg_dilate": ("INT", {"default": 4, "min": 0, "max": 10, "step": 1}),
|
| 1381 |
+
"clipseg_gain": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 3.0, "step": 0.01}),
|
| 1382 |
+
"clipseg_blend": (["fuse", "replace", "intersect"], {"default": "fuse", "tooltip": "How to combine CLIPSeg with any pre-mask (if present)."}),
|
| 1383 |
+
"clipseg_ref_gate": ("BOOLEAN", {"default": False, "tooltip": "If reference provided, boost mask when far from reference (CLIP-Vision)."}),
|
| 1384 |
+
"clipseg_ref_threshold": ("FLOAT", {"default": 0.03, "min": 0.0, "max": 0.2, "step": 0.001}),
|
| 1385 |
+
|
| 1386 |
+
# Polish mode (final hi-res refinement)
|
| 1387 |
+
"polish_enable": ("BOOLEAN", {"default": False, "tooltip": "Polish: keep low-frequency shape from reference while allowing high-frequency details to refine."}),
|
| 1388 |
+
"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)."}),
|
| 1389 |
+
"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)."}),
|
| 1390 |
+
"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)."}),
|
| 1391 |
+
"polish_start_after": ("INT", {"default": 1, "min": 0, "max": 3, "step": 1, "tooltip": "Enable polish after N iterations (0=immediately)."}),
|
| 1392 |
+
"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."}),
|
| 1393 |
+
|
| 1394 |
+
},
|
| 1395 |
+
}
|
| 1396 |
+
|
| 1397 |
+
RETURN_TYPES = ("LATENT", "IMAGE", "INT", "FLOAT", "FLOAT", "IMAGE")
|
| 1398 |
+
RETURN_NAMES = ("LATENT", "IMAGE", "steps", "cfg", "denoise", "mask_preview")
|
| 1399 |
+
FUNCTION = "apply_cade2"
|
| 1400 |
+
CATEGORY = "MagicNodes"
|
| 1401 |
+
|
| 1402 |
+
def apply_cade2(self, model, vae, positive, negative, latent, seed, steps, cfg, denoise,
|
| 1403 |
+
sampler_name, scheduler, noise_offset, iterations=1, steps_delta=0.0,
|
| 1404 |
+
cfg_delta=0.0, denoise_delta=0.0, apply_sharpen=False,
|
| 1405 |
+
apply_upscale=False, apply_ids=False, clip_clean=False,
|
| 1406 |
+
ids_strength=0.5, upscale_method="lanczos", scale_by=1.2, scale_delta=0.0,
|
| 1407 |
+
Sharpnes_strenght=0.300, threshold=0.03, latent_compare=False, accumulation="default",
|
| 1408 |
+
reference_clean=False, reference_image=None, clip_vision=None, ref_preview=224, ref_threshold=0.03, ref_cooldown=1,
|
| 1409 |
+
guidance_mode="RescaleCFG", rescale_multiplier=0.7, momentum_beta=0.0, cfg_curve=0.0, perp_damp=0.0,
|
| 1410 |
+
use_nag=False, nag_scale=4.0, nag_tau=2.5, nag_alpha=0.25,
|
| 1411 |
+
aqclip_enable=False, aq_tile=32, aq_stride=16, aq_alpha=2.0, aq_ema_beta=0.8, aq_attn=False,
|
| 1412 |
+
use_zero_init=False, zero_init_steps=0,
|
| 1413 |
+
fdg_low=0.6, fdg_high=1.3, fdg_sigma=1.0, ze_res_zero_steps=2,
|
| 1414 |
+
ze_adaptive=False, ze_r_switch_hi=0.60, ze_r_switch_lo=0.45,
|
| 1415 |
+
fdg_low_adaptive=False, fdg_low_min=0.45, fdg_low_max=0.70, fdg_ema_beta=0.80,
|
| 1416 |
+
muse_blend=False, muse_blend_strength=0.5,
|
| 1417 |
+
eps_scale_enable=False, eps_scale=0.005,
|
| 1418 |
+
clipseg_enable=False, clipseg_text="", clipseg_preview=224,
|
| 1419 |
+
clipseg_threshold=0.40, clipseg_blur=7.0, clipseg_dilate=4,
|
| 1420 |
+
clipseg_gain=1.0, clipseg_blend="fuse", clipseg_ref_gate=False, clipseg_ref_threshold=0.03,
|
| 1421 |
+
polish_enable=False, polish_keep_low=0.4, polish_edge_lock=0.2, polish_sigma=1.0,
|
| 1422 |
+
polish_start_after=1, polish_keep_low_ramp=0.2,
|
| 1423 |
+
kv_prune_enable=False, kv_keep=0.85, kv_min_tokens=128):
|
| 1424 |
+
# Hard reset of any sticky globals from prior runs
|
| 1425 |
+
try:
|
| 1426 |
+
global CURRENT_ONNX_MASK_BCHW
|
| 1427 |
+
CURRENT_ONNX_MASK_BCHW = None
|
| 1428 |
+
except Exception:
|
| 1429 |
+
pass
|
| 1430 |
+
|
| 1431 |
+
image = safe_decode(vae, latent)
|
| 1432 |
+
|
| 1433 |
+
tuned_steps, tuned_cfg, tuned_denoise = AdaptiveSamplerHelper().tune(
|
| 1434 |
+
image, steps, cfg, denoise)
|
| 1435 |
+
|
| 1436 |
+
current_steps = tuned_steps
|
| 1437 |
+
current_cfg = tuned_cfg
|
| 1438 |
+
current_denoise = tuned_denoise
|
| 1439 |
+
# Work on a detached copy to avoid mutating input latent across runs
|
| 1440 |
+
try:
|
| 1441 |
+
current_latent = {"samples": latent["samples"].clone()}
|
| 1442 |
+
except Exception:
|
| 1443 |
+
current_latent = {"samples": latent["samples"]}
|
| 1444 |
+
current_scale = scale_by
|
| 1445 |
+
|
| 1446 |
+
ref_embed = None
|
| 1447 |
+
if reference_clean and (clip_vision is not None) and (reference_image is not None):
|
| 1448 |
+
try:
|
| 1449 |
+
ref_embed = _encode_clip_image(reference_image, clip_vision, ref_preview)
|
| 1450 |
+
except Exception:
|
| 1451 |
+
ref_embed = None
|
| 1452 |
+
|
| 1453 |
+
# Pre-disable any lingering NAG patch from previous runs and set PV accumulation for this node
|
| 1454 |
+
try:
|
| 1455 |
+
sa_patch.enable_crossattention_nag_patch(False)
|
| 1456 |
+
except Exception:
|
| 1457 |
+
pass
|
| 1458 |
+
prev_accum = getattr(sa_patch, "CURRENT_PV_ACCUM", None)
|
| 1459 |
+
sa_patch.CURRENT_PV_ACCUM = None if accumulation == "default" else accumulation
|
| 1460 |
+
# Enable NAG patch if requested
|
| 1461 |
+
try:
|
| 1462 |
+
sa_patch.enable_crossattention_nag_patch(bool(use_nag), float(nag_scale), float(nag_tau), float(nag_alpha))
|
| 1463 |
+
except Exception:
|
| 1464 |
+
pass
|
| 1465 |
+
|
| 1466 |
+
# Enable attention-entropy probe for AQClip Attn-mode
|
| 1467 |
+
try:
|
| 1468 |
+
if hasattr(sa_patch, "enable_attention_entropy_capture"):
|
| 1469 |
+
sa_patch.enable_attention_entropy_capture(bool(aq_attn), max_tokens=1024, max_heads=4)
|
| 1470 |
+
except Exception:
|
| 1471 |
+
pass
|
| 1472 |
+
|
| 1473 |
+
# Visual separation and start marker
|
| 1474 |
+
try:
|
| 1475 |
+
print("")
|
| 1476 |
+
except Exception:
|
| 1477 |
+
pass
|
| 1478 |
+
try:
|
| 1479 |
+
print("\x1b[32m==== Starting main job ====\x1b[0m")
|
| 1480 |
+
except Exception:
|
| 1481 |
+
pass
|
| 1482 |
+
|
| 1483 |
+
# Enable KV pruning (self-attention) if requested
|
| 1484 |
+
try:
|
| 1485 |
+
if hasattr(sa_patch, "set_kv_prune"):
|
| 1486 |
+
sa_patch.set_kv_prune(bool(kv_prune_enable), float(kv_keep), int(kv_min_tokens))
|
| 1487 |
+
except Exception:
|
| 1488 |
+
pass
|
| 1489 |
+
|
| 1490 |
+
mask_last = None
|
| 1491 |
+
try:
|
| 1492 |
+
with torch.inference_mode():
|
| 1493 |
+
__cade_noop = 0 # ensure non-empty with-block
|
| 1494 |
+
|
| 1495 |
+
# Preflight: reset sticky state and build external masks once (CPU-pinned)
|
| 1496 |
+
try:
|
| 1497 |
+
CURRENT_ONNX_MASK_BCHW = None
|
| 1498 |
+
except Exception:
|
| 1499 |
+
pass
|
| 1500 |
+
pre_mask = None
|
| 1501 |
+
pre_area = 0.0
|
| 1502 |
+
# ONNX mask removed
|
| 1503 |
+
# Build CLIPSeg mask once
|
| 1504 |
+
if bool(clipseg_enable) and isinstance(clipseg_text, str) and clipseg_text.strip() != "":
|
| 1505 |
+
try:
|
| 1506 |
+
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))
|
| 1507 |
+
if cmask is not None:
|
| 1508 |
+
if pre_mask is None:
|
| 1509 |
+
pre_mask = cmask
|
| 1510 |
+
else:
|
| 1511 |
+
if clipseg_blend == "replace":
|
| 1512 |
+
pre_mask = cmask
|
| 1513 |
+
elif clipseg_blend == "intersect":
|
| 1514 |
+
pre_mask = (pre_mask * cmask).clamp(0, 1)
|
| 1515 |
+
else:
|
| 1516 |
+
pre_mask = (1.0 - (1.0 - pre_mask) * (1.0 - cmask)).clamp(0, 1)
|
| 1517 |
+
except Exception:
|
| 1518 |
+
pass
|
| 1519 |
+
if pre_mask is not None:
|
| 1520 |
+
mask_last = pre_mask
|
| 1521 |
+
om = pre_mask.movedim(-1, 1)
|
| 1522 |
+
pre_area = float(om.mean().item())
|
| 1523 |
+
# One-time gentle damping from area
|
| 1524 |
+
try:
|
| 1525 |
+
if pre_area > 0.005:
|
| 1526 |
+
damp = 1.0 - min(0.10, 0.02 + pre_area * 0.08)
|
| 1527 |
+
current_denoise = max(0.10, current_denoise * damp)
|
| 1528 |
+
current_cfg = max(1.0, current_cfg * (1.0 - 0.005))
|
| 1529 |
+
except Exception:
|
| 1530 |
+
pass
|
| 1531 |
+
# Compact status
|
| 1532 |
+
try:
|
| 1533 |
+
clipseg_status = "on" if bool(clipseg_enable) and isinstance(clipseg_text, str) and clipseg_text.strip() != "" else "off"
|
| 1534 |
+
# print preflight info only in debug sessions (muted by default)
|
| 1535 |
+
if False:
|
| 1536 |
+
print(f"[CADE2.5][preflight] clipseg={clipseg_status} device={'cpu' if _CLIPSEG_FORCE_CPU else _CLIPSEG_DEV} mask_area={pre_area:.4f}")
|
| 1537 |
+
except Exception:
|
| 1538 |
+
pass
|
| 1539 |
+
# Freeze per-iteration external mask rebuild
|
| 1540 |
+
clipseg_enable = False
|
| 1541 |
+
# Depth gate cache for micro-detail injection (reuse per resolution)
|
| 1542 |
+
depth_gate_cache = {"size": None, "mask": None}
|
| 1543 |
+
for i in range(iterations):
|
| 1544 |
+
if i % 2 == 0:
|
| 1545 |
+
clear_gpu_and_ram_cache()
|
| 1546 |
+
|
| 1547 |
+
prev_samples = current_latent["samples"].clone().detach()
|
| 1548 |
+
|
| 1549 |
+
iter_seed = seed + i * 7777
|
| 1550 |
+
if noise_offset > 0.0:
|
| 1551 |
+
# Deterministic noise offset tied to iter_seed
|
| 1552 |
+
fade = 1.0 - (i / max(1, iterations))
|
| 1553 |
+
try:
|
| 1554 |
+
gen = torch.Generator(device='cpu')
|
| 1555 |
+
except Exception:
|
| 1556 |
+
gen = torch.Generator()
|
| 1557 |
+
gen.manual_seed(int(iter_seed) & 0xFFFFFFFF)
|
| 1558 |
+
eps = torch.randn(
|
| 1559 |
+
size=current_latent["samples"].shape,
|
| 1560 |
+
dtype=current_latent["samples"].dtype,
|
| 1561 |
+
device='cpu',
|
| 1562 |
+
generator=gen,
|
| 1563 |
+
).to(current_latent["samples"].device)
|
| 1564 |
+
current_latent["samples"] += (noise_offset * fade) * eps
|
| 1565 |
+
|
| 1566 |
+
# ONNX pre-sampling detectors removed
|
| 1567 |
+
|
| 1568 |
+
# CLIPSeg mask (optional)
|
| 1569 |
+
try:
|
| 1570 |
+
if bool(clipseg_enable) and isinstance(clipseg_text, str) and clipseg_text.strip() != "":
|
| 1571 |
+
img_prev2 = safe_decode(vae, current_latent)
|
| 1572 |
+
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))
|
| 1573 |
+
if cmask is not None:
|
| 1574 |
+
if mask_last is None:
|
| 1575 |
+
fused = cmask
|
| 1576 |
+
else:
|
| 1577 |
+
if clipseg_blend == "replace":
|
| 1578 |
+
fused = cmask
|
| 1579 |
+
elif clipseg_blend == "intersect":
|
| 1580 |
+
fused = (mask_last * cmask).clamp(0, 1)
|
| 1581 |
+
else:
|
| 1582 |
+
fused = (1.0 - (1.0 - mask_last) * (1.0 - cmask)).clamp(0, 1)
|
| 1583 |
+
mask_last = fused
|
| 1584 |
+
om = fused.movedim(-1, 1)
|
| 1585 |
+
area = float(om.mean().item())
|
| 1586 |
+
if area > 0.005:
|
| 1587 |
+
damp = 1.0 - min(0.10, 0.02 + area * 0.08)
|
| 1588 |
+
current_denoise = max(0.10, current_denoise * damp)
|
| 1589 |
+
current_cfg = max(1.0, current_cfg * (1.0 - 0.005))
|
| 1590 |
+
# No local guidance toggles here; keep optional mask hook clear
|
| 1591 |
+
except Exception:
|
| 1592 |
+
pass
|
| 1593 |
+
|
| 1594 |
+
# Guidance override via cfg_func when requested
|
| 1595 |
+
sampler_model = _wrap_model_with_guidance(
|
| 1596 |
+
model, guidance_mode, rescale_multiplier, momentum_beta, cfg_curve, perp_damp,
|
| 1597 |
+
use_zero_init=bool(use_zero_init), zero_init_steps=int(zero_init_steps),
|
| 1598 |
+
fdg_low=float(fdg_low), fdg_high=float(fdg_high), fdg_sigma=float(fdg_sigma),
|
| 1599 |
+
midfreq_enable=bool(False), midfreq_gain=float(0.0), midfreq_sigma_lo=float(0.8), midfreq_sigma_hi=float(2.0),
|
| 1600 |
+
ze_zero_steps=int(ze_res_zero_steps),
|
| 1601 |
+
ze_adaptive=bool(ze_adaptive), ze_r_switch_hi=float(ze_r_switch_hi), ze_r_switch_lo=float(ze_r_switch_lo),
|
| 1602 |
+
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),
|
| 1603 |
+
mahiro_plus_enable=bool(muse_blend), mahiro_plus_strength=float(muse_blend_strength),
|
| 1604 |
+
eps_scale_enable=bool(eps_scale_enable), eps_scale=float(eps_scale)
|
| 1605 |
+
)
|
| 1606 |
+
|
| 1607 |
+
if str(scheduler) == "MGHybrid":
|
| 1608 |
+
try:
|
| 1609 |
+
# Build ZeSmart hybrid sigmas with safe defaults
|
| 1610 |
+
sigmas = _build_hybrid_sigmas(
|
| 1611 |
+
sampler_model, int(current_steps), str(sampler_name), "hybrid",
|
| 1612 |
+
mix=0.5, denoise=float(current_denoise), jitter=0.01, seed=int(iter_seed),
|
| 1613 |
+
_debug=False, tail_smooth=0.15, auto_hybrid_tail=True, auto_tail_strength=0.4,
|
| 1614 |
+
)
|
| 1615 |
+
# Prepare latent + noise like in MG_ZeSmartSampler
|
| 1616 |
+
lat_img = current_latent["samples"]
|
| 1617 |
+
lat_img = _sample.fix_empty_latent_channels(sampler_model, lat_img)
|
| 1618 |
+
batch_inds = current_latent.get("batch_index", None)
|
| 1619 |
+
noise = _sample.prepare_noise(lat_img, int(iter_seed), batch_inds)
|
| 1620 |
+
noise_mask = current_latent.get("noise_mask", None)
|
| 1621 |
+
callback = nodes.latent_preview.prepare_callback(sampler_model, int(current_steps))
|
| 1622 |
+
disable_pbar = not _utils.PROGRESS_BAR_ENABLED
|
| 1623 |
+
sampler_obj = _samplers.sampler_object(str(sampler_name))
|
| 1624 |
+
samples = _sample.sample_custom(
|
| 1625 |
+
sampler_model, noise, float(current_cfg), sampler_obj, sigmas,
|
| 1626 |
+
positive, negative, lat_img,
|
| 1627 |
+
noise_mask=noise_mask, callback=callback,
|
| 1628 |
+
disable_pbar=disable_pbar, seed=int(iter_seed)
|
| 1629 |
+
)
|
| 1630 |
+
current_latent = {**current_latent}
|
| 1631 |
+
current_latent["samples"] = samples
|
| 1632 |
+
except Exception as e:
|
| 1633 |
+
# Fallback to original path if anything goes wrong
|
| 1634 |
+
print(f"[CADE2.5][MGHybrid] fallback to common_ksampler due to: {e}")
|
| 1635 |
+
current_latent, = nodes.common_ksampler(
|
| 1636 |
+
sampler_model, iter_seed, int(current_steps), current_cfg, sampler_name, _scheduler_names()[0],
|
| 1637 |
+
positive, negative, current_latent, denoise=current_denoise)
|
| 1638 |
+
else:
|
| 1639 |
+
current_latent, = nodes.common_ksampler(
|
| 1640 |
+
sampler_model, iter_seed, int(current_steps), current_cfg, sampler_name, scheduler,
|
| 1641 |
+
positive, negative, current_latent, denoise=current_denoise)
|
| 1642 |
+
|
| 1643 |
+
if bool(latent_compare):
|
| 1644 |
+
latent_diff = current_latent["samples"] - prev_samples
|
| 1645 |
+
rms = torch.sqrt(torch.mean(latent_diff * latent_diff))
|
| 1646 |
+
drift = float(rms.item())
|
| 1647 |
+
if drift > float(threshold):
|
| 1648 |
+
overshoot = max(0.0, drift - float(threshold))
|
| 1649 |
+
damp = 1.0 - min(0.15, overshoot * 2.0)
|
| 1650 |
+
current_denoise = max(0.20, current_denoise * damp)
|
| 1651 |
+
cfg_damp = 0.997 if damp > 0.9 else 0.99
|
| 1652 |
+
current_cfg = max(1.0, current_cfg * cfg_damp)
|
| 1653 |
+
|
| 1654 |
+
# AQClip-Lite: adaptive soft clipping in latent space (before decode)
|
| 1655 |
+
try:
|
| 1656 |
+
if bool(aqclip_enable):
|
| 1657 |
+
if 'aq_state' not in locals():
|
| 1658 |
+
aq_state = None
|
| 1659 |
+
H_override = None
|
| 1660 |
+
if bool(aq_attn) and hasattr(sa_patch, "get_attention_entropy_map"):
|
| 1661 |
+
try:
|
| 1662 |
+
Hm = sa_patch.get_attention_entropy_map(clear=False)
|
| 1663 |
+
if Hm is not None:
|
| 1664 |
+
H_override = F.interpolate(Hm, size=(current_latent["samples"].shape[-2], current_latent["samples"].shape[-1]), mode="bilinear", align_corners=False)
|
| 1665 |
+
except Exception:
|
| 1666 |
+
H_override = None
|
| 1667 |
+
z_new, aq_state = _aqclip_lite(
|
| 1668 |
+
current_latent["samples"],
|
| 1669 |
+
tile=int(aq_tile), stride=int(aq_stride),
|
| 1670 |
+
alpha=float(aq_alpha), ema_state=aq_state, ema_beta=float(aq_ema_beta),
|
| 1671 |
+
H_override=H_override,
|
| 1672 |
+
)
|
| 1673 |
+
current_latent["samples"] = z_new
|
| 1674 |
+
except Exception:
|
| 1675 |
+
pass
|
| 1676 |
+
|
| 1677 |
+
image = safe_decode(vae, current_latent)
|
| 1678 |
+
|
| 1679 |
+
# Polish mode: keep global form (low frequencies) from reference while letting details refine
|
| 1680 |
+
if bool(polish_enable) and (i >= int(polish_start_after)):
|
| 1681 |
+
try:
|
| 1682 |
+
# Prepare tensors
|
| 1683 |
+
img = image
|
| 1684 |
+
ref = reference_image if (reference_image is not None) else img
|
| 1685 |
+
if ref.shape[1] != img.shape[1] or ref.shape[2] != img.shape[2]:
|
| 1686 |
+
# resize reference to match current image
|
| 1687 |
+
ref_n = ref.movedim(-1, 1)
|
| 1688 |
+
ref_n = F.interpolate(ref_n, size=(img.shape[1], img.shape[2]), mode='bilinear', align_corners=False)
|
| 1689 |
+
ref = ref_n.movedim(1, -1)
|
| 1690 |
+
x = img.movedim(-1, 1)
|
| 1691 |
+
r = ref.movedim(-1, 1)
|
| 1692 |
+
# Low/high split via Gaussian blur
|
| 1693 |
+
rad = max(1, int(round(float(polish_sigma) * 2)))
|
| 1694 |
+
low_x = _gaussian_blur_nchw(x, sigma=float(polish_sigma), radius=rad)
|
| 1695 |
+
low_r = _gaussian_blur_nchw(r, sigma=float(polish_sigma), radius=rad)
|
| 1696 |
+
high_x = x - low_x
|
| 1697 |
+
# Mix low from reference and current with ramp
|
| 1698 |
+
# a starts from polish_keep_low_ramp and linearly ramps to polish_keep_low over remaining iterations
|
| 1699 |
+
try:
|
| 1700 |
+
denom = max(1, int(iterations) - int(polish_start_after))
|
| 1701 |
+
t = max(0.0, min(1.0, (i - int(polish_start_after)) / denom))
|
| 1702 |
+
except Exception:
|
| 1703 |
+
t = 1.0
|
| 1704 |
+
a0 = float(polish_keep_low_ramp)
|
| 1705 |
+
at = float(polish_keep_low)
|
| 1706 |
+
a = a0 + (at - a0) * t
|
| 1707 |
+
low_mix = low_r * a + low_x * (1.0 - a)
|
| 1708 |
+
new = low_mix + high_x
|
| 1709 |
+
# Micro-detail injection on tail: very light HF boost gated by edges+depth
|
| 1710 |
+
try:
|
| 1711 |
+
phase = (i + 1) / max(1, int(iterations))
|
| 1712 |
+
# ramp starts late (>=0.70 of iterations), slightly earlier and wider
|
| 1713 |
+
ramp = max(0.0, min(1.0, (phase - 0.70) / 0.30))
|
| 1714 |
+
if ramp > 0.0:
|
| 1715 |
+
# fine-scale high-pass
|
| 1716 |
+
micro = x - _gaussian_blur_nchw(x, sigma=0.6, radius=1)
|
| 1717 |
+
# edge gate: suppress near strong edges to avoid halos
|
| 1718 |
+
gray = x.mean(dim=1, keepdim=True)
|
| 1719 |
+
sobel_x = torch.tensor([[[-1,0,1],[-2,0,2],[-1,0,1]]], dtype=gray.dtype, device=gray.device).unsqueeze(1)
|
| 1720 |
+
sobel_y = torch.tensor([[[-1,-2,-1],[0,0,0],[1,2,1]]], dtype=gray.dtype, device=gray.device).unsqueeze(1)
|
| 1721 |
+
gx = F.conv2d(gray, sobel_x, padding=1)
|
| 1722 |
+
gy = F.conv2d(gray, sobel_y, padding=1)
|
| 1723 |
+
mag = torch.sqrt(gx*gx + gy*gy)
|
| 1724 |
+
m_edge = (mag - mag.amin()) / (mag.amax() - mag.amin() + 1e-8)
|
| 1725 |
+
g_edge = (1.0 - m_edge).clamp(0.0, 1.0).pow(0.65) # prefer flats/meso-areas
|
| 1726 |
+
# depth gate: prefer nearer surfaces when depth is available
|
| 1727 |
+
try:
|
| 1728 |
+
sz = (int(img.shape[1]), int(img.shape[2]))
|
| 1729 |
+
if depth_gate_cache.get("size") != sz or depth_gate_cache.get("mask") is None:
|
| 1730 |
+
model_path = os.path.join(os.path.dirname(__file__), '..', 'depth-anything', 'depth_anything_v2_vitl.pth')
|
| 1731 |
+
dm = _cf_build_depth_map(img, res=512, model_path=model_path, hires_mode=True)
|
| 1732 |
+
depth_gate_cache = {"size": sz, "mask": dm}
|
| 1733 |
+
dm = depth_gate_cache.get("mask")
|
| 1734 |
+
if dm is not None:
|
| 1735 |
+
g_depth = (dm.movedim(-1, 1).clamp(0,1)) ** 1.35
|
| 1736 |
+
else:
|
| 1737 |
+
g_depth = torch.ones_like(g_edge)
|
| 1738 |
+
except Exception:
|
| 1739 |
+
g_depth = torch.ones_like(g_edge)
|
| 1740 |
+
g = (g_edge * g_depth).clamp(0.0, 1.0)
|
| 1741 |
+
micro_boost = 0.018 * ramp # very gentle, slightly higher
|
| 1742 |
+
new = new + micro_boost * (micro * g)
|
| 1743 |
+
except Exception:
|
| 1744 |
+
pass
|
| 1745 |
+
# Edge-lock: protect edges from drift by biasing toward low_mix along edges
|
| 1746 |
+
el = float(polish_edge_lock)
|
| 1747 |
+
if el > 1e-6:
|
| 1748 |
+
# Sobel edge magnitude on grayscale
|
| 1749 |
+
gray = x.mean(dim=1, keepdim=True)
|
| 1750 |
+
sobel_x = torch.tensor([[[-1,0,1],[-2,0,2],[-1,0,1]]], dtype=gray.dtype, device=gray.device).unsqueeze(1)
|
| 1751 |
+
sobel_y = torch.tensor([[[-1,-2,-1],[0,0,0],[1,2,1]]], dtype=gray.dtype, device=gray.device).unsqueeze(1)
|
| 1752 |
+
gx = F.conv2d(gray, sobel_x, padding=1)
|
| 1753 |
+
gy = F.conv2d(gray, sobel_y, padding=1)
|
| 1754 |
+
mag = torch.sqrt(gx*gx + gy*gy)
|
| 1755 |
+
m = (mag - mag.amin()) / (mag.amax() - mag.amin() + 1e-8)
|
| 1756 |
+
# Blend toward low_mix near edges
|
| 1757 |
+
new = new * (1.0 - el*m) + (low_mix) * (el*m)
|
| 1758 |
+
img2 = new.movedim(1, -1).clamp(0,1)
|
| 1759 |
+
# Feed back to latent for next steps
|
| 1760 |
+
current_latent = {"samples": safe_encode(vae, img2)}
|
| 1761 |
+
image = img2
|
| 1762 |
+
except Exception:
|
| 1763 |
+
pass
|
| 1764 |
+
|
| 1765 |
+
# ONNX detectors removed
|
| 1766 |
+
|
| 1767 |
+
if reference_clean and (ref_embed is not None) and (i % max(1, ref_cooldown) == 0):
|
| 1768 |
+
try:
|
| 1769 |
+
cur_embed = _encode_clip_image(image, clip_vision, ref_preview)
|
| 1770 |
+
dist = _clip_cosine_distance(cur_embed, ref_embed)
|
| 1771 |
+
if dist > ref_threshold:
|
| 1772 |
+
current_denoise = max(0.10, current_denoise * 0.9)
|
| 1773 |
+
current_cfg = max(1.0, current_cfg * 0.99)
|
| 1774 |
+
except Exception:
|
| 1775 |
+
pass
|
| 1776 |
+
|
| 1777 |
+
if apply_upscale and current_scale != 1.0:
|
| 1778 |
+
current_latent, image = MagicUpscaleModule().process_upscale(
|
| 1779 |
+
current_latent, vae, upscale_method, current_scale)
|
| 1780 |
+
# After upscale at large sizes, add a tiny HF sprinkle gated by edges+depth
|
| 1781 |
+
try:
|
| 1782 |
+
H, W = int(image.shape[1]), int(image.shape[2])
|
| 1783 |
+
if max(H, W) > 1536:
|
| 1784 |
+
blur = _gaussian_blur(image, radius=1.0, sigma=0.8)
|
| 1785 |
+
hf = (image - blur).clamp(-1, 1)
|
| 1786 |
+
# Edge gate in image space (luma Sobel)
|
| 1787 |
+
lum = (0.2126 * image[..., 0] + 0.7152 * image[..., 1] + 0.0722 * image[..., 2])
|
| 1788 |
+
kx = torch.tensor([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], device=lum.device, dtype=lum.dtype).view(1, 1, 3, 3)
|
| 1789 |
+
ky = torch.tensor([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], device=lum.device, dtype=lum.dtype).view(1, 1, 3, 3)
|
| 1790 |
+
g = torch.sqrt(F.conv2d(lum.unsqueeze(1), kx, padding=1)**2 + F.conv2d(lum.unsqueeze(1), ky, padding=1)**2).squeeze(1)
|
| 1791 |
+
m = (g - g.amin()) / (g.amax() - g.amin() + 1e-8)
|
| 1792 |
+
g_edge = (1.0 - m).clamp(0,1).pow(0.5).unsqueeze(-1)
|
| 1793 |
+
# Depth gate (once per resolution)
|
| 1794 |
+
try:
|
| 1795 |
+
sz = (H, W)
|
| 1796 |
+
if depth_gate_cache.get("size") != sz or depth_gate_cache.get("mask") is None:
|
| 1797 |
+
model_path = os.path.join(os.path.dirname(__file__), '..', 'depth-anything', 'depth_anything_v2_vitl.pth')
|
| 1798 |
+
dm = _cf_build_depth_map(image, res=512, model_path=model_path, hires_mode=True)
|
| 1799 |
+
depth_gate_cache = {"size": sz, "mask": dm}
|
| 1800 |
+
dm = depth_gate_cache.get("mask")
|
| 1801 |
+
if dm is not None:
|
| 1802 |
+
g_depth = dm.clamp(0,1) ** 1.2
|
| 1803 |
+
else:
|
| 1804 |
+
g_depth = torch.ones_like(g_edge)
|
| 1805 |
+
except Exception:
|
| 1806 |
+
g_depth = torch.ones_like(g_edge)
|
| 1807 |
+
g_tot = (g_edge * g_depth).clamp(0,1)
|
| 1808 |
+
image = (image + 0.045 * hf * g_tot).clamp(0,1)
|
| 1809 |
+
except Exception:
|
| 1810 |
+
pass
|
| 1811 |
+
current_cfg = max(4.0, current_cfg * (1.0 / current_scale))
|
| 1812 |
+
current_denoise = max(0.15, current_denoise * (1.0 / current_scale))
|
| 1813 |
+
|
| 1814 |
+
current_steps = max(1, current_steps - steps_delta)
|
| 1815 |
+
current_cfg = max(0.0, current_cfg - cfg_delta)
|
| 1816 |
+
current_denoise = max(0.0, current_denoise - denoise_delta)
|
| 1817 |
+
current_scale = max(1.0, current_scale - scale_delta)
|
| 1818 |
+
|
| 1819 |
+
if apply_upscale and current_scale != 1.0 and max(image.shape[1:3]) > 1024:
|
| 1820 |
+
current_latent = {"samples": safe_encode(vae, image)}
|
| 1821 |
+
|
| 1822 |
+
finally:
|
| 1823 |
+
# Always disable NAG patch and clear local mask, even on errors
|
| 1824 |
+
try:
|
| 1825 |
+
sa_patch.enable_crossattention_nag_patch(False)
|
| 1826 |
+
except Exception:
|
| 1827 |
+
pass
|
| 1828 |
+
try:
|
| 1829 |
+
sa_patch.CURRENT_PV_ACCUM = prev_accum
|
| 1830 |
+
except Exception:
|
| 1831 |
+
pass
|
| 1832 |
+
try:
|
| 1833 |
+
CURRENT_ONNX_MASK_BCHW = None
|
| 1834 |
+
except Exception:
|
| 1835 |
+
pass
|
| 1836 |
+
|
| 1837 |
+
if apply_ids:
|
| 1838 |
+
image, = IntelligentDetailStabilizer().stabilize(image, ids_strength)
|
| 1839 |
+
|
| 1840 |
+
if apply_sharpen:
|
| 1841 |
+
image, = _sharpen_image(image, 2, 1.0, Sharpnes_strenght)
|
| 1842 |
+
|
| 1843 |
+
# Mask preview as IMAGE (RGB)
|
| 1844 |
+
if mask_last is None:
|
| 1845 |
+
mask_last = torch.zeros((image.shape[0], image.shape[1], image.shape[2], 1), device=image.device, dtype=image.dtype)
|
| 1846 |
+
onnx_mask_img = mask_last.repeat(1, 1, 1, 3).clamp(0, 1)
|
| 1847 |
+
|
| 1848 |
+
# Final pass: remove isolated hot whites ("fireflies") without touching real edges/highlights
|
| 1849 |
+
try:
|
| 1850 |
+
image = _despeckle_fireflies(image, thr=0.998, max_iso=4.0/9.0, grad_gate=0.15)
|
| 1851 |
+
except Exception:
|
| 1852 |
+
pass
|
| 1853 |
+
|
| 1854 |
+
# Cleanup KV pruning state to avoid leaking into other nodes
|
| 1855 |
+
try:
|
| 1856 |
+
if hasattr(sa_patch, "set_kv_prune"):
|
| 1857 |
+
sa_patch.set_kv_prune(False, 1.0, int(kv_min_tokens))
|
| 1858 |
+
except Exception:
|
| 1859 |
+
pass
|
| 1860 |
+
|
| 1861 |
+
return current_latent, image, int(current_steps), float(current_cfg), float(current_denoise), onnx_mask_img
|
| 1862 |
+
|
| 1863 |
+
|
| 1864 |
+
|
mod/hard/mg_controlfusion.py
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import math
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
import comfy.model_management as model_management
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
_DEPTH_INIT = False
|
| 12 |
+
_DEPTH_MODEL = None
|
| 13 |
+
_DEPTH_PROC = None
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _insert_aux_path():
|
| 17 |
+
try:
|
| 18 |
+
base = os.path.dirname(os.path.dirname(__file__)) # .../custom_nodes
|
| 19 |
+
aux_root = os.path.join(base, 'comfyui_controlnet_aux')
|
| 20 |
+
aux_src = os.path.join(aux_root, 'src')
|
| 21 |
+
for p in (aux_src, aux_root):
|
| 22 |
+
if os.path.isdir(p) and p not in sys.path:
|
| 23 |
+
sys.path.insert(0, p)
|
| 24 |
+
except Exception:
|
| 25 |
+
pass
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _try_init_depth_anything(model_path: str):
|
| 29 |
+
global _DEPTH_INIT, _DEPTH_MODEL, _DEPTH_PROC
|
| 30 |
+
if _DEPTH_INIT:
|
| 31 |
+
return _DEPTH_MODEL is not None
|
| 32 |
+
_DEPTH_INIT = True
|
| 33 |
+
# Prefer our vendored implementation first
|
| 34 |
+
try:
|
| 35 |
+
from ...vendor.depth_anything_v2.dpt import DepthAnythingV2 # type: ignore
|
| 36 |
+
# Guess config from filename
|
| 37 |
+
fname = os.path.basename(model_path or '')
|
| 38 |
+
cfgs = {
|
| 39 |
+
'depth_anything_v2_vits.pth': dict(encoder='vits', features=64, out_channels=[48,96,192,384]),
|
| 40 |
+
'depth_anything_v2_vitb.pth': dict(encoder='vitb', features=128, out_channels=[96,192,384,768]),
|
| 41 |
+
'depth_anything_v2_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]),
|
| 42 |
+
'depth_anything_v2_vitg.pth': dict(encoder='vitg', features=384, out_channels=[1536,1536,1536,1536]),
|
| 43 |
+
'depth_anything_v2_metric_vkitti_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]),
|
| 44 |
+
'depth_anything_v2_metric_hypersim_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]),
|
| 45 |
+
}
|
| 46 |
+
# fallback to vitl if unknown
|
| 47 |
+
cfg = cfgs.get(fname, cfgs['depth_anything_v2_vitl.pth'])
|
| 48 |
+
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 49 |
+
m = DepthAnythingV2(**cfg)
|
| 50 |
+
sd = torch.load(model_path, map_location='cpu')
|
| 51 |
+
m.load_state_dict(sd)
|
| 52 |
+
_DEPTH_MODEL = m.to(device).eval()
|
| 53 |
+
_DEPTH_PROC = True
|
| 54 |
+
return True
|
| 55 |
+
except Exception:
|
| 56 |
+
# Try local checkout of comfyui_controlnet_aux (if present)
|
| 57 |
+
_insert_aux_path()
|
| 58 |
+
try:
|
| 59 |
+
from custom_controlnet_aux.depth_anything_v2.dpt import DepthAnythingV2 # type: ignore
|
| 60 |
+
fname = os.path.basename(model_path or '')
|
| 61 |
+
cfgs = {
|
| 62 |
+
'depth_anything_v2_vits.pth': dict(encoder='vits', features=64, out_channels=[48,96,192,384]),
|
| 63 |
+
'depth_anything_v2_vitb.pth': dict(encoder='vitb', features=128, out_channels=[96,192,384,768]),
|
| 64 |
+
'depth_anything_v2_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]),
|
| 65 |
+
'depth_anything_v2_vitg.pth': dict(encoder='vitg', features=384, out_channels=[1536,1536,1536,1536]),
|
| 66 |
+
'depth_anything_v2_metric_vkitti_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]),
|
| 67 |
+
'depth_anything_v2_metric_hypersim_vitl.pth': dict(encoder='vitl', features=256, out_channels=[256,512,1024,1024]),
|
| 68 |
+
}
|
| 69 |
+
cfg = cfgs.get(fname, cfgs['depth_anything_v2_vitl.pth'])
|
| 70 |
+
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 71 |
+
m = DepthAnythingV2(**cfg)
|
| 72 |
+
sd = torch.load(model_path, map_location='cpu')
|
| 73 |
+
m.load_state_dict(sd)
|
| 74 |
+
_DEPTH_MODEL = m.to(device).eval()
|
| 75 |
+
_DEPTH_PROC = True
|
| 76 |
+
return True
|
| 77 |
+
except Exception:
|
| 78 |
+
# Fallback: packaged auxiliary API
|
| 79 |
+
try:
|
| 80 |
+
from controlnet_aux.depth_anything import DepthAnythingDetector, DepthAnythingV2 # type: ignore
|
| 81 |
+
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 82 |
+
_DEPTH_MODEL = DepthAnythingV2(model_path=model_path, device=device)
|
| 83 |
+
_DEPTH_PROC = True
|
| 84 |
+
return True
|
| 85 |
+
except Exception:
|
| 86 |
+
_DEPTH_MODEL = None
|
| 87 |
+
_DEPTH_PROC = False
|
| 88 |
+
return False
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _build_depth_map(image_bhwc: torch.Tensor, res: int, model_path: str, hires_mode: bool = True) -> torch.Tensor:
|
| 92 |
+
B, H, W, C = image_bhwc.shape
|
| 93 |
+
dev = image_bhwc.device
|
| 94 |
+
dtype = image_bhwc.dtype
|
| 95 |
+
# Choose target min-side for processing. In hires mode we allow higher caps and keep aspect.
|
| 96 |
+
# DepthAnything v2 can be memory-hungry on large inputs; cap min-side at 1024
|
| 97 |
+
cap = 1024
|
| 98 |
+
target = int(max(16, min(cap, res)))
|
| 99 |
+
if _try_init_depth_anything(model_path):
|
| 100 |
+
try:
|
| 101 |
+
# to CPU uint8
|
| 102 |
+
img = image_bhwc.detach().to('cpu')
|
| 103 |
+
x = img[0].movedim(-1, 0).unsqueeze(0)
|
| 104 |
+
# keep aspect ratio: scale so that min(H,W) == target
|
| 105 |
+
_, Cc, Ht, Wt = x.shape
|
| 106 |
+
min_side = max(1, min(Ht, Wt))
|
| 107 |
+
scale = float(target) / float(min_side)
|
| 108 |
+
out_h = max(1, int(round(Ht * scale)))
|
| 109 |
+
out_w = max(1, int(round(Wt * scale)))
|
| 110 |
+
x = F.interpolate(x, size=(out_h, out_w), mode='bilinear', align_corners=False)
|
| 111 |
+
# make channels-last and ensure contiguous layout for OpenCV
|
| 112 |
+
arr = (x[0].movedim(0, -1).contiguous().numpy() * 255.0).astype('uint8')
|
| 113 |
+
# Prefer direct DepthAnythingV2 inference if model has infer_image
|
| 114 |
+
if hasattr(_DEPTH_MODEL, 'infer_image'):
|
| 115 |
+
import cv2
|
| 116 |
+
# Drive input_size from desired depth resolution (min side), let DA keep aspect
|
| 117 |
+
input_sz = int(max(224, min(cap, res)))
|
| 118 |
+
depth = _DEPTH_MODEL.infer_image(cv2.cvtColor(arr, cv2.COLOR_RGB2BGR), input_size=input_sz, max_depth=20.0)
|
| 119 |
+
d = np.asarray(depth, dtype=np.float32)
|
| 120 |
+
# Normalize DepthAnythingV2 output (0..max_depth) to 0..1
|
| 121 |
+
d = d / 20.0
|
| 122 |
+
else:
|
| 123 |
+
depth = _DEPTH_MODEL(arr)
|
| 124 |
+
d = np.asarray(depth, dtype=np.float32)
|
| 125 |
+
if d.max() > 1.0:
|
| 126 |
+
d = d / 255.0
|
| 127 |
+
d = torch.from_numpy(d)[None, None] # 1,1,h,w
|
| 128 |
+
d = F.interpolate(d, size=(H, W), mode='bilinear', align_corners=False)
|
| 129 |
+
d = d[0, 0].to(device=dev, dtype=dtype)
|
| 130 |
+
d = d.clamp(0, 1)
|
| 131 |
+
return d
|
| 132 |
+
except Exception:
|
| 133 |
+
pass
|
| 134 |
+
# Fallback pseudo-depth: luminance + gentle blur
|
| 135 |
+
lum = (0.2126 * image_bhwc[..., 0] + 0.7152 * image_bhwc[..., 1] + 0.0722 * image_bhwc[..., 2]).to(dtype=dtype)
|
| 136 |
+
x = lum.movedim(-1, 0).unsqueeze(0) if lum.ndim == 3 else lum.unsqueeze(0).unsqueeze(0)
|
| 137 |
+
x = F.interpolate(x, size=(H, W), mode='bilinear', align_corners=False)
|
| 138 |
+
x = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1)
|
| 139 |
+
return x[0, 0].clamp(0, 1)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def _pyracanny(image_bhwc: torch.Tensor,
|
| 143 |
+
low: int,
|
| 144 |
+
high: int,
|
| 145 |
+
res: int,
|
| 146 |
+
thin_iter: int = 0,
|
| 147 |
+
edge_boost: float = 0.0,
|
| 148 |
+
smart_tune: bool = False,
|
| 149 |
+
smart_boost: float = 0.2,
|
| 150 |
+
preserve_aspect: bool = True) -> torch.Tensor:
|
| 151 |
+
try:
|
| 152 |
+
import cv2
|
| 153 |
+
except Exception:
|
| 154 |
+
# Fallback: simple Sobel magnitude
|
| 155 |
+
x = image_bhwc.movedim(-1, 1)
|
| 156 |
+
xg = x.mean(dim=1, keepdim=True)
|
| 157 |
+
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)
|
| 158 |
+
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)
|
| 159 |
+
mag = torch.sqrt(gx*gx + gy*gy)
|
| 160 |
+
mag = (mag - mag.amin())/(mag.amax()-mag.amin()+1e-6)
|
| 161 |
+
return mag[0,0].clamp(0,1)
|
| 162 |
+
B,H,W,C = image_bhwc.shape
|
| 163 |
+
img = (image_bhwc.detach().to('cpu')[0].contiguous().numpy()*255.0).astype('uint8')
|
| 164 |
+
cap = 4096
|
| 165 |
+
target = int(max(64, min(cap, res)))
|
| 166 |
+
if preserve_aspect:
|
| 167 |
+
scale = float(target) / float(max(1, min(H, W)))
|
| 168 |
+
out_h = max(8, int(round(H * scale)))
|
| 169 |
+
out_w = max(8, int(round(W * scale)))
|
| 170 |
+
img_res = cv2.resize(img, (out_w, out_h), interpolation=cv2.INTER_LINEAR)
|
| 171 |
+
else:
|
| 172 |
+
img_res = cv2.resize(img, (target, target), interpolation=cv2.INTER_LINEAR)
|
| 173 |
+
gray = cv2.cvtColor(img_res, cv2.COLOR_RGB2GRAY)
|
| 174 |
+
pyr_scales = [1.0, 0.5, 0.25]
|
| 175 |
+
acc = None
|
| 176 |
+
for s in pyr_scales:
|
| 177 |
+
if preserve_aspect:
|
| 178 |
+
sz = (max(8, int(round(img_res.shape[1]*s))), max(8, int(round(img_res.shape[0]*s))))
|
| 179 |
+
else:
|
| 180 |
+
sz = (max(8, int(target*s)), max(8, int(target*s)))
|
| 181 |
+
g = cv2.resize(gray, sz, interpolation=cv2.INTER_AREA)
|
| 182 |
+
g = cv2.GaussianBlur(g, (5,5), 0)
|
| 183 |
+
e = cv2.Canny(g, threshold1=int(low*s), threshold2=int(high*s))
|
| 184 |
+
e = cv2.resize(e, (W, H), interpolation=cv2.INTER_LINEAR)
|
| 185 |
+
e = (e.astype(np.float32)/255.0)
|
| 186 |
+
acc = e if acc is None else np.maximum(acc, e)
|
| 187 |
+
# Estimate density and sharpness for smart tuning
|
| 188 |
+
edensity_pre = None
|
| 189 |
+
try:
|
| 190 |
+
edensity_pre = float(np.mean(acc)) if acc is not None else None
|
| 191 |
+
except Exception:
|
| 192 |
+
edensity_pre = None
|
| 193 |
+
lap_var = None
|
| 194 |
+
try:
|
| 195 |
+
g32 = gray.astype(np.float32) / 255.0
|
| 196 |
+
lap = cv2.Laplacian(g32, cv2.CV_32F)
|
| 197 |
+
lap_var = float(lap.var())
|
| 198 |
+
except Exception:
|
| 199 |
+
lap_var = None
|
| 200 |
+
|
| 201 |
+
# optional thinning
|
| 202 |
+
try:
|
| 203 |
+
thin_iter_eff = int(thin_iter)
|
| 204 |
+
if smart_tune:
|
| 205 |
+
# simple heuristic: more thinning on high res and dense edges
|
| 206 |
+
auto = 0
|
| 207 |
+
if target >= 1024:
|
| 208 |
+
auto += 1
|
| 209 |
+
if target >= 1400:
|
| 210 |
+
auto += 1
|
| 211 |
+
if edensity_pre is not None and edensity_pre > 0.12:
|
| 212 |
+
auto += 1
|
| 213 |
+
if edensity_pre is not None and edensity_pre < 0.05:
|
| 214 |
+
auto = max(0, auto - 1)
|
| 215 |
+
thin_iter_eff = max(thin_iter_eff, min(3, auto))
|
| 216 |
+
if thin_iter_eff > 0:
|
| 217 |
+
import cv2
|
| 218 |
+
if hasattr(cv2, 'ximgproc') and hasattr(cv2.ximgproc, 'thinning'):
|
| 219 |
+
th = acc.copy()
|
| 220 |
+
th = (th*255).astype('uint8')
|
| 221 |
+
th = cv2.ximgproc.thinning(th)
|
| 222 |
+
acc = th.astype(np.float32)/255.0
|
| 223 |
+
else:
|
| 224 |
+
# simple erosion-based thinning approximation
|
| 225 |
+
kernel = np.ones((3,3), np.uint8)
|
| 226 |
+
t = (acc*255).astype('uint8')
|
| 227 |
+
for _ in range(int(thin_iter_eff)):
|
| 228 |
+
t = cv2.erode(t, kernel, iterations=1)
|
| 229 |
+
acc = t.astype(np.float32)/255.0
|
| 230 |
+
except Exception:
|
| 231 |
+
pass
|
| 232 |
+
# optional edge boost (unsharp on edge map)
|
| 233 |
+
# We fix a gentle boost for micro‑contrast; smart_tune may nudge it slightly
|
| 234 |
+
boost_eff = 0.10
|
| 235 |
+
if smart_tune:
|
| 236 |
+
try:
|
| 237 |
+
lv = 0.0 if lap_var is None else max(0.0, min(1.0, lap_var / 2.0))
|
| 238 |
+
dens = 0.0 if edensity_pre is None else float(max(0.0, min(1.0, edensity_pre)))
|
| 239 |
+
boost_eff = max(0.05, min(0.20, boost_eff + (1.0 - dens) * 0.05 + (1.0 - lv) * 0.02))
|
| 240 |
+
except Exception:
|
| 241 |
+
pass
|
| 242 |
+
if boost_eff and boost_eff != 0.0:
|
| 243 |
+
try:
|
| 244 |
+
import cv2
|
| 245 |
+
blur = cv2.GaussianBlur(acc, (0,0), sigmaX=1.0)
|
| 246 |
+
acc = np.clip(acc + float(boost_eff)*(acc - blur), 0.0, 1.0)
|
| 247 |
+
except Exception:
|
| 248 |
+
pass
|
| 249 |
+
ed = torch.from_numpy(acc).to(device=image_bhwc.device, dtype=image_bhwc.dtype)
|
| 250 |
+
return ed.clamp(0,1)
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def _blend(depth: torch.Tensor, edges: torch.Tensor, mode: str, factor: float) -> torch.Tensor:
|
| 254 |
+
depth = depth.clamp(0,1)
|
| 255 |
+
edges = edges.clamp(0,1)
|
| 256 |
+
if mode == 'max':
|
| 257 |
+
return torch.maximum(depth, edges)
|
| 258 |
+
if mode == 'edge_over_depth':
|
| 259 |
+
# edges override depth (edge=1) while preserving depth elsewhere
|
| 260 |
+
return (depth * (1.0 - edges) + edges).clamp(0,1)
|
| 261 |
+
# normal
|
| 262 |
+
f = float(max(0.0, min(1.0, factor)))
|
| 263 |
+
return (depth*(1.0-f) + edges*f).clamp(0,1)
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def _apply_controlnet_separate(positive, negative, control_net, image_bhwc: torch.Tensor,
|
| 267 |
+
strength_pos: float, strength_neg: float,
|
| 268 |
+
start_percent: float, end_percent: float, vae=None,
|
| 269 |
+
apply_to_uncond: bool = False,
|
| 270 |
+
stack_prev_control: bool = False):
|
| 271 |
+
control_hint = image_bhwc.movedim(-1,1)
|
| 272 |
+
out_pos = []
|
| 273 |
+
out_neg = []
|
| 274 |
+
# POS
|
| 275 |
+
for t in positive:
|
| 276 |
+
d = t[1].copy()
|
| 277 |
+
prev = d.get('control', None) if stack_prev_control else None
|
| 278 |
+
c_net = control_net.copy().set_cond_hint(control_hint, float(strength_pos), (start_percent, end_percent), vae=vae, extra_concat=[])
|
| 279 |
+
c_net.set_previous_controlnet(prev)
|
| 280 |
+
d['control'] = c_net
|
| 281 |
+
d['control_apply_to_uncond'] = bool(apply_to_uncond)
|
| 282 |
+
out_pos.append([t[0], d])
|
| 283 |
+
# NEG
|
| 284 |
+
for t in negative:
|
| 285 |
+
d = t[1].copy()
|
| 286 |
+
prev = d.get('control', None) if stack_prev_control else None
|
| 287 |
+
c_net = control_net.copy().set_cond_hint(control_hint, float(strength_neg), (start_percent, end_percent), vae=vae, extra_concat=[])
|
| 288 |
+
c_net.set_previous_controlnet(prev)
|
| 289 |
+
d['control'] = c_net
|
| 290 |
+
d['control_apply_to_uncond'] = bool(apply_to_uncond)
|
| 291 |
+
out_neg.append([t[0], d])
|
| 292 |
+
return out_pos, out_neg
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
class MG_ControlFusion:
|
| 296 |
+
@classmethod
|
| 297 |
+
def INPUT_TYPES(cls):
|
| 298 |
+
return {
|
| 299 |
+
"required": {
|
| 300 |
+
"image": ("IMAGE", {"tooltip": "Input RGB image (B,H,W,3) in 0..1."}),
|
| 301 |
+
"positive": ("CONDITIONING", {"tooltip": "Positive conditioning to apply ControlNet to."}),
|
| 302 |
+
"negative": ("CONDITIONING", {"tooltip": "Negative conditioning to apply ControlNet to."}),
|
| 303 |
+
"control_net": ("CONTROL_NET", {"tooltip": "ControlNet module receiving the fused mask as hint."}),
|
| 304 |
+
"vae": ("VAE", {"tooltip": "VAE used by ControlNet when encoding the hint."}),
|
| 305 |
+
},
|
| 306 |
+
"optional": {
|
| 307 |
+
"enable_depth": ("BOOLEAN", {"default": True, "tooltip": "Enable depth map fusion (Depth Anything v2 if available)."}),
|
| 308 |
+
"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)."}),
|
| 309 |
+
"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."}),
|
| 310 |
+
"enable_pyra": ("BOOLEAN", {"default": True, "tooltip": "Enable PyraCanny edge detector."}),
|
| 311 |
+
"pyra_low": ("INT", {"default": 109, "min": 0, "max": 255, "tooltip": "Canny low threshold (0..255)."}),
|
| 312 |
+
"pyra_high": ("INT", {"default": 147, "min": 0, "max": 255, "tooltip": "Canny high threshold (0..255)."}),
|
| 313 |
+
"pyra_resolution": ("INT", {"default": 1024, "min": 64, "max": 4096, "step": 64, "tooltip": "Working resolution for edges (min side, keeps aspect)."}),
|
| 314 |
+
"edge_thin_iter": ("INT", {"default": 0, "min": 0, "max": 10, "step": 1, "tooltip": "Thinning iterations for edges (skeletonize). 0 = off."}),
|
| 315 |
+
"edge_alpha": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Opacity for edges before blending (0..1)."}),
|
| 316 |
+
"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."}),
|
| 317 |
+
"smart_tune": ("BOOLEAN", {"default": False, "tooltip": "Auto-adjust thinning/boost from image edge density and sharpness."}),
|
| 318 |
+
"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."}),
|
| 319 |
+
"blend_mode": (["normal","max","edge_over_depth"], {"default": "normal", "tooltip": "Depth+edges merge: normal (mix), max (strongest), edge_over_depth (edges overlay)."}),
|
| 320 |
+
"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)."}),
|
| 321 |
+
"strength_pos": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01, "tooltip": "ControlNet strength for positive branch."}),
|
| 322 |
+
"strength_neg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01, "tooltip": "ControlNet strength for negative branch."}),
|
| 323 |
+
"start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Start percentage along the sampling schedule."}),
|
| 324 |
+
"end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "End percentage along the sampling schedule."}),
|
| 325 |
+
"preview_res": ("INT", {"default": 1024, "min": 256, "max": 2048, "step": 64, "tooltip": "Preview minimum side (keeps aspect ratio)."}),
|
| 326 |
+
"mask_brightness": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Preview brightness multiplier (visualization only)."}),
|
| 327 |
+
"preview_show_strength": ("BOOLEAN", {"default": True, "tooltip": "Multiply preview by ControlNet strength for visualization."}),
|
| 328 |
+
"preview_strength_branch": (["positive","negative","max","avg"], {"default": "max", "tooltip": "Which strength to reflect in preview (display only)."}),
|
| 329 |
+
"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)."}),
|
| 330 |
+
"apply_to_uncond": ("BOOLEAN", {"default": False, "tooltip": "Apply ControlNet hint to the unconditional branch as well (stronger global hold on very large images)."}),
|
| 331 |
+
"stack_prev_control": ("BOOLEAN", {"default": False, "tooltip": "Chain with any previously attached ControlNet in the conditioning (advanced). Off = replace to avoid memory bloat."}),
|
| 332 |
+
# Split apply: chain Depth and Edges with separate schedules/strengths (fixed order: depth -> edges)
|
| 333 |
+
"split_apply": ("BOOLEAN", {"default": False, "tooltip": "Apply Depth and Edges as two chained ControlNets (fixed order: depth then edges)."}),
|
| 334 |
+
"edge_start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Edges start percent (when split is enabled)."}),
|
| 335 |
+
"edge_end_percent": ("FLOAT", {"default": 0.6, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Edges end percent (when split is enabled)."}),
|
| 336 |
+
"depth_start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Depth start percent (when split is enabled)."}),
|
| 337 |
+
"depth_end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Depth end percent (when split is enabled)."}),
|
| 338 |
+
"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."}),
|
| 339 |
+
"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."}),
|
| 340 |
+
# Extra edge controls (bottom)
|
| 341 |
+
"edge_width": ("FLOAT", {"default": 0.0, "min": -0.5, "max": 1.5, "step": 0.05, "tooltip": "Edge thickness adjust: negative thins, positive thickens."}),
|
| 342 |
+
"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)."}),
|
| 343 |
+
"edge_single_line": ("BOOLEAN", {"default": False, "tooltip": "Try to collapse double outlines into a single centerline."}),
|
| 344 |
+
"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."}),
|
| 345 |
+
"edge_depth_gate": ("BOOLEAN", {"default": False, "tooltip": "Weigh edges by depth so distant lines are fainter."}),
|
| 346 |
+
"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."}),
|
| 347 |
+
}
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
RETURN_TYPES = ("CONDITIONING","CONDITIONING","IMAGE")
|
| 351 |
+
RETURN_NAMES = ("positive","negative","mask_preview")
|
| 352 |
+
FUNCTION = "apply"
|
| 353 |
+
CATEGORY = "MagicNodes"
|
| 354 |
+
|
| 355 |
+
def apply(self, image, positive, negative, control_net, vae,
|
| 356 |
+
enable_depth=True, depth_model_path="", depth_resolution=1024,
|
| 357 |
+
enable_pyra=True, pyra_low=109, pyra_high=147, pyra_resolution=1024,
|
| 358 |
+
edge_thin_iter=0, edge_alpha=1.0, edge_boost=0.0,
|
| 359 |
+
smart_tune=False, smart_boost=0.2,
|
| 360 |
+
blend_mode="normal", blend_factor=0.02,
|
| 361 |
+
strength_pos=1.0, strength_neg=1.0, start_percent=0.0, end_percent=1.0,
|
| 362 |
+
preview_res=1024, mask_brightness=1.0,
|
| 363 |
+
preview_show_strength=True, preview_strength_branch="max",
|
| 364 |
+
hires_mask_auto=True, apply_to_uncond=False, stack_prev_control=False,
|
| 365 |
+
edge_width=0.0, edge_smooth=0.0, edge_single_line=False, edge_single_strength=0.0,
|
| 366 |
+
edge_depth_gate=False, edge_depth_gamma=1.5,
|
| 367 |
+
split_apply=False, edge_start_percent=0.0, edge_end_percent=0.6,
|
| 368 |
+
depth_start_percent=0.0, depth_end_percent=1.0,
|
| 369 |
+
edge_strength_mul=1.0, depth_strength_mul=1.0):
|
| 370 |
+
|
| 371 |
+
dev = image.device
|
| 372 |
+
dtype = image.dtype
|
| 373 |
+
B,H,W,C = image.shape
|
| 374 |
+
# Build depth/edges
|
| 375 |
+
depth = None
|
| 376 |
+
edges = None
|
| 377 |
+
if enable_depth:
|
| 378 |
+
model_path = depth_model_path or os.path.join(os.path.dirname(__file__), '..','depth-anything','depth_anything_v2_vitl.pth')
|
| 379 |
+
depth = _build_depth_map(image, int(depth_resolution), model_path, bool(hires_mask_auto))
|
| 380 |
+
if enable_pyra:
|
| 381 |
+
edges = _pyracanny(image,
|
| 382 |
+
int(pyra_low), int(pyra_high), int(pyra_resolution),
|
| 383 |
+
int(edge_thin_iter), float(edge_boost),
|
| 384 |
+
bool(smart_tune), float(smart_boost), bool(hires_mask_auto))
|
| 385 |
+
if depth is None and edges is None:
|
| 386 |
+
# Nothing to do: return inputs and zero preview
|
| 387 |
+
prev = torch.zeros((B, max(H,1), max(W,1), 3), device=dev, dtype=dtype)
|
| 388 |
+
return positive, negative, prev
|
| 389 |
+
|
| 390 |
+
if depth is None:
|
| 391 |
+
depth = torch.zeros_like(edges)
|
| 392 |
+
if edges is None:
|
| 393 |
+
edges = torch.zeros_like(depth)
|
| 394 |
+
|
| 395 |
+
# Edge post-process: width/single-line/smooth
|
| 396 |
+
def _edges_post(acc_t: torch.Tensor) -> torch.Tensor:
|
| 397 |
+
try:
|
| 398 |
+
import cv2, numpy as _np
|
| 399 |
+
acc = acc_t.detach().to('cpu').numpy()
|
| 400 |
+
img = (acc*255.0).astype(_np.uint8)
|
| 401 |
+
k = _np.ones((3,3), _np.uint8)
|
| 402 |
+
# Adjust thickness
|
| 403 |
+
w = float(edge_width)
|
| 404 |
+
if abs(w) > 1e-6:
|
| 405 |
+
it = int(abs(w))
|
| 406 |
+
frac = abs(w) - it
|
| 407 |
+
op = cv2.dilate if w > 0 else cv2.erode
|
| 408 |
+
y = img.copy()
|
| 409 |
+
for _ in range(max(0, it)):
|
| 410 |
+
y = op(y, k, iterations=1)
|
| 411 |
+
if frac > 1e-6:
|
| 412 |
+
y2 = op(y, k, iterations=1)
|
| 413 |
+
y = ((1.0-frac)*y.astype(_np.float32) + frac*y2.astype(_np.float32)).astype(_np.uint8)
|
| 414 |
+
img = y
|
| 415 |
+
# Collapse double lines to single centerline
|
| 416 |
+
if bool(edge_single_line) and float(edge_single_strength) > 1e-6:
|
| 417 |
+
try:
|
| 418 |
+
s = float(edge_single_strength)
|
| 419 |
+
close = cv2.morphologyEx(img, cv2.MORPH_CLOSE, k, iterations=1)
|
| 420 |
+
if hasattr(cv2, 'ximgproc') and hasattr(cv2.ximgproc, 'thinning'):
|
| 421 |
+
sk = cv2.ximgproc.thinning(close)
|
| 422 |
+
else:
|
| 423 |
+
# limited-iteration morphological skeletonization
|
| 424 |
+
iters = max(1, int(round(2 + 6*s)))
|
| 425 |
+
sk = _np.zeros_like(close)
|
| 426 |
+
src = close.copy()
|
| 427 |
+
elem = cv2.getStructuringElement(cv2.MORPH_CROSS, (3,3))
|
| 428 |
+
for _ in range(iters):
|
| 429 |
+
er = cv2.erode(src, elem, iterations=1)
|
| 430 |
+
op = cv2.morphologyEx(er, cv2.MORPH_OPEN, elem)
|
| 431 |
+
tmp = cv2.subtract(er, op)
|
| 432 |
+
sk = cv2.bitwise_or(sk, tmp)
|
| 433 |
+
src = er
|
| 434 |
+
if not _np.any(src):
|
| 435 |
+
break
|
| 436 |
+
# Blend skeleton back with original according to strength
|
| 437 |
+
img = ((_np.float32(1.0 - s) * img.astype(_np.float32)) + (_np.float32(s) * sk.astype(_np.float32))).astype(_np.uint8)
|
| 438 |
+
except Exception:
|
| 439 |
+
pass
|
| 440 |
+
# Smooth
|
| 441 |
+
if float(edge_smooth) > 1e-6:
|
| 442 |
+
sigma = max(0.1, min(2.0, float(edge_smooth) * 1.2))
|
| 443 |
+
img = cv2.GaussianBlur(img, (0,0), sigmaX=sigma)
|
| 444 |
+
out = torch.from_numpy((img.astype(_np.float32)/255.0)).to(device=acc_t.device, dtype=acc_t.dtype)
|
| 445 |
+
return out.clamp(0,1)
|
| 446 |
+
except Exception:
|
| 447 |
+
# Torch fallback: light blur-only
|
| 448 |
+
if float(edge_smooth) > 1e-6:
|
| 449 |
+
s = max(1, int(round(float(edge_smooth)*2)))
|
| 450 |
+
return F.avg_pool2d(acc_t.unsqueeze(0).unsqueeze(0), kernel_size=2*s+1, stride=1, padding=s)[0,0].clamp(0,1)
|
| 451 |
+
return acc_t
|
| 452 |
+
|
| 453 |
+
edges = _edges_post(edges)
|
| 454 |
+
|
| 455 |
+
# Depth gating of edges
|
| 456 |
+
if bool(edge_depth_gate):
|
| 457 |
+
# Inverted gating per feedback: use depth^gamma (nearer = stronger if depth is larger)
|
| 458 |
+
g = (depth.clamp(0,1)) ** float(edge_depth_gamma)
|
| 459 |
+
edges = (edges * g).clamp(0,1)
|
| 460 |
+
|
| 461 |
+
# Apply edge alpha before blending
|
| 462 |
+
edges = (edges * float(edge_alpha)).clamp(0,1)
|
| 463 |
+
|
| 464 |
+
fused = _blend(depth, edges, str(blend_mode), float(blend_factor))
|
| 465 |
+
# Apply as split (Edges then Depth) or single fused hint
|
| 466 |
+
if bool(split_apply):
|
| 467 |
+
# Fixed order for determinism: Depth first, then Edges
|
| 468 |
+
hint_edges = edges.unsqueeze(-1).repeat(1,1,1,3)
|
| 469 |
+
hint_depth = depth.unsqueeze(-1).repeat(1,1,1,3)
|
| 470 |
+
# Depth first
|
| 471 |
+
pos_mid, neg_mid = _apply_controlnet_separate(
|
| 472 |
+
positive, negative, control_net, hint_depth,
|
| 473 |
+
float(strength_pos) * float(depth_strength_mul),
|
| 474 |
+
float(strength_neg) * float(depth_strength_mul),
|
| 475 |
+
float(depth_start_percent), float(depth_end_percent), vae,
|
| 476 |
+
bool(apply_to_uncond), True
|
| 477 |
+
)
|
| 478 |
+
# Then edges
|
| 479 |
+
pos_out, neg_out = _apply_controlnet_separate(
|
| 480 |
+
pos_mid, neg_mid, control_net, hint_edges,
|
| 481 |
+
float(strength_pos) * float(edge_strength_mul),
|
| 482 |
+
float(strength_neg) * float(edge_strength_mul),
|
| 483 |
+
float(edge_start_percent), float(edge_end_percent), vae,
|
| 484 |
+
bool(apply_to_uncond), True
|
| 485 |
+
)
|
| 486 |
+
else:
|
| 487 |
+
hint = fused.unsqueeze(-1).repeat(1,1,1,3)
|
| 488 |
+
pos_out, neg_out = _apply_controlnet_separate(
|
| 489 |
+
positive, negative, control_net, hint,
|
| 490 |
+
float(strength_pos), float(strength_neg),
|
| 491 |
+
float(start_percent), float(end_percent), vae,
|
| 492 |
+
bool(apply_to_uncond), bool(stack_prev_control)
|
| 493 |
+
)
|
| 494 |
+
# Build preview: keep aspect ratio, set minimal side
|
| 495 |
+
prev_res = int(max(256, min(2048, preview_res)))
|
| 496 |
+
scale = prev_res / float(min(H, W))
|
| 497 |
+
out_h = max(1, int(round(H * scale)))
|
| 498 |
+
out_w = max(1, int(round(W * scale)))
|
| 499 |
+
prev = F.interpolate(fused.unsqueeze(0).unsqueeze(0), size=(out_h, out_w), mode='bilinear', align_corners=False)[0,0]
|
| 500 |
+
# Optionally reflect ControlNet strength in preview (display only)
|
| 501 |
+
if bool(preview_show_strength):
|
| 502 |
+
br = str(preview_strength_branch)
|
| 503 |
+
sp = float(strength_pos)
|
| 504 |
+
sn = float(strength_neg)
|
| 505 |
+
if br == 'negative':
|
| 506 |
+
s_vis = sn
|
| 507 |
+
elif br == 'max':
|
| 508 |
+
s_vis = max(sp, sn)
|
| 509 |
+
elif br == 'avg':
|
| 510 |
+
s_vis = 0.5 * (sp + sn)
|
| 511 |
+
else:
|
| 512 |
+
s_vis = sp
|
| 513 |
+
# clamp for display range
|
| 514 |
+
s_vis = max(0.0, min(1.0, s_vis))
|
| 515 |
+
prev = prev * s_vis
|
| 516 |
+
# Apply visualization brightness only for preview
|
| 517 |
+
prev = (prev * float(mask_brightness)).clamp(0.0, 1.0)
|
| 518 |
+
prev = prev.unsqueeze(-1).repeat(1,1,3).to(device=dev, dtype=dtype).unsqueeze(0)
|
| 519 |
+
return (pos_out, neg_out, prev)
|
mod/hard/mg_ids.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
import torch
|
| 5 |
+
|
| 6 |
+
try:
|
| 7 |
+
from scipy.ndimage import gaussian_filter as _scipy_gaussian_filter
|
| 8 |
+
_HAVE_SCIPY = True
|
| 9 |
+
except Exception:
|
| 10 |
+
_HAVE_SCIPY = False
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _torch_gaussian_blur(image: torch.Tensor, sigma: float) -> torch.Tensor:
|
| 14 |
+
# image: BHWC in [0,1]
|
| 15 |
+
if sigma <= 0.0:
|
| 16 |
+
return image
|
| 17 |
+
device = image.device
|
| 18 |
+
dtype = image.dtype
|
| 19 |
+
radius = max(1, int(3.0 * float(sigma)))
|
| 20 |
+
ksize = radius * 2 + 1
|
| 21 |
+
x = torch.arange(-radius, radius + 1, device=device, dtype=dtype)
|
| 22 |
+
g1 = torch.exp(-(x * x) / (2.0 * (sigma ** 2)))
|
| 23 |
+
g1 = (g1 / g1.sum()).view(1, 1, 1, -1)
|
| 24 |
+
g2 = g1.transpose(2, 3)
|
| 25 |
+
xch = image.movedim(-1, 1) # BCHW
|
| 26 |
+
pad = (radius, radius, radius, radius)
|
| 27 |
+
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])
|
| 28 |
+
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])
|
| 29 |
+
return out.movedim(1, -1)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class IntelligentDetailStabilizer:
|
| 33 |
+
"""Alias-preserving move of IDS into mod/ as mg_ids.py.
|
| 34 |
+
Keeps class/key name for backward compatibility.
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
@classmethod
|
| 38 |
+
def INPUT_TYPES(cls):
|
| 39 |
+
return {
|
| 40 |
+
"required": {
|
| 41 |
+
"image": ("IMAGE", {}),
|
| 42 |
+
"ids_strength": (
|
| 43 |
+
"FLOAT",
|
| 44 |
+
{"default": 0.5, "min": -1.0, "max": 1.0, "step": 0.01},
|
| 45 |
+
),
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
RETURN_TYPES = ("IMAGE",)
|
| 50 |
+
RETURN_NAMES = ("IMAGE",)
|
| 51 |
+
FUNCTION = "stabilize"
|
| 52 |
+
CATEGORY = "MagicNodes"
|
| 53 |
+
|
| 54 |
+
def stabilize(self, image: torch.Tensor, ids_strength: float = 0.5):
|
| 55 |
+
sigma = max(float(ids_strength) * 2.0, 1e-3)
|
| 56 |
+
if _HAVE_SCIPY:
|
| 57 |
+
img_np = image.detach().cpu().numpy()
|
| 58 |
+
denoised = _scipy_gaussian_filter(img_np, sigma=(0, sigma, sigma, 0))
|
| 59 |
+
blurred = _scipy_gaussian_filter(denoised, sigma=(0, 1.0, 1.0, 0))
|
| 60 |
+
sharpen = denoised + ids_strength * (denoised - blurred)
|
| 61 |
+
sharpen = np.clip(sharpen, 0.0, 1.0)
|
| 62 |
+
out = torch.from_numpy(sharpen).to(image.device, dtype=image.dtype)
|
| 63 |
+
else:
|
| 64 |
+
denoised = _torch_gaussian_blur(image, sigma=sigma)
|
| 65 |
+
blurred = _torch_gaussian_blur(denoised, sigma=1.0)
|
| 66 |
+
out = (denoised + ids_strength * (denoised - blurred)).clamp(0, 1)
|
| 67 |
+
return (out,)
|
mod/hard/mg_upscale_module.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import comfy.utils
|
| 2 |
+
import torch
|
| 3 |
+
import gc
|
| 4 |
+
import logging
|
| 5 |
+
import comfy.model_management as model_management
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def clear_gpu_and_ram_cache():
|
| 9 |
+
gc.collect()
|
| 10 |
+
if torch.cuda.is_available():
|
| 11 |
+
torch.cuda.empty_cache()
|
| 12 |
+
torch.cuda.ipc_collect()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _smart_decode(vae, latent, tile_size=512):
|
| 16 |
+
try:
|
| 17 |
+
images = vae.decode(latent["samples"])
|
| 18 |
+
except model_management.OOM_EXCEPTION:
|
| 19 |
+
logging.warning("VAE decode OOM, using tiled decode")
|
| 20 |
+
compression = vae.spacial_compression_decode()
|
| 21 |
+
images = vae.decode_tiled(
|
| 22 |
+
latent["samples"],
|
| 23 |
+
tile_x=tile_size // compression,
|
| 24 |
+
tile_y=tile_size // compression,
|
| 25 |
+
overlap=(tile_size // 4) // compression,
|
| 26 |
+
)
|
| 27 |
+
if len(images.shape) == 5:
|
| 28 |
+
images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1])
|
| 29 |
+
return images
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class MagicUpscaleModule:
|
| 33 |
+
"""Moved into mod/ as mg_upscale_module keeping class/key name."""
|
| 34 |
+
upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"]
|
| 35 |
+
|
| 36 |
+
@classmethod
|
| 37 |
+
def INPUT_TYPES(cls):
|
| 38 |
+
return {
|
| 39 |
+
"required": {
|
| 40 |
+
"samples": ("LATENT", {}),
|
| 41 |
+
"vae": ("VAE", {}),
|
| 42 |
+
"upscale_method": (cls.upscale_methods, {"default": "bilinear"}),
|
| 43 |
+
"scale_by": ("FLOAT", {"default": 1.2, "min": 0.01, "max": 8.0, "step": 0.01}),
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
RETURN_TYPES = ("LATENT", "IMAGE")
|
| 48 |
+
RETURN_NAMES = ("LATENT", "Upscaled Image")
|
| 49 |
+
FUNCTION = "process_upscale"
|
| 50 |
+
CATEGORY = "MagicNodes"
|
| 51 |
+
|
| 52 |
+
def process_upscale(self, samples, vae, upscale_method, scale_by):
|
| 53 |
+
clear_gpu_and_ram_cache()
|
| 54 |
+
images = _smart_decode(vae, samples)
|
| 55 |
+
samples_t = images.movedim(-1, 1)
|
| 56 |
+
width = round(samples_t.shape[3] * scale_by)
|
| 57 |
+
height = round(samples_t.shape[2] * scale_by)
|
| 58 |
+
# Align to VAE stride to avoid border artifacts/shape drift
|
| 59 |
+
try:
|
| 60 |
+
stride = int(vae.spacial_compression_decode())
|
| 61 |
+
except Exception:
|
| 62 |
+
stride = 8
|
| 63 |
+
if stride <= 0:
|
| 64 |
+
stride = 8
|
| 65 |
+
def _align_up(x, s):
|
| 66 |
+
return int(((x + s - 1) // s) * s)
|
| 67 |
+
width_al = _align_up(width, stride)
|
| 68 |
+
height_al = _align_up(height, stride)
|
| 69 |
+
up = comfy.utils.common_upscale(samples_t, width_al, height_al, upscale_method, "disabled")
|
| 70 |
+
up = up.movedim(1, -1)
|
| 71 |
+
encoded = vae.encode(up[:, :, :, :3])
|
| 72 |
+
return ({"samples": encoded}, up)
|
mod/hard/mg_zesmart_sampler_v1_1.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn.functional as F # noqa: F401
|
| 6 |
+
|
| 7 |
+
import comfy.utils as _utils
|
| 8 |
+
import comfy.sample as _sample
|
| 9 |
+
import comfy.samplers as _samplers
|
| 10 |
+
from comfy.k_diffusion import sampling as _kds
|
| 11 |
+
|
| 12 |
+
import nodes # latent preview callback
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _smoothstep01(x: torch.Tensor) -> torch.Tensor:
|
| 16 |
+
return x * x * (3.0 - 2.0 * x)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _build_hybrid_sigmas(model, steps: int, base_sampler: str, mode: str,
|
| 20 |
+
mix: float, denoise: float, jitter: float, seed: int,
|
| 21 |
+
_debug: bool = False, tail_smooth: float = 0.0,
|
| 22 |
+
auto_hybrid_tail: bool = True, auto_tail_strength: float = 0.35):
|
| 23 |
+
"""Return 1D tensor of sigmas (len == steps+1), strictly descending and ending with 0.
|
| 24 |
+
|
| 25 |
+
mode: 'karras' | 'beta' | 'hybrid'. If 'hybrid', blend tail toward beta by `mix`.
|
| 26 |
+
We DO NOT apply 'drop penultimate' until the very end to preserve denoise math.
|
| 27 |
+
"""
|
| 28 |
+
ms = model.get_model_object("model_sampling")
|
| 29 |
+
steps = int(steps)
|
| 30 |
+
assert steps >= 1
|
| 31 |
+
|
| 32 |
+
# --- base tracks ---
|
| 33 |
+
sig_k = _samplers.calculate_sigmas(ms, "karras", steps)
|
| 34 |
+
sig_b = _samplers.calculate_sigmas(ms, "beta", steps)
|
| 35 |
+
|
| 36 |
+
mode = str(mode).lower()
|
| 37 |
+
if mode == "karras":
|
| 38 |
+
sig = sig_k
|
| 39 |
+
elif mode == "beta":
|
| 40 |
+
sig = sig_b
|
| 41 |
+
else:
|
| 42 |
+
n = sig_k.shape[0]
|
| 43 |
+
t = torch.linspace(0.0, 1.0, n, device=sig_k.device, dtype=sig_k.dtype)
|
| 44 |
+
m = float(max(0.0, min(1.0, mix)))
|
| 45 |
+
eps = 1e-6 if m < 1e-6 else m
|
| 46 |
+
w = torch.clamp((t - (1.0 - m)) / eps, 0.0, 1.0)
|
| 47 |
+
w = _smoothstep01(w)
|
| 48 |
+
sig = sig_k * (1.0 - w) + sig_b * w
|
| 49 |
+
|
| 50 |
+
# --- Comfy denoise semantics: recompute a "full" track and take the tail of desired length ---
|
| 51 |
+
sig_k_base = sig_k
|
| 52 |
+
sig_b_base = sig_b
|
| 53 |
+
if denoise is not None and 0.0 < float(denoise) < 0.999999:
|
| 54 |
+
new_steps = max(1, int(steps / max(1e-6, float(denoise))))
|
| 55 |
+
sk = _samplers.calculate_sigmas(ms, "karras", new_steps)
|
| 56 |
+
sb = _samplers.calculate_sigmas(ms, "beta", new_steps)
|
| 57 |
+
if mode == "karras":
|
| 58 |
+
sig_full = sk
|
| 59 |
+
elif mode == "beta":
|
| 60 |
+
sig_full = sb
|
| 61 |
+
else:
|
| 62 |
+
n2 = sk.shape[0]
|
| 63 |
+
t2 = torch.linspace(0.0, 1.0, n2, device=sk.device, dtype=sk.dtype)
|
| 64 |
+
m = float(max(0.0, min(1.0, mix)))
|
| 65 |
+
eps = 1e-6 if m < 1e-6 else m
|
| 66 |
+
w2 = torch.clamp((t2 - (1.0 - m)) / eps, 0.0, 1.0)
|
| 67 |
+
w2 = _smoothstep01(w2)
|
| 68 |
+
sig_full = sk * (1.0 - w2) + sb * w2
|
| 69 |
+
need = steps + 1
|
| 70 |
+
if sig_full.shape[0] >= need:
|
| 71 |
+
sig = sig_full[-need:]
|
| 72 |
+
sig_k_base = sk[-need:]
|
| 73 |
+
sig_b_base = sb[-need:]
|
| 74 |
+
else:
|
| 75 |
+
# Worst case: trust what we got; we will still guarantee the last sigma is zero later
|
| 76 |
+
sig = sig_full
|
| 77 |
+
tail = min(need, sk.shape[0])
|
| 78 |
+
sig_k_base = sk[-tail:]
|
| 79 |
+
sig_b_base = sb[-tail:]
|
| 80 |
+
|
| 81 |
+
# --- auto-hybrid tail: blend beta into the tail when the steps become brittle ---
|
| 82 |
+
if bool(auto_hybrid_tail) and sig.numel() > 2:
|
| 83 |
+
n = sig.shape[0]
|
| 84 |
+
t = torch.linspace(0.0, 1.0, n, device=sig.device, dtype=sig.dtype)
|
| 85 |
+
m = float(max(0.0, min(1.0, mix)))
|
| 86 |
+
if mode == "hybrid":
|
| 87 |
+
eps = 1e-6 if m < 1e-6 else m
|
| 88 |
+
w_m = torch.clamp((t - (1.0 - m)) / eps, 0.0, 1.0)
|
| 89 |
+
w_m = _smoothstep01(w_m)
|
| 90 |
+
elif mode == "beta":
|
| 91 |
+
w_m = torch.ones_like(t)
|
| 92 |
+
else:
|
| 93 |
+
w_m = torch.zeros_like(t)
|
| 94 |
+
dif = (sig[1:] - sig[:-1]).abs() / sig[:-1].abs().clamp_min(1e-8)
|
| 95 |
+
dif = torch.cat([dif, dif[-1:]], dim=0)
|
| 96 |
+
dif = (dif - dif.min()) / (dif.max() - dif.min() + 1e-8)
|
| 97 |
+
ramp = _smoothstep01(torch.clamp((t - 0.7) / 0.3, 0.0, 1.0))
|
| 98 |
+
w_a = dif * ramp
|
| 99 |
+
g = float(max(0.0, min(1.0, auto_tail_strength)))
|
| 100 |
+
u = w_m + g * w_a - w_m * g * w_a
|
| 101 |
+
sig = sig_k_base * (1.0 - u) + sig_b_base * u
|
| 102 |
+
|
| 103 |
+
# --- tiny schedule jitter ---
|
| 104 |
+
j = float(max(0.0, min(0.1, float(jitter))))
|
| 105 |
+
if j > 0.0 and sig.numel() > 1:
|
| 106 |
+
gen = torch.Generator(device='cpu')
|
| 107 |
+
gen.manual_seed(int(seed) ^ 0x5EEDCAFE)
|
| 108 |
+
noise = torch.randn(sig.shape, generator=gen, device='cpu').to(sig.device, sig.dtype)
|
| 109 |
+
amp = j * float(sig[0].item() - sig[-1].item()) * 1e-3
|
| 110 |
+
sig = sig + noise * amp
|
| 111 |
+
sig, _ = torch.sort(sig, descending=True)
|
| 112 |
+
|
| 113 |
+
# --- hard guarantee of ending with exact zero ---
|
| 114 |
+
if sig[-1].abs() > 1e-12:
|
| 115 |
+
sig = torch.cat([sig[:-1], sig.new_zeros(1)], dim=0)
|
| 116 |
+
|
| 117 |
+
# --- and only now drop-penultimate for respective samplers ---
|
| 118 |
+
# --- gentle smoothing of sigma tail (adaptive, safe for monotonic decrease) ---
|
| 119 |
+
ts = float(max(0.0, min(1.0, tail_smooth)))
|
| 120 |
+
if ts > 0.0 and sig.numel() > 2:
|
| 121 |
+
s = sig.clone()
|
| 122 |
+
n = int(s.shape[0])
|
| 123 |
+
t = torch.linspace(0.0, 1.0, n, device=s.device, dtype=s.dtype)
|
| 124 |
+
w = (t.pow(2) * ts).clamp(0.0, 1.0)
|
| 125 |
+
for i in range(n - 2, -1, -1):
|
| 126 |
+
a = float(min(0.5, 0.5 * w[i].item()))
|
| 127 |
+
s[i] = (1.0 - a) * s[i] + a * s[i + 1]
|
| 128 |
+
sig = s
|
| 129 |
+
|
| 130 |
+
if base_sampler in _samplers.KSampler.DISCARD_PENULTIMATE_SIGMA_SAMPLERS and sig.numel() >= 2:
|
| 131 |
+
sig = torch.cat([sig[:-2], sig[-1:]], dim=0)
|
| 132 |
+
|
| 133 |
+
sig = sig.to(model.load_device)
|
| 134 |
+
|
| 135 |
+
# Lightweight debug: schedule summary
|
| 136 |
+
if _debug:
|
| 137 |
+
try:
|
| 138 |
+
desc_ok = bool((sig[:-1] > sig[1:]).all().item()) if sig.numel() > 1 else True
|
| 139 |
+
head = ", ".join(f"{float(v):.4g}" for v in sig[:3].tolist()) if sig.numel() >= 3 else \
|
| 140 |
+
", ".join(f"{float(v):.4g}" for v in sig.tolist())
|
| 141 |
+
tail = ", ".join(f"{float(v):.4g}" for v in sig[-3:].tolist()) if sig.numel() >= 3 else head
|
| 142 |
+
print(f"[ZeSmart][dbg] sigmas len={sig.numel()} desc={desc_ok} first={float(sig[0]):.6g} last={float(sig[-1]):.6g}")
|
| 143 |
+
print(f"[ZeSmart][dbg] head: [{head}] tail: [{tail}]")
|
| 144 |
+
except Exception:
|
| 145 |
+
pass
|
| 146 |
+
|
| 147 |
+
return sig
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
class MG_ZeSmartSampler:
|
| 151 |
+
@classmethod
|
| 152 |
+
def INPUT_TYPES(cls):
|
| 153 |
+
return {
|
| 154 |
+
"required": {
|
| 155 |
+
"model": ("MODEL", {}),
|
| 156 |
+
"seed": ("INT", {"default": 0, "min": 0, "max": 2**63-1, "control_after_generate": True}),
|
| 157 |
+
"steps": ("INT", {"default": 20, "min": 1, "max": 4096}),
|
| 158 |
+
"cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 50.0, "step": 0.1}),
|
| 159 |
+
"base_sampler": (_samplers.KSampler.SAMPLERS, {"default": "dpmpp_2m"}),
|
| 160 |
+
"schedule": (["karras", "beta", "hybrid"], {"default": "hybrid", "tooltip": "Sigma curve: karras — soft start; beta — stable tail; hybrid — their mix."}),
|
| 161 |
+
"positive": ("CONDITIONING", {}),
|
| 162 |
+
"negative": ("CONDITIONING", {}),
|
| 163 |
+
"latent": ("LATENT", {}),
|
| 164 |
+
},
|
| 165 |
+
"optional": {
|
| 166 |
+
"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."}),
|
| 167 |
+
"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)."}),
|
| 168 |
+
"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."}),
|
| 169 |
+
"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."}),
|
| 170 |
+
"auto_hybrid_tail": ("BOOLEAN", {"default": True, "tooltip": "Auto‑blend beta on the tail when steps become brittle."}),
|
| 171 |
+
"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)."}),
|
| 172 |
+
"debug_probe": ("BOOLEAN", {"default": False, "tooltip": "Print sigma summary (length, first/last, head/tail)."}),
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
RETURN_TYPES = ("LATENT",)
|
| 177 |
+
RETURN_NAMES = ("LATENT",)
|
| 178 |
+
FUNCTION = "apply"
|
| 179 |
+
CATEGORY = "MagicNodes/Experimental"
|
| 180 |
+
|
| 181 |
+
def apply(self, model, seed, steps, cfg, base_sampler, schedule,
|
| 182 |
+
positive, negative, latent, denoise=1.0, hybrid_mix=0.5,
|
| 183 |
+
jitter_sigma=0.02, tail_smooth=0.07,
|
| 184 |
+
auto_hybrid_tail=True, auto_tail_strength=0.3,
|
| 185 |
+
debug_probe=False):
|
| 186 |
+
# Prepare latent + noise
|
| 187 |
+
lat_img = latent["samples"]
|
| 188 |
+
lat_img = _sample.fix_empty_latent_channels(model, lat_img)
|
| 189 |
+
batch_inds = latent.get("batch_index", None)
|
| 190 |
+
noise = _sample.prepare_noise(lat_img, seed, batch_inds)
|
| 191 |
+
noise_mask = latent.get("noise_mask", None)
|
| 192 |
+
|
| 193 |
+
# Custom sigmas
|
| 194 |
+
sigmas = _build_hybrid_sigmas(model, int(steps), str(base_sampler), str(schedule),
|
| 195 |
+
float(hybrid_mix), float(denoise), float(jitter_sigma), int(seed),
|
| 196 |
+
_debug=bool(debug_probe), tail_smooth=float(tail_smooth),
|
| 197 |
+
auto_hybrid_tail=bool(auto_hybrid_tail),
|
| 198 |
+
auto_tail_strength=float(auto_tail_strength))
|
| 199 |
+
|
| 200 |
+
# Use native sampler; all tweaks happen in sigma schedule only.
|
| 201 |
+
sampler_obj = _samplers.sampler_object(str(base_sampler))
|
| 202 |
+
callback = nodes.latent_preview.prepare_callback(model, int(steps))
|
| 203 |
+
disable_pbar = not _utils.PROGRESS_BAR_ENABLED
|
| 204 |
+
samples = _sample.sample_custom(model, noise, float(cfg), sampler_obj, sigmas,
|
| 205 |
+
positive, negative, lat_img,
|
| 206 |
+
noise_mask=noise_mask, callback=callback,
|
| 207 |
+
disable_pbar=disable_pbar, seed=seed)
|
| 208 |
+
out = {**latent}
|
| 209 |
+
out["samples"] = samples
|
| 210 |
+
return (out,)
|
mod/mg_combinode.py
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import comfy.sd
|
| 2 |
+
import comfy.clip_vision
|
| 3 |
+
import folder_paths
|
| 4 |
+
import comfy.utils
|
| 5 |
+
import torch
|
| 6 |
+
import random
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
import random
|
| 9 |
+
import gc
|
| 10 |
+
import os
|
| 11 |
+
import json
|
| 12 |
+
import re
|
| 13 |
+
|
| 14 |
+
from .hard.mg_upscale_module import clear_gpu_and_ram_cache
|
| 15 |
+
|
| 16 |
+
# Module level caches to reuse loaded models and LoRAs between invocations
|
| 17 |
+
_checkpoint_cache = {}
|
| 18 |
+
_loaded_checkpoint = None
|
| 19 |
+
_lora_cache = {}
|
| 20 |
+
_active_lora_names = set()
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _clear_unused_loras(active_names):
|
| 24 |
+
"""Remove unused LoRAs from cache and clear GPU memory."""
|
| 25 |
+
unused = [n for n in _lora_cache if n not in active_names]
|
| 26 |
+
for n in unused:
|
| 27 |
+
del _lora_cache[n]
|
| 28 |
+
if unused:
|
| 29 |
+
gc.collect()
|
| 30 |
+
if torch.cuda.is_available():
|
| 31 |
+
torch.cuda.empty_cache()
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _load_checkpoint(path):
|
| 35 |
+
"""Load checkpoint from cache or disk."""
|
| 36 |
+
if path in _checkpoint_cache:
|
| 37 |
+
return _checkpoint_cache[path]
|
| 38 |
+
model, clip, vae = comfy.sd.load_checkpoint_guess_config(
|
| 39 |
+
path,
|
| 40 |
+
output_vae=True,
|
| 41 |
+
output_clip=True,
|
| 42 |
+
embedding_directory=folder_paths.get_folder_paths("embeddings"),
|
| 43 |
+
)[:3]
|
| 44 |
+
_checkpoint_cache[path] = (model, clip, vae)
|
| 45 |
+
return model, clip, vae
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _unload_old_checkpoint(current_path):
|
| 49 |
+
"""Unload checkpoint if it's different from the current one."""
|
| 50 |
+
global _loaded_checkpoint
|
| 51 |
+
if _loaded_checkpoint and _loaded_checkpoint != current_path:
|
| 52 |
+
_checkpoint_cache.pop(_loaded_checkpoint, None)
|
| 53 |
+
gc.collect()
|
| 54 |
+
if torch.cuda.is_available():
|
| 55 |
+
torch.cuda.empty_cache()
|
| 56 |
+
_loaded_checkpoint = current_path
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class MagicNodesCombiNode:
|
| 61 |
+
@classmethod
|
| 62 |
+
def INPUT_TYPES(cls):
|
| 63 |
+
return {
|
| 64 |
+
"required": {
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
# --- Checkpoint ---
|
| 69 |
+
"use_checkpoint": ("BOOLEAN", {"default": True}),
|
| 70 |
+
"checkpoint": (folder_paths.get_filename_list("checkpoints"), {}),
|
| 71 |
+
"clear_cache": ("BOOLEAN", {"default": False}),
|
| 72 |
+
|
| 73 |
+
# --- LoRA 1 ---
|
| 74 |
+
"use_lora_1": ("BOOLEAN", {"default": True}),
|
| 75 |
+
"lora_1": (folder_paths.get_filename_list("loras"), {}),
|
| 76 |
+
"strength_model_1": ("FLOAT", {"default": 1.0, "min": -1.5, "max": 1.5, "step": 0.01,}),
|
| 77 |
+
"strength_clip_1": ("FLOAT", {"default": 1.0, "min": -1.5, "max": 1.5, "step": 0.01,}),
|
| 78 |
+
|
| 79 |
+
# --- LoRA 2 ---
|
| 80 |
+
"use_lora_2": ("BOOLEAN", {"default": False}),
|
| 81 |
+
"lora_2": (folder_paths.get_filename_list("loras"), {}),
|
| 82 |
+
"strength_model_2": ("FLOAT", {"default": 0.0, "min": -1.5, "max": 1.5, "step": 0.01,}),
|
| 83 |
+
"strength_clip_2": ("FLOAT", {"default": 0.0, "min": -1.5, "max": 1.5, "step": 0.01,}),
|
| 84 |
+
|
| 85 |
+
# --- LoRA 3 ---
|
| 86 |
+
"use_lora_3": ("BOOLEAN", {"default": False}),
|
| 87 |
+
"lora_3": (folder_paths.get_filename_list("loras"), {}),
|
| 88 |
+
"strength_model_3": ("FLOAT", {"default": 0.0, "min": -1.5, "max": 1.5, "step": 0.01,}),
|
| 89 |
+
"strength_clip_3": ("FLOAT", {"default": 0.0, "min": -1.5, "max": 1.5, "step": 0.01,}),
|
| 90 |
+
|
| 91 |
+
# --- LoRA 4 ---
|
| 92 |
+
"use_lora_4": ("BOOLEAN", {"default": False}),
|
| 93 |
+
"lora_4": (folder_paths.get_filename_list("loras"), {}),
|
| 94 |
+
"strength_model_4": ("FLOAT", {"default": 0.0, "min": -1.5, "max": 1.5, "step": 0.01,}),
|
| 95 |
+
"strength_clip_4": ("FLOAT", {"default": 0.0, "min": -1.5, "max": 1.5, "step": 0.01,}),
|
| 96 |
+
|
| 97 |
+
# --- LoRA 5 ---
|
| 98 |
+
"use_lora_5": ("BOOLEAN", {"default": False}),
|
| 99 |
+
"lora_5": (folder_paths.get_filename_list("loras"), {}),
|
| 100 |
+
"strength_model_5": ("FLOAT", {"default": 0.0, "min": -1.5, "max": 1.5, "step": 0.01,}),
|
| 101 |
+
"strength_clip_5": ("FLOAT", {"default": 0.0, "min": -1.5, "max": 1.5, "step": 0.01,}),
|
| 102 |
+
|
| 103 |
+
# --- LoRA 6 ---
|
| 104 |
+
"use_lora_6": ("BOOLEAN", {"default": False}),
|
| 105 |
+
"lora_6": (folder_paths.get_filename_list("loras"), {}),
|
| 106 |
+
"strength_model_6": ("FLOAT", {"default": 0.0, "min": -1.5, "max": 1.5, "step": 0.01,}),
|
| 107 |
+
"strength_clip_6": ("FLOAT", {"default": 0.0, "min": -1.5, "max": 1.5, "step": 0.01,}),
|
| 108 |
+
},
|
| 109 |
+
"optional": {
|
| 110 |
+
"model_in": ("MODEL", {}),
|
| 111 |
+
"clip_in": ("CLIP", {}),
|
| 112 |
+
"vae_in": ("VAE", {}),
|
| 113 |
+
|
| 114 |
+
# --- Prompts --- (controlled dynamic expansion inside node for determinism)
|
| 115 |
+
"positive_prompt": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": False}),
|
| 116 |
+
"negative_prompt": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": False}),
|
| 117 |
+
|
| 118 |
+
# Optional external conditioning (bypass internal text encode)
|
| 119 |
+
"positive_in": ("CONDITIONING", {}),
|
| 120 |
+
"negative_in": ("CONDITIONING", {}),
|
| 121 |
+
|
| 122 |
+
# --- CLIP Layers ---
|
| 123 |
+
"clip_set_last_layer_positive": ("INT", {"default": -2, "min": -20, "max": 0}),
|
| 124 |
+
"clip_set_last_layer_negative": ("INT", {"default": -2, "min": -20, "max": 0}),
|
| 125 |
+
|
| 126 |
+
# --- Recipes ---
|
| 127 |
+
"recipe_slot": (["Off", "Slot 1", "Slot 2", "Slot 3", "Slot 4"], {"default": "Off", "tooltip": "Choose slot to save/load assembled setup."}),
|
| 128 |
+
"recipe_save": ("BOOLEAN", {"default": False, "tooltip": "Save current setup into the selected slot."}),
|
| 129 |
+
"recipe_use": ("BOOLEAN", {"default": False, "tooltip": "Load and apply setup from the selected slot for this run."}),
|
| 130 |
+
|
| 131 |
+
# --- Standard pipeline (match classic node order for CLIP) ---
|
| 132 |
+
"standard_pipeline": ("BOOLEAN", {"default": False, "tooltip": "Use vanilla order for CLIP: Set Last Layer -> Load LoRA -> Encode (same CLIP logic as standard ComfyUI)."}),
|
| 133 |
+
|
| 134 |
+
# CLIP LoRA gains per branch (effective only when standard_pipeline=true)
|
| 135 |
+
"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)."}),
|
| 136 |
+
"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)."}),
|
| 137 |
+
|
| 138 |
+
# Deterministic dynamic prompts
|
| 139 |
+
"dynamic_pos": ("BOOLEAN", {"default": False, "tooltip": "Deterministically expand choices in positive prompt (uses dyn_seed)."}),
|
| 140 |
+
"dynamic_neg": ("BOOLEAN", {"default": False, "tooltip": "Deterministically expand choices in negative prompt (uses dyn_seed)."}),
|
| 141 |
+
"dyn_seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFF, "tooltip": "Seed for dynamic prompt expansion (same seed used for both prompts)."}),
|
| 142 |
+
"dynamic_break_freeze": ("BOOLEAN", {"default": True, "tooltip": "If enabled, do not expand choices before the first |BREAK| marker; dynamic applies only after it."}),
|
| 143 |
+
"show_expanded_prompts": ("BOOLEAN", {"default": False, "tooltip": "Print expanded Positive/Negative prompts to console when dynamic is enabled."}),
|
| 144 |
+
"save_expanded_prompts": ("BOOLEAN", {"default": False, "tooltip": "Save expanded prompts to mod/dynPrompt/SEED_dd_mm_yyyy.txt when dynamic is enabled."}),
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
RETURN_TYPES = ("MODEL", "CLIP", "CONDITIONING", "CONDITIONING", "VAE")
|
| 150 |
+
RETURN_NAMES = ("MODEL", "CLIP", "Positive", "Negative", "VAE")
|
| 151 |
+
#RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "VAE")
|
| 152 |
+
#RETURN_NAMES = ("MODEL", "Positive", "Negative", "VAE")
|
| 153 |
+
FUNCTION = "apply_magic_node"
|
| 154 |
+
CATEGORY = "MagicNodes"
|
| 155 |
+
|
| 156 |
+
def apply_magic_node(self, model_in=None, clip_in=None, checkpoint=None,
|
| 157 |
+
use_checkpoint=True, clear_cache=False,
|
| 158 |
+
use_lora_1=True, lora_1=None, strength_model_1=1.0, strength_clip_1=1.0,
|
| 159 |
+
use_lora_2=False, lora_2=None, strength_model_2=0.0, strength_clip_2=0.0,
|
| 160 |
+
use_lora_3=False, lora_3=None, strength_model_3=0.0, strength_clip_3=0.0,
|
| 161 |
+
use_lora_4=False, lora_4=None, strength_model_4=0.0, strength_clip_4=0.0,
|
| 162 |
+
use_lora_5=False, lora_5=None, strength_model_5=0.0, strength_clip_5=0.0,
|
| 163 |
+
use_lora_6=False, lora_6=None, strength_model_6=0.0, strength_clip_6=0.0,
|
| 164 |
+
positive_prompt="", negative_prompt="",
|
| 165 |
+
clip_set_last_layer_positive=-2, clip_set_last_layer_negative=-2,
|
| 166 |
+
vae_in=None,
|
| 167 |
+
recipe_slot="Off", recipe_save=False, recipe_use=False,
|
| 168 |
+
standard_pipeline=False,
|
| 169 |
+
clip_lora_pos_gain=1.0, clip_lora_neg_gain=1.0,
|
| 170 |
+
positive_in=None, negative_in=None,
|
| 171 |
+
dynamic_pos=False, dynamic_neg=False, dyn_seed=0, dynamic_break_freeze=True,
|
| 172 |
+
show_expanded_prompts=False, save_expanded_prompts=False):
|
| 173 |
+
|
| 174 |
+
global _loaded_checkpoint
|
| 175 |
+
|
| 176 |
+
# hard scrub of checkpoint cache each call (prevent hidden state)
|
| 177 |
+
_checkpoint_cache.clear()
|
| 178 |
+
if clear_cache:
|
| 179 |
+
_lora_cache.clear()
|
| 180 |
+
gc.collect()
|
| 181 |
+
if torch.cuda.is_available():
|
| 182 |
+
torch.cuda.empty_cache()
|
| 183 |
+
|
| 184 |
+
# Recipe helpers
|
| 185 |
+
def _recipes_path():
|
| 186 |
+
base = os.path.join(os.path.dirname(__file__), "state")
|
| 187 |
+
os.makedirs(base, exist_ok=True)
|
| 188 |
+
return os.path.join(base, "combinode_recipes.json")
|
| 189 |
+
def _recipes_load():
|
| 190 |
+
try:
|
| 191 |
+
with open(_recipes_path(), "r", encoding="utf-8") as f:
|
| 192 |
+
return json.load(f)
|
| 193 |
+
except Exception:
|
| 194 |
+
return {}
|
| 195 |
+
def _recipes_save(data: dict):
|
| 196 |
+
try:
|
| 197 |
+
with open(_recipes_path(), "w", encoding="utf-8") as f:
|
| 198 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 199 |
+
except Exception:
|
| 200 |
+
pass
|
| 201 |
+
|
| 202 |
+
# Apply recipe if requested
|
| 203 |
+
slot_idx = {"Off": 0, "Slot 1": 1, "Slot 2": 2, "Slot 3": 3, "Slot 4": 4}.get(str(recipe_slot), 0)
|
| 204 |
+
if slot_idx and bool(recipe_use):
|
| 205 |
+
rec = _recipes_load().get(str(slot_idx), None)
|
| 206 |
+
if rec is not None:
|
| 207 |
+
try:
|
| 208 |
+
use_checkpoint = rec.get("use_checkpoint", use_checkpoint)
|
| 209 |
+
checkpoint = rec.get("checkpoint", checkpoint)
|
| 210 |
+
clip_set_last_layer_positive = rec.get("clip_pos", clip_set_last_layer_positive)
|
| 211 |
+
clip_set_last_layer_negative = rec.get("clip_neg", clip_set_last_layer_negative)
|
| 212 |
+
positive_prompt = rec.get("pos_text", positive_prompt)
|
| 213 |
+
negative_prompt = rec.get("neg_text", negative_prompt)
|
| 214 |
+
rls = rec.get("loras", [])
|
| 215 |
+
if len(rls) >= 4:
|
| 216 |
+
(use_lora_1, lora_1, strength_model_1, strength_clip_1) = rls[0]
|
| 217 |
+
(use_lora_2, lora_2, strength_model_2, strength_clip_2) = rls[1]
|
| 218 |
+
(use_lora_3, lora_3, strength_model_3, strength_clip_3) = rls[2]
|
| 219 |
+
(use_lora_4, lora_4, strength_model_4, strength_clip_4) = rls[3]
|
| 220 |
+
if len(rls) >= 5:
|
| 221 |
+
(use_lora_5, lora_5, strength_model_5, strength_clip_5) = rls[4]
|
| 222 |
+
if len(rls) >= 6:
|
| 223 |
+
(use_lora_6, lora_6, strength_model_6, strength_clip_6) = rls[5]
|
| 224 |
+
print(f"[CombiNode] Loaded recipe Slot {slot_idx}.")
|
| 225 |
+
except Exception:
|
| 226 |
+
print(f"[CombiNode] Failed to apply recipe Slot {slot_idx}.")
|
| 227 |
+
|
| 228 |
+
# Prompt normalization helper (keeps '|' intact)
|
| 229 |
+
def _norm_prompt(s: str) -> str:
|
| 230 |
+
if not isinstance(s, str) or not s:
|
| 231 |
+
return s or ""
|
| 232 |
+
s2 = s.replace("\r", " ").replace("\n", " ")
|
| 233 |
+
s2 = re.sub(r"\s+", " ", s2)
|
| 234 |
+
s2 = re.sub(r"\s*,\s*", ", ", s2)
|
| 235 |
+
s2 = re.sub(r"(,\s*){2,}", ", ", s2)
|
| 236 |
+
return s2.strip()
|
| 237 |
+
|
| 238 |
+
# Deterministic dynamic prompt expansion: supports {...}, (...), [...] with '|' choices
|
| 239 |
+
def _expand_dynamic(text: str, seed_val: int, freeze_before_break: bool = True) -> str:
|
| 240 |
+
if not isinstance(text, str) or (text.find('|') < 0):
|
| 241 |
+
return text
|
| 242 |
+
# Honor |BREAK|: keep first segment intact when requested
|
| 243 |
+
if freeze_before_break and ('|BREAK|' in text):
|
| 244 |
+
pre, post = text.split('|BREAK|', 1)
|
| 245 |
+
return pre + '|BREAK|' + _expand_dynamic(post, seed_val, freeze_before_break=False)
|
| 246 |
+
rng = random.Random(int(seed_val) & 0xFFFFFFFF)
|
| 247 |
+
def _expand_pattern(t: str, pat: re.Pattern) -> str:
|
| 248 |
+
prev = None
|
| 249 |
+
cur = t
|
| 250 |
+
while prev != cur:
|
| 251 |
+
prev = cur
|
| 252 |
+
def repl(m):
|
| 253 |
+
body = m.group(1)
|
| 254 |
+
choices = [c.strip() for c in body.split('|') if c.strip()]
|
| 255 |
+
if not choices:
|
| 256 |
+
return m.group(0)
|
| 257 |
+
return rng.choice(choices)
|
| 258 |
+
cur = pat.sub(repl, cur)
|
| 259 |
+
return cur
|
| 260 |
+
for rx in (
|
| 261 |
+
re.compile(r"\{([^{}]+)\}"),
|
| 262 |
+
re.compile(r"\(([^()]+)\)"),
|
| 263 |
+
re.compile(r"\[([^\[\]]+)\]"),
|
| 264 |
+
):
|
| 265 |
+
text = _expand_pattern(text, rx)
|
| 266 |
+
return text
|
| 267 |
+
|
| 268 |
+
# Precompute expanded (or original) texts once
|
| 269 |
+
pos_text_expanded = _norm_prompt(_expand_dynamic(positive_prompt, int(dyn_seed), bool(dynamic_break_freeze)) if bool(dynamic_pos) else positive_prompt)
|
| 270 |
+
neg_text_expanded = _norm_prompt(_expand_dynamic(negative_prompt, int(dyn_seed), bool(dynamic_break_freeze)) if bool(dynamic_neg) else negative_prompt)
|
| 271 |
+
|
| 272 |
+
if use_checkpoint and checkpoint:
|
| 273 |
+
checkpoint_path = folder_paths.get_full_path_or_raise("checkpoints", checkpoint)
|
| 274 |
+
_unload_old_checkpoint(checkpoint_path)
|
| 275 |
+
base_model, base_clip, vae = _load_checkpoint(checkpoint_path)
|
| 276 |
+
model = base_model.clone()
|
| 277 |
+
clip = base_clip.clone()
|
| 278 |
+
clip_clean = base_clip.clone() # keep pristine CLIP for standard pipeline path
|
| 279 |
+
|
| 280 |
+
elif model_in and clip_in:
|
| 281 |
+
_unload_old_checkpoint(None)
|
| 282 |
+
model = model_in.clone()
|
| 283 |
+
clip = clip_in.clone()
|
| 284 |
+
clip_clean = clip_in.clone()
|
| 285 |
+
vae = vae_in
|
| 286 |
+
else:
|
| 287 |
+
raise Exception("No model selected!")
|
| 288 |
+
|
| 289 |
+
# single clear at start is enough; avoid double-clearing here
|
| 290 |
+
|
| 291 |
+
# Применение цепочки LoRA
|
| 292 |
+
loras = [
|
| 293 |
+
(use_lora_1, lora_1, strength_model_1, strength_clip_1),
|
| 294 |
+
(use_lora_2, lora_2, strength_model_2, strength_clip_2),
|
| 295 |
+
(use_lora_3, lora_3, strength_model_3, strength_clip_3),
|
| 296 |
+
(use_lora_4, lora_4, strength_model_4, strength_clip_4),
|
| 297 |
+
(use_lora_5, lora_5, strength_model_5, strength_clip_5),
|
| 298 |
+
(use_lora_6, lora_6, strength_model_6, strength_clip_6),
|
| 299 |
+
]
|
| 300 |
+
|
| 301 |
+
active_lora_paths = []
|
| 302 |
+
lora_stack = [] # list of (lora_file, sc, sm)
|
| 303 |
+
defer_clip = bool(standard_pipeline)
|
| 304 |
+
for use_lora, lora_name, sm, sc in loras:
|
| 305 |
+
if use_lora and lora_name:
|
| 306 |
+
lora_path = folder_paths.get_full_path_or_raise("loras", lora_name)
|
| 307 |
+
active_lora_paths.append(lora_path)
|
| 308 |
+
# keep lora object to avoid reloading
|
| 309 |
+
if lora_path in _lora_cache:
|
| 310 |
+
lora_file = _lora_cache[lora_path]
|
| 311 |
+
else:
|
| 312 |
+
lora_file = comfy.utils.load_torch_file(lora_path, safe_load=True)
|
| 313 |
+
_lora_cache[lora_path] = lora_file
|
| 314 |
+
lora_stack.append((lora_file, float(sc), float(sm)))
|
| 315 |
+
sc_apply = 0.0 if defer_clip else sc
|
| 316 |
+
model, clip = comfy.sd.load_lora_for_models(model, clip, lora_file, sm, sc_apply)
|
| 317 |
+
|
| 318 |
+
_clear_unused_loras(active_lora_paths)
|
| 319 |
+
# Warn about duplicate LoRA selections across slots
|
| 320 |
+
try:
|
| 321 |
+
counts = {}
|
| 322 |
+
for p in active_lora_paths:
|
| 323 |
+
counts[p] = counts.get(p, 0) + 1
|
| 324 |
+
dups = [p for p, c in counts.items() if c > 1]
|
| 325 |
+
if dups:
|
| 326 |
+
print(f"[CombiNode] Duplicate LoRA detected across slots: {len(dups)} file(s).")
|
| 327 |
+
except Exception:
|
| 328 |
+
pass
|
| 329 |
+
|
| 330 |
+
# Embeddings Positive и Negative
|
| 331 |
+
# Standard pipeline: optionally use a shared CLIP after clip_layer + CLIP-LoRA
|
| 332 |
+
# Select CLIP source for encoding: pristine when standard pipeline is enabled
|
| 333 |
+
src_clip = clip_clean if bool(standard_pipeline) else clip
|
| 334 |
+
|
| 335 |
+
pos_gain = float(clip_lora_pos_gain)
|
| 336 |
+
neg_gain = float(clip_lora_neg_gain)
|
| 337 |
+
skips_equal = int(clip_set_last_layer_positive) == int(clip_set_last_layer_negative)
|
| 338 |
+
# Use shared CLIP only if gains are equal and skips equal
|
| 339 |
+
use_shared = bool(standard_pipeline) and skips_equal and (abs(pos_gain - neg_gain) < 1e-6)
|
| 340 |
+
|
| 341 |
+
if (positive_in is None) and (negative_in is None) and use_shared:
|
| 342 |
+
shared_clip = src_clip.clone()
|
| 343 |
+
shared_clip.clip_layer(clip_set_last_layer_positive)
|
| 344 |
+
for lora_file, sc, sm in lora_stack:
|
| 345 |
+
try:
|
| 346 |
+
_m_unused, shared_clip = comfy.sd.load_lora_for_models(model, shared_clip, lora_file, 0.0, sc * pos_gain)
|
| 347 |
+
except Exception:
|
| 348 |
+
pass
|
| 349 |
+
tokens_pos = shared_clip.tokenize(pos_text_expanded)
|
| 350 |
+
cond_pos = shared_clip.encode_from_tokens_scheduled(tokens_pos)
|
| 351 |
+
tokens_neg = shared_clip.tokenize(neg_text_expanded)
|
| 352 |
+
cond_neg = shared_clip.encode_from_tokens_scheduled(tokens_neg)
|
| 353 |
+
else:
|
| 354 |
+
# CLIP Set Last Layer + Positive conditioning
|
| 355 |
+
clip_pos = src_clip.clone()
|
| 356 |
+
clip_pos.clip_layer(clip_set_last_layer_positive)
|
| 357 |
+
if bool(standard_pipeline):
|
| 358 |
+
for lora_file, sc, sm in lora_stack:
|
| 359 |
+
try:
|
| 360 |
+
_m_unused, clip_pos = comfy.sd.load_lora_for_models(model, clip_pos, lora_file, 0.0, sc * pos_gain)
|
| 361 |
+
except Exception:
|
| 362 |
+
pass
|
| 363 |
+
if positive_in is not None:
|
| 364 |
+
cond_pos = positive_in
|
| 365 |
+
else:
|
| 366 |
+
tokens_pos = clip_pos.tokenize(pos_text_expanded)
|
| 367 |
+
cond_pos = clip_pos.encode_from_tokens_scheduled(tokens_pos)
|
| 368 |
+
|
| 369 |
+
# CLIP Set Last Layer + Negative conditioning
|
| 370 |
+
clip_neg = src_clip.clone()
|
| 371 |
+
clip_neg.clip_layer(clip_set_last_layer_negative)
|
| 372 |
+
if bool(standard_pipeline):
|
| 373 |
+
for lora_file, sc, sm in lora_stack:
|
| 374 |
+
try:
|
| 375 |
+
_m_unused, clip_neg = comfy.sd.load_lora_for_models(model, clip_neg, lora_file, 0.0, sc * neg_gain)
|
| 376 |
+
except Exception:
|
| 377 |
+
pass
|
| 378 |
+
if negative_in is not None:
|
| 379 |
+
cond_neg = negative_in
|
| 380 |
+
else:
|
| 381 |
+
tokens_neg = clip_neg.tokenize(neg_text_expanded)
|
| 382 |
+
cond_neg = clip_neg.encode_from_tokens_scheduled(tokens_neg)
|
| 383 |
+
|
| 384 |
+
# Optional: show/save expanded prompts if dynamic used anywhere
|
| 385 |
+
dyn_used = bool(dynamic_pos) or bool(dynamic_neg)
|
| 386 |
+
if dyn_used and (bool(show_expanded_prompts) or bool(save_expanded_prompts)):
|
| 387 |
+
# Console print
|
| 388 |
+
if bool(show_expanded_prompts):
|
| 389 |
+
try:
|
| 390 |
+
print(f"[CombiNode] Expanded prompts (dyn_seed={int(dyn_seed)}):")
|
| 391 |
+
def _print_block(name, src, expanded):
|
| 392 |
+
print(name + ":")
|
| 393 |
+
if bool(dynamic_break_freeze) and ('|BREAK|' in src) and ((name=="Positive" and bool(dynamic_pos)) or (name=="Negative" and bool(dynamic_neg))):
|
| 394 |
+
print(" static")
|
| 395 |
+
print(" " + expanded)
|
| 396 |
+
_print_block("Positive", positive_prompt, pos_text_expanded)
|
| 397 |
+
_print_block("Negative", negative_prompt, neg_text_expanded)
|
| 398 |
+
except Exception:
|
| 399 |
+
pass
|
| 400 |
+
# File save
|
| 401 |
+
if bool(save_expanded_prompts):
|
| 402 |
+
try:
|
| 403 |
+
base = os.path.join(os.path.dirname(__file__), "dynPrompt")
|
| 404 |
+
os.makedirs(base, exist_ok=True)
|
| 405 |
+
now = datetime.now()
|
| 406 |
+
fname = f"{int(dyn_seed)}_{now.day:02d}_{now.month:02d}_{now.year}.txt"
|
| 407 |
+
path = os.path.join(base, fname)
|
| 408 |
+
lines = []
|
| 409 |
+
def _append_block(name, src, expanded):
|
| 410 |
+
lines.append(name + ":\n")
|
| 411 |
+
if bool(dynamic_break_freeze) and ('|BREAK|' in src) and ((name=="Positive" and bool(dynamic_pos)) or (name=="Negative" and bool(dynamic_neg))):
|
| 412 |
+
lines.append("static\n")
|
| 413 |
+
lines.append(expanded + "\n\n")
|
| 414 |
+
_append_block("Positive", positive_prompt, pos_text_expanded)
|
| 415 |
+
_append_block("Negative", negative_prompt, neg_text_expanded)
|
| 416 |
+
with open(path, 'w', encoding='utf-8') as f:
|
| 417 |
+
f.writelines(lines)
|
| 418 |
+
except Exception:
|
| 419 |
+
pass
|
| 420 |
+
|
| 421 |
+
# Save recipe if requested
|
| 422 |
+
if slot_idx and bool(recipe_save):
|
| 423 |
+
data = _recipes_load()
|
| 424 |
+
data[str(slot_idx)] = {
|
| 425 |
+
"use_checkpoint": bool(use_checkpoint),
|
| 426 |
+
"checkpoint": checkpoint,
|
| 427 |
+
"clip_pos": int(clip_set_last_layer_positive),
|
| 428 |
+
"clip_neg": int(clip_set_last_layer_negative),
|
| 429 |
+
"pos_text": str(positive_prompt),
|
| 430 |
+
"neg_text": str(negative_prompt),
|
| 431 |
+
"loras": [
|
| 432 |
+
[bool(use_lora_1), lora_1, float(strength_model_1), float(strength_clip_1)],
|
| 433 |
+
[bool(use_lora_2), lora_2, float(strength_model_2), float(strength_clip_2)],
|
| 434 |
+
[bool(use_lora_3), lora_3, float(strength_model_3), float(strength_clip_3)],
|
| 435 |
+
[bool(use_lora_4), lora_4, float(strength_model_4), float(strength_clip_4)],
|
| 436 |
+
[bool(use_lora_5), lora_5, float(strength_model_5), float(strength_clip_5)],
|
| 437 |
+
[bool(use_lora_6), lora_6, float(strength_model_6), float(strength_clip_6)],
|
| 438 |
+
],
|
| 439 |
+
}
|
| 440 |
+
_recipes_save(data)
|
| 441 |
+
print(f"[CombiNode] Saved recipe Slot {slot_idx}.")
|
| 442 |
+
|
| 443 |
+
# Return the CLIP instance consistent with encoding path
|
| 444 |
+
return (model, src_clip if bool(standard_pipeline) else clip, cond_pos, cond_neg, vae)
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
|
| 448 |
+
|