Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81d5922258 | ||
|
|
00700d19ea | ||
|
|
b407dec0f2 | ||
|
|
662bc5fec5 | ||
|
|
09ab8c85fc | ||
|
|
a80df3813f | ||
|
|
a011912aec | ||
|
|
03f0346264 | ||
|
|
4349b33736 |
@@ -4,6 +4,78 @@ All notable changes to ComfyUI-ACES-IO are documented here.
|
||||
|
||||
---
|
||||
|
||||
## [1.3.8] — 2026-03-16
|
||||
|
||||
### Changed
|
||||
- **Video Saver — path browser**: Replaced single `output_path` string with `output_dir` (directory browser, same `ACES_PATH` widget as EXR Saver) + `filename` string input. Extension is still appended automatically.
|
||||
- **Video Saver — color transform**: Added optional `ocio_config`, `input_transform`, and `colorspace` inputs (same pattern as EXR Saver). Converts `input_transform → colorspace` before encoding; skipped when `ocio_config` is disconnected.
|
||||
|
||||
---
|
||||
|
||||
## [1.3.7] — 2026-03-16
|
||||
|
||||
### Fixed
|
||||
- **ProRes MOV export crash**: `fps` is now wrapped in `Fraction` before being passed to PyAV's `add_stream(rate=...)`. PyAV's `to_avrational()` requires an object with a `.numerator` attribute (int or Fraction) — passing a plain float caused `AttributeError: 'float' object has no attribute 'numerator'`.
|
||||
|
||||
---
|
||||
|
||||
## [1.3.6] — 2026-03-16
|
||||
|
||||
### Changed
|
||||
- **README**: Updated documentation to cover all v1.3.x additions — OCIODisplay node, EXR color transforms, ProRes MOV export, ACES 1.2 auto-download, and Video Loader. Added dedicated sections for Display Transform, EXR Loader/Saver color transform inputs, and expanded Video Saver format table.
|
||||
- **CHANGELOG**: Back-filled entries for v1.3.0 through v1.3.4.
|
||||
|
||||
---
|
||||
|
||||
## [1.3.5] — 2026-03-16
|
||||
|
||||
### Added
|
||||
- **ACES IO — Display Transform (OCIODisplay)**: New node mirroring Nuke's `OCIODisplay` node. Applies a Display View Transform (input colorspace → display + view pipeline) and bakes the result into image data — unlike the Viewer node which is preview-only. Includes an **Invert** boolean to reverse the transform direction (display-referred → scene-referred), matching Nuke's `invert` knob exactly.
|
||||
|
||||
---
|
||||
|
||||
## [1.3.4] — 2026-03-15
|
||||
|
||||
### Changed
|
||||
- **EXR Saver** — added optional `ocio_config` input, `input_transform`, and `colorspace` knobs. Converts `input_transform → colorspace` before writing, mirroring Nuke's Write node. Conversion is skipped when `ocio_config` is disconnected.
|
||||
- **EXR Loader** — added optional `ocio_config` input, `colorspace`, and `output_transform` knobs. Converts `colorspace → output_transform` after loading, mirroring Nuke's Read node. Conversion is skipped when `ocio_config` is disconnected.
|
||||
|
||||
---
|
||||
|
||||
## [1.3.3] — 2026-03-15
|
||||
|
||||
### Changed
|
||||
- Version bump (`pyproject.toml`).
|
||||
|
||||
---
|
||||
|
||||
## [1.3.2] — 2026-03-15
|
||||
|
||||
### Changed
|
||||
- **ACES 1.2 always in dropdown**: `ACES 1.2` is now a permanent entry in `BUILTIN_CONFIGS` alongside ACES 1.3/2.0 — no longer conditionally injected only when the file already exists.
|
||||
- **Auto-download on first use**: `load_config()` now triggers the ACES 1.2 download automatically when the config file is missing, then loads it immediately — no manual path required.
|
||||
- Renamed preset label `"Custom path (ACES 1.2 / other)"` → `"Custom path (other)"` since ACES 1.2 has its own dedicated entry.
|
||||
|
||||
---
|
||||
|
||||
## [1.3.1] — 2026-03-15
|
||||
|
||||
### Changed
|
||||
- Version bump (`pyproject.toml`).
|
||||
|
||||
---
|
||||
|
||||
## [1.3.0] — 2026-03-15
|
||||
|
||||
### Added
|
||||
- **ProRes MOV export**: Video Saver now supports MOV ProRes 422, ProRes 422 HQ, ProRes 4444, and ProRes 4444 XQ formats via PyAV (`prores_ks` encoder; 10-bit YUV 4:2:2 / 4:4:4; alpha channel preserved for 4444 variants when input has 4 channels).
|
||||
|
||||
### Fixed
|
||||
- **ACES 1.2 auto-download not running on fresh installs**: `download_aces12()` and `_refresh_aces12()` are now called from `__init__.py` at startup so the config is fetched before the first node execution.
|
||||
- **`BUILTIN_CONFIG_KEYS` stale-reference bug**: The list is now mutated in-place in `_refresh_aces12()` so `nodes.py`'s imported binding always reflects updated keys.
|
||||
|
||||
---
|
||||
|
||||
## [1.2.0] — 2026-03-11
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
# ComfyUI-ACES-IO
|
||||
|
||||
Professional OpenColorIO / ACES color-management nodes for ComfyUI, mirroring Nuke's OCIO node set.
|
||||
Supports **ACES 2.0**, **ACES 1.3**, and **ACES 1.2** — with Nuke-style colorspace pickers, EXR sequence read/write, animated preview, and video export.
|
||||
Supports **ACES 2.0**, **ACES 1.3**, and **ACES 1.2** — with Nuke-style colorspace pickers, EXR sequence read/write, animated preview, ProRes MOV export, and video output.
|
||||
|
||||
---
|
||||
|
||||
## What's New in v1.3
|
||||
|
||||
- **OCIODisplay node** — bake a Display View Transform into image data with a one-click **Invert** toggle (display → scene-linear roundtrip)
|
||||
- **EXR Loader color transforms** — optional `ocio_config` input + `colorspace` / `output_transform` convert on load, mirroring Nuke's Read node
|
||||
- **EXR Saver color transforms** — optional `ocio_config` input + `input_transform` / `colorspace` convert before write, mirroring Nuke's Write node
|
||||
- **ProRes MOV export** — Video Saver now supports ProRes 422, 422 HQ, 4444, and 4444 XQ via PyAV (10-bit; alpha preserved for 4444)
|
||||
- **ACES 1.2 always in dropdown** — no longer requires a pre-existing download; auto-fetches the config (~130 MB) on first use
|
||||
|
||||
---
|
||||
|
||||
@@ -9,12 +19,13 @@ Supports **ACES 2.0**, **ACES 1.3**, and **ACES 1.2** — with Nuke-style colors
|
||||
|
||||
- **Full OCIO pipeline** — every node mirrors its Nuke counterpart
|
||||
- **ACES 2.0 & 1.3 built-in** — no download needed (bundled with PyOpenColorIO 2.3+)
|
||||
- **ACES 1.2 support** — point the Config Loader at your own `.ocio` / `.ocioz` file
|
||||
- **ACES 1.2 support** — always in the preset dropdown; auto-downloads on first use
|
||||
- **Nuke-style colorspace picker** — tabbed family browser with live search (ACES / Display / Input/ARRI / Input/Sony / Utility …)
|
||||
- **EXR Loader (Nuke Read node)** — auto-detects full sequences from any single frame; supports `render.0001.exr`, `render_0001.exr`, `####`, `%04d`; `all` / `range` / `single` frame modes; `error` / `black` / `hold` missing-frame policy
|
||||
- **EXR Loader (Nuke Read node)** — auto-detects full sequences from any single frame; supports `render.0001.exr`, `render_0001.exr`, `####`, `%04d`; `all` / `range` / `single` frame modes; `error` / `black` / `hold` missing-frame policy; optional color transform on load
|
||||
- **EXR Saver (Nuke Write node)** — full 16f / 32f EXR with ZIP, PIZ, DWAA and all standard codecs; optional color transform before write
|
||||
- **Animated preview** — sequence loads play back as animated WebP directly in the node
|
||||
- **Video Saver** — export IMAGE batches to MP4 (H.264), Animated WebP, or Animated GIF
|
||||
- **EXR Saver** — full 16f / 32f EXR with ZIP, PIZ, DWAA and all standard codecs
|
||||
- **Video Loader** — loads `.mov`, `.mp4`, `.mxf` and other formats; full ProRes support via PyAV
|
||||
- **Video Saver** — export IMAGE batches to MP4 (H.264), MOV ProRes (422 / 422 HQ / 4444 / 4444 XQ), Animated WebP, or Animated GIF
|
||||
- **Cache bypass** — every node re-executes on each queue run so colorspace changes always take effect
|
||||
|
||||
---
|
||||
@@ -33,7 +44,8 @@ Supports **ACES 2.0**, **ACES 1.3**, and **ACES 1.2** — with Nuke-style colors
|
||||
| ACES IO — Config Info | — (utility) | ACES IO/Utility |
|
||||
| ACES IO — EXR Loader | Read node | ACES IO/EXR |
|
||||
| ACES IO — EXR Saver | Write node | ACES IO/EXR |
|
||||
| ACES IO — Video Saver | — (MP4 / WebP / GIF export) | ACES IO/EXR |
|
||||
| ACES IO — Video Loader | — (MOV/ProRes/MP4 input) | ACES IO/EXR |
|
||||
| ACES IO — Video Saver | — (MP4 / ProRes / WebP / GIF export) | ACES IO/EXR |
|
||||
| ACES IO — Preview | PreviewImage | ACES IO |
|
||||
|
||||
---
|
||||
@@ -66,12 +78,13 @@ Then restart ComfyUI.
|
||||
| `Pillow` | Preview thumbnails, animated WebP / GIF export |
|
||||
| `opencv-python` | MP4 video export + EXR fallback if OpenEXR is unavailable |
|
||||
| `OpenEXR` *(optional)* | Full EXR read/write with all compression codecs |
|
||||
| `av` (PyAV) *(optional)* | Video input (MOV/MP4/MXF) + ProRes MOV export |
|
||||
|
||||
`PyOpenColorIO`, `numpy`, `Pillow`, and `opencv-python` install automatically via `pip install -r requirements.txt`.
|
||||
For full EXR compression support install OpenEXR:
|
||||
For full EXR compression support and ProRes export:
|
||||
|
||||
```bash
|
||||
pip install openexr
|
||||
pip install openexr av
|
||||
```
|
||||
|
||||
---
|
||||
@@ -107,6 +120,47 @@ Every colorspace, display, and view input has a **Browse** button that opens a N
|
||||
|
||||
---
|
||||
|
||||
## Display Transform Node (OCIODisplay)
|
||||
|
||||
`ACES IO — Display Transform` bakes a Display View Transform into the image data — unlike the Viewer node which is for preview only.
|
||||
|
||||
| Input | Description |
|
||||
|---|---|
|
||||
| `input_colorspace` | Source colorspace (e.g. ACEScg) |
|
||||
| `display` | Target display device (e.g. sRGB - Display) |
|
||||
| `view` | View transform (e.g. ACES 2.0 - SDR 100 nits (Rec.709)) |
|
||||
| `invert` | Reverses the transform — display-referred → input colorspace (scene-linear roundtrip) |
|
||||
|
||||
The `invert` toggle maps directly to Nuke's OCIODisplay `invert` knob, running the full display pipeline in reverse via `TRANSFORM_DIR_INVERSE`.
|
||||
|
||||
---
|
||||
|
||||
## EXR Loader — Color Transforms
|
||||
|
||||
Connect an `ocio_config` to apply a color transform on load:
|
||||
|
||||
| Input | Description |
|
||||
|---|---|
|
||||
| `colorspace` | Color space the EXR file is stored as (file/camera space) |
|
||||
| `output_transform` | Color space to deliver downstream (working/pipe space) |
|
||||
|
||||
When `ocio_config` is disconnected both fields are ignored and pixels pass through unchanged — mirrors Nuke's Read node behaviour.
|
||||
|
||||
---
|
||||
|
||||
## EXR Saver — Color Transforms
|
||||
|
||||
Connect an `ocio_config` to apply a color transform before writing:
|
||||
|
||||
| Input | Description |
|
||||
|---|---|
|
||||
| `input_transform` | Color space of the incoming image (working/pipe space) |
|
||||
| `colorspace` | Color space to store in the EXR file |
|
||||
|
||||
When `ocio_config` is disconnected both fields are ignored — mirrors Nuke's Write node behaviour.
|
||||
|
||||
---
|
||||
|
||||
## EXR Loader — Frame Modes
|
||||
|
||||
| `frame_mode` | Behaviour |
|
||||
@@ -144,6 +198,10 @@ Export any IMAGE batch to a video file directly from your graph:
|
||||
| Format | Notes |
|
||||
|---|---|
|
||||
| **MP4 (H.264)** | Standard video via OpenCV — plays in any media player |
|
||||
| **MOV ProRes 422** | Apple ProRes 422 — 10-bit YUV 4:2:2 via PyAV |
|
||||
| **MOV ProRes 422 HQ** | Apple ProRes 422 HQ — higher bitrate 10-bit via PyAV |
|
||||
| **MOV ProRes 4444** | Apple ProRes 4444 — 10-bit 4:4:4, alpha channel preserved via PyAV |
|
||||
| **MOV ProRes 4444 XQ** | Apple ProRes 4444 XQ — highest quality + alpha via PyAV |
|
||||
| **Animated WebP** | High-quality, plays in browsers and most modern viewers |
|
||||
| **Animated GIF** | Universal compatibility; 256-colour limit |
|
||||
|
||||
@@ -159,10 +217,10 @@ The node passes the IMAGE tensor through unchanged so it can sit anywhere in a g
|
||||
| ACES 2.0 Studio | 2.5 | Recommended for live-action / studio |
|
||||
| ACES 1.3 CG | 2.1 / 2.3 / 2.4 | Legacy, three OCIO versions |
|
||||
| ACES 1.3 Studio | 2.1 / 2.3 / 2.4 | Legacy, three OCIO versions |
|
||||
| ACES 1.2 | v1 (colour-science) | Supply path to downloaded config |
|
||||
| ACES 1.2 | v1 (colour-science) | Auto-downloads on first use (~130 MB) |
|
||||
| Custom path | any | Point to your own `.ocio` / `.ocioz` file |
|
||||
|
||||
ACES 1.2 config can be downloaded from [colour-science/OpenColorIO-Configs](https://github.com/colour-science/OpenColorIO-Configs).
|
||||
ACES 1.2 config can also be downloaded manually from [colour-science/OpenColorIO-Configs](https://github.com/colour-science/OpenColorIO-Configs).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+17
-17
@@ -133,29 +133,29 @@ def download_aces12():
|
||||
with zipfile.ZipFile(zip_path, "r") as zf:
|
||||
zf.extractall(extract_dir)
|
||||
|
||||
# The zip contains a top-level folder named OpenColorIO-Config-ACES-1.2/
|
||||
src_dir = os.path.join(extract_dir, "OpenColorIO-Config-ACES-1.2")
|
||||
if not os.path.isdir(src_dir):
|
||||
# Fallback: find any directory inside extract_dir
|
||||
entries = [
|
||||
e for e in os.listdir(extract_dir)
|
||||
if os.path.isdir(os.path.join(extract_dir, e))
|
||||
]
|
||||
if entries:
|
||||
src_dir = os.path.join(extract_dir, entries[0])
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"[ACES IO] Could not locate config directory inside zip."
|
||||
)
|
||||
# Find the directory that actually contains config.ocio — don't
|
||||
# assume a fixed top-level folder name as it varies across releases.
|
||||
src_dir = None
|
||||
for root, dirs, files in os.walk(extract_dir):
|
||||
if "config.ocio" in files:
|
||||
src_dir = root
|
||||
break
|
||||
if src_dir is None:
|
||||
raise RuntimeError(
|
||||
"[ACES IO] config.ocio not found anywhere in the downloaded zip."
|
||||
)
|
||||
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
shutil.copytree(src_dir, dest_dir, dirs_exist_ok=True)
|
||||
print(f"[ACES IO] ACES 1.2 config installed to: {dest_dir}")
|
||||
|
||||
except Exception as exc:
|
||||
print(f"[ACES IO] Warning: could not download ACES 1.2 config: {exc}")
|
||||
print("[ACES IO] You can install it manually from:")
|
||||
print(f"[ACES IO] {url}")
|
||||
raise RuntimeError(
|
||||
f"[ACES IO] Could not download ACES 1.2 config: {exc}\n"
|
||||
f" URL: {url}\n"
|
||||
f" Target: {dest_dir}\n"
|
||||
" Check network connectivity or download the zip manually and extract it there."
|
||||
) from exc
|
||||
finally:
|
||||
try:
|
||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||
|
||||
@@ -5,6 +5,7 @@ Mirrors Nuke's OCIO node set exactly:
|
||||
ACESIOConfig ≈ Project Settings → OCIO config
|
||||
ACESIOColorSpace ≈ OCIOColorSpace node
|
||||
ACESIOViewer ≈ Nuke Viewer (display + exposure + gamma + channel view)
|
||||
ACESIODisplay ≈ OCIODisplay node (display view transform + invert)
|
||||
ACESIOLook ≈ OCIOLookTransform node
|
||||
ACESIOFileLUT ≈ OCIOFileTransform node
|
||||
ACESIOLogConvert ≈ OCIOLogConvert node
|
||||
@@ -330,6 +331,88 @@ class ACESIOViewer:
|
||||
return (apply_processor(image, proc),)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 4b. ACESIODisplay — display view transform (OCIODisplay)
|
||||
# ============================================================================
|
||||
|
||||
class ACESIODisplay:
|
||||
"""
|
||||
Apply an OCIO Display View Transform to an image.
|
||||
Mirrors Nuke's OCIODisplay node.
|
||||
|
||||
Converts the image from input_colorspace through the selected
|
||||
display + view transform pipeline and bakes the result into the image data
|
||||
(unlike the Viewer node, which is preview-only).
|
||||
|
||||
invert When enabled, reverses the transform direction:
|
||||
display-referred image → input_colorspace (scene-referred).
|
||||
Useful for recovering scene-linear from a display-baked image
|
||||
or for roundtrip work.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"ocio_config": ("OCIO_CONFIG",),
|
||||
"input_colorspace": ("ACES_COLORSPACE", {"default": "ACEScg"}),
|
||||
"display": ("ACES_DISPLAY", {"default": "sRGB - Display"}),
|
||||
"view": ("ACES_VIEW", {"default": "ACES 2.0 - SDR 100 nits (Rec.709)"}),
|
||||
"invert": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "Invert the transform: display-referred → input colorspace",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
FUNCTION = "apply_display"
|
||||
CATEGORY = "ACES IO/Transform"
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, **kwargs):
|
||||
return float("nan")
|
||||
|
||||
def apply_display(self, image, ocio_config, input_colorspace, display, view, invert):
|
||||
cfg = ocio_config["config"]
|
||||
|
||||
# Validate display/view against the loaded config; fall back to defaults
|
||||
# when stored widget values don't match the selected config version.
|
||||
available_displays = get_displays(cfg)
|
||||
resolved_display = display.strip()
|
||||
if resolved_display not in available_displays:
|
||||
resolved_display = cfg.getDefaultDisplay()
|
||||
|
||||
available_views = get_views(cfg, resolved_display)
|
||||
resolved_view = view.strip()
|
||||
if resolved_view not in available_views:
|
||||
resolved_view = cfg.getDefaultView(resolved_display)
|
||||
|
||||
dv = ocio.DisplayViewTransform()
|
||||
dv.setSrc(input_colorspace.strip())
|
||||
dv.setDisplay(resolved_display)
|
||||
dv.setView(resolved_view)
|
||||
if invert:
|
||||
dv.setDirection(ocio.TRANSFORM_DIR_INVERSE)
|
||||
|
||||
try:
|
||||
proc = cfg.getProcessor(dv)
|
||||
except ocio.Exception as e:
|
||||
raise ValueError(
|
||||
f"ACESIODisplay: failed to build display transform.\n"
|
||||
f" input_colorspace = '{input_colorspace}'\n"
|
||||
f" display = '{resolved_display}'\n"
|
||||
f" view = '{resolved_view}'\n"
|
||||
f" invert = {invert}\n"
|
||||
f"OCIO error: {e}\n"
|
||||
f"Available displays: {available_displays}"
|
||||
)
|
||||
|
||||
return (apply_processor(image, proc),)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 5. ACESIOLook — apply one or more OCIO Looks (OCIOLookTransform)
|
||||
# ============================================================================
|
||||
@@ -607,13 +690,25 @@ class ACESIOInfo:
|
||||
return {"ui": {"text": [text]}, "result": (text,)}
|
||||
|
||||
|
||||
def _apply_colorspace(tensor: torch.Tensor, ocio_config: dict,
|
||||
src: str, dst: str) -> torch.Tensor:
|
||||
"""Apply an OCIO color space conversion to a tensor; no-op if src == dst."""
|
||||
src, dst = src.strip(), dst.strip()
|
||||
if src == dst:
|
||||
return tensor
|
||||
try:
|
||||
return apply_processor(tensor, ocio_config["config"].getProcessor(src, dst))
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Color transform '{src}' → '{dst}' failed: {e}") from e
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 9. ACESIOEXRSaver — save IMAGE tensor as OpenEXR
|
||||
# ============================================================================
|
||||
|
||||
class ACESIOEXRSaver:
|
||||
"""
|
||||
Save a ComfyUI IMAGE tensor as an OpenEXR file.
|
||||
Save a ComfyUI IMAGE tensor as an OpenEXR file — mirrors Nuke's Write node.
|
||||
|
||||
Supports full 16-bit half-float and 32-bit float with all standard
|
||||
EXR compression codecs (ZIP, PIZ, DWAA, …).
|
||||
@@ -621,6 +716,14 @@ class ACESIOEXRSaver:
|
||||
output_dir Directory to write into (use the Browse button to pick one).
|
||||
filename Base filename without extension; %04d is replaced by the
|
||||
frame/batch index, e.g. render_%04d → render_0001.exr
|
||||
|
||||
Color transform (Nuke Write-node style)
|
||||
────────────────────────────────────────
|
||||
Connect ocio_config then set:
|
||||
input_transform — color space the incoming image is in (working space)
|
||||
colorspace — color space to store in the EXR file (file space)
|
||||
The node converts input_transform → colorspace before writing.
|
||||
Leave ocio_config disconnected to write pixels as-is.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@@ -637,7 +740,16 @@ class ACESIOEXRSaver:
|
||||
{"default": COMPRESSION_KEYS[0]}),
|
||||
},
|
||||
"optional": {
|
||||
"start_frame": ("INT", {"default": 1, "min": 0, "max": 99999}),
|
||||
"start_frame": ("INT", {"default": 1, "min": 0, "max": 99999}),
|
||||
"ocio_config": ("OCIO_CONFIG",),
|
||||
"input_transform": ("ACES_COLORSPACE", {
|
||||
"default": "ACEScg",
|
||||
"tooltip": "Color space of the incoming image (working / pipe space)",
|
||||
}),
|
||||
"colorspace": ("ACES_COLORSPACE", {
|
||||
"default": "ACES2065-1",
|
||||
"tooltip": "Color space to store in the EXR file",
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -648,7 +760,12 @@ class ACESIOEXRSaver:
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def save(self, image, output_dir, filename, bit_depth, compression,
|
||||
start_frame=1):
|
||||
start_frame=1, ocio_config=None,
|
||||
input_transform="ACEScg", colorspace="ACES2065-1"):
|
||||
|
||||
if ocio_config is not None:
|
||||
image = _apply_colorspace(image, ocio_config, input_transform, colorspace)
|
||||
|
||||
B = image.shape[0]
|
||||
last_path = ""
|
||||
for b in range(B):
|
||||
@@ -848,6 +965,14 @@ class ACESIOEXRLoader:
|
||||
black Substitute a black frame for any missing frame.
|
||||
hold Repeat the last successfully loaded frame.
|
||||
|
||||
Color transform (Nuke Read-node style)
|
||||
────────────────────────────────────────
|
||||
Connect ocio_config then set:
|
||||
colorspace — color space the EXR is stored as (file space)
|
||||
output_transform — color space to deliver downstream (working space)
|
||||
The node converts colorspace → output_transform after loading.
|
||||
Leave ocio_config disconnected to return raw pixel values.
|
||||
|
||||
Outputs: image [B, H, W, C] · frame_count · first_frame · last_frame
|
||||
"""
|
||||
|
||||
@@ -879,6 +1004,15 @@ class ACESIOEXRLoader:
|
||||
"default": 24.0, "min": 1.0, "max": 120.0, "step": 0.5,
|
||||
"tooltip": "Animated preview playback speed",
|
||||
}),
|
||||
"ocio_config": ("OCIO_CONFIG",),
|
||||
"colorspace": ("ACES_COLORSPACE", {
|
||||
"default": "ACES2065-1",
|
||||
"tooltip": "Color space the EXR file is stored in (Nuke: colorspace knob)",
|
||||
}),
|
||||
"output_transform": ("ACES_COLORSPACE", {
|
||||
"default": "ACEScg",
|
||||
"tooltip": "Color space to deliver to downstream nodes (working / pipe space)",
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -896,7 +1030,10 @@ class ACESIOEXRLoader:
|
||||
frame_mode="all",
|
||||
first_frame=1001, last_frame=1001,
|
||||
missing_frames="error",
|
||||
preview_fps=24.0):
|
||||
preview_fps=24.0,
|
||||
ocio_config=None,
|
||||
colorspace="ACES2065-1",
|
||||
output_transform="ACEScg"):
|
||||
|
||||
path = os.path.expanduser(file_path.strip())
|
||||
if not path:
|
||||
@@ -913,6 +1050,8 @@ class ACESIOEXRLoader:
|
||||
arr = np.array(img, dtype=np.float32) / 255.0
|
||||
tensor = torch.from_numpy(arr).unsqueeze(0) # [1, H, W, C]
|
||||
print(f"[ACES IO] EXR Loader: plain image ({ext}) {path}")
|
||||
if ocio_config is not None:
|
||||
tensor = _apply_colorspace(tensor, ocio_config, colorspace, output_transform)
|
||||
previews = _save_animated_preview(tensor.clamp(0.0, 1.0), fps=preview_fps)
|
||||
return {
|
||||
"ui": {"images": previews},
|
||||
@@ -933,7 +1072,7 @@ class ACESIOEXRLoader:
|
||||
frame_set = set(detected)
|
||||
|
||||
if frame_mode == "all":
|
||||
frames_to_load = detected # every frame on disk
|
||||
frames_to_load = detected
|
||||
out_first, out_last = disk_first, disk_last
|
||||
|
||||
elif frame_mode == "range":
|
||||
@@ -956,6 +1095,10 @@ class ACESIOEXRLoader:
|
||||
f"{out_first}–{out_last} ({tensor.shape[0]} frames) "
|
||||
f"disk: {disk_first}–{disk_last}")
|
||||
|
||||
# ── optional output color transform ───────────────────────────────
|
||||
if ocio_config is not None:
|
||||
tensor = _apply_colorspace(tensor, ocio_config, colorspace, output_transform)
|
||||
|
||||
frame_count = tensor.shape[0]
|
||||
preview_tensor = tensor.clamp(0.0, 1.0)
|
||||
previews = _save_animated_preview(preview_tensor, fps=preview_fps)
|
||||
@@ -1115,9 +1258,17 @@ class ACESIOVideoSaver:
|
||||
Animated WebP High-quality lossless/near-lossless, plays in browsers.
|
||||
Animated GIF Universal compatibility; limited to 256 colours.
|
||||
|
||||
output_path
|
||||
Full path including filename. The correct extension is appended
|
||||
automatically if missing (.mp4 / .mov / .webp / .gif).
|
||||
output_dir Directory to write into (use the Browse button to pick one).
|
||||
filename Base filename without extension; the correct extension is
|
||||
appended automatically (.mp4 / .mov / .webp / .gif).
|
||||
|
||||
Color transform (Nuke Write-node style)
|
||||
────────────────────────────────────────
|
||||
Connect ocio_config then set:
|
||||
input_transform — color space the incoming image is in (working space)
|
||||
colorspace — color space to encode into the video file
|
||||
The node converts input_transform → colorspace before encoding.
|
||||
Leave ocio_config disconnected to encode pixels as-is.
|
||||
|
||||
The node passes the IMAGE tensor through unchanged so it can sit inline
|
||||
in any graph without interrupting the flow.
|
||||
@@ -1137,11 +1288,15 @@ class ACESIOVideoSaver:
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"output_path": ("ACES_PATH", {
|
||||
"default": os.path.expanduser("~/output.mp4"),
|
||||
"mode": "file",
|
||||
"placeholder": "/path/to/output (.mp4 / .mov / .webp / .gif)",
|
||||
"images": ("IMAGE",),
|
||||
"output_dir": ("ACES_PATH", {
|
||||
"default": os.path.expanduser("~/"),
|
||||
"mode": "dir",
|
||||
}),
|
||||
"filename": ("STRING", {
|
||||
"default": "output",
|
||||
"multiline": False,
|
||||
"placeholder": "output (extension added automatically)",
|
||||
}),
|
||||
"format": (cls.FORMATS, {"default": "MP4 (H.264)"}),
|
||||
"fps": ("FLOAT", {
|
||||
@@ -1149,6 +1304,17 @@ class ACESIOVideoSaver:
|
||||
"tooltip": "Frames per second",
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
"ocio_config": ("OCIO_CONFIG",),
|
||||
"input_transform": ("ACES_COLORSPACE", {
|
||||
"default": "ACEScg",
|
||||
"tooltip": "Color space of the incoming image (working / pipe space)",
|
||||
}),
|
||||
"colorspace": ("ACES_COLORSPACE", {
|
||||
"default": "sRGB Encoded Rec.709 (sRGB)",
|
||||
"tooltip": "Color space to encode into the video file",
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "STRING")
|
||||
@@ -1157,29 +1323,38 @@ class ACESIOVideoSaver:
|
||||
CATEGORY = "ACES IO/EXR"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def save_video(self, images, output_path, format, fps):
|
||||
path = output_path.strip()
|
||||
if not path:
|
||||
raise ValueError("ACESIOVideoSaver: output_path is empty.")
|
||||
def save_video(self, images, output_dir, filename, format, fps,
|
||||
ocio_config=None, input_transform="ACEScg",
|
||||
colorspace="sRGB Encoded Rec.709 (sRGB)"):
|
||||
|
||||
if ocio_config is not None:
|
||||
images = _apply_colorspace(images, ocio_config, input_transform, colorspace)
|
||||
|
||||
fname = filename.strip() or "output"
|
||||
_EXT = {
|
||||
"MP4 (H.264)": ".mp4",
|
||||
"MOV ProRes 422": ".mov",
|
||||
"MOV ProRes 422 HQ": ".mov",
|
||||
"MOV ProRes 4444": ".mov",
|
||||
"MOV ProRes 4444 XQ":".mov",
|
||||
"Animated WebP": ".webp",
|
||||
"Animated GIF": ".gif",
|
||||
}
|
||||
ext = _EXT.get(format, "")
|
||||
if not fname.lower().endswith(ext):
|
||||
fname += ext
|
||||
|
||||
path = os.path.join(output_dir.strip(), fname)
|
||||
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
|
||||
|
||||
if format == "MP4 (H.264)":
|
||||
if not path.lower().endswith(".mp4"):
|
||||
path += ".mp4"
|
||||
_write_mp4(images, path, fps)
|
||||
elif format in _PRORES_PROFILES:
|
||||
if not path.lower().endswith(".mov"):
|
||||
path += ".mov"
|
||||
profile, is_4444 = _PRORES_PROFILES[format]
|
||||
_write_prores_mov(images, path, fps, profile, is_4444)
|
||||
elif format == "Animated WebP":
|
||||
if not path.lower().endswith(".webp"):
|
||||
path += ".webp"
|
||||
_write_webp(images, path, fps)
|
||||
elif format == "Animated GIF":
|
||||
if not path.lower().endswith(".gif"):
|
||||
path += ".gif"
|
||||
_write_gif(images, path, fps)
|
||||
|
||||
B = images.shape[0]
|
||||
@@ -1226,8 +1401,9 @@ def _write_prores_mov(tensor: torch.Tensor, path: str, fps: float,
|
||||
src_fmt = "rgba64le" if has_alpha else "rgb48le"
|
||||
pix_fmt = "yuva444p10le" if is_4444 else "yuv422p10le"
|
||||
|
||||
from fractions import Fraction
|
||||
container = av.open(path, mode="w", format="mov")
|
||||
stream = container.add_stream("prores_ks", rate=fps)
|
||||
stream = container.add_stream("prores_ks", rate=Fraction(fps).limit_denominator(10000))
|
||||
stream.width = W
|
||||
stream.height = H
|
||||
stream.pix_fmt = pix_fmt
|
||||
@@ -1305,6 +1481,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"ACESIOConfig": ACESIOConfig,
|
||||
"ACESIOColorSpace": ACESIOColorSpace,
|
||||
"ACESIOViewer": ACESIOViewer,
|
||||
"ACESIODisplay": ACESIODisplay,
|
||||
"ACESIOLook": ACESIOLook,
|
||||
"ACESIOFileLUT": ACESIOFileLUT,
|
||||
"ACESIOLogConvert": ACESIOLogConvert,
|
||||
@@ -1320,6 +1497,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ACESIOConfig": "ACES IO — Config Loader",
|
||||
"ACESIOColorSpace": "ACES IO — ColorSpace (OCIOColorSpace)",
|
||||
"ACESIOViewer": "ACES IO — Viewer (Nuke Viewer)",
|
||||
"ACESIODisplay": "ACES IO — Display Transform (OCIODisplay)",
|
||||
"ACESIOLook": "ACES IO — Look Transform (OCIOLookTransform)",
|
||||
"ACESIOFileLUT": "ACES IO — File LUT (OCIOFileTransform)",
|
||||
"ACESIOLogConvert": "ACES IO — Log Convert (OCIOLogConvert)",
|
||||
|
||||
+2
-5
@@ -104,12 +104,9 @@ def load_config(preset: str, custom_path: str = "") -> ocio.Config:
|
||||
if builtin_name == _ACES12_CFG:
|
||||
logger.info("[ACES IO] ACES 1.2 config not found — downloading now …")
|
||||
from .install import download_aces12
|
||||
download_aces12()
|
||||
download_aces12() # raises RuntimeError with details on failure
|
||||
if not os.path.isfile(builtin_name):
|
||||
raise FileNotFoundError(
|
||||
f"OCIO config not found: '{builtin_name}'\n"
|
||||
"Download failed or was interrupted."
|
||||
)
|
||||
raise FileNotFoundError(f"OCIO config not found: '{builtin_name}'")
|
||||
if builtin_name not in _config_cache:
|
||||
_config_cache[builtin_name] = ocio.Config.CreateFromFile(builtin_name)
|
||||
return _config_cache[builtin_name]
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "comfyui-aces-io"
|
||||
version = "1.3.2"
|
||||
version = "1.3.8"
|
||||
description = "OpenColorIO / ACES color-management nodes for ComfyUI — mirrors Nuke's OCIO node set with ACES 2.0, 1.3, and 1.2 support, EXR sequence loading, animated preview, and video export."
|
||||
license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
|
||||
Reference in New Issue
Block a user