Speech Bubble Overlay (吹き出し合成)
Your model still can't spell — so draw the speech bubble yourself
- image
- image
You got the manga panel you wanted. Then you look at the balloon the model drew and it says ホわウア ゴわフ, or nothing, or a very confident string of alien glyphs. Even Qwen-Image - the best text renderer going - gives you a font you didn't choose at a size you didn't choose. For specific words in a specific balloon, prompting is the wrong tool.
SpeechBubbleNode is the right one. It's the single node in a one-node pack by miya805843, and it composites an actual manga balloon with actual font glyphs onto your image after the sampler. No diffusion, no seed lottery, no re-rolling.
Why it exists: as of mid-2025 the answer on r/comfyui to "can I add speech balloons in ComfyUI?" was no, open Photoshop - one poster had given up searching entirely, because "ComfyUI + speech" keeps returning text-to-speech nodes.
What it actually draws
A typography node wearing a post-processing costume. Asking a diffusion model to generate letters is doing with a sampler what Pillow does in milliseconds. From the source, not the README: it builds a silhouette mask for the bubble shape, dilates it with a max-filter to lay down a seamless border, then pastes the fill colour through that same mask. Text is drawn separately, as real glyphs. Everything renders on a 2× supersampled canvas and downsamples with Lanczos - that's the smooth outlines.
Because border and text are separate from the fill, bubble_opacity does exactly what the author claims: drop the whites to 60% so hair shows through, and the dialogue and outline stay fully crisp. It's the setting that makes a bubble feel placed rather than pasted.
Four shapes: ellipse, rounded_rect (narration box), thought (cloud plus trailing dots), shout (spiky burst). The last two are procedural from seed - thought_bump_size plus a new seed gets you endless organic variants.
The inputs you'll actually touch
image- your tensor.VAEDecode, or an upscaler if you're finishing big.text- multiline dialogue. Full-width spaces () and blank lines are your indentation tools. Convert the widget to an input and you can feed it from an LLM instead.direction-vertical(right-to-left columns) orhorizontal. Defaults to vertical, which is the point of the pack.bubble_type,tail_direction,tail_length- shape and pointer.tail_length: 0.0means no tail.pos_x/pos_y- the bubble's centre, normalized. The defaults (0.5, 0.35) land upper-middle, roughly where a face isn't.font_name/font_size- where people get burned. See below.
The rest - three colour fields, border_width, padding, max_chars_per_line, line_spacing, thought_bump_size, opacity - is tuning. One output, image, at input resolution; the node isn't an output node, so keep a SaveImage on the end.
Install
Manager, search ComfyUI-Speech-Bubble. Or:
cd ComfyUI/custom_nodes
git clone https://github.com/miya805843/ComfyUI-Speech-Bubble.git
Restart, done. requirements.txt asks only for Pillow>=9.5.0 and numpy>=1.23.0, both already in any working install, and torch is optional. No weights, no downloads. Against the dependency hell this ecosystem is notorious for, it's a holiday. It's MIT, version 1.0.0, packaged for the Comfy Registry - and new enough that you'd be an early adopter, so expect rough edges rather than polish.
Fonts are the whole job
font_name is built by scanning fonts/ inside the pack, then the OS font folders for Windows, macOS or Linux, reading each TrueType name table so you see 源暎アンチック v5 instead of yusei_magic.ttf. Three traps follow.
The list is whatever your machine has. On the box this node was indexed on, the default was URWGothic-Book - a Ghostscript font with zero Japanese coverage. Feed it kana and you get empty boxes; nothing's broken, the font can't draw them. Install a CJK font (fonts-noto-cjk or fonts-ipafont-gothic on Debian/Ubuntu) or drop a manga font into fonts/. Real manga type is antick style - 源暎アンチック and アンチックセシル are the ones the author names - and it beats Noto by a mile.
The scan is cached per process. Add a font file, then restart ComfyUI - refreshing the tab won't do it.
The font name travels inside the workflow. Open a Windows-built graph on Linux and the stored name doesn't exist there. Re-pick it. One click, if you know.
Where it bites
max_chars_per_line is a character count, not a width. Horizontal mode slices every N characters, so English breaks mid-word - write your own breaks and set N generously. Vertically it's characters per column; 10 is a sane manga default, but long lines stack into many columns and the balloon swallows the frame.
No bounds clamping. The balloon sizes itself to the text and centres on pos_x/pos_y; dialogue near an edge is cropped off the canvas.
It doesn't erase text the model already drew. If your panel came out with gibberish in a balloon, mask or inpaint that first. Clean pipeline: prompt for an empty balloon, then typeset.
In a batch the seed shifts per frame (the code adds the image index to seed), so each thought bubble varies. Lovely for variety, wrong for a consistent multi-panel sequence - run those one at a time, seed pinned.
Set it up once with a good CJK font and it stops being a node you think about - just the step before SaveImage, doing the one thing a 20-billion-parameter model is still worse at than a forty-year-old typesetting tradition.
Inputs (21)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — | |
| text | STRING | こんにちは! ComfyUIへようこそ! | — |
| direction | COMBO | vertical | 2 options: vertical, horizontal |
| bubble_type | COMBO | ellipse | 4 options: ellipse, rounded_rect, thought, shout |
| tail_direction | COMBO | bottom_left | 5 options: bottom_left, bottom_right, top_left, top_right, none |
| tail_length | FLOAT | 0.700–3 | — |
| pos_x | FLOAT | 0.500–1 | — |
| pos_y | FLOAT | 0.350–1 | — |
| font_name | COMBO | URWGothic-Book | 47 options: C059-BdIta, C059-Bold, C059-Italic, C059-Roman, D050000L, DejaVu Sans, +41 |
| font_size | INT | 368–256 | — |
| text_color | STRING | #000000 | — |
| bubble_color | STRING | #FFFFFF | — |
| border_color | STRING | #000000 | — |
| border_width | INT | 30–30 | — |
| bubble_opacityopt | FLOAT | 1.000–1 | — |
| paddingopt | INT | 240–200 | — |
| max_chars_per_lineopt | INT | 101–100 | — |
| line_spacingopt | INT | 60–50 | — |
| opacityopt | FLOAT | 1.000–1 | — |
| thought_bump_sizeopt | FLOAT | 1.000.2–3 | — |
| seedopt | INT | 00–18446744073709550000 | — |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |