🎬AGSoft Video Compare
Judging video output without leaving ComfyUI
- video_a
- video_b
- wipe_preview
- side_by_side_preview
- diff_frames
You rendered the same shot twice - 480p from the sampler versus the same clip through an upscaler, or two LoRA strengths, or Wan against LTX - and now you have two files and no honest way to compare them. The default method is two players and a pause button, which is how you talk yourself into believing the expensive pipeline is better.
🎬AGSoft Video Compare moves that judgement into the graph: two inputs, a synced player in the node body, a divider you drag with the mouse, and a difference view for when your eyes start lying to you.
What it's doing under the hood
Both inputs are declared as the wildcard type *, so they accept a VIDEO object, an IMAGE frame batch, a dict with images/pixels, or a raw tensor, and each side is normalized to a float32 [N, H, W, C] tensor in 0–1. That's why it can sit downstream of a Load Video, a VAE decode or an upscale pass without complaint.
Then it gets careful about memory. start_second and compare_duration define a window applied to both sides, and for VIDEO inputs that window is applied before decoding through ComfyUI's own video.as_trimmed(), so the footage you skipped never enters RAM. IMAGE batches have no such API: they're materialized and sliced as a view, with the window seconds converted to frames using image_fps.
Finally it encodes four H.264 preview streams via PyAV (A, B, D for the amplified difference, S for the side-by-side concat) into temp/agsoft_video_compare, keyed on a hash of both sides' content plus the processing options - so streams re-encode only when the result would change, and dragging the wipe slider never touches the server.
The controls you'll actually touch
- video_a and video_b -
video_ais the reference,video_bthe candidate. The layouts are opinionated about that: B sits on the left of the wipe divider, A on the right. Swap them and you'll misread which side won. - view_mode -
wipe,side_by_side, ordifference(|A-B|*diff_scale+0.5, default 5x amplification). - start_second and compare_duration - default 0 and 20 seconds. Use the first to skip a title card. Read the second twice:
0means the whole clip, which the author warns needs huge RAM on long video. - image_fps - ignored entirely for VIDEO inputs, which carry their own fps. For IMAGE batches it's the only timing the node has, so if your clip is 16 fps (Wan) and you leave the default 24, your "10 second" window isn't 10 seconds.
Defaults cover the boring mismatches: resize_mode=to_a with lanczos, length_mode=trim_shortest. Set either to none or require_equal and you get a bilingual error instead of silent resampling. frame_step thins the image outputs (previews always keep every frame of the window); preview_width caps the cached stream resolution.
Outputs are three IMAGE batches - wipe_preview, side_by_side_preview, diff_frames - wiring straight into Save Image, or into the pack's Images To Video / Video Save nodes if you want an mp4 of the comparison. The player's save button drops the displayed frame into ComfyUI/output as AGSoft_VideoCompare_<mode>_<timestamp>.png, the least painful way to attach evidence to a comparison post.
Installing it
ComfyUI Manager: search comfyui-AGSoft. Manually:
cd ComfyUI/custom_nodes
git clone https://github.com/Art-xmaster/comfyui-AGSoft
Restart ComfyUI. requirements.txt pulls numpy, opencv-python and translators - that last one is for the pack's Translate nodes, not this one. No model downloads.
The player needs PyAV, which core ComfyUI already ships. Without it you get PyAV missing: preview streams disabled in the console, an empty viewport, and working IMAGE outputs.
It's a terminal node (OUTPUT_NODE), so loaders plus this node is a valid graph that runs, and IS_CHANGED = NaN means it recomputes on every queue instead of caching. Fine for a viewer - just expect it to re-decode both sides each run.
Where people get burned
Hard-refresh the browser (Ctrl+Shift+R) after installing - the player is a DOM widget injected by web/js/agsoft_video_compare.js, and a stale frontend shows a node with no viewport. It also sits on a "run the graph" placeholder until metadata arrives, polled every 1.5 seconds.
Feels slow, or the machine starts swapping. Almost always compare_duration = 0 on a long clip, or a huge preview_width.
"Size mismatch" or "Length mismatch" errors. Two branches diverged in resolution (common after an upscaler) or frame count (common after RIFE). Pick the matching resize_mode / length_mode instead of leaving the strict ones on.
A result that isn't the node's fault. A 30-step render against a 20-step one, or a forgotten control_after_generate, tells you nothing except that you changed two things. The KB's upscaling write-up makes the same point: a misconfigured entrant is the usual reason the new thing appears to lose.
Frontend churn. This is a custom DOM widget in the node body - the surface ComfyUI's Nodes 2.0 Vue rewrite broke for other packs. If the player renders weirdly, try the legacy canvas.
One caveat worth naming: this is a single-author pack with no English-language Reddit footprint, so you're early. But it's readable, so grep the class when something misbehaves.
Inputs (13)
| Name | Type | Default | Description |
|---|---|---|---|
| video_a | * | Reference side: VIDEO object or IMAGE frame batch. --- Эталон: объект VIDEO или батч кадров IMAGE. | |
| video_b | * | Candidate side: VIDEO object or IMAGE frame batch. --- Кандидат: объект VIDEO или батч кадров IMAGE. | |
| view_mode | COMBO | wipe | UI view mode: wipe slider, server stream S with both full frames side by side, or the amplified difference stream. --- Режим просмотра UI: wipe-слайдер, серверный поток S с оба полными кадрами рядом или усиленный поток разницы. |
| wipe_position | INT | 500–100 | Divider position in percent: left of it shows B, right shows A. Also used for the wipe_preview output and the saved frame. --- Позиция разделителя в процентах: слева B, справа A. Используется для выхода wipe_preview и сохраняемого кадра. |
| diff_scale | FLOAT | 5.00.1–50 | Amplification of the visual difference: |A-B|*scale+0.5. --- Усиление визуальной разницы: |A-B|*scale+0.5. |
| start_second | FLOAT | 0.00–86400 | Window start in seconds for BOTH sides (e.g. 10.0 to skip the intro). Decoded via video.as_trimmed(), so skipped part never loads into RAM. --- Начало окна в секундах для ОБЕИХ сторон (например 10.0, чтобы пропустить вступление). Декодируется через video.as_trimmed(), пропущенная часть в RAM не загружается. |
| compare_duration | FLOAT | 20.00–86400 | Window length in seconds (default 20). 0 = compare until the end of the clip — WARNING: full decode of long clips needs huge RAM. --- Длина окна в секундах (по умолчанию 20). 0 = сравнивать до конца ролика — ВНИМАНИЕ: полный декод длинных роликов требует огромной RAM. |
| image_fps | FLOAT | 24.01–240 | Assumed fps for IMAGE-batch inputs (they carry no fps metadata): converts the window seconds to frames and times the preview streams. Ignored for VIDEO inputs, which carry their own fps. --- Предполагаемый fps для входов батчем IMAGE (у них нет метаданных fps): переводит секунды окна в кадры и задаёт тайминг превью-потоков. Игнорируется для входов VIDEO — у них свой fps. |
| resize_mode | COMBO | to_a | Size mismatch handling: resize B to A, A to B, both to the smaller, or none (error on mismatch). --- Обработка несовпадения размеров: B к A, A к B, оба к меньшему, или none (ошибка при несовпадении). |
| resize_method | COMBO | lanczos | Resampling algorithm used when sizes differ. --- Алгоритм ресемплинга при различии размеров. |
| length_mode | COMBO | trim_shortest | Length mismatch handling: trim to the shortest, pad the shorter with black frames, or require equal lengths. --- Обработка разной длины: обрезка до короткого, добивка короткого чёрными кадрами или требование равенства. |
| frame_step | INT | 11–240 | Use every N-th frame for the image outputs. Preview streams always use all frames of the window. --- Брать каждый N-й кадр для выходов IMAGE. Превью-потоки всегда используют все кадры окна. |
| preview_width | INT | 00–7680 | Max width of cached preview streams (0 = original; stream S is twice as wide as a single clip). --- Макс. ширина кэшируемых превью-потоков (0 = оригинал; поток S вдвое шире одного клипа). |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| wipe_preview | IMAGE | Frames split at wipe_position: left part from B, right part from A. --- Кадры, разрезанные по wipe_position: слева часть B, справа часть A. |
| side_by_side_preview | IMAGE | Full frames glued side by side: B on the left, A on the right (same layout as the side_by_side player mode). --- Полные кадры, склеенные рядом: B слева, A справа (та же раскладка, что и в режиме плеера side_by_side). |
| diff_frames | IMAGE | Amplified per-frame difference |A-B|*diff_scale+0.5 (compared frames). --- Усиленная покадровая разница |A-B|*diff_scale+0.5 (сравниваемые кадры). |