视频字幕稳定避让 / Subtitle Safe Zone
One Fixed Box for the Whole Shot, So Your Captions Stop Twitching
- images
- protect_mask
- preview
- region_mask
- x
- y
- width
- height
- mask_constraint_met
- report_json
You generated a clip and want captions on it. Detect the subject every frame and place text per frame, and the box shifts constantly - your captions crawl along the bottom like they're nervous. Broadcast subtitling solved this the boring way: pick one rectangle for the shot and keep it there.
That's the whole job of SubtitleSafeZonePlanner from jinny-wj's ComfyUI-Subtitle-Safe-Zone. It's a planner, not a renderer: no face detection, no SRT parsing, no glyphs drawn. Give it frames and a protection mask, and it hands back one rectangle - x, y, width, height - for whatever node actually lays out your text. Fair warning: 0.1.1 is a development build, tested on CPU and MPS with A100 explicitly untested.
How it decides where the box goes
It builds a grid of up to 9×9 candidate boxes inside your margin and scores each over the frames you asked for. Mask coverage matters most: how much of that rectangle is white in your protect_mask, averaged per frame at original resolution via an integral-image sum, so thin protected features like a stray hand survive. The worst frame wins, not the average frame. Then visual busyness - edge strength plus motion, on a low-res copy.
Feasible candidates are those whose worst-case overlap is under max_overlap; among them it picks the least busy, and only then does your top / bottom / center preference break ties, weighted 0.025 against visual cost. If nothing is feasible it returns the least-bad candidate anyway and tells you it failed. Mask feasibility first, quiet pixels second, your preference last.
The inputs worth touching
images- your frames, a normal ComfyUI IMAGE batch. Wire aVHS Load VideoIMAGE output (or any video loader) in here.protect_mask(optional) - white means do not cover. The input that makes the node smart. Feed it subject segmentation masks, merged if you have several. One mask broadcasts across the range; a sequence works if its frame count matches the video.box_height_ratioandmargin_ratio- defaults 0.14 and 0.04. Bump the height until two lines of text plus stroke and padding genuinely fit; it's easy to plan a box your font then overflows.
The rest you set once and forget: box_width_ratio (0.6), preferred, start_frame / end_frame (-1 = to the end, exclusive), sample_step, max_overlap (0.02).
The outputs
x, y, width, height are plain INTs in pixels, top-left origin - your text node must speak the same coordinates, and if it anchors centered you convert yourself. region_mask is one [1,H,W] mask, white inside the chosen box. mask_constraint_met is the boolean you'd gate on. report_json carries the box, the sampled frame indices, the overlap numbers and a warnings list.
preview is up to three frames (first, middle, last of your range) with the box drawn on: green if the constraint held across every checked frame, orange if it didn't. Debug picture, not your video. Note the flip - white in protect_mask means keep away, white in region_mask means put text here.
Install
No pip step, no models, no network calls - pyproject.toml declares dependencies = [], and the node imports the host's NumPy and PyTorch at run time.
cd ComfyUI/custom_nodes
git clone https://github.com/jinny-wj/ComfyUI-Subtitle-Safe-Zone.git
Restart ComfyUI and search Subtitle Safe Zone or 视频字幕稳定避让. Manager may find it too, though the repo's own notes say don't count on the registry listing until its security check clears - the clone always works. Start with examples/subtitle-safe-zone.json: eight 640×360 solid frames, a SolidMask protecting the bottom half. If it fails to load, that's your host libraries, not this pack:
python -m unittest discover -s tests -v # 19 tests, CPU/MPS, no GPU needed
python tools/check_environment.py # read-only numpy/torch/python inventory
Where people will get burned
The sharpest trap is in the source. mask_constraint_met is only true when a mask was provided, a feasible box was found and the number of sampled frames equals the full frame range. Raise sample_step to speed things up and you can never get green, however good the answer is - you traded a verified plan for an unverified one. Keep sample_step = 1 for anything you care about.
Passing no mask degrades it to "least busy pixels", which will happily choose a static talking face: no motion, and smooth skin has almost no edges. max_overlap is a mean of soft mask values, so the default 2% can still sit on a small important detail - if your subject's hands must never be covered, set it to 0 and widen the mask.
Wrong input errors rather than silently misbehaving: a protect_mask that doesn't match the frame size, a box plus margin that won't fit the canvas, an IMAGE that isn't [frames, height, width, 3]. Frame numbers index the batch you passed, so if you trim the video or change fps upstream, recalculate start_frame / end_frame.
Last thing: this repo is only the planner. Picture-in-picture lives in a separate ComfyUI-Video-PiP repo - and if you installed the old merged prototype that bundled both, remove it first. Same node IDs, duplicate registration.
Inputs (10)
| Name | Type | Default | Description |
|---|---|---|---|
| images | IMAGE | — | |
| box_width_ratio | FLOAT | 0.600.05–1 | — |
| box_height_ratio | FLOAT | 0.140.02–1 | — |
| margin_ratio | FLOAT | 0.040–0.4 | — |
| preferred | COMBO | 3 options: bottom, top, center | |
| start_frame | INT | 0 | — |
| end_frame | INT | -1 | 不含结束帧;-1 表示直到视频末尾。 |
| sample_step | INT | 11–120 | 1 为逐帧检查;增加会漏掉短暂遮挡。 |
| max_overlap | FLOAT | 0.0200–1 | — |
| protect_maskopt | MASK | 白色=避让;单张广播或与视频逐帧对应,尺寸须匹配。 |
Outputs (8)
| Name | Type | Description |
|---|---|---|
| preview | IMAGE | — |
| region_mask | MASK | — |
| x | INT | — |
| y | INT | — |
| width | INT | — |
| height | INT | — |
| mask_constraint_met | BOOLEAN | — |
| report_json | STRING | — |