Nodes/Subtitle Safe Zone · 视频字幕稳定避让/视频字幕稳定避让 / Subtitle Safe Zone
ComfyUI Node

视频字幕稳定避让 / Subtitle Safe Zone

One Fixed Box for the Whole Shot, So Your Captions Stop Twitching

By jinny-wj·Created 6 days ago·Updated 3 days ago· 0
视频字幕稳定避让 / Subtitle Safe Zone
  • images
  • protect_mask
  • preview
  • region_mask
  • x
  • y
  • width
  • height
  • mask_constraint_met
  • report_json
box_width_ratio0.60
box_height_ratio0.14
margin_ratio0.04
preferred
start_frame0
end_frame-1
sample_step1
max_overlap0.020

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 a VHS Load Video IMAGE 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_ratio and margin_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.

CategoryVideo/Subtitle Safe Zone

Inputs (10)

NameTypeDefaultDescription
imagesIMAGE
box_width_ratioFLOAT0.600.05–1
box_height_ratioFLOAT0.140.02–1
margin_ratioFLOAT0.040–0.4
preferredCOMBO3 options: bottom, top, center
start_frameINT0
end_frameINT-1不含结束帧;-1 表示直到视频末尾。
sample_stepINT11–1201 为逐帧检查;增加会漏掉短暂遮挡。
max_overlapFLOAT0.0200–1
protect_maskoptMASK白色=避让;单张广播或与视频逐帧对应,尺寸须匹配。

Outputs (8)

NameTypeDescription
previewIMAGE
region_maskMASK
xINT
yINT
widthINT
heightINT
mask_constraint_metBOOLEAN
report_jsonSTRING