Nodes/Comfyui-Tiktok-Caption/TikTok Caption (tscaps)
ComfyUI Node

TikTok Caption (tscaps)

Render animated subtitles and composite them over your video

By obvirm·Created 9 months ago·Updated about a month ago· 2
TikTok Caption (tscaps)
  • preview_image
  • IMAGE
  • MASK
srt1 00:00:00,000 --> 00:00:02,000 WAH GILA BANGET! 2 00:00:02,000 --> 00:00:04,000 RENDER PAKAI TSCAPS
width540
height960
font_family(template default)
font_size0.0
font_weight0
letter_spacing0.000
word_spacing0.00
line_spacing0.00
text_align(auto)
text_case(auto)
italic(auto)
underline(auto)
strikethrough(auto)
vertical_alignbottom
vertical_offset0.85
horizontal_aligncenter
horizontal_offset0.50
rotation0
text_color
highlight_color
outline0.02
outline_color
outline_stylefalse
split_by_speakerfalse
split_by(auto)
max_letters0
min_letters0
lines_max0
lines_min0
max_line_width0.00
gap_freefalse
css.segment{ font-family: var(--tscaps-font-family, 'Montserrat'), system-ui, sans-serif; font-weight: 800; font-style: var(--tscaps-font-style, normal); /* cqh = proportional to FRAME height. So when you change the output resolution, the caption ZOOMS together with the frame (text grows/shrinks with the frame), it does NOT re-grow relative to the preview box. This is exactly the "proxy" look you want: the preview is just a zoomed-to-fit view of the real frame. The preview canvas is rendered at the output resolution and CSS-scaled down to the box (see takumi_caption.ts). */ font-size: var(--tscaps-font-size, 13cqh); letter-spacing: var(--tscaps-letter-spacing, 0em); text-align: center; text-transform: none; text-decoration: none; color: var(--tscaps-primary-color, #ffffff); -webkit-text-stroke: var(--tscaps-outline-width, 0.02em) var(--tscaps-outline-color, #000); paint-order: stroke fill; text-shadow: var(--tscaps-outline-shadow, none); line-height: 1.3; } .line { display: block; text-align: center; white-space: normal; } .line + .line { margin-top: 0.1em; } .word { display: inline-block; margin: 0 0.14em; } .word.word-being-narrated { color: var(--tscaps-highlight-color, #ffd400); } .word.word-already-narrated { color: #e0e0e0; } .quote { color: var(--tscaps-quote-color, #e6d647); font-style: italic; } /* split_words_into_letters: per-letter spans for staggered animations */ .letter { display: inline-block; }
template(none / custom)
template_overrides{}

You know those captions on every TikTok, Reel, and Short - big bold text, each word flashing yellow as it's spoken? That's what this node renders, and it's one of the few pieces of the "make it look like a real short" pipeline that's genuinely hard to fake by hand. If you're generating vertical video in ComfyUI and shipping it anywhere, you will eventually want that look, and TikTokCaptionNode gives it to you without a browser plugin or a trip to CapCut.

It's a rendering node, not an AI node. You feed it subtitles, it burns them into a batch of frames at exactly your output resolution. No model downloads, no API calls, no key.

How it works

Under the hood it's a vendored copy of tscaps, a CSS-based subtitle rendering engine. The pipeline: take your SRT (or plain text), parse it into timed segments, apply CSS styling - fonts, colors, shadows, outlines, per-word highlight animation - and draw each frame at a fixed internal 30 FPS. The engine splits text into lines by balanced pixel width and applies .word-being-narrated / .word-already-narrated classes based on playback time, which is what drives the classic "current word glows yellow" effect.

The clever part is the split between preview and final render. The live preview runs in your browser via a custom frontend widget - a 9:16 box inside the node, animated in real time as you type. The actual output runs through CloakBrowser, a headless Chromium, so final frames come from a real browser engine rendering the same CSS. The README's whole pitch is pixel-identical output: what you see in the preview is exactly what lands in the tensor.

The node returns two things: IMAGE, a batch of frames at width×height (float32, 0–1, default 540×960), and MASK, which carries the alpha channel. That pairing matters: transparent areas in the caption come out black in the IMAGE, with the alpha in MASK - so you don't just take IMAGE alone and slap it over your video. Feed both into any composite node and it blends cleanly.

The inputs that matter

There are a lot of widgets here, but you can ignore most of them. The ones a beginner actually sets:

  • srt - your transcript. Real SRT works (it detects the --> timestamps); plain text works too, each line becoming a 2-second segment.
  • template - 31 built-in looks (anya, cleo, freya, kai, luna, …). Pick one and half your styling work disappears.
  • width / height - output frame size. Default 540×960 is exactly 9:16, right for vertical short-form.
  • font_family, font_size, text_color, highlight_color - the overrides you'll actually touch when a template isn't quite right. Zero or (auto) means "leave the template default alone."
  • vertical_align / vertical_offset, horizontal_align / horizontal_offset - where the caption sits. Default is bottom, 85% down.

The rest - rotation, outline, split_by, gap_free, the raw css string - is for when you want to go full custom. Note that font_size is in cqh units (percent of frame height), so text scales with resolution instead of reflowing weirdly at different sizes.

There's also an optional preview_image input: a reference frame shown behind the caption in the preview so you can see how text reads over your actual footage. It is never baked into the exported frames - the output stays transparent. Nice touch.

Installing it

Easiest path is ComfyUI Manager - search for "TikTok Caption". Otherwise:

cd ComfyUI/custom_nodes
git clone https://github.com/obvirm/Comfyui-Tiktok-Caption.git
cd Comfyui-Tiktok-Caption
pip install -r requirements.txt   # numpy, pillow, cloakbrowser

Restart ComfyUI. The only real dependency weight is cloakbrowser, which pulls Chromium on first render (and needs a working internet connection to do it). Node.js is listed as a requirement but only for building the frontend bundle - you don't need it at runtime.

Common issues

  • First render hangs or takes forever. That's Chromium downloading. Let it finish once and it's cached.
  • You get black frames. The node fails quietly - if rendering throws, it returns black IMAGE plus a zero MASK instead of crashing your graph (ComfyUI's stderr is broken, and the code deliberately avoids tracebacks). Check the ComfyUI log for a Caption render failed line.
  • Captions look washed out or have black boxes over your video. You're feeding the IMAGE output straight into the video without the MASK. Composite both together.
  • Text timing is off. Plain text lines are hardcoded to 2 seconds each. If you need real timing, write actual SRT with timestamps.

This pack is new and essentially unknown in the community right now - zero search impressions, no threads to speak of - so the usual "which node is better" comparison doesn't exist yet. But for getting that specific short-form caption look into your render pipeline, it's the only purpose-built tool in town, and the live preview makes it a joy to dial in.

Categoryimage/text

Inputs (36)

NameTypeDefaultDescription
srtSTRING1 00:00:00,000 --> 00:00:02,000 WAH GILA BANGET! 2 00:00:02,000 --> 00:00:04,000 RENDER PAKAI TSCAPS
widthINT5401–4096
heightINT9601–4096
font_familyCOMBO(template default)31 options: (template default), Inter Variable, Poppins, Montserrat Variable, Roboto, Anton, +25
font_sizeFLOAT0.00–100
font_weightINT00–900
letter_spacingFLOAT0.000-0.2–0.5
word_spacingFLOAT0.00-0.5–1
line_spacingFLOAT0.00-0.5–1
text_alignCOMBO(auto)4 options: (auto), left, center, right
text_caseCOMBO(auto)4 options: (auto), none, uppercase, lowercase
italicCOMBO(auto)3 options: (auto), on, off
underlineCOMBO(auto)3 options: (auto), on, off
strikethroughCOMBO(auto)3 options: (auto), on, off
vertical_alignCOMBObottom3 options: top, center, bottom
vertical_offsetFLOAT0.850–1
horizontal_alignCOMBOcenter3 options: left, center, right
horizontal_offsetFLOAT0.500–1
rotationFLOAT0-180–180
text_colorSTRING
highlight_colorSTRING
outlineFLOAT0.020–0.5
outline_colorSTRING
outline_styleBOOLEANfalse
split_by_speakerBOOLEANfalse
split_byCOMBO(auto)4 options: (auto), none, sentence, clause
max_lettersINT00–200
min_lettersINT00–200
lines_maxINT00–8
lines_minINT00–8
max_line_widthFLOAT0.000–1
gap_freeBOOLEANfalse
cssSTRING.segment{ font-family: var(--tscaps-font-family, 'Montserrat'), system-ui, sans-serif; font-weight: 800; font-style: var(--tscaps-font-style, normal); /* cqh = proportional to FRAME height. So when you change the output resolution, the caption ZOOMS together with the frame (text grows/shrinks with the frame), it does NOT re-grow relative to the preview box. This is exactly the "proxy" look you want: the preview is just a zoomed-to-fit view of the real frame. The preview canvas is rendered at the output resolution and CSS-scaled down to the box (see takumi_caption.ts). */ font-size: var(--tscaps-font-size, 13cqh); letter-spacing: var(--tscaps-letter-spacing, 0em); text-align: center; text-transform: none; text-decoration: none; color: var(--tscaps-primary-color, #ffffff); -webkit-text-stroke: var(--tscaps-outline-width, 0.02em) var(--tscaps-outline-color, #000); paint-order: stroke fill; text-shadow: var(--tscaps-outline-shadow, none); line-height: 1.3; } .line { display: block; text-align: center; white-space: normal; } .line + .line { margin-top: 0.1em; } .word { display: inline-block; margin: 0 0.14em; } .word.word-being-narrated { color: var(--tscaps-highlight-color, #ffd400); } .word.word-already-narrated { color: #e0e0e0; } .quote { color: var(--tscaps-quote-color, #e6d647); font-style: italic; } /* split_words_into_letters: per-letter spans for staggered animations */ .letter { display: inline-block; }
templateCOMBO(none / custom)32 options: (none / custom), anya, cleo, elio, freya, iris, +26
template_overridesSTRING{}
preview_imageoptIMAGE

Outputs (2)

NameTypeDescription
IMAGEIMAGE
MASKMASK