9 Commits
Author SHA1 Message Date
Bishoy SamaanandClaude Sonnet 4.6 81d5922258 v1.3.8: Video Saver — dir browser + color transform inputs (mirrors EXR Saver)
- output_path → output_dir (ACES_PATH dir browser) + filename string,
  matching EXR Saver layout exactly
- Added optional ocio_config / input_transform / colorspace inputs;
  applies color conversion before encoding, skipped when disconnected

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 08:09:50 +01:00
Bishoy SamaanandClaude Sonnet 4.6 00700d19ea v1.3.7: version bump + changelog entry for ProRes fps fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 08:01:09 +01:00
Bishoy SamaanandClaude Sonnet 4.6 b407dec0f2 fix: ProRes MOV export crash — wrap fps in Fraction for PyAV add_stream
PyAV's add_stream rate parameter calls to_avrational() which expects an
object with a .numerator attribute (int, Fraction, etc.), not a plain float.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 07:59:37 +01:00
Bishoy SamaanandClaude Sonnet 4.6 662bc5fec5 v1.3.6: README docs update — cover all v1.3.x changes + back-fill changelog
- README: added What's New in v1.3 section, Video Loader to nodes table,
  dedicated sections for OCIODisplay, EXR color transform inputs (Loader +
  Saver), expanded ProRes MOV format table, updated ACES 1.2 preset note
- CHANGELOG: back-filled entries for v1.3.0 through v1.3.4
- pyproject.toml: bump version to 1.3.6

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 07:41:53 +01:00
Bishoy SamaanandClaude Sonnet 4.6 09ab8c85fc v1.3.5: Add OCIODisplay node with invert boolean (mirrors Nuke OCIODisplay)
New ACESIODisplay node applies a Display View Transform and bakes it into
image data, unlike the Viewer node which is preview-only. Includes an
Invert boolean to reverse the transform direction (display → scene-linear),
exactly matching Nuke's OCIODisplay invert knob.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 07:26:11 +01:00
Bishoy SamaanandClaude Sonnet 4.6 a80df3813f v1.3.4: EXR Read/Write nodes — Nuke-style color transforms + config input
EXR Saver (Write node):
  - ocio_config input (optional) — connect to set ACES version
  - input_transform — color space of incoming image (working/pipe space)
  - colorspace      — color space to store in the EXR file
  Converts input_transform → colorspace before writing (mirrors Nuke Write)

EXR Loader (Read node):
  - ocio_config input (optional) — connect to set ACES version
  - colorspace       — color space the EXR file is stored as (file space)
  - output_transform — color space to deliver downstream (working/pipe space)
  Converts colorspace → output_transform after loading (mirrors Nuke Read)

Both nodes skip conversion when ocio_config is disconnected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 16:11:36 +01:00
Bishoy SamaanandClaude Sonnet 4.6 a011912aec v1.3.3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 15:54:04 +01:00
Bishoy SamaanandClaude Sonnet 4.6 03f0346264 fix: find config.ocio by walking extracted zip instead of assuming fixed path
The zip structure varies — walk the extracted tree to find the directory
containing config.ocio rather than hardcoding 'OpenColorIO-Config-ACES-1.2'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 15:48:48 +01:00
Bishoy SamaanandClaude Sonnet 4.6 4349b33736 fix: surface actual ACES 1.2 download error instead of swallowing it
download_aces12() now raises RuntimeError with the real exception,
URL, and target path so the user can see exactly what went wrong.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 15:43:41 +01:00
6 changed files with 364 additions and 59 deletions
+72
View File
@@ -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
+68 -10
View File
@@ -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
View File
@@ -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)
+204 -26
View File
@@ -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
View File
@@ -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
View File
@@ -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"