VideoMark Overlay
Stamp your name on the frames, whatever generated them
- images
- logo
- logo_mask
- images
A watermark node that has never heard of Wan
Every local video pipeline narrows to the same shape at the end: the model decodes to an IMAGE batch - Wan 2.2, LTX-2, MiniMax H3, Hunyuan, AnimateDiff, indistinguishable from here - and then something turns that batch back into a file. VideoMark Overlay sits in that gap. IMAGE batch in, your text and/or a PNG logo painted onto every frame, IMAGE batch out. No model load, no audio, no file I/O, no idea what generated the frames.
That sounds dull and it's the point. ComfyUI's post-processing options tend to be either "throw another diffusion pass at it" or nothing at all, and a watermark is neither - it's a deterministic pixel op, the kind of job a compositor does perfectly and a sampler only ruins. It also skips the export-review-reimport loop, and single images work unchanged: one photo is a batch of length 1, laid out from its own width and height.
Fair warning: this pack is obscure. Searching the r/StableDiffusion + r/comfyui + r/civitai corpus for "videomark" across two years returns nothing, so there's no adoption story to point you at - and the watermark talk that does exist mostly runs the other way, people removing marks rather than adding them.
How it actually works
The mechanism lives in render.py and it's the right one: at the start of a run the node renders a single RGBA "stamp" at the size of your frames - text, optional logo, stroke and shadow already baked in - then alpha-composites that one sprite onto each frame. Cost per frame is flat, so a 240-frame clip doesn't pay for 240 text renders. It's numpy + Pillow underneath, not torch math: no VRAM cost, nothing to download. Frames are copied before compositing, so upstream tensors aren't mutated.
The number that matters most is scale, the stamp's width as a percentage of frame width rather than pixels. That's why settings tuned at 480p survive a jump to 1440p.
The inputs you'll actually touch
mode-corner(the default; which corner viaposition),center,floating,tile. Corner is the answer most of the time:bottom_rightunless a platform's like/share buttons live there, in which casetop_left.text,scale(20% is a sane start for text, 8–18 for a logo),margin(distance from the edge, in pixels - 24–40 for 736×992, 36–64 for 1080p),opacity(0.85 at a corner, and drop to ~0.15 if you gocenter).style- a one-click preset:soft(default),plain,outline,outline_shadow,manual. While a preset is selected the four stroke/shadow controls are ignored entirely, so switch tomanualbefore hand-tuning.use_logois off by default, so a connected logo does nothing until you flip it. The image comes from the optionallogoIMAGE input (a wire beats the file) or fromlogo_file, theinput-folder filename the panel's "Upload Logo" button fills in for you.
Optional text_in takes a STRING from any upstream node - Text Concatenate, String Function - and connected, non-empty text overrides the widget. Blank upstream falls back to the widget rather than silently deleting your watermark, which is the sane behaviour when the upstream node returns "".
One output, images: SaveImage for a still, or your video-combine node (VHS Video Combine, Create Video → Save Video) for frames.
Install
# ComfyUI Manager: search "comfyui-videomark"
cd ComfyUI/custom_nodes
git clone https://github.com/kaerozhi/comfyui-videomark.git
Registry route: comfy node install comfyui-videomark. Either way there's no pip step - the pack declares zero dependencies and rides on the torch/numpy/Pillow ComfyUI already ships, which is rare enough in a node pack to be worth saying out loud. Restart afterwards: the visual panel lives in web/, and that directory is only scanned at startup.
Where people get burned
Fonts. 黑体 (simhei) has no © glyph, so a copyright notice renders as boxes; use 微软雅黑 / 宋体 / 楷体, or point font_file at an absolute path. If the dropdown shows only the built-in fallback, font_file is your only route to non-box characters.
Stroke eats your width budget. scale measures the stamp including the stroke, so cranking stroke_width makes the letters smaller. If the mark feels too heavy, lower stroke_opacity first - 1.0 is a solid black ring, 0.5–0.6 a soft edge, 0 is off.
Dark shadow on dark footage is invisible. A black drop shadow on a night shot contributes nothing. Keep the stroke, or set shadow_color light. shadow_offset: 0 turns the shadow into a symmetric halo - lighter to look at, and it doesn't eat into your margin.
On a single photo, start_pct, end_pct, fade_frames and float_cycles do nothing: there's no timeline to walk and no previous frame to fade from. The photo is frame 0, which the default 0→1 window already covers.
Batching mixed photos isn't this node's job. IMAGE is a same-size tensor; 1200×800 and 800×1200 can't share a batch.
Failures pass through silently. If rendering throws, you get your frames back unchanged and one [VideoMark] line in the console. No error - just an unmarked video. The panel also has no live preview by design; you see the result by rendering one.
Editing anything under web/ needs a ComfyUI restart. A JS-only tweak gets away with Ctrl+Shift+R.
Inputs (35)
| Name | Type | Default | Description |
|---|---|---|---|
| images | IMAGE | — | |
| mode | COMBO | corner | Watermark mode: corner fixed in one corner (least intrusive, best default) center dead centre - keep opacity around 0.15~0.25 floating wanders inside the safe area (hardest to crop out, worst to watch) tile repeated across the whole frame - hardest to remove in post |
| position | COMBO | bottom_right | Which corner to use in corner mode. Bottom-right blocks the least; switch to top-left if the platform overlays its own UI (like/share buttons) there. |
| text | STRING | © 2026 kaero | Watermark text. Multiple lines supported - just press Enter. |
| opacity | FLOAT | 0.850.02–1 | Overall opacity. Reference values: corner 0.75~1.0 (needs to stay legible) centre / tile 0.12~0.22 (higher starts to hurt viewing) floating 0.25~0.45 (too low gets erased after compression) |
| scale | FLOAT | 20.01–100 | Watermark width as a percentage of the frame width. Resolution independent - you never retune it. 15~25 for text, 8~18 for a logo. |
| margin | INT | 320–600 | Safe margin from the frame edge, in pixels. Floating and tiled stamps respect it too, so nothing ever gets clipped. 24~40 for 736x992; 36~64 for 1080p. |
| use_text | BOOLEAN | true | Turn off to show the logo only without deleting the text. |
| use_logo | BOOLEAN | false | Master switch for drawing the logo. Source is either the panel upload (stored as logo_file above) or an IMAGE linked to the logo input - the link wins. |
| layout | COMBO | vertical | How logo and text stack when both are present: vertical (logo on top) or horizontal (logo on the left). |
| color | STRING | #FFFFFF | Text colour. Accepts #RRGGBB / #RGB / r,g,b / a colour name. |
| style | COMBO | soft | Style preset - one click swaps the whole stroke / shadow set: soft both on but subtle (default) readable on light and dark footage, without grabbing attention plain both off - cleanest, but may vanish on bright backgrounds outline solid stroke only - only needed when contrast swings hard outline_shadow both maxed - most legible, also most noticeable manual ignore presets, use the four controls below Note: presets override the stroke / shadow values below - pick manual to tune them yourself. |
| use_stroke | BOOLEAN | true | Stroke switch (only read when style=manual; presets take over otherwise). A stroke rings the glyphs and reads louder than the text itself on high-contrast shots - if the watermark feels heavy, lower stroke_opacity before turning it off. |
| stroke_width | INT | 30–24 | Stroke thickness in pixels. Only applies when the stroke is on. 2~4 is plenty. |
| stroke_color | STRING | #000000 | Stroke colour. |
| stroke_opacity | FLOAT | 0.550–1 | Opacity of the stroke itself - lower this first when the watermark feels heavy; keeps more legibility than switching it off. 1.0 = solid black outline (loudest); 0.5~0.6 = soft ring (default); 0 = same as off. |
| use_shadow | BOOLEAN | true | Shadow switch (only read when style=manual). A shadow thickens the silhouette without the hard edge a stroke leaves - ⚠ a dark shadow is invisible on night / black footage - use a stroke there, or lighten shadow_color. |
| shadow_color | STRING | #000000 | Shadow colour. Dark shadows disappear on dark footage; use a light colour like #FFFFFF. |
| shadow_opacity | FLOAT | 0.400–1 | Opacity of the shadow. 0.3~0.5 reads as 'visible outline, not in the way'. |
| shadow_offset | INT | 5-80–80 | Shadow offset in pixels (positive = down-right). 4~8 looks natural; 0 turns it into a symmetrical soft glow - lighter, and costs no margin in a corner. |
| shadow_blur | INT | 80–100 | Shadow blur radius in pixels. Bigger is softer. 6~14 is natural; 0 = a hard duplicate. |
| angle | FLOAT | 0.0-180–180 | Rotate the whole watermark. For tiled marks, -20~-30 degrees is much harder to align and erase. |
| font | COMBO | (built-in fallback font - CJK glyphs render as boxes) | Font. Sorted CJK-first on Windows (Microsoft YaHei / SimHei / SimSun / KaiTi ...). Fonts you drop into ComfyUI's input/fonts folder show up in this list too. Note: SimHei has no glyph for © - it renders as a box. Use Microsoft YaHei / SimSun / KaiTi for text containing ©. |
| font_file | STRING | Font file path relative to ComfyUI's input folder, e.g. fonts/MyFont.otf. When set and the file exists it overrides the font list above. Absolute paths and '..' are rejected. | |
| float_path | COMBO | diagonal | Path followed while floating: diagonal / horizontal / vertical constant back-and-forth (triangle wave) circle elliptical loop random uniform random jumps - strongest anti-theft, most jarring |
| float_cycles | FLOAT | 1.00.1–200 | How many round trips across the whole clip. 1 = one very slow pass; 4~8 = clearly wandering but still readable. For the random path this means 'number of position changes' - try 2x the clip length in seconds. A single still has nothing to travel across: the seed pins it to one spot. |
| seed | INT | 00–4294967295 | Used by the random float path only. Fix it to reproduce the same positions. |
| start_pct | FLOAT | 0.0000–1 | Where the watermark starts appearing, as a fraction of total length. 0 = from frame one. Only meaningful for multi-frame sequences: a single still counts as frame 0. |
| end_pct | FLOAT | 1.0000–1 | Where it stops, as a fraction of total length. 1 = stays to the end. e.g. 0.6 -> 1.0 shows it only in the last 40%. Only meaningful for multi-frame sequences: a single still counts as frame 0. |
| fade_frames | INT | 00–240 | Fade-in / fade-out length in frames. 0 = hard cut. Only meaningful for multi-frame sequences: a single still has nothing to fade. |
| tile_gap | INT | 900–1200 | Spacing between tiled watermarks, in pixels. |
| logo_file | STRING | Logo file name, relative to ComfyUI's input folder. Clicking 'Upload logo' in the visual panel fills this in; you normally never edit it by hand. You can also type the name of a file already placed in the input folder, e.g. logo/kaero.png. Note: a linked IMAGE on the logo input always wins over this. | |
| text_inopt | STRING | Optional. Feed the text in from another node (any node that builds a string). When connected and non-empty the wired text wins - whatever you typed in the panel is ignored. Disconnected, or an empty string upstream, falls back to the panel text. Typical use: while comparing parameter variants (resolutions / LoRA strengths / seeds), splice this run's parameters into the frame so you can tell at a glance which settings produced it. Note: it takes one plain string - nothing is filled in for you, so build the text you want upstream. | |
| logoopt | IMAGE | Optional. A PNG logo with an alpha channel. Only drawn when use_logo is on. Transparency comes from logo_mask first; falls back to the PNG's own alpha. | |
| logo_maskopt | MASK | Optional. Transparency mask for the logo; falls back to the PNG's own alpha. ComfyUI's Load Image outputs a PNG's transparency as a MASK - just wire it in. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| images | IMAGE | — |