AM Write Video
ProRes out of ComfyUI, alpha included
- image
- mask
- audio
- video
- image
- mask
- resolved_path
- info
- width
- height
- frame_rate
- frame_count
- video
ComfyUI's stock video save path is mp4 and h264, end of story. AM Write Video is the node that hands you a professional codec menu: ProRes 422/4444, DNxHR, h264/h265, VP9, in mov/mp4/mkv/webm containers - with real bitrate control, 10-bit pixel formats, and an audio track to mux in. For anyone delivering video that has to survive next to real footage, this is the exit node.
It's part of comfyui-am-vfx-tools ("AM VFX Tools" category), Adrian Meyer's 13-node VFX toolkit. PyAV on the encode side, the same engine as AM Read Video.
How it works
Pick a file_path (📂 Browse), an ext (mov/mp4/mkv/webm), then the three widgets that actually matter:
codec- prefixed by its canonical container:mov/prores,mov/dnxhr,mp4/h264,mp4/h265,mkv/h265,webm/vp9. The container half is guidance - other legal pairings are permitted.codec_profile- must match the codec's prefix (prores/422,h264/high...). A mismatched prefix raises at execute time, loudly, on purpose. ProRes options run proxy → lt → 422 → 422hq → 4444 → 4444xq.bitrate_or_crf- the quality knob. Empty = codec default. For h264/h265/vp9:crf=18(lower = better) or a bitrate like8M. For ProRes/DNxHR: bitrate only; CRF is ignored.
Then the pro details: pixel_format empty = auto per codec/profile, or override with e.g. yuv422p10le for 10-bit 4:2:2. frame_rate defaults to 25. gop_size 0 = codec default. The working_colorspace → output_colorspace OCIO transform defaults to Gamma 2.2 Rec.709 - the studio dailies standard. embed_workflow (default on) tags the graph into container metadata, so the file round-trips through the pack's drag-drop.
Alpha is the feature nobody else's save node does: wire a mask and, for ProRes 4444 / 4444 XQ only, it's encoded as alpha. h264/h265/vp9/dnxhr/ProRes 422 log a warning and drop the mask at the encoder boundary - the mask still appears on the output socket, it just doesn't survive into the container.
The video input: remux vs. transcode
Wire a video input and things get smart: it calls video.save_to(path), which packet-copies when the source codec matches the destination - no decode, near-instant, like remuxing in ffmpeg - and falls back to a full transcode otherwise. Either way no IMAGE batch is materialised. VideoFromComponents sources (like AM Read Image's video socket) only support MP4+H264 output, which is a real constraint to remember.
Inputs and outputs
image to encode, optional mask (alpha, ProRes 4444 only), optional audio to mux, optional video (remux/transcode). Outputs: image/mask passthroughs, resolved_path, info, width, height, frame_rate, frame_count, and video (the same VIDEO that was written, available downstream). Plus the same load_saved_from_disk trick as AM Write Image: flip it on to decode the existing container back into working colorspace instead of encoding.
Installing it
cd ComfyUI/custom_nodes
git clone https://github.com/am-pipeline-prod/comfyui-am-vfx-tools.git
cd comfyui-am-vfx-tools
pip install -r requirements.txt
Restart ComfyUI, or search comfyui-am-vfx-tools in ComfyUI Manager. The av>=14.0.0 PyAV dependency comes from that pip line - and PyAV's bundled FFmpeg is what actually provides the ProRes encoder, so if you went out of your way to link a system FFmpeg, make sure it has prores_ks with the 4444 profiles.
Where people get burned
The codec/profile prefix mismatch raising at execute time is a feature - let it. The bigger trap is expecting alpha in a codec that can't carry it: wire your matte into h264 and you'll get a warning and a container with no alpha. That's not a bug, it's codec physics. And if your output looks washed out compared to what you previewed, check the output colorspace - you may have written display-referred values into a container tagged for something else.
Inputs (31)
| Name | Type | Default | Description |
|---|---|---|---|
| file_path | STRING | Absolute output container path. Use the 📂 Browse button for the native dialog. | |
| ext | COMBO | mov | Container format. The codec dropdown below is filtered to codecs the chosen container natively pairs with. |
| seed | INT | -1-9223372036854776000–9223372036854776000 | Generation seed — metadata-only, never in the filename. -1 = look up the AM Seed registry by id(prompt). Any other value (typed or wired) wins over the registry. |
| use_batch | BOOLEAN | false | When On, append a runtime-discovered `_bNNNN` suffix (queue-iteration counter, scanned from the output dir). Off = no suffix; queue iterations overwrite each other. |
| frame_mode | COMBO | all | Which input batch frames to encode. single = only `first_frame`. range = `first_frame`..`last_frame` inclusive. all = every frame in the input batch. |
| frame_rate | FLOAT | 25.000.1–480 | Output container's encoded time base. |
| first_frame | INT | 11–999999 | Input batch index (1-based) for single mode; lower bound for range mode. Ignored in all mode. |
| last_frame | INT | -1-1–999999 | Input batch upper bound (1-based, inclusive) for range mode. -1 = auto = batch length. |
| codec | COMBO | mov/prores | Encoding codec, prefixed by its canonical container (`mov/prores`, `mp4/h264`, ...). The container half is guidance — `validate_container_codec` permits other legal pairings if `ext` is set differently. |
| codec_profile | COMBO | prores/422 | Codec profile, prefixed by the bare codec name (`prores/422`, `h264/main`). Must match the codec chosen above — mismatched prefix raises at execute time. |
| pixel_format | STRING | Output pixel format. Empty = (auto) per codec/profile. Override for specific subsampling / bit depth (e.g. `yuv422p10le` for 10-bit 4:2:2). | |
| bitrate_or_crf | STRING | Quality / bitrate setting. Empty = codec default. h264/h265/vp9: `crf=18` (quality, lower=better) or `8M` / `500k` (bitrate). prores/dnxhr: bitrate only (CRF ignored). | |
| gop_size | INT | 00–600 | 0 = codec default (typically 250 for h264/h265). Lower = more keyframes, larger files. |
| working_colorspace | COMBO | Display/sRGB - Display | Source colorspace — the space the upstream IMAGE tensor is in. The OCIO transform converts from this to `output_colorspace`. |
| raw_data | BOOLEAN | false | When On, skip the OCIO transform — pixels encoded verbatim. `working_colorspace` and `output_colorspace` are ignored. |
| output_colorspace | COMBO | Display/Gamma 2.2 Rec.709 - Display | Destination colorspace. The OCIO transform converts to this, and the value is tagged into the encoded container metadata (default `Gamma 2.2 Rec.709 - Display` — the studio dailies standard, falling back to `Rec.1886 Rec.709 - Display` on OCIO configs without the gamma 2.2 display variant). |
| embed_workflow | BOOLEAN | true | Embed the API graph (`comfyui/prompt`) as container metadata so the file is round-tripped via AM-Pipe drag-drop. Off = clean deliverable. Honors ComfyUI's global `--disable-metadata` flag. |
| reformat_mode | COMBO | off | Reformat mode. off = bypass, output matches input. scale = uniform scale by `scale` (other widgets ignored). to_box = resize/crop to a target W×H from `preset` or `target_width`/`target_height`. |
| scale | FLOAT | 1.000.01–16 | Uniform scale factor. Used when `reformat_mode=scale`; ignored otherwise. Output dimensions are round(input × scale). |
| preset | COMBO | Width/Height | Named output format. Used when `reformat_mode=to_box`. `Width/Height` = use the `target_width` / `target_height` widgets below. Any other entry overrides those widgets with the preset's resolution. |
| target_width | INT | 19201–16384 | Target output width in pixels. Used when `reformat_mode=to_box` AND `preset=Width/Height`; ignored when a named preset is selected. |
| target_height | INT | 10801–16384 | Target output height in pixels. Used when `reformat_mode=to_box` AND `preset=Width/Height`; ignored when a named preset is selected. |
| resize_type | COMBO | fit | How input maps into the target box. Used when `reformat_mode=to_box`. width/height = scale uniformly to match that edge. fit = scale to fit inside the box (letterbox; black where the box exceeds the scaled image). fill = scale to cover the box (crops the overflow). distort = scale W and H independently to exactly match the box (changes aspect). none = no scale; place input centered in the box (crop if larger, pad if smaller). Cropped-away/padded regions are TRANSPARENT — RGB sources are promoted to RGBA with alpha=0 in the padded area so downstream compositing is clean. |
| filter | COMBO | cubic | Pixel filter for resampling. impulse = nearest-neighbor (mask passes, exact pixel preservation). linear = bilinear (cheap, smooth). cubic = bicubic (default; the safe Nuke-equivalent). Lanczos4 = sharpest; for high-quality stills / final delivery. area = best for downscaling — anti-aliased mean pooling, softer but artifact-free. |
| output_dtype | COMBO | fp16 | Output tensor dtype. fp32 = ComfyUI default (4 bytes/sample). fp16 = half memory + half VRAM (2 bytes/sample). EXR-native precision; fits the [0,1] LDR + scene-linear range with headroom up to ~65504. Some downstream nodes assume fp32 — flip back to fp32 if you hit dtype errors. |
| show_preview | BOOLEAN | true | Show a thumbnail of the first encoded frame on the node. |
| load_saved_from_disk | BOOLEAN | false | OFF (default): encode upstream IMAGE batch to a video container. ON: skip the encode entirely, decode the existing container at the same path the Write would resolve, and apply the INVERSE OCIO transform to land back in `working_colorspace`. Upstream nodes feeding image/mask/audio are NOT evaluated when ON. IMAGE output is in `working_colorspace` in BOTH modes (the OCIO transform is scoped to the encoded container; downstream nodes always see working-cs). Honors `raw_data` (skips OCIO both ways). |
| imageopt | IMAGE | Image batch to encode. | |
| maskopt | MASK | Optional alpha channel as MASK (N×H×W float in [0,1]). MASK CONVENTION (stock ComfyUI): mask = 1 - alpha white (1.0) = 'area to inpaint' (source was transparent) black (0.0) = 'keep' (source was opaque) empty mask = all zeros (source has no alpha = fully visible) This is the SD-inpainting convention every stock ComfyUI mask-using node expects (LoadImage, MaskComposite, SetLatentNoiseMask, ImpactPack mask pipeline, etc.). Drop-in compatible with all of them. If you want NUKE-STYLE natural alpha (mask = alpha, where 1.0 = opaque), wire a MaskInvert node between this socket and your downstream consumer. When wired, the file is encoded with alpha = 1 - mask. Mismatched mask dimensions are auto-resized (cubic) to match the image's H,W. When the input IMAGE is already 4-channel and a MASK is also wired, MASK overrides the embedded alpha. Codec-aware: only ProRes 4444 / 4444 XQ in our codec table actually encode the alpha channel into the container. Wiring a MASK into a non-alpha codec (h264 / h265 / vp9 / dnxhr / ProRes 422 etc.) logs a warning and drops the mask at the ENCODER boundary — but the mask still appears on the `mask` OUTPUT socket for downstream use. | |
| audioopt | AUDIO | Audio track to mux into the container. Optional. | |
| videoopt | VIDEO | Optional VIDEO input. When wired, calls `video.save_to(path)` — packet-copy when source codec matches dest (no decode), PyAV transcode otherwise. Either way no IMAGE batch is materialised. Upstream image/mask/audio are lazy-skipped. OCIO/Reformat don't apply — the video is written as-is. `VideoFromComponents` sources only support MP4+H264 output. |
Outputs (9)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | Sliced IMAGE passthrough — the input image post-frame-slice / post-mask-fold / post-reformat, in `working_colorspace`. The OCIO transform is scoped to the encoded container only; downstream nodes see the same colorspace as the upstream chain. RGB only. |
| mask | MASK | MASK passthrough — the alpha that was written to disk (or would have been, if the codec supports alpha). MASK CONVENTION (stock ComfyUI): mask = 1 - alpha white (1.0) = 'area to inpaint' (source was transparent) black (0.0) = 'keep' (source was opaque) empty mask = all zeros (source has no alpha = fully visible) This is the SD-inpainting convention every stock ComfyUI mask-using node expects (LoadImage, MaskComposite, SetLatentNoiseMask, ImpactPack mask pipeline, etc.). Drop-in compatible with all of them. If you want NUKE-STYLE natural alpha (mask = alpha, where 1.0 = opaque), wire a MaskInvert node between this socket and your downstream consumer. Sources, in priority order: 1. The wired `mask` input (if any). 2. Embedded alpha from a 4-channel IMAGE input. 3. Empty mask (zeros) — input was 3-channel with no MASK wired. Sized + reformatted to match the IMAGE output. Useful when chaining another node after a Write without re-reading from disk. |
| resolved_path | STRING | Absolute path of the written container. |
| info | STRING | Human-readable summary: dimensions, codec/profile, fps, frame count. |
| width | INT | Encoded frame width in pixels. |
| height | INT | Encoded frame height in pixels. |
| frame_rate | FLOAT | Container's encoded frame rate (the value used by the encoder). |
| frame_count | INT | Number of frames encoded into the container. |
| video | VIDEO | VIDEO passthrough — the same VIDEO that was written, available downstream without re-reading from disk. When the IMAGE branch fired, emits a `VideoFromComponents` wrapping the IMAGE batch + frame_rate. When the VIDEO branch fired, emits the input VIDEO as-is. None on no-op. |