docs+tooling: VAE user guide + versioning ref + 3 skills + preflight CI (#48)
* docs+tooling: VAE user guide + versioning ref + 3 skills + preflight CI
Documentation
- New user-guide tree: docs/user_guide/nodes/radiance_koolook_v2_3_3/
with README, per-node pages for Easy_hdr_VAE_encode/decode, an
encode/decode pairing guide (4 worked examples + ASCII connectivity
diagram), and an HDR modes deep-dive (per-pixel comparison table).
Folder name mirrors the code path under forks/radiance_koolook/versions/.
- New cross-cutting reference: docs/reference/versioning.md explains the
three independent version axes (pack version, fork wrapper version,
upstream pinned commit), when each one bumps, and which skill/doc
owns it. Plus the v1/v2 node-ID suffix policy.
- Cross-links updated in docs/user_guide/README.md, docs/reference/
README.md, docs/maintainers/README.md.
Skills (all under .claude/skills/)
- docs-sync — controlled find-and-replace for version tags across docs,
code, and manifests; --scope and --dry-run flags.
- menu — lists all project skills with one-liners.
- preflight-release — companion doc for tools/preflight_release.py.
Pre-flight tooling
- tools/preflight_release.py — four checks, exit-code-driven for CI:
(1) static AST extraction of NODE_CLASS_MAPPINGS keys (no torch needed),
(2) stub-VAE dispatch test verifying all 5 rank/VAE-type branches,
(3) drift check against ComfyUI-Manager's extension-node-map.json
(catches the issue #44 phantom-nodes class of bug automatically),
(4) saved-workflow regression check across tests/workflows/*.json.
- .github/workflows/ci.yml — new "Pre-flight Release" job runs checks
1, 2, and 4 on every PR; check 3 (manager-meta) skipped pending the
upstream issue #44 fix.
- tests/workflows/ — 2 starter fixtures (Depth-Anything3, Hires-FIX-wanT2V)
+ README documenting the contribution convention. Coverage grows as
new fixtures are added; no code changes needed.
All four offline pre-flight checks pass on this branch. The
manager-meta check correctly identifies the 39 phantom IDs documented
in issue #44.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(preflight): satisfy ruff lint + bandit security gate
* fix(preflight): address blocking review findings
Three fixes from /review-pr feedback on this PR:
1. Sanitize tests/workflows/*.json — removed 4 unique absolute paths that
leaked internal job/client info ('baconx/Toy-Hix/9110',
'menneskehavn_3029', etc.). Replaced with placeholders like
'path/to/sequence_a' and 'path/to/source_video'. Both fixtures still
parse as valid JSON; grep for the old patterns returns zero hits.
2. Broaden the Koolook-ID heuristic regex from '^easy_[a-z]' to
'^easy_[A-Za-z]'. The old pattern excluded 'easy_ImageBatch' (capital I
after the underscore), which would have silently bypassed the
workflow-fixture stability check for that node. Verified the new
pattern correctly matches it.
3. Wrap check_dispatch() body in try/finally and track every sys.modules
key the function injects in a list, so cleanup runs on the success
path AND on any exception during exec_module / VAE-constructor
execution. Previously only the three torch* stubs were popped, never
the three _pf_pkg* entries — and even those weren't popped on
exception paths. Verified zero leaked modules after the function
returns.
The five dispatch test cases were factored into a helper
_check_dispatch_run_cases() so the try/finally wrapping stays focused on
module injection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(preflight): address remaining review findings
Follow-up to 7844037 — picks up all the HIGH / MEDIUM / LOW review
findings that were deferred. Pre-flight still passes locally.
HIGH
- Extract check_dispatch (~195 lines) to tools/_preflight_dispatch.py.
Main script drops from 680 → 508 lines. Module exposes a single
run(verbose, repo_root) -> (bool, list[str]) entry; main script keeps
the [2/4] header + PASS/FAIL line.
- Install _HttpsOnlyRedirectHandler that re-validates each redirect
hop's scheme. Closes the gap that urlopen would otherwise follow a
30x to http:// or file:// silently. The Bandit B310 suppression
comment now accurately describes the protection.
- check_static docstring now honestly states it only walks
NODE_CLASS_MAPPINGS literal dicts and does NOT apply
NAMESPACE_SUFFIX / SKIP_VERSION_SUFFIX logic. Notes that the workflow
check (4) is the safety net that surfaces drift the day a non-skipped
ID lands.
- check_workflows now FAILS when the fixtures dir exists with a README
but contains no *.json files — previously silently SKIPPED. If
someone deletes fixtures or typoes the dir, that's a regression in
test coverage and should fail loudly.
MEDIUM
- check_manager_meta validates the upstream entry shape before diffing.
Previous code silently produced an empty set if the schema changed,
reporting every one of our IDs as 'missing'. Now SKIPs with a clear
message when shape is unexpected.
- check_workflows accumulates total_koolook_refs inside the main loop;
drops the redundant second-pass that re-read every fixture file just
to compute the summary count.
- _extract_workflow_node_types uses explicit detection: a canvas-format
file with nodes: null no longer falls through to API-format and pulls
class_type from unrelated keys.
- check_static return type simplified from (bool, set, list) to
(bool, set). Third element was discarded at every call site.
- check_workflows now does two-pronged Koolook-ID matching: heuristic
(catches drift even if static missed an ID) plus actual_ids (catches
IDs that don't fit the heuristic, e.g. a future Koolook node without
Easy / easy_ / koolook in its name).
LOW
- ANSI escape codes named in a constants dict (_ANSI["green"] etc.)
instead of bare string literals.
- Removed dead 'cases: list[tuple[str, callable]] = []' initializer
(was shadowed three statements later).
- check_static's per-ID listing now gated behind --verbose, matching
check_dispatch and check_workflows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
b7b34de7e2
commit
cc6bc47866
@@ -0,0 +1,186 @@
|
||||
---
|
||||
name: docs-sync
|
||||
description: Find-and-replace a version tag (or any string token) across docs, code constants, and manifests in one controlled pass. Use when bumping a fork from one version to another (e.g. v2_3_3 -> v2_4_0), renaming a node ID across the repo, or generally propagating a string change everywhere it's referenced. Takes <old-tag> and <new-tag>; optional --scope flag (all / docs / code) and --dry-run. Always shows every match before applying and re-verifies post-edit.
|
||||
---
|
||||
|
||||
# docs-sync
|
||||
|
||||
A controlled find-and-replace across the repo for keeping docs, manifests,
|
||||
and code constants in sync when something tagged by a version string moves.
|
||||
|
||||
The skill does **not** rename folders or move files on its own — that would
|
||||
be too easy to break things with. It only changes the **content of files**
|
||||
that already exist. Folder renames (e.g. `versions/v2_3_3/` -> `versions/v2_4_0/`)
|
||||
are a deliberate manual `git mv` step that this skill expects you to run
|
||||
either before or after, and it'll re-grep to flag any stale references
|
||||
after the move.
|
||||
|
||||
## When to use it
|
||||
|
||||
- Bumping a fork's version tag everywhere it appears (most common use).
|
||||
- Renaming a node ID and propagating that to docs / changelog / manifests.
|
||||
- Any string-level rename where you'd otherwise grep manually and worry
|
||||
about missing one.
|
||||
|
||||
## When NOT to use it
|
||||
|
||||
- Refactoring code structure (use a code-aware tool like an IDE refactor).
|
||||
- Renaming things that have *non-textual* references (e.g. a JSON key that
|
||||
some tool computes by hash) — those need targeted edits, not a string
|
||||
sweep.
|
||||
|
||||
## Inputs
|
||||
|
||||
```
|
||||
docs-sync <old-tag> <new-tag> [--scope all|docs|code|manifests] [--dry-run]
|
||||
```
|
||||
|
||||
- `<old-tag>` — the string to find. Required. Can be a version like `v2_3_3`,
|
||||
a display version like `v2.3.3`, a node ID like `Easy_hdr_VAE_encode`, or
|
||||
a free-form tag.
|
||||
- `<new-tag>` — the replacement string. Required.
|
||||
- `--scope` — optional. Restrict where the find-and-replace applies:
|
||||
- `all` (default): docs, code, and manifests.
|
||||
- `docs`: only `docs/` and `README.md`.
|
||||
- `code`: only `*.py` source files.
|
||||
- `manifests`: only `pyproject.toml`, `forks/*.yaml`, `forks/*.md`.
|
||||
- `--dry-run` — show every match without applying changes.
|
||||
|
||||
## Common usage patterns
|
||||
|
||||
### Bumping a fork version
|
||||
|
||||
When you're bumping `radiance_koolook` from v2.3.3 to v2.4.0, run the
|
||||
skill **twice** to cover the two textual representations the codebase
|
||||
uses:
|
||||
|
||||
```
|
||||
docs-sync v2_3_3 v2_4_0 # underscored form (folder names, NAMESPACE_SUFFIX, docs paths)
|
||||
docs-sync v2.3.3 v2.4.0 # dotted form (DISPLAY_SUFFIX, prose in docs and CHANGELOG)
|
||||
```
|
||||
|
||||
You can also run with `--dry-run` first to see the scope of changes
|
||||
before committing. The skill will not rename the actual `versions/v2_3_3/`
|
||||
folder — do that manually with `git mv` after the string sweep is clean.
|
||||
|
||||
### Renaming a node ID after a v2 break
|
||||
|
||||
Before any tag change, every reference to the old ID gets updated:
|
||||
|
||||
```
|
||||
docs-sync Easy_hdr_VAE_encode Easy_hdr_VAE_encode_v2 --scope docs
|
||||
```
|
||||
|
||||
The `--scope docs` is important here — you almost certainly do *not*
|
||||
want to update the Python `NODE_CLASS_MAPPINGS` key, because that's the
|
||||
*old* ID that needs to remain registered for back-compat. Code edits
|
||||
for new ID registration belong in a focused PR, not a sweep.
|
||||
|
||||
## Procedure
|
||||
|
||||
1. **Validate inputs.**
|
||||
- Ensure `<old-tag>` and `<new-tag>` are non-empty and distinct.
|
||||
- Reject if `<old-tag>` looks dangerous as a substring (very short
|
||||
literal like a single letter or common word). Ask the user to
|
||||
confirm if `<old-tag>` is shorter than 4 characters.
|
||||
|
||||
2. **Search.** Use ripgrep (via the agent's Grep tool):
|
||||
- Scope `all`: search the whole worktree, excluding `__pycache__/`,
|
||||
`.git/`, `node_modules/`, and binary files (default ripgrep behavior).
|
||||
- Scope `docs`: restrict to `docs/**/*.md`, `README.md`, top-level `*.md`.
|
||||
- Scope `code`: restrict to `**/*.py`.
|
||||
- Scope `manifests`: restrict to `pyproject.toml`, `forks/forks_manifest.yaml`,
|
||||
`forks/THIRD_PARTY.md`, `forks/**/UPSTREAM_PIN.yaml`.
|
||||
|
||||
3. **Categorize matches.** Group hits by file:
|
||||
- Code (`.py` files)
|
||||
- Manifests (`.yaml`, `.toml`, `THIRD_PARTY.md`)
|
||||
- Docs (`docs/**/*.md`, `*.md`)
|
||||
- Other (anything not in the above)
|
||||
|
||||
4. **Present the list.** Show every match with file path + line number +
|
||||
snippet of the line. Group by category. Total count at the bottom.
|
||||
|
||||
5. **Confirm.** Print: "Apply replacement of `<old>` -> `<new>` to N
|
||||
occurrences across M files? (y/N — or use --dry-run to preview only)"
|
||||
Wait for user confirmation. Skip this step if `--dry-run`.
|
||||
|
||||
6. **Apply.** For each file with matches, do an in-place text replacement.
|
||||
Use the agent's Edit tool with `replace_all=true` if multiple hits in
|
||||
one file, otherwise per-occurrence with enough context to be unique.
|
||||
|
||||
7. **Verify.** Re-grep for `<old-tag>` in the same scope. Report any
|
||||
remaining occurrences (these are likely intentional — e.g. CHANGELOG
|
||||
entries describing the *previous* version, which should reference the
|
||||
old tag literally and not get rewritten). Highlight them as
|
||||
"intentional remainders to keep" so the user can confirm.
|
||||
|
||||
8. **Report.** Final summary:
|
||||
- Files modified: N
|
||||
- Replacements applied: M
|
||||
- Remaining occurrences (if any): list
|
||||
- Suggested next steps (folder renames if applicable, commit message,
|
||||
dev-sync if relevant).
|
||||
|
||||
## Safety guardrails
|
||||
|
||||
- **Never delete or move files.** The skill only edits content within
|
||||
existing files.
|
||||
- **Never auto-commit.** All changes stay in the working tree for the
|
||||
user to review with `git diff` before committing.
|
||||
- **Word-boundary intent.** When `<old-tag>` is short or could be a
|
||||
substring of unrelated tokens (e.g. `v2` could match `v2.3.3` and
|
||||
`v2_pipeline_test`), prompt the user to use a more specific tag or
|
||||
add anchoring characters (e.g. `v2_3_3` rather than `v2`).
|
||||
- **Skip changelog entries describing past releases.** When `--scope all`
|
||||
finds matches in `CHANGELOG.md` under sections that describe a
|
||||
*previous* release (e.g. `## [0.1.5]` or `## [0.2.0]`), default to
|
||||
*not* rewriting those — they're historical record. Surface them as
|
||||
"found but kept as historical" in the report.
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1 — bump radiance_koolook from v2.3.3 to v2.4.0
|
||||
|
||||
```
|
||||
docs-sync v2_3_3 v2_4_0
|
||||
```
|
||||
|
||||
Expected scope of changes:
|
||||
- `forks/radiance_koolook/__init__.py` (import path)
|
||||
- `forks/radiance_koolook/versions/v2_3_3/__init__.py` (NAMESPACE_SUFFIX
|
||||
constant) — but only after the folder is renamed via `git mv`
|
||||
- `forks/THIRD_PARTY.md` (section heading + body references)
|
||||
- `forks/forks_manifest.yaml` (notes field)
|
||||
- `docs/user_guide/nodes/radiance_koolook_v2_3_3/` (path references in
|
||||
page bodies — but folder rename via `git mv` happens manually)
|
||||
- `CHANGELOG.md` `[Unreleased]` section (new version note)
|
||||
|
||||
Then a separate run for the dotted form:
|
||||
|
||||
```
|
||||
docs-sync v2.3.3 v2.4.0
|
||||
```
|
||||
|
||||
Picks up:
|
||||
- `DISPLAY_SUFFIX = " (Koolook v2.3.3)"` in the moved `__init__.py`
|
||||
- Prose in docs that says "v2.3.3 baseline"
|
||||
- THIRD_PARTY.md prose
|
||||
- CLAUDE.md fork policy section
|
||||
|
||||
### Example 2 — preview a node-ID rename
|
||||
|
||||
```
|
||||
docs-sync Easy_hdr_VAE_encode Easy_hdr_VAE_encode_v2 --scope docs --dry-run
|
||||
```
|
||||
|
||||
Reports every doc page that mentions the old ID without making any
|
||||
changes. Useful before deciding whether to do the rename in code first.
|
||||
|
||||
## Related skills
|
||||
|
||||
- `bump-fork-version` *(planned, not yet built)* — orchestrates the full
|
||||
fork bump including folder rename, constant updates, and CHANGELOG
|
||||
entry. Will internally invoke `docs-sync` for the string-sweep step.
|
||||
- `add-external-fork` — for setting up a brand-new fork, not bumping an
|
||||
existing one.
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
name: menu
|
||||
description: List all project-level skills available under .claude/skills/ with their one-line descriptions, so the maintainer can browse what tools are available without remembering each one. Use when the user types /menu, "list skills", "what skills do we have", or "show me the skill index". This is the project-specific inventory; user-level skills under ~/.claude/skills/ are listed separately when relevant.
|
||||
---
|
||||
|
||||
# menu
|
||||
|
||||
Quick inventory of the project-level skills under `.claude/skills/`.
|
||||
Run when the user wants a one-look overview of what's available without
|
||||
having to remember each skill by name.
|
||||
|
||||
## Trigger phrases
|
||||
|
||||
- `/menu`
|
||||
- "list skills"
|
||||
- "what skills do we have"
|
||||
- "show me the skill index"
|
||||
- "skill menu"
|
||||
|
||||
## Procedure
|
||||
|
||||
1. **List skills under `.claude/skills/`.** Use the agent's Glob or Bash
|
||||
tool to enumerate `.claude/skills/*/SKILL.md`.
|
||||
|
||||
2. **Parse each SKILL.md frontmatter.** YAML frontmatter is delimited by
|
||||
`---` lines at the top of each file. Extract:
|
||||
- `name` — the skill's canonical name.
|
||||
- `description` — the one-liner used for trigger matching. Often
|
||||
long; truncate to the first sentence (text up to the first period
|
||||
or 200 characters, whichever comes first) for the menu display.
|
||||
|
||||
3. **Render as a table.** Markdown table with columns:
|
||||
- **Name** — the skill name (also functions as a link if rendered in
|
||||
a viewer that supports relative links).
|
||||
- **One-liner** — the truncated description.
|
||||
- **How to invoke** — the trigger phrase. Default form is `/<name>`
|
||||
(e.g. `/docs-sync`) or the first phrase from the description if
|
||||
the skill has more specific triggers.
|
||||
|
||||
4. **(Optional) List user-level skills.** If the user asks for "all
|
||||
skills" or the project list is short (< 3 skills), also enumerate
|
||||
`~/.claude/skills/*/SKILL.md` (or the equivalent on Windows) under
|
||||
a separate "User-level skills" heading. Skip this section by default
|
||||
to keep the output tight.
|
||||
|
||||
5. **End with a short reminder** of how to add new skills:
|
||||
*"To add a new project skill: create `.claude/skills/<name>/SKILL.md`
|
||||
with YAML frontmatter (`name:`, `description:`) and a markdown body
|
||||
describing the procedure. Re-run `/menu` to see it appear here."*
|
||||
|
||||
## Output format
|
||||
|
||||
Match this layout (substitute real values):
|
||||
|
||||
```
|
||||
## Project skills
|
||||
|
||||
| Name | One-liner | Invoke |
|
||||
|---|---|---|
|
||||
| docs-sync | Find-and-replace a version tag across docs, code, and manifests in one controlled pass. | `/docs-sync <old> <new>` |
|
||||
| add-external-fork | Set up an external upstream repository as a pinned reference checkout under ../ComfyUI-Forks. | `/add-external-fork` |
|
||||
| license-pre-check | Run a license-compatibility audit before incorporating any third-party code. | `/license-pre-check` |
|
||||
| menu | List all project-level skills with their one-line descriptions. | `/menu` |
|
||||
|
||||
To add a new project skill: create `.claude/skills/<name>/SKILL.md`
|
||||
with YAML frontmatter and a markdown body. Re-run `/menu` to see it.
|
||||
```
|
||||
|
||||
## Safety guardrails
|
||||
|
||||
- **Read-only.** This skill never edits files.
|
||||
- **No external calls.** Reads only the local filesystem.
|
||||
|
||||
## Related skills
|
||||
|
||||
- (none — this skill is purely an indexer)
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
name: preflight-release
|
||||
description: Run the project's release-blocking sanity checks before merging or shipping. Verifies (1) all *.py files parse and the registered node-ID list can be statically extracted, (2) the VAE wrapper's rank/VAE-type dispatch branches still work via a stub-VAE roundtrip, (3) ComfyUI-Manager's upstream extension-node-map.json matches our actual node IDs (catches phantom-nodes drift like issue #44), and (4) every Koolook node ID referenced in tests/workflows/*.json fixtures is still registered (catches accidental ID rename / removal that would break saved workflows). Use before cutting a release, before merging a PR that touches node IDs or fork structure, or whenever the user mentions "preflight" / "release sanity check" / "are we good to ship". Wraps tools/preflight_release.py.
|
||||
---
|
||||
|
||||
# preflight-release
|
||||
|
||||
Project-specific release-blocking sanity check. Runs four independent
|
||||
checks via `tools/preflight_release.py`; any failure means "do not ship
|
||||
yet, investigate first."
|
||||
|
||||
This skill is distinct from the user-level `preflight` skill (which is
|
||||
about repo infrastructure — branch protection, labels, git state).
|
||||
This one is about **code/metadata correctness**.
|
||||
|
||||
## When to use it
|
||||
|
||||
- **Before cutting a release.** Run it after the release-prep PR is
|
||||
open, before triggering the publish workflow.
|
||||
- **Before merging any PR that touches node IDs or fork structure.** A
|
||||
changed `NODE_CLASS_MAPPINGS` or a renamed fork directory is exactly
|
||||
the kind of edit that can silently break saved workflows.
|
||||
- **After running [`docs-sync`](../docs-sync/SKILL.md) on a fork-version
|
||||
bump.** The string-sweep can leave references stale if anything is
|
||||
missed; pre-flight catches it before merge.
|
||||
- **On demand** when the user asks "are we good to ship?" /
|
||||
"run preflight" / "release sanity check".
|
||||
|
||||
## How to invoke
|
||||
|
||||
Run the wrapped script directly:
|
||||
|
||||
```bash
|
||||
python tools/preflight_release.py # all four checks
|
||||
python tools/preflight_release.py -v # verbose per-file output
|
||||
python tools/preflight_release.py --check static --check workflows
|
||||
python tools/preflight_release.py --skip manager-meta # offline mode
|
||||
```
|
||||
|
||||
Exit code 0 = ship-clean. Exit code 1 = fix something. Exit code 2 =
|
||||
invalid CLI usage.
|
||||
|
||||
## What each check actually does
|
||||
|
||||
### 1. `static` — AST extraction of NODE_CLASS_MAPPINGS
|
||||
|
||||
- Walks every `*.py` in the repo (excluding `__pycache__`, `.venv`,
|
||||
`upscaler_FIX/`, `nuke_CAM_exporter/`).
|
||||
- AST-parses each one. **No imports** — works without torch / ComfyUI
|
||||
installed, which means CI doesn't need to install heavy deps.
|
||||
- Collects the literal string keys of every `NODE_CLASS_MAPPINGS = {...}`
|
||||
literal-dict assignment found across the repo.
|
||||
- Reports any parse errors with file:line.
|
||||
|
||||
**Limitation:** doesn't follow runtime-computed mappings (dict comprehensions,
|
||||
`_namespace_mappings()` calls, etc.). For the current Radiance Koolook
|
||||
v2_3_3 fork this happens to match reality because all exposed IDs are
|
||||
in `SKIP_VERSION_SUFFIX`, so the source-dict keys equal the registered
|
||||
keys. Future fork additions that rely on suffix mangling will need the
|
||||
extraction extended.
|
||||
|
||||
### 2. `dispatch` — VAE rank/VAE-type branches
|
||||
|
||||
- Mocks `torch` with a `FakeTensor` stub that mimics just enough of the
|
||||
tensor API to drive the wrapper's dispatch logic (no real math).
|
||||
- Loads `forks/radiance_koolook/versions/v2_3_3/{nodes_vae,color_helpers}.py`
|
||||
via `importlib`.
|
||||
- Runs five test cases through `Easy_hdr_VAE_encode/decode`:
|
||||
1. 4-D image input + 2-D image VAE → `path=image`
|
||||
2. 5-D video input + 2-D image VAE → `path=video-iter` (frame iteration)
|
||||
3. 5-D video input + 3-D-aware video VAE → `path=video-3d` (passthrough)
|
||||
4. 5-D latent + 2-D VAE decode → frame iteration, 4-D output
|
||||
5. 4-D latent decode → standard path
|
||||
- Asserts each case produces the expected shape + dispatch path string in
|
||||
`debug_info`.
|
||||
|
||||
Catches regressions to the rank-handling logic that the v0.2.0 release
|
||||
introduced.
|
||||
|
||||
### 3. `manager-meta` — extension-node-map.json drift
|
||||
|
||||
- Fetches `https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/extension-node-map.json`.
|
||||
- Looks up our entry (keyed on `https://github.com/malkuthro/ComfyUI-Koolook`).
|
||||
- Diffs the upstream node-ID list against the AST-extracted list from check 1.
|
||||
- Reports **phantom IDs** (in upstream but not in our code — Manager has
|
||||
stale data) and **missing IDs** (in our code but not in upstream — our
|
||||
code is ahead of the upstream metadata).
|
||||
|
||||
**Network-dependent.** Skipped automatically when offline (e.g. `--skip
|
||||
manager-meta`, or in CI where it's currently always skipped per
|
||||
`.github/workflows/ci.yml` because the drift is tracked separately as
|
||||
issue #44 and fails will be noisy until the upstream PR lands).
|
||||
|
||||
When the upstream issue is resolved, remove the `--skip manager-meta`
|
||||
from the CI job and let it become a release blocker.
|
||||
|
||||
### 4. `workflows` — fixture node-ID stability
|
||||
|
||||
- Walks `tests/workflows/*.json`.
|
||||
- For each fixture, parses the JSON and extracts every node `type` (handles
|
||||
both ComfyUI canvas format with a top-level `nodes` array, and API
|
||||
format where each top-level dict value has a `class_type` key).
|
||||
- Filters for IDs that look like Koolook IDs: matches `(?i)koolook`,
|
||||
`^Easy[A-Z_]`, or `^easy_[a-z]`.
|
||||
- Asserts every Koolook ID found in the fixtures is still in the
|
||||
AST-extracted set from check 1.
|
||||
|
||||
If a fixture references a node ID we no longer register, **the saved
|
||||
workflow would break for any user who has it**. The pre-flight fails
|
||||
loudly so the rename gets either reverted, aliased for back-compat, or
|
||||
treated as a `_v2`-suffix breaking change with a deprecation cycle.
|
||||
|
||||
Adding more fixtures over time grows coverage without code changes:
|
||||
drop a new `*.json` into `tests/workflows/`, the script picks it up
|
||||
automatically. See `tests/workflows/README.md` for the contribution
|
||||
convention.
|
||||
|
||||
## How failures usually get fixed
|
||||
|
||||
| Check | Common failure | Usual fix |
|
||||
|---|---|---|
|
||||
| static | Syntax error in a `.py` file | Fix the syntax. ruff lint should also catch this. |
|
||||
| static | "no NODE_CLASS_MAPPINGS literal dicts found" | Probably an accidental refactor that broke the registration; revert or restructure. |
|
||||
| dispatch | Assertion fails on one of the five branches | Look at the changed file in `forks/radiance_koolook/versions/v2_3_3/`. Trace the dispatch logic in `nodes_vae.py:Easy_hdr_VAE_{encode,decode}`. The branch that broke tells you which rank/VAE combination regressed. |
|
||||
| manager-meta (phantom IDs) | Upstream Manager metadata has IDs we don't register | File / update issue #44; not a code fix on our side. Skip the check. |
|
||||
| manager-meta (missing IDs) | Our code has IDs upstream Manager doesn't list yet | Wait for the next upstream "update DB" run, or open an upstream PR. Skip the check until it propagates. |
|
||||
| workflows | Fixture references a node ID no longer registered | (a) Add the old ID back as a `NODE_CLASS_MAPPINGS` alias pointing at the renamed class, OR (b) update the fixture if the rename was intentional + announced + the user was warned, OR (c) introduce a `_v2` suffix on the new node and start a deprecation cycle. |
|
||||
|
||||
## Companion docs
|
||||
|
||||
- `tools/preflight_release.py` — the actual implementation.
|
||||
- `tests/workflows/README.md` — how to add new fixtures.
|
||||
- `docs/maintainers/releasing.md` — the release procedure that this
|
||||
pre-flight feeds into.
|
||||
- `docs/reference/versioning.md` — the three version axes that
|
||||
determine when each check matters.
|
||||
- Issue #44 — the canonical example of the kind of drift `manager-meta`
|
||||
detects.
|
||||
@@ -44,3 +44,20 @@ jobs:
|
||||
|
||||
- name: Bandit scan
|
||||
run: bandit -r . -x ./.venv,./forks,./upscaler_FIX,./nuke_CAM_exporter -ll
|
||||
|
||||
preflight:
|
||||
name: Pre-flight Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Run pre-flight (static + dispatch + workflow fixtures)
|
||||
# manager-meta is skipped on PRs because the upstream node-map drift
|
||||
# is owned by an external repo (ltdrdata/ComfyUI-Manager) and is
|
||||
# tracked separately via issue #44. Re-enable by removing --skip
|
||||
# once that issue is resolved upstream.
|
||||
run: python tools/preflight_release.py --skip manager-meta -v
|
||||
|
||||
@@ -18,9 +18,14 @@ and should be kept in sync with these prose docs.
|
||||
| [`workflows-sidebar.md`](workflows-sidebar.md) | The "Workflows" section of the same sidebar. Save canvas / save selection / directories / archive on collision / load with named tab. Storage shape, action semantics, reset procedure. |
|
||||
| [`dev-iteration-loop.md`](dev-iteration-loop.md) | The fast iteration pattern we used to build the sidebar — the dev-sync loop, agent trigger phrases (`go`, `dev-sync`, paste-JSON), what survives across re-syncs vs what doesn't, push/publish gates. Read this when returning to UI work after a break. |
|
||||
|
||||
> **Read this first if you're confused about version numbers:**
|
||||
> [`../reference/versioning.md`](../reference/versioning.md) explains the three independent version axes in this codebase (pack version, fork wrapper version, upstream pinned commit) and which skill/doc owns each one. Worth a one-time read before starting any version-bump work.
|
||||
|
||||
## Companion agent skills
|
||||
|
||||
| Skill (in `.claude/skills/`) | Mirrors this doc |
|
||||
|---|---|
|
||||
| `license-pre-check` | Runs *before* any third-party fork; refuses to proceed on incompatible licenses. The principles also apply when a human is doing it manually. |
|
||||
| `add-external-fork` | The `forks/` workflow — clone outside MAIN, register in `forks_manifest.yaml`, namespace the IDs. Phase 0 calls `license-pre-check`. |
|
||||
| `docs-sync` | Find-and-replace a version tag (or any string token) across docs, code constants, and manifests in one controlled pass. Used during fork wrapper version bumps. |
|
||||
| `menu` | Lists all project skills with one-liners — quick inventory when you can't remember what's available. |
|
||||
|
||||
@@ -8,6 +8,7 @@ search for to answer a "what is X?" or "which nodes do Y?" question.
|
||||
| File | What it is |
|
||||
|---|---|
|
||||
| [`glossary.md`](glossary.md) | Plain-English definitions for the terms used in this repo's workflow (MAIN, fork, namespace suffix, sibling project, etc.) |
|
||||
| [`versioning.md`](versioning.md) | The three independent version axes in this codebase (pack version, fork wrapper version, upstream pinned commit) — what each one means, when each one bumps, and which skill/doc owns it |
|
||||
| [`comfyUI_nodes-inventory_by_NAME.png`](comfyUI_nodes-inventory_by_NAME.png) | Screenshot inventory of all currently-registered nodes by display name (helpful for matching what you see in ComfyUI back to the source files) |
|
||||
|
||||
## When to add a reference doc here
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# Versioning — three numbers, three meanings
|
||||
|
||||
This codebase carries **three independent version numbers** that often get
|
||||
confused. They mean different things, bump on different schedules, and live
|
||||
in different places.
|
||||
|
||||
## At a glance
|
||||
|
||||
| Axis | What it is | Where it lives | Example |
|
||||
|---|---|---|---|
|
||||
| **Pack version** | The Koolook release that ships to GitHub + Comfy Registry. Semver. | [`pyproject.toml`](../../pyproject.toml) `version = "X.Y.Z"`, [`CHANGELOG.md`](../../CHANGELOG.md) `## [X.Y.Z]` | `0.2.0` |
|
||||
| **Fork wrapper version** | A slot under `versions/` that holds Koolook's modified code for a particular upstream baseline. Underscored. | `forks/<fork>/versions/vX_Y_Z/` directory name + the `NAMESPACE_SUFFIX` constant inside it | `v2_3_3` |
|
||||
| **Upstream pinned commit** | The exact commit of the upstream third-party repo that the fork wrapper is matched to. SHA. | [`forks/<fork>/versions/vX_Y_Z/UPSTREAM_PIN.yaml`](../../forks/radiance_koolook/versions/v2_3_3/UPSTREAM_PIN.yaml), [`forks/forks_manifest.yaml`](../../forks/forks_manifest.yaml), [`forks/THIRD_PARTY.md`](../../forks/THIRD_PARTY.md) | `f262f47ddfda01ece154bf80c22769b1e4cef795` |
|
||||
|
||||
A current snapshot reads: *"Koolook **0.2.0** ships the **v2_3_3** Radiance
|
||||
fork wrapper, which is pinned to upstream Radiance commit **f262f47**."*
|
||||
All three are unrelated numbers that happen to all describe the same
|
||||
release.
|
||||
|
||||
## When each one bumps
|
||||
|
||||
### Pack version (`0.2.0` → `0.2.1` / `0.3.0` / `1.0.0`)
|
||||
|
||||
Bumps on **every release of the Koolook pack itself**. Drives the Comfy
|
||||
Registry publish workflow. Follow [`docs/maintainers/releasing.md`](../maintainers/releasing.md)
|
||||
for the canonical procedure.
|
||||
|
||||
- **Patch** (`0.2.0 → 0.2.1`): bug fixes, internal refactors, no surface change.
|
||||
- **Minor** (`0.2.0 → 0.3.0`): new nodes, new node options, anything that adds surface but doesn't break existing workflows.
|
||||
- **Major** (`0.2.0 → 1.0.0`): only after a v2-style breaking change has shipped (a removed-after-deprecation node ID, an INPUT_TYPES change a saved workflow can't tolerate, etc.).
|
||||
|
||||
### Fork wrapper version (`v2_3_3` → `v2_4_0` / `v2_3_4`)
|
||||
|
||||
Bumps when **upstream releases a new version we want to track** AND the
|
||||
new upstream version requires meaningfully different wrapper code.
|
||||
|
||||
Two patterns, decided per-fork:
|
||||
|
||||
- **In-place upgrade** (used by Radiance Koolook today). All exposed node
|
||||
IDs are listed in `SKIP_VERSION_SUFFIX` so the user-facing IDs are
|
||||
stable across versions (e.g. `Easy_hdr_VAE_encode` stays as itself
|
||||
whether the wrapper is `v2_3_3` or `v2_4_0`). When upstream bumps, the
|
||||
whole `versions/v2_3_3/` directory gets renamed to `versions/v2_4_0/`
|
||||
in-place; there's no parallel coexistence because both versions would
|
||||
collide on the same ID. Saved workflows continue loading because the
|
||||
ID didn't change.
|
||||
- **Versioned coexistence**. Node IDs carry the `__koolook_vX_Y_Z`
|
||||
suffix (the default behavior unless explicitly added to
|
||||
`SKIP_VERSION_SUFFIX`). When upstream bumps, the new `versions/v2_4_0/`
|
||||
is added alongside the old one. Both are registered simultaneously
|
||||
for one release cycle. Old workflows keep loading via the suffixed
|
||||
old ID; new workflows can adopt the new suffixed ID. The old version
|
||||
is removed in the next release.
|
||||
|
||||
### Upstream pinned commit (`f262f47` → some new SHA)
|
||||
|
||||
Bumps when **we re-sync the wrapper against a newer upstream commit**.
|
||||
Recorded in:
|
||||
|
||||
- `forks/<fork>/versions/<vX_Y_Z>/UPSTREAM_PIN.yaml` (the per-version pin)
|
||||
- `forks/forks_manifest.yaml` (the project-level fork inventory)
|
||||
- `forks/THIRD_PARTY.md` (the human-readable attribution + change notes)
|
||||
|
||||
A new pinned commit may or may not require a new fork wrapper version —
|
||||
small upstream patches can land at the same wrapper slot if our wrapper
|
||||
code doesn't have to change. Bigger upstream releases usually warrant a
|
||||
new wrapper version too.
|
||||
|
||||
## Public node-ID versioning (the `_v2` rule)
|
||||
|
||||
Independent of the three axes above, individual node IDs can carry a
|
||||
public version suffix when a breaking change forces it:
|
||||
|
||||
- Original ID (no suffix): `Easy_hdr_VAE_encode`. Stays this forever.
|
||||
- Bumped ID after a breaking change: `Easy_hdr_VAE_encode_v2`. Only
|
||||
introduced when the original would otherwise break saved workflows.
|
||||
- Both IDs register simultaneously for **one full release cycle** (the
|
||||
old one displays as `Easy HDR VAE Encode (Koolook, v1 — please migrate)`
|
||||
during this window).
|
||||
- Old ID is dropped one release later. Pack version goes major (`1.0.0`)
|
||||
to signal the breakage.
|
||||
|
||||
This is the rare path. Most version changes don't need it because either
|
||||
the change isn't actually breaking, or the fork wrapper version mechanism
|
||||
above already handles the back-compat.
|
||||
|
||||
## Which skill / doc owns which axis
|
||||
|
||||
| Axis | Skill | Doc |
|
||||
|---|---|---|
|
||||
| Pack version | (none, manual via release PR) | [`docs/maintainers/releasing.md`](../maintainers/releasing.md) |
|
||||
| Fork wrapper version | [`docs-sync`](../../.claude/skills/docs-sync/SKILL.md) (string-sweep), [`add-external-fork`](../../.claude/skills/add-external-fork/SKILL.md) (initial setup) | [`forks/README.md`](../../forks/README.md), [`docs/maintainers/node-versioning.md`](../maintainers/node-versioning.md) |
|
||||
| Upstream pinned commit | (none — manual edit to `UPSTREAM_PIN.yaml` + `forks_manifest.yaml`) | [`forks/THIRD_PARTY.md`](../../forks/THIRD_PARTY.md) |
|
||||
| Node-ID `_v2` suffix | (none — manual code change with deprecation alias) | (this document) |
|
||||
|
||||
## Example — a hypothetical Radiance v2.4.0 release
|
||||
|
||||
Suppose upstream Radiance ships v2.4.0. We re-sync and the wrapper code
|
||||
changes only slightly — same node IDs, same interface. The bumps:
|
||||
|
||||
- **Upstream pinned commit:** `f262f47…` → whatever the v2.4.0 release commit is. Update `UPSTREAM_PIN.yaml`, `forks_manifest.yaml`, `THIRD_PARTY.md`.
|
||||
- **Fork wrapper version:** `v2_3_3` → `v2_4_0`. Rename `versions/v2_3_3/` to `versions/v2_4_0/`. Update `NAMESPACE_SUFFIX` and `DISPLAY_SUFFIX` constants inside. Update parent `__init__.py` import. Rename matching docs folder `docs/user_guide/nodes/radiance_koolook_v2_3_3/` → `radiance_koolook_v2_4_0/`. Update inline references via [`docs-sync v2_3_3 v2_4_0`](../../.claude/skills/docs-sync/SKILL.md).
|
||||
- **Pack version:** `0.2.0` → `0.3.0`. Cut a release PR per [`releasing.md`](../maintainers/releasing.md).
|
||||
- **Node IDs:** unchanged (stable IDs). No `_v2` suffix introduced. Saved workflows keep loading.
|
||||
|
||||
If the v2.4.0 wrapper required a *behaviorally* breaking change to one of
|
||||
the node IDs — say `Easy_hdr_VAE_encode` now returns a different shape
|
||||
that downstream samplers can't handle — only then would we add
|
||||
`Easy_hdr_VAE_encode_v2` and start the deprecation cycle.
|
||||
|
||||
## See also
|
||||
|
||||
- [`docs/maintainers/releasing.md`](../maintainers/releasing.md) — pack version release procedure.
|
||||
- [`docs/maintainers/node-versioning.md`](../maintainers/node-versioning.md) — the rules for safely changing `INPUT_TYPES` / `RETURN_TYPES` / class names without breaking saved workflows.
|
||||
- [`forks/THIRD_PARTY.md`](../../forks/THIRD_PARTY.md) — upstream attribution and change notes for each fork.
|
||||
- [`forks/forks_manifest.yaml`](../../forks/forks_manifest.yaml) — machine-readable fork inventory.
|
||||
- [`.claude/skills/docs-sync/SKILL.md`](../../.claude/skills/docs-sync/SKILL.md) — the find-and-replace skill for fork-version bumps.
|
||||
+44
-16
@@ -1,26 +1,54 @@
|
||||
# User guide
|
||||
# User Guide
|
||||
|
||||
End-user documentation for the nodes shipped by `ComfyUI-Koolook`. Search for a node by display name in ComfyUI (type "koolook" in the node-add menu) and see the corresponding guide here.
|
||||
End-user documentation for nodes shipped by `ComfyUI-Koolook`. Pages
|
||||
mirror the ComfyUI category each node lives under, so the structure here
|
||||
matches what you see when you type "koolook" in the node-add menu.
|
||||
|
||||
## Available guides
|
||||
Each per-node page includes a **Source** block at the top that records
|
||||
whether the node is Koolook-native or imported from a fork — so fork
|
||||
attribution is visible without needing a fork-shaped directory tree.
|
||||
|
||||
*(Stub — guides are added as they're written. The starter image in [`img/Easy_Image_Batch_v0.1.5.png`](img/Easy_Image_Batch_v0.1.5.png) is the first one queued.)*
|
||||
## Browse by category
|
||||
|
||||
| ComfyUI menu | Status | Nodes |
|
||||
|---|---|---|
|
||||
| `Koolook/Pipeline` | 🚧 page TBD | `EasyAIPipeline` |
|
||||
| `Koolook/Image` | 🚧 page TBD | `EasyResize_Koolook`, `easy_ImageBatch` |
|
||||
| `Koolook/Camera` | 🚧 page TBD | `KoolookLoadCameraPosesAbsolute` |
|
||||
| `Koolook/Wan_Video` | 🚧 page TBD | `EasyWan22Prompt` |
|
||||
| `Koolook/VAE` | ✅ [`nodes/radiance_koolook_v2_3_3/`](nodes/radiance_koolook_v2_3_3/) | `Easy_hdr_VAE_encode`, `Easy_hdr_VAE_decode` |
|
||||
|
||||
## Cross-cutting concepts
|
||||
|
||||
Pages here cover ideas that span more than one node:
|
||||
|
||||
- [VAE encode/decode pairing](nodes/radiance_koolook_v2_3_3/encode_decode_pairing.md) — which
|
||||
encoder fields must match the decoder for a clean roundtrip, with
|
||||
worked examples for SDR / HDR / cinema-log / video workflows.
|
||||
- [HDR modes deep dive](nodes/radiance_koolook_v2_3_3/hdr_modes.md) — what `Clip (SDR)`,
|
||||
`Soft Clip`, `Compress (Log)`, and `Passthrough` actually do to your
|
||||
pixels before they hit the VAE, with per-pixel worked examples.
|
||||
|
||||
## When to add a guide
|
||||
|
||||
If a node has any of:
|
||||
- non-obvious input semantics (e.g. aspect-ratio strings, frame indexing conventions),
|
||||
- a workflow recipe more useful than the per-input tooltips,
|
||||
- a known limitation or compatibility caveat,
|
||||
A node deserves a page if it has any of:
|
||||
|
||||
→ create a per-node markdown file here, name it after the canonical node ID
|
||||
(e.g. `easy_image_batch.md`, `easy_resize_koolook.md`), and add it to the
|
||||
"Available guides" table above.
|
||||
- Non-obvious input semantics (e.g. aspect-ratio strings, frame indexing
|
||||
conventions).
|
||||
- A workflow recipe more useful than the per-input tooltips.
|
||||
- A known limitation or compatibility caveat.
|
||||
- An encode/decode or input/output pairing relationship with other nodes.
|
||||
|
||||
Screenshots and helper images go in [`img/`](img/), named to match the file
|
||||
they're embedded in (e.g. `Easy_Image_Batch_v0.1.5.png`).
|
||||
Convention:
|
||||
|
||||
## Where to find the node IDs
|
||||
- One markdown file per canonical node ID, named after the ID
|
||||
(`easy_hdr_vae_encode.md`, `easy_image_batch.md`).
|
||||
- Place it in the folder that matches the node's ComfyUI category.
|
||||
- Screenshots and helper images go in [`img/`](img/), named to match the
|
||||
page they're embedded in (e.g. `easy_hdr_vae_encode_pairing.png`).
|
||||
|
||||
The canonical list is in the project [`README.md`](../../README.md) and
|
||||
[`reference/`](../reference/).
|
||||
## Where to find the canonical node ID list
|
||||
|
||||
The repo [`README.md`](../../README.md) lists all currently-registered
|
||||
node IDs and display names. The historical inventory (including removed
|
||||
nodes) is in the [`CHANGELOG.md`](../../CHANGELOG.md).
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# Koolook/VAE — User Guide
|
||||
|
||||
Cinema-grade VAE encode/decode with HDR / wide-gamut color awareness.
|
||||
Designed to work with image VAEs (SD1.5, SDXL, Flux) **and** video VAEs
|
||||
(Wan 2.2, Hunyuan, CogVideoX, LTX) using a single rank-aware code path.
|
||||
|
||||
## Pages in this folder
|
||||
|
||||
| Page | Use it to … |
|
||||
|---|---|
|
||||
| [`easy_hdr_vae_encode.md`](easy_hdr_vae_encode.md) | Look up what each input on the encoder does, and how its outputs are wired |
|
||||
| [`easy_hdr_vae_decode.md`](easy_hdr_vae_decode.md) | Same, for the decoder |
|
||||
| [`encode_decode_pairing.md`](encode_decode_pairing.md) | Get the connectivity right between encoder and decoder for a clean roundtrip — which fields must match, which are independent, with worked examples for SDR / HDR / cinema-log / Wan 2.2 video |
|
||||
| [`hdr_modes.md`](hdr_modes.md) | Understand the four HDR modes (`Clip (SDR)`, `Soft Clip`, `Compress (Log)`, `Passthrough`) — what each one does to pixels before hitting the VAE, with per-pixel worked examples |
|
||||
|
||||
## Quick mental model
|
||||
|
||||
```
|
||||
pixels (any of 12 color spaces, any rank)
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────┐
|
||||
│ Easy_hdr_VAE_encode │ source_space → linear → exposure
|
||||
│ │ → hdr_mode-dependent gamma/log encoding
|
||||
│ │ → vae.encode (with sample/mean/mode)
|
||||
└──────────────────────────┘
|
||||
│ samples (LATENT)
|
||||
│ debug_info (STRING — per-run diagnostic)
|
||||
│ alpha (IMAGE — for round-tripping alpha around the VAE)
|
||||
│
|
||||
▼
|
||||
KSampler (or whatever) — optional
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────┐
|
||||
│ Easy_hdr_VAE_decode │ vae.decode → 5-D-to-4-D normalization
|
||||
│ │ → hdr_mode-dependent recovery (log
|
||||
│ │ decompress + soft-shoulder if Log mode)
|
||||
│ │ → exposure_adjust (linear)
|
||||
│ │ → linear → target_space
|
||||
└──────────────────────────┘
|
||||
│ image (IMAGE)
|
||||
│ debug_info (STRING)
|
||||
```
|
||||
|
||||
## Source
|
||||
|
||||
| Aspect | Value |
|
||||
|---|---|
|
||||
| Origin | Koolook fork of [fxtdstudios/radiance](https://github.com/fxtdstudios/radiance) v2.3.3 (commit `f262f47`) |
|
||||
| Slim port | Drops upstream's 4K cosine-blend tile engine that was incompatible with video VAEs |
|
||||
| Local path | [`forks/radiance_koolook/versions/v2_3_3/`](../../../../forks/radiance_koolook/versions/v2_3_3/) |
|
||||
| Attribution + change notes | [`forks/THIRD_PARTY.md`](../../../../forks/THIRD_PARTY.md) |
|
||||
| License | GPL-3.0 (inherited from upstream Radiance) |
|
||||
@@ -0,0 +1,107 @@
|
||||
# Easy HDR VAE Decode (Koolook)
|
||||
|
||||
| Aspect | Value |
|
||||
|---|---|
|
||||
| Display name | `Easy HDR VAE Decode (Koolook)` |
|
||||
| Node ID | `Easy_hdr_VAE_decode` |
|
||||
| ComfyUI category | `Koolook/VAE` |
|
||||
| Source | Koolook fork of fxtdstudios/radiance v2.3.3 (slim port — drops upstream's 4K cosine-blend tile engine) |
|
||||
| Source file | [`forks/radiance_koolook/versions/v2_3_3/nodes_vae.py`](../../../../forks/radiance_koolook/versions/v2_3_3/nodes_vae.py) |
|
||||
| Pairs with | [`Easy_hdr_VAE_encode`](easy_hdr_vae_encode.md) |
|
||||
| Cross-cutting | [HDR modes](hdr_modes.md), [Encode/decode pairing](encode_decode_pairing.md) |
|
||||
|
||||
## What it does
|
||||
|
||||
Decodes a `LATENT` back into pixel space, with cinema-grade colour-space
|
||||
output and HDR awareness. Mirrors `Easy_hdr_VAE_encode` and accepts both
|
||||
4-D image latents and 5-D video latents from 3-D-aware VAEs (Wan 2.2,
|
||||
Hunyuan, CogVideoX, LTX).
|
||||
|
||||
For 3-D-aware VAEs that return a 5-D `(B, F, H, W, C)` tensor, the
|
||||
decoder unconditionally reshapes it to ComfyUI's standard 4-D
|
||||
`(B*F, H, W, C)` IMAGE batch — so `SaveImage` and other downstream
|
||||
IMAGE-typed nodes see one IMAGE per frame and write `_00001.png`,
|
||||
`_00002.png`, … as expected.
|
||||
|
||||
## Inputs
|
||||
|
||||
### Required
|
||||
|
||||
| Input | Type | Description |
|
||||
|---|---|---|
|
||||
| `samples` | `LATENT` | The latent to decode. Same VAE that produced this latent must be wired to `vae`. |
|
||||
| `vae` | `VAE` | The VAE to decode through. **Must match the encoder's VAE.** |
|
||||
| `target_space` | enum (12 options) | The colour space the output IMAGE should be in. Same set of 12 values as the encoder's `source_space`. |
|
||||
|
||||
### Optional
|
||||
|
||||
| Input | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `exposure_adjust` | `FLOAT` | `0.0` | Post-decode exposure adjustment in stops, applied **in linear space**. Range: `-10.0` to `+10.0`. |
|
||||
| `hdr_mode` | enum (4 options) | `Clip (SDR)` | How to interpret the VAE's output before linearising. **Must match the encoder's `hdr_mode`** for clean roundtrips. `Compress (Log)` triggers the recovery pipeline (soft-shoulder + log-domain highlight denoise + log-curve decompression). Other modes treat the VAE output as sRGB-gamma data. See [HDR modes deep dive](hdr_modes.md). |
|
||||
| `source_space` | enum (12 options) | `Linear` | **Only relevant when `hdr_mode = Compress (Log)`.** Tells the decoder which log curve was used at encode time, so it knows how to reverse the encoding. Set this to whatever you set `source_space` to on the encoder. **Ignored for all other `hdr_mode` values.** |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Type | Description |
|
||||
|---|---|---|
|
||||
| `image` | `IMAGE` | Decoded image in the requested `target_space`. Always 4-D `(B, H, W, C)` — for 5-D video latents, the temporal dimension is collapsed into the batch dimension. |
|
||||
| `debug_info` | `STRING` | Per-run diagnostic — output shape, dispatch path (`image` / `video-iter` / `video-3d`), and parameter values. |
|
||||
|
||||
## Valid values for `target_space` and `source_space`
|
||||
|
||||
Same 12 colour spaces as the encoder's `source_space`. See
|
||||
[`easy_hdr_vae_encode.md`](easy_hdr_vae_encode.md#valid-values-for-source_space)
|
||||
for the full list.
|
||||
|
||||
## Internal dispatch (what the wrapper actually does)
|
||||
|
||||
Pseudocode, ignoring error handling:
|
||||
|
||||
```
|
||||
latent = samples["samples"]
|
||||
is_video = latent.ndim == 5
|
||||
is_3d_vae = hasattr(vae, "latent_dim") and vae.latent_dim == 3
|
||||
|
||||
# Sequence-aware decode dispatch
|
||||
if is_video and not is_3d_vae:
|
||||
img = cat([vae.decode(latent[:, :, fi, ...]) for fi in range(F)], dim=0)
|
||||
else:
|
||||
img = vae.decode(latent)
|
||||
|
||||
# Normalize 5-D output to 4-D (B*F, H, W, C) for ComfyUI IMAGE compatibility
|
||||
if img.ndim == 5:
|
||||
img = img.reshape(-1, H, W, C)
|
||||
|
||||
if target_space == "Raw":
|
||||
img *= 2 ** exposure_adjust
|
||||
else:
|
||||
if hdr_mode == "Compress (Log)":
|
||||
# Recovery pipeline for log-coded VAE output
|
||||
log_space = source_space if is_log_space(source_space) else "ARRI LogC4"
|
||||
knee, ceiling, threshold, strength = LOG_PROFILE_HDR_PARAMS[log_space]
|
||||
img = soft_log_shoulder(img, knee=knee, ceiling=ceiling)
|
||||
img = denoise_log_highlights(img, threshold=threshold, strength=strength)
|
||||
img = to_linear(img, log_space) # log → linear Rec.709
|
||||
else:
|
||||
img = srgb_to_linear(img) # sRGB-gamma → linear Rec.709
|
||||
|
||||
img *= 2 ** exposure_adjust # exposure in linear
|
||||
img = from_linear(img, target_space) # linear → target colour space
|
||||
|
||||
return (img, debug_info)
|
||||
```
|
||||
|
||||
The full implementation lives at [`nodes_vae.py:Easy_hdr_VAE_decode.decode`](../../../../forks/radiance_koolook/versions/v2_3_3/nodes_vae.py).
|
||||
|
||||
## Common mistakes
|
||||
|
||||
- **Mismatched `hdr_mode` between encoder and decoder.** Most common is the encoder using `Compress (Log)` while the decoder uses `Clip (SDR)` (the default), or vice versa. Result: very wrong colour. The encoder's `debug_info` will say `hdr_mode=Compress (Log)`; the decoder's will say `hdr_mode=Clip (SDR)`. They must agree.
|
||||
- **Forgetting to set `source_space` on the decoder when using `Compress (Log)`.** The decoder defaults `source_space = Linear`, which means "if Compress (Log) is on, use the default ARRI LogC4 log curve" — fine if the encoder also fell back to LogC4, *not fine* if you encoded with a different log curve. Set the decoder's `source_space` equal to the encoder's `source_space`.
|
||||
- **Expecting a 5-D output from a 3-D-aware video VAE.** The decoder always returns a 4-D `(B*F, H, W, C)` IMAGE — that's ComfyUI convention for video sequences. If you need temporal grouping, that's downstream of this node.
|
||||
|
||||
## See also
|
||||
|
||||
- [HDR modes deep dive](hdr_modes.md)
|
||||
- [Encode/decode pairing](encode_decode_pairing.md)
|
||||
- [`Easy_hdr_VAE_encode`](easy_hdr_vae_encode.md)
|
||||
@@ -0,0 +1,108 @@
|
||||
# Easy HDR VAE Encode (Koolook)
|
||||
|
||||
| Aspect | Value |
|
||||
|---|---|
|
||||
| Display name | `Easy HDR VAE Encode (Koolook)` |
|
||||
| Node ID | `Easy_hdr_VAE_encode` |
|
||||
| ComfyUI category | `Koolook/VAE` |
|
||||
| Source | Koolook fork of fxtdstudios/radiance v2.3.3 (slim port — drops upstream's 4K cosine-blend tile engine) |
|
||||
| Source file | [`forks/radiance_koolook/versions/v2_3_3/nodes_vae.py`](../../../../forks/radiance_koolook/versions/v2_3_3/nodes_vae.py) |
|
||||
| Pairs with | [`Easy_hdr_VAE_decode`](easy_hdr_vae_decode.md) |
|
||||
| Cross-cutting | [HDR modes](hdr_modes.md), [Encode/decode pairing](encode_decode_pairing.md) |
|
||||
|
||||
## What it does
|
||||
|
||||
Encodes a 4-D `(B, H, W, C)` image batch *or* a 5-D `(B, F, H, W, C)`
|
||||
video tensor into a VAE latent, with cinema-grade colour-space handling
|
||||
and HDR awareness. Works with image VAEs (SD 1.5 / SDXL / Flux) **and**
|
||||
3-D-aware video VAEs (Wan 2.2 / Hunyuan / CogVideoX / LTX) using a
|
||||
single rank-aware code path.
|
||||
|
||||
## Inputs
|
||||
|
||||
### Required
|
||||
|
||||
| Input | Type | Description |
|
||||
|---|---|---|
|
||||
| `pixels` | `IMAGE` | The image batch or video tensor to encode. Accepts 3-channel (RGB) or 4-channel (RGBA). Rank can be 4-D or 5-D. |
|
||||
| `vae` | `VAE` | The VAE to encode through. Same VAE must be wired to the matching decoder. |
|
||||
| `source_space` | enum (12 options) | What colour space the input pixels are in. Drives a linearisation step before exposure / HDR processing. See [valid values](#valid-values-for-source_space) below. |
|
||||
|
||||
### Optional
|
||||
|
||||
| Input | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `exposure` | `FLOAT` | `0.0` | Exposure adjustment in stops, applied **in linear space** (correct semantics regardless of `source_space`). Range: `-10.0` to `+10.0`. |
|
||||
| `alpha_handling` | `Preserve` / `Ignore` | `Preserve` | If `Preserve` and the input has 4 channels, the alpha channel is surfaced as the third output (a separate `IMAGE`). If `Ignore` (or input has only 3 channels), the alpha output is a zeros tensor of compatible shape. The VAE itself only ever encodes the first 3 channels regardless. |
|
||||
| `hdr_mode` | enum (4 options) | `Soft Clip` | What to do with linearised values just before they hit `vae.encode()`. **Must match the decoder's `hdr_mode`** for clean roundtrips. See [HDR modes deep dive](hdr_modes.md). |
|
||||
| `latent_sampling` | `sample` / `mean` / `mode` | `sample` | How to sample from the VAE posterior distribution. `sample` is ComfyUI's default (random per-run). `mean` and `mode` are deterministic — pick the posterior mean for img2img-style minimum-noise reconstruction. Falls back to `sample` if the underlying VAE class doesn't expose the raw `first_stage_model` for direct mean access. |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Type | Description |
|
||||
|---|---|---|
|
||||
| `samples` | `LATENT` | Standard ComfyUI LATENT dict `{"samples": tensor}`. For 5-D video paths through a 3-D-aware VAE, the inner tensor is also 5-D `(B, C, F, latH, latW)`. |
|
||||
| `debug_info` | `STRING` | Per-run diagnostic — input shape, dispatch path (`image` / `video-iter` / `video-3d`), and parameter values. Wire to a Show Text / Display Any node to verify which dispatch branch fired. |
|
||||
| `alpha` | `IMAGE` | Alpha channel surfaced separately so it can be routed *around* the VAE (which can't encode alpha) and re-composited with the decoded image downstream. Zeros tensor of compatible shape if the input had no alpha or `alpha_handling = Ignore`. |
|
||||
|
||||
## Valid values for `source_space`
|
||||
|
||||
The full set of 12 colour spaces:
|
||||
|
||||
| Group | Values |
|
||||
|---|---|
|
||||
| Common | `Linear`, `sRGB`, `Raw` |
|
||||
| ACES family | `ACEScg`, `ACES 2065-1` |
|
||||
| Other linear gamuts | `Rec.2020 Linear` |
|
||||
| Cinema log curves | `ARRI LogC3`, `ARRI LogC4`, `Sony S-Log3`, `Panasonic V-Log`, `DaVinci Intermediate`, `RED Log3G10` |
|
||||
|
||||
`Raw` bypasses all colour processing — the input pixels go straight to the VAE unchanged (after exposure, which still applies as a raw multiplication). All other values trigger the appropriate to-linear-Rec.709 conversion before exposure and HDR processing.
|
||||
|
||||
## Internal dispatch (what the wrapper actually does)
|
||||
|
||||
Pseudocode, ignoring error handling:
|
||||
|
||||
```
|
||||
if source_space == "Raw":
|
||||
img *= 2 ** exposure # raw-domain multiplication
|
||||
img_for_vae = img # no colour or HDR processing
|
||||
else:
|
||||
img = to_linear(img, source_space) # source → linear Rec.709
|
||||
img *= 2 ** exposure # exposure in linear
|
||||
if hdr_mode == "Compress (Log)":
|
||||
log_space = source_space if is_log_space(source_space) else "ARRI LogC4"
|
||||
img_for_vae = from_linear(img, log_space)
|
||||
else:
|
||||
img_gamma = linear_to_srgb(img)
|
||||
if hdr_mode == "Clip (SDR)": img_for_vae = clamp(img_gamma, 0, 1)
|
||||
elif hdr_mode == "Soft Clip": img_for_vae = soft_clip(img_gamma, knee=0.85)
|
||||
elif hdr_mode == "Passthrough": img_for_vae = img_gamma # no clamp
|
||||
|
||||
# Sequence-aware encode dispatch
|
||||
is_video = img_for_vae.ndim == 5
|
||||
is_3d_vae = hasattr(vae, "latent_dim") and vae.latent_dim == 3
|
||||
|
||||
if is_video and not is_3d_vae:
|
||||
# Iterate frames, stack on temporal dim
|
||||
latent = stack([encode_with_sampling_mode(vae, img_for_vae[:, fi, ...], latent_sampling)
|
||||
for fi in range(F)], dim=2)
|
||||
else:
|
||||
latent = encode_with_sampling_mode(vae, img_for_vae, latent_sampling)
|
||||
|
||||
return ({"samples": latent}, debug_info, alpha_out)
|
||||
```
|
||||
|
||||
The full implementation lives at [`nodes_vae.py:Easy_hdr_VAE_encode.encode`](../../../../forks/radiance_koolook/versions/v2_3_3/nodes_vae.py).
|
||||
|
||||
## Common mistakes
|
||||
|
||||
- **Setting `source_space = Linear` on a typical ComfyUI sRGB-gamma input.** Most ComfyUI image-loading nodes produce sRGB-gamma data. If you set `source_space = Linear`, the wrapper will linearise data that's already linear (apply sRGB EOTF on already-EOTF'd data). Set to `sRGB` for typical ComfyUI inputs.
|
||||
- **Using `Passthrough` as a "do nothing" mode for HDR roundtrip.** It actually does the worst job for HDR fidelity — the VAE was trained on `[0, 1]` data and produces noise on out-of-domain inputs. Use `Compress (Log)` for HDR or `Clip (SDR)` for SDR; reserve `Passthrough` for diagnostics.
|
||||
- **Forgetting to match `hdr_mode` on the decoder.** The decoder's `hdr_mode` determines how it interprets the VAE's output; mismatched modes give wrong colour by construction. See [encode/decode pairing](encode_decode_pairing.md).
|
||||
- **Wiring the `alpha` output back into a 4-channel-expecting node downstream.** Most ComfyUI IMAGE-typed nodes assume 3-channel; the `alpha` output is a separate single-channel IMAGE meant for explicit re-compositing.
|
||||
|
||||
## See also
|
||||
|
||||
- [HDR modes deep dive](hdr_modes.md)
|
||||
- [Encode/decode pairing](encode_decode_pairing.md)
|
||||
- [`Easy_hdr_VAE_decode`](easy_hdr_vae_decode.md)
|
||||
@@ -0,0 +1,177 @@
|
||||
# `Easy_hdr_VAE_encode` ↔ `Easy_hdr_VAE_decode` — pairing guide
|
||||
|
||||
Quick reference for which fields on the decoder must match the encoder
|
||||
(and which are independent) to get a clean roundtrip.
|
||||
|
||||
## At a glance — pairing rules
|
||||
|
||||
| Encoder field | Pairs with decoder field | Rule |
|
||||
|---|---|---|
|
||||
| `pixels` (IMAGE in) | — | Connect your image / video stream here |
|
||||
| `vae` (VAE) | `vae` (VAE) | **Must be the same VAE on both sides** — otherwise the latent is meaningless |
|
||||
| `source_space` | `target_space` (decoder) | Independent. Set them to match if you want the same colour space out as in (typical roundtrip). Use a different `target_space` to convert colour space *as part of the VAE round-trip*. |
|
||||
| `source_space` | `source_space` (decoder) | **Only matters when `hdr_mode = Compress (Log)`.** When in Log mode, the decoder's `source_space` tells it which log curve was used at encode time so it knows how to decompress. **Set the decoder's `source_space` equal to the encoder's `source_space`** when both are in Compress (Log) mode. Ignored otherwise. |
|
||||
| `exposure` | `exposure_adjust` | Independent. Both apply in linear space, both default to 0. Use them as creative exposure controls — the encoder bumps before VAE, the decoder bumps after VAE. They don't need to be inverses unless you want the roundtrip exposure-neutral. |
|
||||
| `alpha_handling` | (none) | Encode-only flag. The encoder's `alpha` IMAGE output carries the alpha **around** the VAE; the VAE itself never sees alpha. Re-composite the alpha with the decoded image downstream if you need 4-channel output. |
|
||||
| `hdr_mode` | `hdr_mode` | **Must match** for clean roundtrips, especially for `Compress (Log)`. Mismatched modes will produce wrong colours by design — the decoder will misinterpret what the VAE saw. |
|
||||
| `latent_sampling` | (none) | Encode-only. `sample` is non-deterministic (default). `mean` / `mode` are deterministic — best for img2img where you want minimum reconstruction noise. |
|
||||
| `samples` (LATENT out) | `samples` (LATENT in) | Direct wire. KSampler / other nodes can sit between encode and decode here. |
|
||||
| `debug_info` (STRING out) | `debug_info` (STRING out) | Independent diagnostics. Wire to a Show Text / Display Any node to see the actual dispatch path the wrapper took (e.g. `path=video-3d` for a 3-D-aware video VAE). |
|
||||
|
||||
## Worked example #1 — pure SDR roundtrip (cleanest reconstruction)
|
||||
|
||||
Use case: standard sRGB image input, want it back out matching the input as closely as possible.
|
||||
|
||||
```
|
||||
ENCODER DECODER
|
||||
──────────────────── ────────────────────
|
||||
source_space = sRGB target_space = sRGB
|
||||
exposure = 0.0 exposure_adjust = 0.0
|
||||
alpha_handling = Preserve hdr_mode = Clip (SDR)
|
||||
hdr_mode = Clip (SDR) source_space = (irrelevant — not in Log mode)
|
||||
latent_sampling = mean
|
||||
```
|
||||
|
||||
Why these choices:
|
||||
|
||||
- **`source_space`/`target_space` both sRGB** — input is gamma-coded sRGB and output should match.
|
||||
- **`hdr_mode = Clip (SDR)` on both sides** — VAE was trained on sRGB-gamma in `[0, 1]`, this gives it exactly that.
|
||||
- **`latent_sampling = mean`** — deterministic encode → reproducible test runs and minimum reconstruction noise.
|
||||
|
||||
## Worked example #2 — ACEScg HDR roundtrip (preserves highlights and wide gamut)
|
||||
|
||||
Use case: scene-linear ACEScg input from a 3-D render or EXR sequence with HDR highlights and wide-gamut colour, want it back out the same way.
|
||||
|
||||
```
|
||||
ENCODER DECODER
|
||||
───────────────────── ─────────────────────
|
||||
source_space = ACEScg target_space = ACEScg
|
||||
exposure = 0.0 exposure_adjust = 0.0
|
||||
alpha_handling = Preserve hdr_mode = Compress (Log) ← matches encoder
|
||||
hdr_mode = Compress (Log) source_space = ACEScg ← tells decoder which log curve was used
|
||||
latent_sampling = mean
|
||||
```
|
||||
|
||||
Why these choices:
|
||||
|
||||
- **`hdr_mode = Compress (Log)` on both sides** — encodes through a cinema log curve (defaults to ARRI LogC4 because `ACEScg` is not itself a log space), preserves the wide dynamic range through the VAE, and the decoder side knows to apply the soft-shoulder + log-domain highlight denoise that this pipeline relies on.
|
||||
- **Decoder's `source_space = ACEScg`** — the encoder picks ARRI LogC4 as the log curve when the source is ACEScg; the decoder needs to know what was picked, and matching `source_space` to the encoder is the way to communicate it. *This is the field most commonly forgotten when people debug "Compress (Log) gave me weird colour."*
|
||||
- **Both `target_space` and `source_space` set to ACEScg on the decoder** — `target_space` is "what colour space do I want the output in"; `source_space` is "what colour space did encode see, so I know which log curve was used."
|
||||
|
||||
## Worked example #3 — cinema log workflow (LogC4 in, sRGB out for preview)
|
||||
|
||||
Use case: ARRI LogC4 source footage, viewing on a standard sRGB monitor.
|
||||
|
||||
```
|
||||
ENCODER DECODER
|
||||
───────────────────── ─────────────────────
|
||||
source_space = ARRI LogC4 target_space = sRGB
|
||||
exposure = 0.0 exposure_adjust = 0.0
|
||||
alpha_handling = Ignore hdr_mode = Compress (Log)
|
||||
hdr_mode = Compress (Log) source_space = ARRI LogC4 ← tells decoder this is LogC4
|
||||
latent_sampling = mean
|
||||
```
|
||||
|
||||
Why these choices:
|
||||
|
||||
- **Encoder `source_space = ARRI LogC4`** — input is LogC4-coded; we want the VAE to see exactly that (the log-to-log "round-trip" path is exact for `exposure = 0` because `to_linear` followed by `from_linear` to the same log space cancels out).
|
||||
- **Decoder `target_space = sRGB`** — output goes to a standard monitor; the decoder converts log-decoded linear back to sRGB-gamma for display.
|
||||
- **Decoder `source_space = ARRI LogC4`** — *required* so the decoder knows which log curve to use when reversing the VAE's log-coded output.
|
||||
|
||||
## Worked example #4 — Wan 2.2 (or other 3-D-native video VAE)
|
||||
|
||||
Use case: image sequence input through a video VAE that handles temporal natively.
|
||||
|
||||
```
|
||||
ENCODER DECODER
|
||||
────────────────── ──────────────────
|
||||
source_space = Linear target_space = Linear
|
||||
exposure = 0.0 exposure_adjust = 0.0
|
||||
alpha_handling = Ignore hdr_mode = Soft Clip
|
||||
hdr_mode = Soft Clip source_space = (irrelevant — not in Log mode)
|
||||
latent_sampling = sample
|
||||
```
|
||||
|
||||
What you'll see in `debug_info`:
|
||||
|
||||
```
|
||||
Easy_hdr_VAE_encode (Koolook v2.3.3) | input shape (1, 41, 1056, 1872, 3) |
|
||||
path=video-3d | source_space=Linear | exposure=+0.00 |
|
||||
hdr_mode=Soft Clip | latent_sampling=sample | alpha_handling=Ignore
|
||||
```
|
||||
|
||||
The `path=video-3d` confirms the wrapper detected a 5-D video tensor + a 3-D-aware VAE (the `vae.latent_dim == 3` check) and passed it through directly to Wan VAE's native temporal handling.
|
||||
|
||||
The decoder will produce a 4-D `(B*F, H, W, C)` IMAGE batch (i.e. one IMAGE per frame, ready for `SaveImage` to write `_00001.png`, `_00002.png`, …) — that 5-D-to-4-D normalization happens unconditionally on the decode side, so SaveImage gets the layout it expects.
|
||||
|
||||
## "I'm getting wrong colour" — quick diagnostic checklist
|
||||
|
||||
Before suspecting the VAE itself:
|
||||
|
||||
1. **Same `vae` connected to both nodes?** Mismatched VAE = garbage latent.
|
||||
2. **`hdr_mode` matches between encoder and decoder?** Different modes = wrong colour by design.
|
||||
3. **If using `Compress (Log)`, does the decoder's `source_space` match the encoder's `source_space`?** This is the single most common mistake.
|
||||
4. **Is your input actually in the colour space you set for `source_space`?** A common gotcha: ComfyUI nodes default to passing sRGB-gamma data; if a node upstream produces sRGB but you set `source_space = Linear`, the wrapper will linearise data that's already linear. Set `source_space = sRGB` for typical ComfyUI image inputs.
|
||||
5. **`debug_info` STRING output wired to a Show Text node?** Look at the `path=…` field — if it shows `video-iter` when you expected `video-3d`, your VAE doesn't have the `latent_dim == 3` attribute (likely not a video VAE), and the wrapper is iterating frames manually. That's still correct, just slower.
|
||||
6. **For HDR / wide-gamut roundtrip with visible shift:** try switching `hdr_mode` from `Passthrough` to `Compress (Log)` on both sides. `Passthrough` is the worst HDR mode for fidelity because it lets out-of-domain values into the VAE.
|
||||
|
||||
If after all that the colour is still wrong, that's a real bug worth filing — the wrapper internals are covered by the dispatch tests in the test suite, but real-world VAE behaviour on edge cases can surface issues. Wire `debug_info` on both nodes, capture both strings, and open an issue with the input + output and both debug strings attached.
|
||||
|
||||
## Reference connectivity diagram
|
||||
|
||||
For a typical SDR roundtrip with KSampler in between (text-to-image, img2img, etc.):
|
||||
|
||||
```
|
||||
IMAGE input
|
||||
│
|
||||
│ ┌────────────┐
|
||||
│ │ Load VAE │
|
||||
│ └────┬───────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────────────────────────────┐
|
||||
│ Easy_hdr_VAE_encode │
|
||||
│ pixels ●───── │
|
||||
│ vae ●───── │
|
||||
│ source_space = sRGB │
|
||||
│ exposure = 0.0 │
|
||||
│ alpha_handling= Preserve │
|
||||
│ hdr_mode = Clip (SDR) │ ← these three (source_space,
|
||||
│ latent_sampling = mean │ hdr_mode, and on log workflows
|
||||
│ samples ●───── │ the source_space) drive what
|
||||
│ debug_info ●── │ the decoder needs to mirror
|
||||
│ alpha ●──── │
|
||||
└──────────────────────────────────────┘
|
||||
│ samples
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ KSampler │ (denoise = 0 for a "passthrough" roundtrip
|
||||
│ (or none) │ test — bypasses diffusion)
|
||||
└──────┬───────┘
|
||||
│ samples
|
||||
▼
|
||||
┌──────────────────────────────────────┐
|
||||
│ Easy_hdr_VAE_decode │
|
||||
│ samples ●───── │
|
||||
│ vae ●───── │
|
||||
│ target_space = sRGB │
|
||||
│ exposure_adjust = 0.0 │
|
||||
│ hdr_mode = Clip (SDR) │ ← matches encoder's hdr_mode
|
||||
│ source_space = (n/a — not Log) │ ← only matters in Compress (Log)
|
||||
│ image ●────│
|
||||
│ debug_info ●────│
|
||||
└──────────────────────────────────────┘
|
||||
│ image
|
||||
▼
|
||||
┌──────────┐
|
||||
│SaveImage │
|
||||
└──────────┘
|
||||
```
|
||||
|
||||
For the alpha path, route the encoder's `alpha` IMAGE around the VAE chain and re-composite with the decoded `image` downstream (e.g. via an `ImageCompositeMasked` or similar). The VAE itself only ever sees the first 3 channels; alpha bypasses it.
|
||||
|
||||
## See also
|
||||
|
||||
- [`hdr_modes.md`](hdr_modes.md) — what each HDR mode does to your pixels (per-pixel worked examples).
|
||||
- [`easy_hdr_vae_encode.md`](easy_hdr_vae_encode.md) — encoder per-input reference.
|
||||
- [`easy_hdr_vae_decode.md`](easy_hdr_vae_decode.md) — decoder per-input reference.
|
||||
@@ -0,0 +1,89 @@
|
||||
# HDR Modes — what each one does to your pixels
|
||||
|
||||
`hdr_mode` is a parameter on both `Easy_hdr_VAE_encode` and
|
||||
`Easy_hdr_VAE_decode`. It controls **what happens to linearised pixel
|
||||
values just before they enter `vae.encode()`** (and, on the decode side,
|
||||
how the VAE's output is interpreted on the way back to linear).
|
||||
|
||||
For roundtrip workflows the encoder's `hdr_mode` and the decoder's
|
||||
`hdr_mode` should **match**. See [`encode_decode_pairing.md`](encode_decode_pairing.md)
|
||||
for the cross-node pairing rules.
|
||||
|
||||
## The four modes side by side (encode side)
|
||||
|
||||
| Mode | What it does to values | What VAE sees | What's lost |
|
||||
|---|---|---|---|
|
||||
| **Clip (SDR)** | Linear → sRGB-gamma → **hard clamp to [0, 1]** | Standard SDR-range data — what most VAEs were trained on | Anything above `1.0` (HDR highlights) → cut to white. Anything below `0` (out-of-gamut negatives from wide-gamut sources like ACEScg) → cut to black. |
|
||||
| **Soft Clip** | Linear → sRGB-gamma → **tanh rolloff above 0.85**, then clamp to [0, 1] | SDR-range data with smoothly curved highlights instead of a hard cliff | Highlights above ~0.85 progressively compressed; very bright pixels still end up near 1.0 but with no harsh clipped-white line. Negatives still clamped to 0. |
|
||||
| **Compress (Log)** | Linear → **cinema log curve** (ARRI LogC4 by default, or matches `source_space` if `source_space` is itself a log space). No clamp. | Log-coded HDR data — squashes a wide dynamic range into [0, ~1.08] | Almost nothing — the log curve is designed for this. Caveat: the VAE wasn't trained on log-coded data, so reconstruction has its own characteristic noise. The decoder addresses this with per-profile soft-shoulder + log-domain highlight denoise — but it only kicks in when the decoder also has `hdr_mode = Compress (Log)`. |
|
||||
| **Passthrough** | Linear → sRGB-gamma. **No clamp at all.** Negatives stay negative; values above 1.0 stay above 1.0. | Whatever the input gives it — including out-of-domain values | VAE behaviour on values it never saw in training (`>1` or `<0`) is undefined. It typically mashes them — clips highlights anyway, makes negatives weird. Output is often *worse* than `Clip (SDR)` in practice. |
|
||||
|
||||
## Concrete per-pixel examples
|
||||
|
||||
### SDR midtone — linear input value `0.4`
|
||||
|
||||
| Mode | sRGB-gamma stage | Final to VAE |
|
||||
|---|---|---|
|
||||
| Clip (SDR) | `0.665` | `0.665` (in [0, 1]) |
|
||||
| Soft Clip | `0.665` | `0.665` (below 0.85 knee, passthrough) |
|
||||
| Compress (Log) | (skips sRGB step entirely) | `~0.55` LogC4-coded |
|
||||
| Passthrough | `0.665` | `0.665` |
|
||||
|
||||
All four modes preserve a normal SDR midtone identically — they only diverge at the extremes.
|
||||
|
||||
### Super-white HDR highlight — linear input value `5.0` (e.g. sun, explosion, VDB fire)
|
||||
|
||||
| Mode | sRGB-gamma stage | Final to VAE |
|
||||
|---|---|---|
|
||||
| Clip (SDR) | `1.94` | `1.0` ← **lost super-white** |
|
||||
| Soft Clip | `1.94` | `~0.99` ← **lost most super-white**, but smoothly |
|
||||
| Compress (Log) | (skips sRGB) | `~0.79` LogC4-coded ← **preserved** |
|
||||
| Passthrough | `1.94` | `1.94` ← VAE will likely mash this anyway |
|
||||
|
||||
`Compress (Log)` is the only mode that actually preserves HDR highlights through the VAE.
|
||||
|
||||
### Out-of-gamut negative — linear input value `-0.05` (e.g. saturated cyan from ACEScg → Rec.709)
|
||||
|
||||
| Mode | Behaviour |
|
||||
|---|---|
|
||||
| Clip (SDR) | Clamped to `0.0` ← lost |
|
||||
| Soft Clip | Clamped to `0.0` ← lost |
|
||||
| Compress (Log) | Floored to `0.0` (no valid signal below 0 in log space) ← lost |
|
||||
| Passthrough | Stays at `-0.05` ← VAE mashes it; usually worse than just clamping |
|
||||
|
||||
Wide-gamut colour preservation isn't really a job the VAE can do; if you need it, keep your wide-gamut data outside the VAE chain.
|
||||
|
||||
## When to use which
|
||||
|
||||
| Mode | Use when |
|
||||
|---|---|
|
||||
| **Clip (SDR)** | You have a normal SDR image / sequence (values fit in [0, 1] after colour-space conversion) and you want the cleanest VAE reconstruction in the SDR range. The VAE is in its native domain, so this gives the highest fidelity for non-HDR content. |
|
||||
| **Soft Clip** | You have bright/HDR-ish content but you don't want to deal with log curves on both sides. The smooth highlight rolloff is more pleasant than hard clipping for things like rim-lit subjects, sky highlights, etc. |
|
||||
| **Compress (Log)** | You're doing actual HDR / cinema / film work — ACES pipelines, ARRI / Sony / RED / Panasonic source material, scene-linear EXR sequences, etc. **Pair with the same mode on decode** plus `source_space` set to whichever log curve you encoded with. Best dynamic-range fidelity, at the cost of needing matched encode/decode settings. |
|
||||
| **Passthrough** | Almost never the right answer for production. It's an "I know what I'm doing, give me the raw values" escape hatch. For roundtrip fidelity it's usually worse than `Clip (SDR)` because the VAE has nothing trained for out-of-domain inputs and produces characteristic noise. Useful for diagnosing what the VAE is actually receiving. |
|
||||
|
||||
## Trade-off matrix
|
||||
|
||||
| | SDR fidelity | HDR preservation | Highlight smoothness | Roundtrip cleanliness | Requires matched decode mode |
|
||||
|---|---|---|---|---|---|
|
||||
| Clip (SDR) | ★★★★★ | ✗ | ✗ (hard cut) | ★★★★★ (in-range) | No — same path on decode |
|
||||
| Soft Clip | ★★★★ | ✗ | ★★★★ | ★★★★ | No — same path on decode |
|
||||
| Compress (Log) | ★★★ | ★★★★★ | ★★★ (per-profile) | ★★★★ (with matched decode) | **Yes** — decode also Compress (Log) + matching `source_space` |
|
||||
| Passthrough | ★★ | ★ | ✗ | ★ (often worse than Clip) | No — same path on decode |
|
||||
|
||||
## Behind the scenes — where each mode is implemented
|
||||
|
||||
- **Encode side:** [`forks/radiance_koolook/versions/v2_3_3/nodes_vae.py`](../../../../forks/radiance_koolook/versions/v2_3_3/nodes_vae.py),
|
||||
search for `if hdr_mode == "Compress (Log)"` to see the dispatch.
|
||||
- **Log curves + soft-shoulder + log-domain denoise:**
|
||||
[`forks/radiance_koolook/versions/v2_3_3/color_helpers.py`](../../../../forks/radiance_koolook/versions/v2_3_3/color_helpers.py).
|
||||
- **Per-profile HDR parameters** (`LOG_PROFILE_HDR_PARAMS`): tuned per
|
||||
curve based on each curve's slope at code 1.0. RED Log3G10 (the
|
||||
steepest, slope > 1100) gets the most conservative knee and most
|
||||
aggressive denoise; Sony S-Log3 / ARRI LogC4 (moderate-steep) get the
|
||||
loosest. See the docstring in `color_helpers.py` for the full table.
|
||||
- **Why we don't just hard-clamp Compress (Log)**: VAE reconstruction
|
||||
noise is roughly uniform in log-coded space, but log→linear amplifies
|
||||
that noise exponentially in the highlights. A hard clamp at 1.0 would
|
||||
destroy legitimate super-white signal. The soft-shoulder + log-space
|
||||
denoise approach is the same principle DaVinci Resolve and Nuke use.
|
||||
@@ -0,0 +1,611 @@
|
||||
{
|
||||
"id": "2b0a8c0c-abe2-423b-99a6-ac7940f66066",
|
||||
"revision": 0,
|
||||
"last_node_id": 1266,
|
||||
"last_link_id": 1981,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 992,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
-7932.9859136470905,
|
||||
9673.132708942472
|
||||
],
|
||||
"size": [
|
||||
259.8657531738281,
|
||||
246.0000000000001
|
||||
],
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 1583
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.64",
|
||||
"Node name for S&R": "PreviewImage",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"version": "7.2.2",
|
||||
"input_ue_unconnectable": {}
|
||||
}
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 993,
|
||||
"type": "LoadEXR",
|
||||
"pos": [
|
||||
-7936.962735728316,
|
||||
9406.273945790455
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
194
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "filepath",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "filepath"
|
||||
},
|
||||
"link": 1603
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "RGB",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
1583
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "alpha",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "batch_size",
|
||||
"type": "INT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"title": "ONE frame",
|
||||
"properties": {
|
||||
"cnr_id": "ComfyUI-HQ-Image-Save",
|
||||
"ver": "52b4579a195727729bf768032737a67220512128",
|
||||
"Node name for S&R": "LoadEXR",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {
|
||||
"filepath": true
|
||||
},
|
||||
"version": "7.2.2",
|
||||
"input_ue_unconnectable": {}
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
"path to directory or .exr file",
|
||||
"linear",
|
||||
1,
|
||||
0,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 997,
|
||||
"type": "Label (rgthree)",
|
||||
"pos": [
|
||||
-7873.6231611078665,
|
||||
8978.78128013376
|
||||
],
|
||||
"size": [
|
||||
352.9296875,
|
||||
50
|
||||
],
|
||||
"flags": {
|
||||
"allow_interaction": true
|
||||
},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"title": "SOURCE depth",
|
||||
"properties": {
|
||||
"fontSize": 50,
|
||||
"fontFamily": "Arial",
|
||||
"fontColor": "#ffffff",
|
||||
"textAlign": "left",
|
||||
"backgroundColor": "transparent",
|
||||
"padding": 0,
|
||||
"borderRadius": 0,
|
||||
"angle": 0,
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"version": "7.2.2",
|
||||
"input_ue_unconnectable": {}
|
||||
}
|
||||
},
|
||||
"color": "#fff0",
|
||||
"bgcolor": "#fff0"
|
||||
},
|
||||
{
|
||||
"id": 1006,
|
||||
"type": "LoadEXR",
|
||||
"pos": [
|
||||
-7606.047936758987,
|
||||
9397.38306603451
|
||||
],
|
||||
"size": [
|
||||
285.9300537109375,
|
||||
349.7812194824219
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "filepath",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "filepath"
|
||||
},
|
||||
"link": 1602
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "RGB",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
1587,
|
||||
1635
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "alpha",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "batch_size",
|
||||
"type": "INT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "ComfyUI-HQ-Image-Save",
|
||||
"ver": "52b4579a195727729bf768032737a67220512128",
|
||||
"Node name for S&R": "LoadEXR",
|
||||
"ue_properties": {
|
||||
"version": "7.1",
|
||||
"widget_ue_connectable": {},
|
||||
"input_ue_unconnectable": {}
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
"path to directory or .exr file",
|
||||
"linear",
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"color": "#232",
|
||||
"bgcolor": "#353"
|
||||
},
|
||||
{
|
||||
"id": 1018,
|
||||
"type": "Text Multiline",
|
||||
"pos": [
|
||||
-7933.365538801957,
|
||||
9209.448315354415
|
||||
],
|
||||
"size": [
|
||||
593.5753886350403,
|
||||
123.31427023937988
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
1602,
|
||||
1603,
|
||||
1604
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "was-node-suite-comfyui",
|
||||
"ver": "1.0.2",
|
||||
"Node name for S&R": "Text Multiline",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {
|
||||
"text": true
|
||||
},
|
||||
"version": "7.1",
|
||||
"input_ue_unconnectable": {}
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
"path/to/sequence_b"
|
||||
],
|
||||
"color": "#232",
|
||||
"bgcolor": "#353"
|
||||
},
|
||||
{
|
||||
"id": 1036,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
-7195.925843067886,
|
||||
8728.97470868871
|
||||
],
|
||||
"size": [
|
||||
728.4322134178647,
|
||||
394.6436280241378
|
||||
],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 1635
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.8.2",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 1002,
|
||||
"type": "EasyAIPipeline",
|
||||
"pos": [
|
||||
-6976.199736846407,
|
||||
9648.149788665098
|
||||
],
|
||||
"size": [
|
||||
421.5911559488459,
|
||||
642.1453522689535
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "base_directory_path",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "base_directory_path"
|
||||
},
|
||||
"link": 1604
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "WRITE_file_path",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
1591
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "output_name",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "version_string",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "output_directory",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "shot_duration",
|
||||
"type": "INT",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "seed_value",
|
||||
"type": "INT",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "shot_name",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "koolook",
|
||||
"ver": "f24db5ce965cd22ccad7a4af850dd7e212d6a791",
|
||||
"Node name for S&R": "EasyAIPipeline",
|
||||
"aux_id": "malkuthro/Koolook_Wan_22",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"input_ue_unconnectable": {},
|
||||
"version": "7.2.2"
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
81,
|
||||
453453453,
|
||||
"Place your base folder path in the FIELD below",
|
||||
"path/to/source_video",
|
||||
".%04d.exr",
|
||||
"Source-depth",
|
||||
"v2-vikitti",
|
||||
1,
|
||||
true,
|
||||
true,
|
||||
"path/to/source_depth",
|
||||
null,
|
||||
null
|
||||
],
|
||||
"color": "#2a363b",
|
||||
"bgcolor": "#3f5159"
|
||||
},
|
||||
{
|
||||
"id": 998,
|
||||
"type": "DepthAnything_V2",
|
||||
"pos": [
|
||||
-6827.780390691215,
|
||||
9375.615594844607
|
||||
],
|
||||
"size": [
|
||||
214.82899913241127,
|
||||
53.8174710455969
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "da_model",
|
||||
"type": "DAMODEL",
|
||||
"link": 1586
|
||||
},
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 1587
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
1590
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-depthanythingv2",
|
||||
"ver": "d505cbca99803fc63327b8305618a23e59a18b42",
|
||||
"Node name for S&R": "DepthAnything_V2",
|
||||
"ue_properties": {
|
||||
"version": "7.1",
|
||||
"widget_ue_connectable": {},
|
||||
"input_ue_unconnectable": {}
|
||||
}
|
||||
},
|
||||
"widgets_values": [],
|
||||
"color": "#223",
|
||||
"bgcolor": "#335"
|
||||
},
|
||||
{
|
||||
"id": 1001,
|
||||
"type": "DownloadAndLoadDepthAnythingV2Model",
|
||||
"pos": [
|
||||
-7080.8878988856995,
|
||||
9213.36792146106
|
||||
],
|
||||
"size": [
|
||||
451.25884409922674,
|
||||
87.58390788971155
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "da_v2_model",
|
||||
"type": "DAMODEL",
|
||||
"links": [
|
||||
1586
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-depthanythingv2",
|
||||
"ver": "d505cbca99803fc63327b8305618a23e59a18b42",
|
||||
"Node name for S&R": "DownloadAndLoadDepthAnythingV2Model",
|
||||
"ue_properties": {
|
||||
"version": "7.1",
|
||||
"widget_ue_connectable": {},
|
||||
"input_ue_unconnectable": {}
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
"depth_anything_v2_metric_vkitti_vitl_fp32.safetensors"
|
||||
],
|
||||
"color": "#223",
|
||||
"bgcolor": "#335"
|
||||
},
|
||||
{
|
||||
"id": 1000,
|
||||
"type": "SaveEXRFrames",
|
||||
"pos": [
|
||||
-6511.004406427462,
|
||||
9375.22296859613
|
||||
],
|
||||
"size": [
|
||||
338.36407470703125,
|
||||
615.3633422851562
|
||||
],
|
||||
"flags": {},
|
||||
"order": 9,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 1590
|
||||
},
|
||||
{
|
||||
"name": "filepath",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "filepath"
|
||||
},
|
||||
"link": 1591
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "ComfyUI-HQ-Image-Save",
|
||||
"ver": "52b4579a195727729bf768032737a67220512128",
|
||||
"Node name for S&R": "SaveEXRFrames",
|
||||
"ue_properties": {
|
||||
"version": "7.1",
|
||||
"widget_ue_connectable": {},
|
||||
"input_ue_unconnectable": {}
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
"path/to/frame%04d.exr",
|
||||
"linear",
|
||||
1001,
|
||||
false,
|
||||
"ui"
|
||||
],
|
||||
"color": "#323",
|
||||
"bgcolor": "#535"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1583,
|
||||
993,
|
||||
0,
|
||||
992,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
1586,
|
||||
1001,
|
||||
0,
|
||||
998,
|
||||
0,
|
||||
"DAMODEL"
|
||||
],
|
||||
[
|
||||
1587,
|
||||
1006,
|
||||
0,
|
||||
998,
|
||||
1,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
1590,
|
||||
998,
|
||||
0,
|
||||
1000,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
1591,
|
||||
1002,
|
||||
0,
|
||||
1000,
|
||||
1,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
1602,
|
||||
1018,
|
||||
0,
|
||||
1006,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
1603,
|
||||
1018,
|
||||
0,
|
||||
993,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
1604,
|
||||
1018,
|
||||
0,
|
||||
1002,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
1635,
|
||||
1006,
|
||||
0,
|
||||
1036,
|
||||
0,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.505447028499295,
|
||||
"offset": [
|
||||
8829.571508342653,
|
||||
-7999.799463578146
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.35.9",
|
||||
"workflowRendererVersion": "LG",
|
||||
"node_versions": {
|
||||
"ComfyUI-WanVideoWrapper": "5a2383621a05825d0d0437781afcb8552d9590fd",
|
||||
"comfy-core": "0.3.26",
|
||||
"ComfyUI-KJNodes": "a5bd3c86c8ed6b83c55c2d0e7a59515b15a0137f",
|
||||
"ComfyUI-VideoHelperSuite": "0a75c7958fe320efcb052f1d9f8451fd20c730a8"
|
||||
},
|
||||
"VHS_latentpreview": false,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true,
|
||||
"ue_links": [],
|
||||
"links_added_by_ue": []
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -0,0 +1,898 @@
|
||||
{
|
||||
"id": "b24a0624-6eeb-4a53-ae01-96976b90118b",
|
||||
"revision": 0,
|
||||
"last_node_id": 37,
|
||||
"last_link_id": 34,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "SaveEXRFrames",
|
||||
"pos": [
|
||||
3318.6065813153327,
|
||||
-56.874361817991016
|
||||
],
|
||||
"size": [
|
||||
338.36407470703125,
|
||||
615.3633422851562
|
||||
],
|
||||
"flags": {},
|
||||
"order": 13,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"name": "filepath",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "filepath"
|
||||
},
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "ComfyUI-HQ-Image-Save",
|
||||
"ver": "52b4579a195727729bf768032737a67220512128",
|
||||
"Node name for S&R": "SaveEXRFrames",
|
||||
"ue_properties": {
|
||||
"version": "7.1",
|
||||
"widget_ue_connectable": {},
|
||||
"input_ue_unconnectable": {}
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
"path/to/frame%04d.exr",
|
||||
"linear",
|
||||
1001,
|
||||
false,
|
||||
"ui"
|
||||
],
|
||||
"color": "#323",
|
||||
"bgcolor": "#535"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "LoadEXR",
|
||||
"pos": [
|
||||
3691.5625811634764,
|
||||
79.92192775119034
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
194
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "filepath",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "filepath"
|
||||
},
|
||||
"link": 14
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "RGB",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
15
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "alpha",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "batch_size",
|
||||
"type": "INT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "ComfyUI-HQ-Image-Save",
|
||||
"ver": "52b4579a195727729bf768032737a67220512128",
|
||||
"Node name for S&R": "LoadEXR",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"input_ue_unconnectable": {},
|
||||
"version": "7.2.2"
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
"path to directory or .exr file",
|
||||
"linear",
|
||||
0,
|
||||
0,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "GetImageSizeAndCount",
|
||||
"pos": [
|
||||
1481.0290842611375,
|
||||
30.722427113459077
|
||||
],
|
||||
"size": [
|
||||
196.2994140625,
|
||||
86
|
||||
],
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"label": "1920 width",
|
||||
"name": "width",
|
||||
"type": "INT",
|
||||
"links": [
|
||||
7,
|
||||
22
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "1080 height",
|
||||
"name": "height",
|
||||
"type": "INT",
|
||||
"links": [
|
||||
4,
|
||||
23
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "81 count",
|
||||
"name": "count",
|
||||
"type": "INT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-kjnodes",
|
||||
"ver": "1.1.9",
|
||||
"Node name for S&R": "GetImageSizeAndCount",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"input_ue_unconnectable": {},
|
||||
"version": "7.2.2"
|
||||
}
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "easy showAnything",
|
||||
"pos": [
|
||||
1690.9158486091035,
|
||||
229.5702849687141
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
88
|
||||
],
|
||||
"flags": {},
|
||||
"order": 9,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "anything",
|
||||
"shape": 7,
|
||||
"type": "*",
|
||||
"link": 7
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "output",
|
||||
"type": "*",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-easy-use",
|
||||
"ver": "1.3.4",
|
||||
"Node name for S&R": "easy showAnything",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"input_ue_unconnectable": {},
|
||||
"version": "7.2.2"
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
"1920"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "easy showAnything",
|
||||
"pos": [
|
||||
1447.2646213349187,
|
||||
229.38469471423394
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
88
|
||||
],
|
||||
"flags": {},
|
||||
"order": 10,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "anything",
|
||||
"shape": 7,
|
||||
"type": "*",
|
||||
"link": 4
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "output",
|
||||
"type": "*",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-easy-use",
|
||||
"ver": "1.3.4",
|
||||
"Node name for S&R": "easy showAnything",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"input_ue_unconnectable": {},
|
||||
"version": "7.2.2"
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
"1080"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "Reroute",
|
||||
"pos": [
|
||||
1126.8771433051406,
|
||||
-245.4533858492232
|
||||
],
|
||||
"size": [
|
||||
75,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"link": 10
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
8,
|
||||
24
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"showOutputText": false,
|
||||
"horizontal": false,
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"version": "7.2.2",
|
||||
"input_ue_unconnectable": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "ImageScaleBy",
|
||||
"pos": [
|
||||
1409.4331609425535,
|
||||
-113.371733507784
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 8
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
5
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.75",
|
||||
"Node name for S&R": "ImageScaleBy",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"input_ue_unconnectable": {},
|
||||
"version": "7.2.2"
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
"area",
|
||||
1.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
3701.0363198667783,
|
||||
327.7900569355131
|
||||
],
|
||||
"size": [
|
||||
1201.9980600596919,
|
||||
669.1034801466919
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 15
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.76",
|
||||
"Node name for S&R": "PreviewImage",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"input_ue_unconnectable": {},
|
||||
"version": "7.2.2"
|
||||
}
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"type": "Text Multiline",
|
||||
"pos": [
|
||||
235.8074140117446,
|
||||
-221.48664554792862
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
200
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
19
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "was-node-suite-comfyui",
|
||||
"ver": "1.0.2",
|
||||
"Node name for S&R": "Text Multiline",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"input_ue_unconnectable": {},
|
||||
"version": "7.2.2"
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
"path/to/sequence_a"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"type": "LoadEXR",
|
||||
"pos": [
|
||||
719.5796614098845,
|
||||
-285.81230652231585
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
194
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "filepath",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "filepath"
|
||||
},
|
||||
"link": 19
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "RGB",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
10,
|
||||
13
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "alpha",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "batch_size",
|
||||
"type": "INT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "ComfyUI-HQ-Image-Save",
|
||||
"ver": "52b4579a195727729bf768032737a67220512128",
|
||||
"Node name for S&R": "LoadEXR",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {
|
||||
"filepath": true,
|
||||
"tonemap": true,
|
||||
"image_load_cap": true,
|
||||
"skip_first_images": true,
|
||||
"select_every_nth": true
|
||||
},
|
||||
"version": "7.1",
|
||||
"input_ue_unconnectable": {}
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
"path to directory or .exr file",
|
||||
"linear",
|
||||
81,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"color": "#232",
|
||||
"bgcolor": "#353"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"type": "EasyAIPipeline",
|
||||
"pos": [
|
||||
2733.0110767703527,
|
||||
35.23518934271729
|
||||
],
|
||||
"size": [
|
||||
546.119671913797,
|
||||
682.1913422648058
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "WRITE_file_path",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "output_name",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "version_string",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "output_directory",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
14
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "shot_duration",
|
||||
"type": "INT",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "seed_value",
|
||||
"type": "INT",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "shot_name",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "koolook",
|
||||
"ver": "f24db5ce965cd22ccad7a4af850dd7e212d6a791",
|
||||
"Node name for S&R": "EasyAIPipeline",
|
||||
"aux_id": "malkuthro/Koolook_Wan_22",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"input_ue_unconnectable": {},
|
||||
"version": "7.2.2"
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
81,
|
||||
453453453,
|
||||
"Place your base folder path in the FIELD below",
|
||||
"path/to/sequence_a",
|
||||
".%04d.exr",
|
||||
"UPres-0.3",
|
||||
"",
|
||||
7,
|
||||
false,
|
||||
false,
|
||||
"path/to/sequence_a/UPres-0.3/v003/UPres-0.3__v003.%04d.exr",
|
||||
null,
|
||||
null
|
||||
],
|
||||
"color": "#233",
|
||||
"bgcolor": "#355"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"type": "9b637afd-eb99-4941-a288-438642a560c0",
|
||||
"pos": [
|
||||
2066.8846360099283,
|
||||
-242.67261203922772
|
||||
],
|
||||
"size": [
|
||||
480.3395560479155,
|
||||
494.196318307497
|
||||
],
|
||||
"flags": {},
|
||||
"order": 11,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 24
|
||||
},
|
||||
{
|
||||
"name": "width",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "width"
|
||||
},
|
||||
"link": 22
|
||||
},
|
||||
{
|
||||
"name": "height",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "height"
|
||||
},
|
||||
"link": 23
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
34
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"proxyWidgets": [
|
||||
[
|
||||
"-1",
|
||||
"width"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"height"
|
||||
],
|
||||
[
|
||||
"6",
|
||||
"text"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"seed"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"denoise"
|
||||
]
|
||||
],
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.67",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"version": "7.2.2",
|
||||
"input_ue_unconnectable": {}
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
1920,
|
||||
1080
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
401.05847433521893,
|
||||
-685.6696159345132
|
||||
],
|
||||
"size": [
|
||||
573.048406068714,
|
||||
319.33091697515795
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 13
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.76",
|
||||
"Node name for S&R": "PreviewImage",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"input_ue_unconnectable": {},
|
||||
"version": "7.2.2"
|
||||
}
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "GetImageSizeAndCount",
|
||||
"pos": [
|
||||
2729.415657149835,
|
||||
-242.45581167978264
|
||||
],
|
||||
"size": [
|
||||
196.2994140625,
|
||||
86
|
||||
],
|
||||
"flags": {},
|
||||
"order": 12,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 34
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "width",
|
||||
"name": "width",
|
||||
"type": "INT",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"label": "height",
|
||||
"name": "height",
|
||||
"type": "INT",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"label": "count",
|
||||
"name": "count",
|
||||
"type": "INT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-kjnodes",
|
||||
"ver": "1.1.9",
|
||||
"Node name for S&R": "GetImageSizeAndCount",
|
||||
"ue_properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"input_ue_unconnectable": {},
|
||||
"version": "7.2.2"
|
||||
}
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
4,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
2,
|
||||
20,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
4,
|
||||
6,
|
||||
2,
|
||||
5,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
5,
|
||||
10,
|
||||
0,
|
||||
6,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
7,
|
||||
6,
|
||||
1,
|
||||
9,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
8,
|
||||
12,
|
||||
0,
|
||||
10,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
10,
|
||||
23,
|
||||
0,
|
||||
12,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
13,
|
||||
23,
|
||||
0,
|
||||
16,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
14,
|
||||
20,
|
||||
3,
|
||||
17,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
15,
|
||||
17,
|
||||
0,
|
||||
18,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
19,
|
||||
15,
|
||||
0,
|
||||
23,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
22,
|
||||
6,
|
||||
1,
|
||||
25,
|
||||
1,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
23,
|
||||
6,
|
||||
2,
|
||||
25,
|
||||
2,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
24,
|
||||
12,
|
||||
0,
|
||||
25,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
34,
|
||||
25,
|
||||
0,
|
||||
4,
|
||||
0,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.22379213535069217,
|
||||
"offset": [
|
||||
2086.065795358603,
|
||||
2357.105094980027
|
||||
]
|
||||
},
|
||||
"workflowRendererVersion": "LG",
|
||||
"frontendVersion": "1.35.9",
|
||||
"VHS_latentpreview": false,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# Workflow regression fixtures
|
||||
|
||||
Pinned ComfyUI workflow JSONs that exercise Koolook nodes. Used by the
|
||||
pre-flight script (TBD — see `tools/preflight_release.py`) to verify
|
||||
that every Koolook node ID referenced in any of these workflows still
|
||||
exists in the current `NODE_CLASS_MAPPINGS` (or has a back-compat alias).
|
||||
|
||||
The pre-flight test only checks **Koolook-pack node IDs**. Other-pack
|
||||
node IDs (DepthAnything, rgthree, etc.) are silently ignored — those
|
||||
packs may or may not be installed on a given machine, and tracking
|
||||
their stability isn't our job.
|
||||
|
||||
## How to add a new fixture
|
||||
|
||||
1. In ComfyUI, build a workflow that exercises one or more Koolook nodes.
|
||||
2. Click **Save** to export the workflow JSON.
|
||||
3. Name the file descriptively: `<feature>-<short-tag>-v<NN>.json`
|
||||
(e.g. `vae-roundtrip-srgb-v01.json`).
|
||||
4. Drop it in this folder.
|
||||
5. Add a one-line entry to the inventory table below.
|
||||
6. **Sanity-check before committing:**
|
||||
- No Windows-style absolute paths (`C:\…`, `D:\…`).
|
||||
- No huge base64 blobs (embedded preview thumbnails) — strip them in
|
||||
ComfyUI's save dialog if present.
|
||||
- No tokens / credentials / personal info in any node parameter.
|
||||
- File size under ~50 KB ideally; certainly under 500 KB.
|
||||
|
||||
The pre-flight script picks the file up automatically — no need to
|
||||
register it anywhere else.
|
||||
|
||||
## Fixture inventory
|
||||
|
||||
| File | Koolook nodes referenced | Notes |
|
||||
|---|---|---|
|
||||
| [`Depth-Anything3-v01.json`](Depth-Anything3-v01.json) | `EasyAIPipeline` | Depth-Anything v3 workflow with Koolook pipeline node + EXR I/O |
|
||||
| [`Hires-FIX-wanT2V-v01.json`](Hires-FIX-wanT2V-v01.json) | `EasyAIPipeline` | Hires-fix variant for Wan T2V workflows |
|
||||
|
||||
## What the pre-flight script does (for reference)
|
||||
|
||||
For each `*.json` in this folder:
|
||||
|
||||
1. Parse the JSON.
|
||||
2. Walk the `nodes` array (ComfyUI canvas format) — for each node, read its `type` field.
|
||||
3. Filter: keep only types that look like Koolook node IDs (heuristics: starts with `Easy`, `Koolook`, contains `_koolook_`, etc. — full pattern lives in the script).
|
||||
4. For each Koolook ID, verify it appears in the current `NODE_CLASS_MAPPINGS` (computed by importing the package).
|
||||
5. **Fail** the pre-flight if any referenced ID is missing — that's a breaking change to a saved workflow that needs either a back-compat alias or an explicit major version bump.
|
||||
|
||||
If the JSON is in API format (different shape, often produced by `ComfyUI` API export), the script also handles that: walk the top-level dict where each key is a node instance and each value has `class_type`.
|
||||
@@ -0,0 +1,297 @@
|
||||
"""
|
||||
Stub-VAE dispatch test for the v2_3_3 wrapper, extracted from
|
||||
`preflight_release.py` to keep the main script under the project's
|
||||
~400-line file-size guideline.
|
||||
|
||||
This module:
|
||||
- Builds a `FakeTensor` (`FT`) class that mimics just enough of
|
||||
`torch.Tensor` to drive the wrapper's dispatch logic (no real math).
|
||||
- Builds a `torch` stub module + `torch.nn` / `torch.nn.functional`
|
||||
submodule stubs.
|
||||
- Loads the v2_3_3 wrapper code (`color_helpers.py` + `nodes_vae.py`)
|
||||
via `importlib` against the stub torch.
|
||||
- Runs five test cases through `Easy_hdr_VAE_encode` / `_decode`,
|
||||
asserting that the per-case dispatch path appears in `debug_info`.
|
||||
- Cleans up `sys.modules` in a `finally` block — including the
|
||||
`_pf_pkg*` entries — so a partial-load failure can't leak the stub
|
||||
torch into anything else running in the same process.
|
||||
|
||||
Public surface: `run(verbose: bool, repo_root: Path) -> tuple[bool, list[str]]`.
|
||||
The caller (in `preflight_release.py`) is responsible for the `[2/4] …`
|
||||
header and the PASS/FAIL line; this module just executes and reports.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
# FakeTensor stub
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class FakeTensor:
|
||||
"""Tensor-shaped duck type that records its shape and supports the
|
||||
handful of operations the v2_3_3 wrapper does at dispatch time."""
|
||||
|
||||
def __init__(self, shape):
|
||||
self.shape = tuple(shape)
|
||||
self.dtype = "float32"
|
||||
self.device = "cpu"
|
||||
|
||||
@property
|
||||
def ndim(self):
|
||||
return len(self.shape)
|
||||
|
||||
@property
|
||||
def T(self):
|
||||
s = list(self.shape)
|
||||
if len(s) >= 2:
|
||||
s[-1], s[-2] = s[-2], s[-1]
|
||||
return FakeTensor(s)
|
||||
|
||||
def clone(self):
|
||||
return FakeTensor(self.shape)
|
||||
|
||||
def float(self):
|
||||
return self
|
||||
|
||||
def __getitem__(self, key):
|
||||
# Slicing patterns the wrapper actually uses:
|
||||
# pixels[:, fi, ...] → drop dim 1, keep rest
|
||||
# latent[:, :, fi, ...] → drop dim 2, keep rest
|
||||
# img[..., :3] → keep leading dims, last dim = 3
|
||||
if isinstance(key, tuple) and len(key) == 3 and key[2] is Ellipsis:
|
||||
return FakeTensor((self.shape[0],) + self.shape[2:])
|
||||
if isinstance(key, tuple) and len(key) == 4 and key[3] is Ellipsis:
|
||||
return FakeTensor((self.shape[0], self.shape[1]) + self.shape[3:])
|
||||
if isinstance(key, tuple) and key[0] is Ellipsis:
|
||||
return FakeTensor(self.shape[:-1] + (3,))
|
||||
return self
|
||||
|
||||
def reshape(self, *a):
|
||||
return FakeTensor(a)
|
||||
|
||||
def to(self, **k):
|
||||
return self
|
||||
|
||||
def __mul__(self, o): return self
|
||||
def __rmul__(self, o): return self
|
||||
def __pow__(self, o): return self
|
||||
def __sub__(self, o): return self
|
||||
def clamp(self, *a, **k): return self
|
||||
|
||||
def __matmul__(self, o):
|
||||
return FakeTensor(self.shape[:-1] + (o.shape[-1],))
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
# torch stub
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _build_torch_stub() -> tuple[types.ModuleType, types.ModuleType, types.ModuleType]:
|
||||
"""Returns the (`torch`, `torch.nn`, `torch.nn.functional`) stub triple."""
|
||||
torch_stub = types.ModuleType("torch")
|
||||
torch_stub.Tensor = FakeTensor
|
||||
torch_stub.stack = lambda ts, dim=0: (
|
||||
lambda s: (s.insert(dim, len(ts)), FakeTensor(s))[1]
|
||||
)(list(ts[0].shape))
|
||||
torch_stub.cat = lambda ts, dim=0: (
|
||||
lambda s: (s.__setitem__(dim, sum(x.shape[dim] for x in ts)), FakeTensor(s))[1]
|
||||
)(list(ts[0].shape))
|
||||
torch_stub.where = lambda c, a, b: a
|
||||
torch_stub.sign = lambda x: x
|
||||
torch_stub.abs = lambda x: x
|
||||
torch_stub.pow = lambda x, p: x
|
||||
torch_stub.tanh = lambda x: x
|
||||
torch_stub.clamp = lambda x, *a, **k: x
|
||||
torch_stub.tensor = lambda *a, **k: FakeTensor((3, 3))
|
||||
torch_stub.float32 = "float32"
|
||||
torch_stub.zeros = lambda shape, dtype=None, device=None: FakeTensor(shape)
|
||||
|
||||
nn_F = types.ModuleType("torch.nn.functional")
|
||||
nn_F.pad = lambda x, *a, **k: x
|
||||
nn_F.avg_pool2d = lambda x, *a, **k: x
|
||||
|
||||
nn_mod = types.ModuleType("torch.nn")
|
||||
nn_mod.functional = nn_F
|
||||
torch_stub.nn = nn_mod
|
||||
|
||||
return torch_stub, nn_mod, nn_F
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
# Stub VAE classes
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class Fake2DVAE:
|
||||
"""A vanilla 2D image VAE — no `latent_dim` attribute, so the wrapper
|
||||
treats it as the standard SD/SDXL/Flux case."""
|
||||
|
||||
def encode(self, pixels):
|
||||
return FakeTensor(
|
||||
(1, 16, pixels.shape[1] // 8, pixels.shape[2] // 8)
|
||||
)
|
||||
|
||||
def decode(self, lat):
|
||||
return FakeTensor(
|
||||
(lat.shape[0], lat.shape[2] * 8, lat.shape[3] * 8, 3)
|
||||
)
|
||||
|
||||
|
||||
class Fake3DVAE:
|
||||
"""A 3D-aware video VAE — exposes `latent_dim = 3` so the wrapper
|
||||
detects it and short-circuits the per-frame iteration on encode."""
|
||||
|
||||
latent_dim = 3
|
||||
|
||||
def encode(self, pixels):
|
||||
if pixels.ndim == 5:
|
||||
return FakeTensor((
|
||||
1, 16,
|
||||
pixels.shape[1] // 4,
|
||||
pixels.shape[2] // 8,
|
||||
pixels.shape[3] // 8,
|
||||
))
|
||||
return FakeTensor((1, 16, pixels.shape[1] // 8, pixels.shape[2] // 8))
|
||||
|
||||
def decode(self, lat):
|
||||
if lat.ndim == 5:
|
||||
return FakeTensor((
|
||||
1, lat.shape[2] * 4,
|
||||
lat.shape[3] * 8, lat.shape[4] * 8, 3,
|
||||
))
|
||||
return FakeTensor((lat.shape[0], lat.shape[2] * 8, lat.shape[3] * 8, 3))
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
# Public entry point
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def run(verbose: bool, repo_root: Path) -> tuple[bool, list[str]]:
|
||||
"""Run the five dispatch test cases and report.
|
||||
|
||||
Returns:
|
||||
(success, failures)
|
||||
- `success`: True if every case passed.
|
||||
- `failures`: list of "<case_name>: <error>" strings (empty on success).
|
||||
|
||||
On success, a per-case "OK" line is printed when `verbose=True`.
|
||||
Header / PASS-FAIL formatting is the caller's responsibility — this
|
||||
module is the mechanical part.
|
||||
"""
|
||||
torch_stub, nn_mod, nn_F = _build_torch_stub()
|
||||
pkg_path = repo_root / "forks" / "radiance_koolook" / "versions" / "v2_3_3"
|
||||
|
||||
# Track every sys.modules key we add so the finally block can pop them
|
||||
# cleanly even if exec_module / constructors raise.
|
||||
injected_modules: list[str] = []
|
||||
|
||||
try:
|
||||
sys.modules["torch"] = torch_stub
|
||||
injected_modules.append("torch")
|
||||
sys.modules["torch.nn"] = nn_mod
|
||||
injected_modules.append("torch.nn")
|
||||
sys.modules["torch.nn.functional"] = nn_F
|
||||
injected_modules.append("torch.nn.functional")
|
||||
|
||||
pkg_spec = importlib.machinery.ModuleSpec(
|
||||
name="_pf_pkg", loader=None, is_package=True
|
||||
)
|
||||
pkg_spec.submodule_search_locations = [str(pkg_path)]
|
||||
pkg = importlib.util.module_from_spec(pkg_spec)
|
||||
sys.modules["_pf_pkg"] = pkg
|
||||
injected_modules.append("_pf_pkg")
|
||||
|
||||
ch_spec = importlib.util.spec_from_file_location(
|
||||
"_pf_pkg.color_helpers", str(pkg_path / "color_helpers.py")
|
||||
)
|
||||
ch_mod = importlib.util.module_from_spec(ch_spec)
|
||||
sys.modules["_pf_pkg.color_helpers"] = ch_mod
|
||||
injected_modules.append("_pf_pkg.color_helpers")
|
||||
ch_spec.loader.exec_module(ch_mod)
|
||||
|
||||
nv_spec = importlib.util.spec_from_file_location(
|
||||
"_pf_pkg.nodes_vae", str(pkg_path / "nodes_vae.py")
|
||||
)
|
||||
nv_mod = importlib.util.module_from_spec(nv_spec)
|
||||
sys.modules["_pf_pkg.nodes_vae"] = nv_mod
|
||||
injected_modules.append("_pf_pkg.nodes_vae")
|
||||
nv_spec.loader.exec_module(nv_mod)
|
||||
|
||||
return _run_cases(nv_mod, verbose)
|
||||
finally:
|
||||
for mod in injected_modules:
|
||||
sys.modules.pop(mod, None)
|
||||
|
||||
|
||||
def _run_cases(nv_mod, verbose: bool) -> tuple[bool, list[str]]:
|
||||
"""The actual five dispatch test cases. Receives the loaded
|
||||
`nodes_vae` module — which now sees our stub torch."""
|
||||
enc = nv_mod.Easy_hdr_VAE_encode()
|
||||
dec = nv_mod.Easy_hdr_VAE_decode()
|
||||
common_enc = dict(
|
||||
source_space="Raw", hdr_mode="Passthrough",
|
||||
exposure=0.0, alpha_handling="Preserve", latent_sampling="sample",
|
||||
)
|
||||
common_dec = dict(
|
||||
target_space="Raw", exposure_adjust=0.0,
|
||||
hdr_mode="Clip (SDR)", source_space="Linear",
|
||||
)
|
||||
|
||||
def case_4d_image():
|
||||
lat, dbg, _ = enc.encode(FakeTensor((2, 64, 64, 3)), Fake2DVAE(), **common_enc)
|
||||
assert lat["samples"].ndim == 4 and "path=image" in dbg
|
||||
|
||||
def case_5d_2dvae():
|
||||
lat, dbg, _ = enc.encode(FakeTensor((1, 8, 64, 64, 3)), Fake2DVAE(), **common_enc)
|
||||
assert (
|
||||
lat["samples"].ndim == 5
|
||||
and lat["samples"].shape[2] == 8
|
||||
and "path=video-iter" in dbg
|
||||
)
|
||||
|
||||
def case_5d_3dvae():
|
||||
lat, dbg, _ = enc.encode(FakeTensor((1, 8, 64, 64, 3)), Fake3DVAE(), **common_enc)
|
||||
assert "path=video-3d" in dbg
|
||||
|
||||
def case_5d_lat_decode():
|
||||
img, dbg = dec.decode(
|
||||
{"samples": FakeTensor((1, 16, 8, 8, 8))}, Fake2DVAE(), **common_dec
|
||||
)
|
||||
assert img.ndim == 4 and img.shape[0] == 8 and "path=video-iter" in dbg
|
||||
|
||||
def case_4d_lat_decode():
|
||||
img, dbg = dec.decode(
|
||||
{"samples": FakeTensor((2, 16, 8, 8))}, Fake2DVAE(), **common_dec
|
||||
)
|
||||
assert "path=image" in dbg
|
||||
|
||||
cases = [
|
||||
("4D image, 2D-VAE", case_4d_image),
|
||||
("5D + 2D-VAE iter", case_5d_2dvae),
|
||||
("5D + 3D-VAE pass", case_5d_3dvae),
|
||||
("5D latent decode iter", case_5d_lat_decode),
|
||||
("4D latent decode pass", case_4d_lat_decode),
|
||||
]
|
||||
|
||||
failures: list[str] = []
|
||||
for name, fn in cases:
|
||||
try:
|
||||
fn()
|
||||
if verbose:
|
||||
# Caller can repaint this with color if it wants —
|
||||
# here we just emit a plain line so the module stays
|
||||
# color-helper-free.
|
||||
print(f" OK {name}")
|
||||
except Exception as exc:
|
||||
failures.append(f"{name}: {type(exc).__name__}: {exc}")
|
||||
|
||||
return (not failures), failures
|
||||
@@ -0,0 +1,508 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pre-flight release validation for ComfyUI-Koolook.
|
||||
|
||||
Runs four checks and exits non-zero on any failure:
|
||||
|
||||
1. static AST extraction of NODE_CLASS_MAPPINGS keys; verifies all
|
||||
*.py files parse and that we can produce a definitive list
|
||||
of registered node IDs without importing torch.
|
||||
2. dispatch Stub-VAE round through the v2_3_3 wrapper; verifies all
|
||||
five rank/VAE dispatch branches still wire correctly.
|
||||
3. manager-meta Fetches ComfyUI-Manager's extension-node-map.json upstream
|
||||
and diffs the entry for our repo against the AST-extracted
|
||||
NODE_CLASS_MAPPINGS. Catches phantom-nodes / missing-nodes
|
||||
drift (the issue #44 class of bug).
|
||||
4. workflows Walks tests/workflows/*.json, extracts node IDs that look
|
||||
like Koolook-pack IDs, verifies each one is currently
|
||||
registered (i.e. wouldn't break a saved workflow).
|
||||
|
||||
Usage:
|
||||
python tools/preflight_release.py # run all checks
|
||||
python tools/preflight_release.py --check static
|
||||
python tools/preflight_release.py --check workflows --verbose
|
||||
python tools/preflight_release.py --skip manager-meta # offline mode
|
||||
|
||||
Exit codes:
|
||||
0 all run checks passed
|
||||
1 one or more checks failed
|
||||
2 invalid CLI usage
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
REPO_URL = "https://github.com/malkuthro/ComfyUI-Koolook"
|
||||
MANAGER_EXT_MAP_URL = (
|
||||
"https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/"
|
||||
"main/extension-node-map.json"
|
||||
)
|
||||
|
||||
|
||||
class _HttpsOnlyRedirectHandler(urllib.request.HTTPRedirectHandler):
|
||||
"""Refuses to follow a 30x redirect to a non-https target.
|
||||
|
||||
`urllib.request.urlopen` follows redirects by default; a one-time
|
||||
https URL check at the call site doesn't protect against an
|
||||
intermediate hop redirecting to file:// or http:// (Bandit B310's
|
||||
real concern). This handler re-validates each redirect target.
|
||||
"""
|
||||
|
||||
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||||
if not newurl.startswith("https://"):
|
||||
raise urllib.error.URLError(
|
||||
f"refusing redirect to non-https scheme: {newurl}"
|
||||
)
|
||||
return super().redirect_request(req, fp, code, msg, headers, newurl)
|
||||
|
||||
|
||||
# ANSI escape codes (https://en.wikipedia.org/wiki/ANSI_escape_code).
|
||||
# No-op when piped, redirected, or NO_COLOR is set.
|
||||
_ANSI = {
|
||||
"red": "31",
|
||||
"green": "32",
|
||||
"yellow": "33",
|
||||
"cyan": "36",
|
||||
"dim": "2",
|
||||
}
|
||||
|
||||
|
||||
def _color(s: str, code: str) -> str:
|
||||
if not sys.stdout.isatty():
|
||||
return s
|
||||
return f"\033[{code}m{s}\033[0m"
|
||||
|
||||
|
||||
def _green(s: str) -> str: return _color(s, _ANSI["green"])
|
||||
def _red(s: str) -> str: return _color(s, _ANSI["red"])
|
||||
def _yellow(s: str) -> str: return _color(s, _ANSI["yellow"])
|
||||
def _cyan(s: str) -> str: return _color(s, _ANSI["cyan"])
|
||||
def _dim(s: str) -> str: return _color(s, _ANSI["dim"])
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
# Check 1 — static (AST extraction)
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
# Files / folders to skip when walking for *.py.
|
||||
_AST_SKIP_DIRS = {
|
||||
"__pycache__", ".venv", ".git", "upscaler_FIX",
|
||||
"nuke_CAM_exporter", "node_modules",
|
||||
}
|
||||
|
||||
|
||||
def _iter_python_files() -> Iterable[Path]:
|
||||
for p in REPO_ROOT.rglob("*.py"):
|
||||
if any(part in _AST_SKIP_DIRS for part in p.parts):
|
||||
continue
|
||||
yield p
|
||||
|
||||
|
||||
def _extract_literal_dict_keys(node: ast.AST) -> set[str]:
|
||||
"""Pull literal string keys out of an ast.Dict node, ignoring computed keys."""
|
||||
if not isinstance(node, ast.Dict):
|
||||
return set()
|
||||
keys = set()
|
||||
for k in node.keys:
|
||||
if isinstance(k, ast.Constant) and isinstance(k.value, str):
|
||||
keys.add(k.value)
|
||||
return keys
|
||||
|
||||
|
||||
def check_static(verbose: bool = False) -> tuple[bool, set[str]]:
|
||||
"""
|
||||
Walk all .py files, AST-parse each, and collect every literal-dict
|
||||
`NODE_CLASS_MAPPINGS = {...}` source-side key. Returns (ok, ids).
|
||||
|
||||
What this DOES NOT do (and the workflow check is the safety net):
|
||||
- Follow runtime-computed mappings (dict comprehensions, function
|
||||
calls like `_namespace_mappings(...)` in the v2_3_3 wrapper).
|
||||
- Apply `NAMESPACE_SUFFIX` / `SKIP_VERSION_SUFFIX` logic — the
|
||||
current code only walks `NODE_CLASS_MAPPINGS`, not the fork's
|
||||
suffix-mangling glue.
|
||||
|
||||
For the Radiance Koolook v2_3_3 fork, this happens to work because
|
||||
every exposed VAE ID is in `SKIP_VERSION_SUFFIX`, so the source-dict
|
||||
keys match the user-facing IDs. The first time a non-skipped ID lands
|
||||
in a fork, the workflow-fixture check (4) will surface the mismatch
|
||||
by reporting the suffixed live ID as "missing" — at that point this
|
||||
function should grow SKIP_VERSION_SUFFIX extraction.
|
||||
"""
|
||||
print(_cyan("[1/4] static — AST extraction of NODE_CLASS_MAPPINGS"))
|
||||
parse_errors: list[str] = []
|
||||
all_node_ids: set[str] = set()
|
||||
files_seen = 0
|
||||
|
||||
for py_path in _iter_python_files():
|
||||
files_seen += 1
|
||||
try:
|
||||
source = py_path.read_text(encoding="utf-8")
|
||||
tree = ast.parse(source, filename=str(py_path))
|
||||
except (SyntaxError, UnicodeDecodeError) as exc:
|
||||
parse_errors.append(f"{py_path.relative_to(REPO_ROOT)}: {exc}")
|
||||
continue
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
if (
|
||||
isinstance(target, ast.Name)
|
||||
and target.id == "NODE_CLASS_MAPPINGS"
|
||||
):
|
||||
keys = _extract_literal_dict_keys(node.value)
|
||||
all_node_ids.update(keys)
|
||||
if verbose and keys:
|
||||
rel = py_path.relative_to(REPO_ROOT)
|
||||
print(_dim(f" {rel}: {sorted(keys)}"))
|
||||
|
||||
if parse_errors:
|
||||
print(_red(f" FAIL — {len(parse_errors)} parse errors:"))
|
||||
for err in parse_errors:
|
||||
print(_red(f" {err}"))
|
||||
return False, all_node_ids
|
||||
|
||||
if not all_node_ids:
|
||||
print(_red(" FAIL — no NODE_CLASS_MAPPINGS literal dicts found anywhere"))
|
||||
return False, all_node_ids
|
||||
|
||||
print(
|
||||
_green(f" PASS — {files_seen} *.py files parsed, "
|
||||
f"{len(all_node_ids)} unique node IDs collected"
|
||||
+ (":" if verbose else ""))
|
||||
)
|
||||
if verbose:
|
||||
for nid in sorted(all_node_ids):
|
||||
print(_dim(f" {nid}"))
|
||||
return True, all_node_ids
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
# Check 2 — dispatch (stub-VAE roundtrip)
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def check_dispatch(verbose: bool = False) -> bool:
|
||||
"""
|
||||
Run the v2_3_3 VAE wrapper through every rank/VAE-type combination using
|
||||
a FakeTensor stub. No torch dependency — the stub mimics just enough of
|
||||
`torch.Tensor` to drive the dispatch logic.
|
||||
|
||||
Implementation lives in `tools/_preflight_dispatch.py` (extracted to keep
|
||||
this orchestrator under the project's ~400-line file-size guideline).
|
||||
"""
|
||||
print(_cyan("[2/4] dispatch — VAE rank/VAE-type branches"))
|
||||
|
||||
# Lazy import: the sibling module shares the same directory as this file
|
||||
# but isn't importable as `tools._preflight_dispatch` unless tools/ is on
|
||||
# sys.path. Load it directly via importlib so working dir doesn't matter.
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"_preflight_dispatch",
|
||||
Path(__file__).resolve().parent / "_preflight_dispatch.py",
|
||||
)
|
||||
dispatch = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(dispatch)
|
||||
|
||||
success, failures = dispatch.run(verbose=verbose, repo_root=REPO_ROOT)
|
||||
|
||||
if failures:
|
||||
print(_red(f" FAIL — {len(failures)} dispatch branch(es) broken:"))
|
||||
for f in failures:
|
||||
print(_red(f" {f}"))
|
||||
return False
|
||||
print(_green(" PASS — all 5 dispatch branches OK"))
|
||||
return success
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
# Check 3 — manager-meta (extension-node-map drift)
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def check_manager_meta(actual_ids: set[str], verbose: bool = False) -> bool:
|
||||
"""
|
||||
Compare ComfyUI-Manager's upstream extension-node-map entry for our repo
|
||||
against the AST-extracted node IDs. Reports phantom IDs (in upstream but
|
||||
not in our code) and missing IDs (in our code but not in upstream).
|
||||
"""
|
||||
print(_cyan("[3/4] manager-meta — extension-node-map.json drift"))
|
||||
if not MANAGER_EXT_MAP_URL.startswith("https://"):
|
||||
# Hardcoded URL above is https; this guard ensures any future edit
|
||||
# can't accidentally introduce a non-https scheme.
|
||||
print(_yellow(" SKIP — manager-meta URL is not https"))
|
||||
return True
|
||||
try:
|
||||
# Custom opener with a redirect handler that re-validates each hop
|
||||
# for an https scheme — closes the gap that urlopen by default would
|
||||
# follow a 30x to http:// or file:// silently.
|
||||
opener = urllib.request.build_opener(_HttpsOnlyRedirectHandler())
|
||||
# Bandit B310: scheme is enforced by the guard above + the redirect
|
||||
# handler below; the URL is hardcoded to a github.com raw path.
|
||||
with opener.open(MANAGER_EXT_MAP_URL, timeout=20) as resp: # nosec B310
|
||||
data = json.loads(resp.read().decode("utf-8"))
|
||||
except (urllib.error.URLError, json.JSONDecodeError, TimeoutError) as exc:
|
||||
print(_yellow(f" SKIP — could not fetch upstream map: {exc}"))
|
||||
return True
|
||||
|
||||
entry = data.get(REPO_URL)
|
||||
if entry is None:
|
||||
print(_yellow(f" SKIP — no entry for {REPO_URL} in upstream map yet"))
|
||||
return True
|
||||
|
||||
# Defensive shape-check: upstream entry should be `[list_of_ids, metadata]`.
|
||||
# Without this, a future schema change at upstream would silently produce
|
||||
# an empty `set()` here and report every one of our IDs as `missing`.
|
||||
if not (isinstance(entry, list) and entry and isinstance(entry[0], list)):
|
||||
print(_yellow(
|
||||
f" SKIP — upstream entry has unexpected shape: "
|
||||
f"{type(entry).__name__}"
|
||||
))
|
||||
return True
|
||||
|
||||
upstream_ids = {x for x in entry[0] if isinstance(x, str)}
|
||||
|
||||
phantom = upstream_ids - actual_ids
|
||||
missing = actual_ids - upstream_ids
|
||||
|
||||
if phantom or missing:
|
||||
print(_red(" FAIL — drift between code and Manager metadata:"))
|
||||
if phantom:
|
||||
print(_red(f" Manager lists {len(phantom)} ID(s) not in our code:"))
|
||||
for x in sorted(phantom):
|
||||
print(_red(f" - {x}"))
|
||||
if missing:
|
||||
print(_red(f" Our code registers {len(missing)} ID(s) not in Manager:"))
|
||||
for x in sorted(missing):
|
||||
print(_red(f" - {x}"))
|
||||
print(
|
||||
_yellow(
|
||||
" -> Manager metadata is community-maintained. "
|
||||
"Drift here means an issue or PR to ltdrdata/ComfyUI-Manager. "
|
||||
"See issue #44 for the canonical write-up."
|
||||
)
|
||||
)
|
||||
return False
|
||||
|
||||
print(_green(f" PASS — Manager metadata matches our {len(actual_ids)} IDs"))
|
||||
return True
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
# Check 4 — workflows (fixture node-ID stability)
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
# Heuristic: a node `type` value in a workflow JSON is treated as a Koolook
|
||||
# pack ID if it matches any of these patterns.
|
||||
_KOOLOOK_ID_PATTERNS = [
|
||||
re.compile(r"(?i)koolook"), # contains "koolook" anywhere
|
||||
re.compile(r"^Easy[A-Z_]"), # starts with "Easy" + capital/underscore
|
||||
re.compile(r"^easy_[A-Za-z]"), # starts with "easy_" + any letter
|
||||
# (covers easy_ImageBatch with capital I)
|
||||
]
|
||||
|
||||
|
||||
def _looks_like_koolook_id(nid: str) -> bool:
|
||||
return any(p.search(nid) for p in _KOOLOOK_ID_PATTERNS)
|
||||
|
||||
|
||||
def _extract_workflow_node_types(data: Any) -> set[str]:
|
||||
"""
|
||||
Extract node `type` / `class_type` strings from a workflow JSON.
|
||||
Handles two ComfyUI export formats:
|
||||
- Canvas format: top-level dict with a `nodes` array of {type, ...}.
|
||||
- API format: top-level dict where every value is a node spec
|
||||
with a `class_type` key.
|
||||
Returns an empty set if the shape doesn't match either format —
|
||||
caller decides whether that's a soft or hard failure.
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
return set()
|
||||
|
||||
# Canvas format
|
||||
if isinstance(data.get("nodes"), list):
|
||||
return {
|
||||
n["type"] for n in data["nodes"]
|
||||
if isinstance(n, dict) and isinstance(n.get("type"), str)
|
||||
}
|
||||
|
||||
# API format — only accept if EVERY value has class_type, so a malformed
|
||||
# canvas (e.g. nodes: null) doesn't accidentally fall through and pull
|
||||
# class_type from unrelated keys.
|
||||
values = list(data.values())
|
||||
if values and all(
|
||||
isinstance(v, dict) and isinstance(v.get("class_type"), str) for v in values
|
||||
):
|
||||
return {v["class_type"] for v in values}
|
||||
|
||||
return set()
|
||||
|
||||
|
||||
def check_workflows(actual_ids: set[str], verbose: bool = False) -> bool:
|
||||
print(_cyan("[4/4] workflows — fixture node-ID stability"))
|
||||
fixtures_dir = REPO_ROOT / "tests" / "workflows"
|
||||
if not fixtures_dir.is_dir():
|
||||
print(_yellow(" SKIP — no tests/workflows/ folder yet"))
|
||||
return True
|
||||
|
||||
fixtures = sorted(fixtures_dir.glob("*.json"))
|
||||
if not fixtures:
|
||||
# If the dir exists with a README but contains no *.json, that's a
|
||||
# regression in fixture coverage (someone deleted them or typoed
|
||||
# the dir name). Fail loudly rather than silently pass.
|
||||
if (fixtures_dir / "README.md").exists():
|
||||
print(_red(
|
||||
" FAIL — fixtures dir exists with README but no *.json files. "
|
||||
"If this is intentional, delete the README too."
|
||||
))
|
||||
return False
|
||||
print(_yellow(" SKIP — no *.json fixtures present"))
|
||||
return True
|
||||
|
||||
failures: list[str] = []
|
||||
total_koolook_refs = 0
|
||||
|
||||
for fx in fixtures:
|
||||
try:
|
||||
data = json.loads(fx.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError as exc:
|
||||
failures.append(f"{fx.name}: invalid JSON ({exc})")
|
||||
continue
|
||||
|
||||
types = _extract_workflow_node_types(data)
|
||||
|
||||
# Two-pronged matching:
|
||||
# (a) heuristic match — anything that LOOKS like a Koolook ID by
|
||||
# the name pattern. Catches drift where a fixture references
|
||||
# a renamed/removed Koolook ID even if the fork's suffixing
|
||||
# is opaque to the static check.
|
||||
# (b) actual_ids match — anything in the AST-extracted set is by
|
||||
# definition Koolook. Catches IDs that don't fit the heuristic
|
||||
# (e.g. future Koolook nodes named without an "Easy"/"easy_"
|
||||
# prefix and without "koolook" in the ID itself).
|
||||
# Union for the "this fixture references N Koolook IDs" report;
|
||||
# heuristic-minus-actual for the "missing ID" failure flag.
|
||||
koolook_by_heuristic = {t for t in types if _looks_like_koolook_id(t)}
|
||||
koolook_by_actual = types & actual_ids
|
||||
koolook_total = koolook_by_heuristic | koolook_by_actual
|
||||
missing = koolook_by_heuristic - actual_ids
|
||||
|
||||
total_koolook_refs += len(koolook_total)
|
||||
|
||||
if verbose:
|
||||
print(_dim(
|
||||
f" {fx.name}: {len(types)} types, "
|
||||
f"{len(koolook_total)} Koolook (heuristic={len(koolook_by_heuristic)}, "
|
||||
f"actual={len(koolook_by_actual)}), "
|
||||
f"{len(missing)} missing"
|
||||
))
|
||||
if missing:
|
||||
for nid in sorted(missing):
|
||||
failures.append(f"{fx.name}: {nid} (no longer registered)")
|
||||
|
||||
if failures:
|
||||
print(_red(f" FAIL — {len(failures)} workflow regression(s):"))
|
||||
for f in failures:
|
||||
print(_red(f" {f}"))
|
||||
print(
|
||||
_yellow(
|
||||
" -> Either add a back-compat alias for the missing ID, "
|
||||
"or update the fixture if the rename was intentional + announced."
|
||||
)
|
||||
)
|
||||
return False
|
||||
|
||||
print(_green(
|
||||
f" PASS — {len(fixtures)} fixture(s) reference "
|
||||
f"{total_koolook_refs} Koolook node ID(s), all currently registered"
|
||||
))
|
||||
return True
|
||||
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
# Orchestration
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
CHECKS = ("static", "dispatch", "manager-meta", "workflows")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check", choices=CHECKS, action="append",
|
||||
help="Run only the named check (repeatable). Default: run all.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip", choices=CHECKS, action="append",
|
||||
help="Skip the named check (repeatable).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--verbose", action="store_true",
|
||||
help="Print per-check details.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
selected = set(args.check or CHECKS)
|
||||
skipped = set(args.skip or ())
|
||||
to_run = [c for c in CHECKS if c in selected and c not in skipped]
|
||||
if not to_run:
|
||||
print(_red("No checks selected after applying --check / --skip"), file=sys.stderr)
|
||||
return 2
|
||||
|
||||
print(_cyan(f"Pre-flight running: {', '.join(to_run)}"))
|
||||
print()
|
||||
|
||||
actual_ids: set[str] = set()
|
||||
results: dict[str, bool] = {}
|
||||
|
||||
if "static" in to_run:
|
||||
ok, actual_ids = check_static(verbose=args.verbose)
|
||||
results["static"] = ok
|
||||
print()
|
||||
|
||||
if "dispatch" in to_run:
|
||||
results["dispatch"] = check_dispatch(verbose=args.verbose)
|
||||
print()
|
||||
|
||||
if "manager-meta" in to_run:
|
||||
if not actual_ids and "static" not in to_run:
|
||||
ok, actual_ids = check_static(verbose=False)
|
||||
results["manager-meta"] = check_manager_meta(actual_ids, verbose=args.verbose)
|
||||
print()
|
||||
|
||||
if "workflows" in to_run:
|
||||
if not actual_ids and "static" not in to_run:
|
||||
ok, actual_ids = check_static(verbose=False)
|
||||
results["workflows"] = check_workflows(actual_ids, verbose=args.verbose)
|
||||
print()
|
||||
|
||||
failed = [n for n, ok in results.items() if not ok]
|
||||
|
||||
print(_cyan("Summary"))
|
||||
for n in to_run:
|
||||
marker = _green("PASS") if results.get(n) else _red("FAIL")
|
||||
print(f" {marker} {n}")
|
||||
|
||||
if failed:
|
||||
print()
|
||||
print(_red(f"{len(failed)} check(s) failed: {', '.join(failed)}"))
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user