Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c64e93180 | ||
|
|
26f188f0e0 | ||
|
|
a73d04dfe8 | ||
|
|
5d7377287e | ||
|
|
95e929863d | ||
|
|
a0eeebbfbf | ||
|
|
f93908247d | ||
|
|
2e566fc2e8 | ||
|
|
93f28874d5 | ||
|
|
d7708c7cd9 | ||
|
|
6d1d321691 | ||
|
|
e6441bbae3 | ||
|
|
76c82b7af5 | ||
|
|
03b0096be6 | ||
|
|
86f8686d30 | ||
|
|
63253a9267 | ||
|
|
28f8d58532 | ||
|
|
1dd948580c | ||
|
|
a58e0bf17d | ||
|
|
269e3a4f5b | ||
|
|
95dc2adf9a | ||
|
|
b02167cfda | ||
|
|
58f79cf91f |
@@ -6,163 +6,184 @@
|
||||
|
||||
**Originally inspired by:** [mikkel/ComfyUI-text-overlay](https://github.com/mikkel/ComfyUI-text-overlay)
|
||||
|
||||
This node extends and refines the classic ComfyUI Text Overlay concept with a **re-engineered rendering engine**, **precise alignment logic**, **new customization parameters**, and **enhanced batch handling** — making it the most advanced text overlay solution available for ComfyUI.
|
||||
This module provides the **most feature‑rich and precise text overlay system available for ComfyUI**, including:
|
||||
|
||||
* Completely re‑engineered layout engine
|
||||
* Pixel‑perfect multiline stroke alignment
|
||||
* Opacity‑aware backgrounds and shadows
|
||||
* Letter/line spacing, padding, and alignment controls
|
||||
* Full animation engine (fade + directional movement)
|
||||
* Batch-aware rendering with smart caching
|
||||
* Full video processing node with audio‑preserving re‑mux
|
||||
|
||||
|
||||
---
|
||||

|
||||
## ✨ Key Features
|
||||
|
||||
## ✨ Key Improvements & New Features
|
||||
### ✔️ 1. Rebuilt Rendering Engine
|
||||
|
||||
### 🧱 1. Rebuilt Rendering Engine
|
||||
* Accurate bounding boxes for multiline text
|
||||
* Consistent stroke widths (even‑pixel enforcement)
|
||||
* Crisp, stable text edges with alpha‑aware compositing
|
||||
|
||||
* Fully restructured text rendering logic for higher precision and cleaner anti-aliasing.
|
||||
* Pixel-accurate bounding box calculations ensure text aligns perfectly to the intended position.
|
||||
* Consistent stroke behavior across font sizes with improved blending and color stability.
|
||||
### ✔️ 2. Advanced Layout and Typography
|
||||
|
||||
### 🎨 2. Extended Styling and Layout Control
|
||||
* **Letter spacing** and **line spacing**
|
||||
* **Padding** around the text block
|
||||
* Horizontal and vertical alignment: `left` / `center` / `right`, `top` / `middle` / `bottom`
|
||||
* **Per‑axis offset** (`x_shift`, `y_shift`)
|
||||
* Automatic multiline wrapping based on image width
|
||||
|
||||
* **Line spacing**, **letter spacing** and **padding** parameters for advanced multi-line composition.
|
||||
* **Independent X/Y shifting** for fine-tuned manual positioning.
|
||||
* **Full hex color control** for both text fill and stroke.
|
||||
* Supports transparent overlays for non-destructive compositing.
|
||||
### ✔️ 3. Styling Options
|
||||
|
||||
### 🧭 3. Smarter Alignment and Positioning
|
||||
* Fill + alpha
|
||||
* Stroke + alpha
|
||||
* Shadow + alpha + offset
|
||||
* Rounded background box with padding, color, radius, and alpha
|
||||
|
||||
* Expanded support for:
|
||||
### ✔️ 4. Animation System
|
||||
|
||||
* Horizontal alignment: `left`, `center`, `right`
|
||||
* Vertical alignment: `top`, `middle`, `bottom`
|
||||
* Reliable anchor-based alignment even on varying resolutions and batch images.
|
||||
Supports:
|
||||
|
||||
### 🔠 4. Font Management
|
||||
* `fade_in`
|
||||
* `fade_out`
|
||||
* `move_from_top`
|
||||
* `move_from_bottom`
|
||||
* `move_from_left`
|
||||
* `move_from_right`
|
||||
|
||||
* Custom fonts can be loaded directly from the repo’s `/fonts` directory.
|
||||
* Supports both `.ttf` and `.otf` formats.
|
||||
* Graceful fallback to a system font if a specified font is unavailable.
|
||||
With easing:
|
||||
|
||||
### 🎞️ 5. New Animation Feature
|
||||
* `linear`
|
||||
* `ease_in`
|
||||
* `ease_out`
|
||||
* `ease_in_out`
|
||||
|
||||
* The node now supports smooth text animations via the new animations.py module.
|
||||
* Each animation type manipulates opacity and/or position offsets frame-by-frame.
|
||||
Animation Parameters:
|
||||
|
||||
Supported Animation Types:
|
||||
| Animation Kind | Description |
|
||||
| -------------------- | --------------------------------------------------------- |
|
||||
| **fade_in** | Gradually increases opacity from 0 → target. |
|
||||
| **fade_out** | Gradually decreases opacity from target → 0. |
|
||||
| **move_from_top** | Moves text upward from off-screen top into position. |
|
||||
| **move_from_bottom** | Moves text downward from off-screen bottom into position. |
|
||||
| **move_from_left** | Moves text from left edge into position. |
|
||||
| **move_from_right** | Moves text from right edge into position. |
|
||||
* `animation_frames`
|
||||
* `animation_opacity_target`
|
||||
* `pause_frames_before_start`
|
||||
|
||||
Easing Modes:
|
||||
| Easing | Behavior |
|
||||
| --------------- | ------------------------------------- |
|
||||
| **linear** | Uniform speed. |
|
||||
| **ease_in** | Starts slow, speeds up. |
|
||||
| **ease_out** | Starts fast, slows down. |
|
||||
| **ease_in_out** | Smooth acceleration and deceleration. |
|
||||
### ✔️ 5. Batch-Smart Processing
|
||||
|
||||
Animation Parameters
|
||||
| Parameter | Type | Description |
|
||||
| ---------------------------- | ------- | ----------------------------------------------------------- |
|
||||
| **animate** | Boolean | Enable or disable animation. |
|
||||
| **animation_kind** | Enum | Selects animation type (`fade_in`, `move_from_left`, etc.). |
|
||||
| **animation_frames** | Integer | Number of frames to complete the animation. |
|
||||
| **animation_ease** | Enum | Easing curve for smooth motion. |
|
||||
| **animation_opacity_target** | Float | Target opacity multiplier for animation. |
|
||||
* Automatically caches layout on first frame
|
||||
* Maintains perfect consistency across all frames
|
||||
* For animated batches: animates through first *N* frames, then holds the final pose
|
||||
|
||||
Behavior:
|
||||
### ✔️ 6. Full Video Support
|
||||
|
||||
* For single images, the node outputs either one static frame or an animated sequence.
|
||||
The **Advanced Text Overlay – Video** node:
|
||||
|
||||
* For batches/videos, animation applies over the first animation_frames frames, then holds the final pose.
|
||||
|
||||
Technical Summary:
|
||||
|
||||
The animation module provides:
|
||||
|
||||
* progress(t, T_minus1, ease): easing interpolation over frames.
|
||||
|
||||
* compute_opacity(kind, p, target): calculates alpha fade transitions.
|
||||
|
||||
* compute_offsets(kind, p, img_w, img_h): determines X/Y displacement for move animations.
|
||||
|
||||
These functions are integrated directly into the node’s rendering loop for seamless animated overlays
|
||||
|
||||
### ⚙️ 6. Batch and Workflow Enhancements
|
||||
|
||||
* Native **batch support** — automatically applies consistent overlay logic across all input images.
|
||||
* Optimized performance and memory use when processing large image sets.
|
||||
* Node parameters persist and preview correctly in ComfyUI sessions.
|
||||
|
||||
### 🧩 7. Developer & Extensibility Features
|
||||
|
||||
* Modularized code for easier maintenance and feature additions.
|
||||
* Clean separation between text layout and rendering logic.
|
||||
* Ready for further extensions such as shadow layers, gradient text, or dynamic variable injection.
|
||||
* Reads a video frame‑by‑frame
|
||||
* Applies the same overlay logic
|
||||
* Writes a new MP4
|
||||
* **Automatically re‑injects the original audio track with ffmpeg**
|
||||
* Optional `delete_original` flag
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Installation
|
||||
## 📥 Installation
|
||||
|
||||
To install the **Advanced Text Overlay** node:
|
||||
Clone the repository inside your ComfyUI `custom_nodes` directory:
|
||||
|
||||
1. Locate your ComfyUI `custom_nodes` directory.
|
||||
Typically found at:
|
||||
```bash
|
||||
git clone https://github.com/scofano/ComfyUI-Advanced-TextOverlay
|
||||
```
|
||||
|
||||
```
|
||||
ComfyUI/custom_nodes/
|
||||
```
|
||||
2. Clone this repository into that directory:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/scofano/ComfyUI-Advanced-TextOverlay
|
||||
```
|
||||
3. Restart ComfyUI.
|
||||
The node will appear under the **`image/text`** category as **“Advanced Text Overlay”**.
|
||||
Restart ComfyUI — the nodes will appear under **Advanced Text Overlay**.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Usage
|
||||
|
||||
1. Add the **Advanced Text Overlay** node to your ComfyUI workflow.
|
||||
2. Connect your image source to the node’s **image input**.
|
||||
3. Configure text and style parameters:
|
||||
### **Image Node: `Advanced Text Overlay`**
|
||||
|
||||
* Text content
|
||||
* Font and size
|
||||
* Fill and stroke colors
|
||||
* Alignment, spacing, and offset adjustments
|
||||
4. Connect the node’s output to your desired destination (e.g., preview, save image).
|
||||
Connect an image or batch → configure text parameters → render.
|
||||
|
||||
### **Video Node: `Advanced Text Overlay – Video`**
|
||||
|
||||
Provide a path to a video → configure overlay → output is written to ComfyUI's output directory.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Input Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ------------------------ | ------------- | ----------------------------------------------------------------------------- |
|
||||
| **image** | Image / Batch | Input image(s) to overlay text onto. |
|
||||
| **text** | String | The text content to render on the image. Supports multi-line text. |
|
||||
| **font** | String | Font name or filename (e.g., `arial.ttf`). Looks in `/fonts` or system fonts. |
|
||||
| **font_size** | Integer | Size of the text in pixels. |
|
||||
| **fill_color_hex** | String | Text fill color in hex format (e.g., `#FFFFFF`). |
|
||||
| **stroke_color_hex** | String | Stroke (outline) color in hex format (e.g., `#000000`). |
|
||||
| **stroke_thickness** | Float | Thickness of the text outline. |
|
||||
| **padding** | Integer | Additional space around the text block. Useful for multi-line layouts. |
|
||||
| **horizontal_alignment** | Enum | `left`, `center`, or `right` horizontal alignment. |
|
||||
| **vertical_alignment** | Enum | `top`, `middle`, or `bottom` vertical alignment. |
|
||||
| **x_shift** | Integer | Horizontal offset adjustment (pixels). |
|
||||
| **y_shift** | Integer | Vertical offset adjustment (pixels). |
|
||||
| **line_spacing** | Float | Vertical spacing between lines of text. |
|
||||
| **letter_spacing** | Float | Horizontal spacing between letters. |
|
||||
### Text & Typography
|
||||
|
||||
| Parameter | Description |
|
||||
| ---------------- | ----------------------------------------- |
|
||||
| `text` | Text to draw; supports multiline `\n` |
|
||||
| `all_caps` | Force uppercase |
|
||||
| `font` | Font name or file; auto-searches `/fonts` |
|
||||
| `font_size` | Pixel size |
|
||||
| `letter_spacing` | Per-character spacing |
|
||||
| `line_spacing` | Spacing between lines |
|
||||
|
||||
### Color & Stroke
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------------ | ---------------------------------------- |
|
||||
| `fill_color_hex` | Hex fill color |
|
||||
| `fill_alpha` | Opacity 0–1 |
|
||||
| `stroke_enable` | Toggle outline |
|
||||
| `stroke_color_hex` | Hex outline color |
|
||||
| `stroke_alpha` | Outline opacity |
|
||||
| `stroke_thickness` | Relative thickness (scaled by font size) |
|
||||
|
||||
### Layout & Position
|
||||
|
||||
| Parameter | Description |
|
||||
| ---------------------- | --------------------------- |
|
||||
| `padding` | Padding around text block |
|
||||
| `horizontal_alignment` | `left` / `center` / `right` |
|
||||
| `vertical_alignment` | `top` / `middle` / `bottom` |
|
||||
| `x_shift`, `y_shift` | Pixel offsets |
|
||||
|
||||
### Background Box
|
||||
|
||||
| Parameter | Description |
|
||||
| -------------- | ------------------------- |
|
||||
| `bg_enable` | Toggle background box |
|
||||
| `bg_padding` | Padding around text block |
|
||||
| `bg_radius` | Rounded corner radius |
|
||||
| `bg_color_hex` | Hex color |
|
||||
| `bg_alpha` | Opacity |
|
||||
|
||||
### Shadow
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------------ | ------------- |
|
||||
| `shadow_enable` | Toggle shadow |
|
||||
| `shadow_distance` | Pixel offset |
|
||||
| `shadow_color_hex` | Shadow color |
|
||||
| `shadow_alpha` | Opacity |
|
||||
|
||||
### Animation
|
||||
|
||||
| Parameter | Description |
|
||||
| --------------------------- | ------------------------------- |
|
||||
| `animate` | Enable animation |
|
||||
| `animation_kind` | Fade or movement animation |
|
||||
| `animation_frames` | How many frames animation lasts |
|
||||
| `animation_opacity_target` | Target alpha |
|
||||
| `animation_ease` | Easing curve |
|
||||
| `pause_frames_before_start` | Delay before animation begins |
|
||||
|
||||
### Video-Specific Parameters
|
||||
|
||||
| Parameter | Description |
|
||||
| ---------------------------- | ------------------------------------ |
|
||||
| `video_path` | Input video |
|
||||
| `filename_prefix` | Naming prefix for output |
|
||||
| `delete_original` | Remove input after processing |
|
||||
| `pause_seconds_before_start` | Pause duration (converted to frames) |
|
||||
|
||||
---
|
||||
|
||||
## 🖼️ Output
|
||||
## 🧪 Output
|
||||
|
||||
The node outputs the modified image (or batch of images) with the specified text overlay applied according to the given configuration.
|
||||
* Image node → modified image/batch
|
||||
* Video node → MP4 written to ComfyUI output directory, with audio preserved
|
||||
|
||||
---
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 629 KiB |
@@ -1,7 +1,22 @@
|
||||
import os
|
||||
import subprocess
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageChops
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
import imageio.v2 as imageio
|
||||
|
||||
try:
|
||||
from tqdm import tqdm
|
||||
except ImportError:
|
||||
# If tqdm isn't installed, fall back to a no-op wrapper
|
||||
def tqdm(x, **kwargs):
|
||||
return x
|
||||
|
||||
try:
|
||||
from comfy.utils import ProgressBar
|
||||
except Exception:
|
||||
ProgressBar = None
|
||||
|
||||
# Relative import so it works as a package module in ComfyUI
|
||||
from . import animations
|
||||
@@ -76,12 +91,15 @@ class TextOverlay:
|
||||
"animation_frames": ("INT", {"default": 32, "min": 1, "max": 1000, "step": 1}),
|
||||
"animation_ease": (["linear","ease_in","ease_out","ease_in_out"], {"default": "ease_in_out"}),
|
||||
"animation_opacity_target": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "display": "slider"}),
|
||||
|
||||
# how long to wait before starting overlay (frames for this node, seconds for the Video node)
|
||||
"pause_frames_before_start": ("INT", {"default": 0, "min": 0, "max": 100000, "step": 1}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "batch_process"
|
||||
CATEGORY = "image/text"
|
||||
CATEGORY = "Advanced Text Overlay"
|
||||
|
||||
# ---------------- helpers ----------------
|
||||
|
||||
@@ -369,7 +387,8 @@ class TextOverlay:
|
||||
animation_kind='fade_in',
|
||||
animation_frames=24,
|
||||
animation_ease='ease_out',
|
||||
animation_opacity_target=1.0
|
||||
animation_opacity_target=1.0,
|
||||
pause_frames_before_start=0,
|
||||
):
|
||||
"""
|
||||
Single image (H,W,C):
|
||||
@@ -378,16 +397,23 @@ class TextOverlay:
|
||||
|
||||
Batch (B,H,W,C) video:
|
||||
- animate=False -> draw per-frame with no animation changes
|
||||
- animate=True -> animation starts at frame 0 and completes at frame `animation_frames-1`.
|
||||
From then on, the final pose is held until frame B-1.
|
||||
- animate=True -> animation starts after `pause_before_start` frames,
|
||||
then runs for up to `animation_frames` frames.
|
||||
Frames after that hold the final pose.
|
||||
|
||||
NOTE: This node has no FPS info; here `pause_before_start` is effectively
|
||||
in frames (not seconds). For 30fps video and a 1s pause, use 30.
|
||||
"""
|
||||
|
||||
pause_frames = max(0, int(pause_frames_before_start))
|
||||
|
||||
# Single image (H, W, C)
|
||||
if len(image.shape) == 3:
|
||||
np_img = image.cpu().numpy()
|
||||
pil_img = Image.fromarray((np_img * 255).astype(np.uint8))
|
||||
|
||||
if not animate:
|
||||
# No timeline here → pause_before_start is ignored
|
||||
out_img = self.draw_text(
|
||||
pil_img, text, all_caps,
|
||||
font_size, letter_spacing, font,
|
||||
@@ -406,7 +432,7 @@ class TextOverlay:
|
||||
T = max(1, int(animation_frames))
|
||||
outs = []
|
||||
|
||||
# Prime layout cache
|
||||
# Prime layout cache (fills self._cached)
|
||||
_ = self.draw_text(
|
||||
pil_img, text, all_caps,
|
||||
font_size, letter_spacing, font,
|
||||
@@ -422,25 +448,34 @@ class TextOverlay:
|
||||
use_cache = True
|
||||
|
||||
for t_idx in range(T):
|
||||
p = animations.progress(t_idx, max(1, T - 1), animation_ease)
|
||||
op = animations.compute_opacity(animation_kind, p, float(animation_opacity_target))
|
||||
dx, dy = animations.compute_offsets(animation_kind, p, pil_img.width, pil_img.height)
|
||||
if t_idx < pause_frames:
|
||||
# Before the pause ends: no text overlay at all
|
||||
out_img = pil_img.copy()
|
||||
else:
|
||||
active_frames = max(1, T - pause_frames)
|
||||
local_idx = t_idx - pause_frames
|
||||
eff_local = min(local_idx, active_frames - 1)
|
||||
|
||||
p = animations.progress(eff_local, max(1, active_frames - 1), animation_ease)
|
||||
op = animations.compute_opacity(animation_kind, p, float(animation_opacity_target))
|
||||
dx, dy = animations.compute_offsets(animation_kind, p, pil_img.width, pil_img.height)
|
||||
|
||||
out_img = self.draw_text(
|
||||
pil_img, text, all_caps,
|
||||
font_size, letter_spacing, font,
|
||||
fill_color_hex, fill_alpha,
|
||||
stroke_enable,
|
||||
stroke_color_hex, stroke_alpha, stroke_thickness,
|
||||
padding, horizontal_alignment, vertical_alignment,
|
||||
x_shift, y_shift, line_spacing,
|
||||
bg_enable, bg_color_hex, bg_alpha, bg_padding, bg_radius,
|
||||
shadow_enable, shadow_color_hex, shadow_alpha, shadow_distance, font_alignment,
|
||||
use_cache=use_cache,
|
||||
opacity_scale=op,
|
||||
dx=dx,
|
||||
dy=dy,
|
||||
)
|
||||
|
||||
out_img = self.draw_text(
|
||||
pil_img, text, all_caps,
|
||||
font_size, letter_spacing, font,
|
||||
fill_color_hex, fill_alpha,
|
||||
stroke_enable,
|
||||
stroke_color_hex, stroke_alpha, stroke_thickness,
|
||||
padding, horizontal_alignment, vertical_alignment,
|
||||
x_shift, y_shift, line_spacing,
|
||||
bg_enable, bg_color_hex, bg_alpha, bg_padding, bg_radius,
|
||||
shadow_enable, shadow_color_hex, shadow_alpha, shadow_distance, font_alignment,
|
||||
use_cache=use_cache,
|
||||
opacity_scale=op,
|
||||
dx=dx,
|
||||
dy=dy,
|
||||
)
|
||||
outs.append(np.array(out_img).astype(np.float32) / 255.0)
|
||||
|
||||
return (torch.tensor(np.stack(outs)),)
|
||||
@@ -451,27 +486,35 @@ class TextOverlay:
|
||||
|
||||
B, H, W, C = image.shape
|
||||
|
||||
# Non-animated batch
|
||||
if not animate:
|
||||
out_list = []
|
||||
for i in range(B):
|
||||
np_img = image[i].cpu().numpy()
|
||||
pil_img = Image.fromarray((np_img * 255).astype(np.uint8))
|
||||
out_img = self.draw_text(
|
||||
pil_img, text, all_caps,
|
||||
font_size, letter_spacing, font,
|
||||
fill_color_hex, fill_alpha,
|
||||
stroke_enable,
|
||||
stroke_color_hex, stroke_alpha, stroke_thickness,
|
||||
padding, horizontal_alignment, vertical_alignment,
|
||||
x_shift, y_shift, line_spacing,
|
||||
bg_enable, bg_color_hex, bg_alpha, bg_padding, bg_radius,
|
||||
shadow_enable, shadow_color_hex, shadow_alpha, shadow_distance, font_alignment,
|
||||
use_cache=False,
|
||||
)
|
||||
|
||||
if i < pause_frames:
|
||||
# Pass-through until pause is over
|
||||
out_img = pil_img
|
||||
else:
|
||||
out_img = self.draw_text(
|
||||
pil_img, text, all_caps,
|
||||
font_size, letter_spacing, font,
|
||||
fill_color_hex, fill_alpha,
|
||||
stroke_enable,
|
||||
stroke_color_hex, stroke_alpha, stroke_thickness,
|
||||
padding, horizontal_alignment, vertical_alignment,
|
||||
x_shift, y_shift, line_spacing,
|
||||
bg_enable, bg_color_hex, bg_alpha, bg_padding, bg_radius,
|
||||
shadow_enable, shadow_color_hex, shadow_alpha, shadow_distance, font_alignment,
|
||||
use_cache=False,
|
||||
)
|
||||
|
||||
out_list.append(np.array(out_img).astype(np.float32) / 255.0)
|
||||
return (torch.tensor(np.stack(out_list)),)
|
||||
|
||||
# Animated batch: animate on frames [0 .. T-1], then hold on frames [T .. B-1]
|
||||
# Animated batch: animate on frames [pause_frames .. pause_frames+T-1],
|
||||
# then hold on frames after that
|
||||
T = max(1, int(animation_frames))
|
||||
|
||||
# Prime cache once using first frame
|
||||
@@ -496,28 +539,319 @@ class TextOverlay:
|
||||
np_img = image[i].cpu().numpy()
|
||||
pil_img = Image.fromarray((np_img * 255).astype(np.uint8))
|
||||
|
||||
eff_t = min(i, T - 1) # frames beyond T-1 hold the last pose
|
||||
p = animations.progress(eff_t, max(1, T - 1), animation_ease)
|
||||
op = animations.compute_opacity(animation_kind, p, float(animation_opacity_target))
|
||||
dx, dy = animations.compute_offsets(animation_kind, p, pil_img.width, pil_img.height)
|
||||
if i < pause_frames:
|
||||
# No overlay yet
|
||||
out_img = pil_img
|
||||
else:
|
||||
eff_idx = i - pause_frames
|
||||
eff_t = min(eff_idx, T - 1) # frames beyond animation hold the last pose
|
||||
|
||||
p = animations.progress(eff_t, max(1, T - 1), animation_ease)
|
||||
op = animations.compute_opacity(animation_kind, p, float(animation_opacity_target))
|
||||
dx, dy = animations.compute_offsets(animation_kind, p, pil_img.width, pil_img.height)
|
||||
|
||||
out_img = self.draw_text(
|
||||
pil_img, text, all_caps,
|
||||
font_size, letter_spacing, font,
|
||||
fill_color_hex, fill_alpha,
|
||||
stroke_enable,
|
||||
stroke_color_hex, stroke_alpha, stroke_thickness,
|
||||
padding, horizontal_alignment, vertical_alignment,
|
||||
x_shift, y_shift, line_spacing,
|
||||
bg_enable, bg_color_hex, bg_alpha, bg_padding, bg_radius,
|
||||
shadow_enable, shadow_color_hex, shadow_alpha, shadow_distance, font_alignment,
|
||||
use_cache=use_cache,
|
||||
opacity_scale=op,
|
||||
dx=dx,
|
||||
dy=dy,
|
||||
)
|
||||
|
||||
out_img = self.draw_text(
|
||||
pil_img, text, all_caps,
|
||||
font_size, letter_spacing, font,
|
||||
fill_color_hex, fill_alpha,
|
||||
stroke_enable,
|
||||
stroke_color_hex, stroke_alpha, stroke_thickness,
|
||||
padding, horizontal_alignment, vertical_alignment,
|
||||
x_shift, y_shift, line_spacing,
|
||||
bg_enable, bg_color_hex, bg_alpha, bg_padding, bg_radius,
|
||||
shadow_enable, shadow_color_hex, shadow_alpha, shadow_distance, font_alignment,
|
||||
use_cache=use_cache,
|
||||
opacity_scale=op,
|
||||
dx=dx,
|
||||
dy=dy,
|
||||
)
|
||||
out_list.append(np.array(out_img).astype(np.float32) / 255.0)
|
||||
|
||||
return (torch.tensor(np.stack(out_list)),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"Advanced Text Overlay": TextOverlay}
|
||||
class TextOverlayVideo:
|
||||
"""
|
||||
Video version of Advanced Text Overlay.
|
||||
|
||||
- Input: full video file path (STRING).
|
||||
- Output: STRING with full path of processed video in ComfyUI's output folder.
|
||||
- Uses the same text / animation controls as TextOverlay, and animates
|
||||
over the first `animation_frames` frames, then holds the final pose.
|
||||
"""
|
||||
|
||||
_horizontal_alignments = TextOverlay._horizontal_alignments
|
||||
_vertical_alignments = TextOverlay._vertical_alignments
|
||||
|
||||
# Make this an output node so the prompt has outputs
|
||||
OUTPUT_NODE = True
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
base = TextOverlay.INPUT_TYPES()["required"].copy()
|
||||
base.pop("image")
|
||||
|
||||
# 🔧 rename the pause key for the video node
|
||||
base["pause_seconds_before_start"] = base.pop("pause_frames_before_start")
|
||||
|
||||
required = {
|
||||
"video_path": ("STRING", {"multiline": False, "default": ""}),
|
||||
"filename_prefix": ("STRING", {"default": "TxtOver"}),
|
||||
"delete_original": ("BOOLEAN", {"default": False}),
|
||||
}
|
||||
required.update(base)
|
||||
|
||||
return {"required": required}
|
||||
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("video_path",)
|
||||
FUNCTION = "process_video"
|
||||
CATEGORY = "Advanced Text Overlay"
|
||||
|
||||
def _get_output_dir(self):
|
||||
# Default ComfyUI output folder if available
|
||||
try:
|
||||
import folder_paths
|
||||
return folder_paths.get_output_directory()
|
||||
except Exception:
|
||||
# Fallback if run standalone
|
||||
out_dir = os.path.join(os.getcwd(), "output")
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
return out_dir
|
||||
|
||||
def _make_unique_path(self, out_dir, filename_prefix, src_path):
|
||||
src_base = os.path.splitext(os.path.basename(src_path))[0]
|
||||
base_name = f"{filename_prefix}_{src_base}.mp4"
|
||||
out_full = os.path.join(out_dir, base_name)
|
||||
idx = 1
|
||||
while os.path.exists(out_full):
|
||||
base_name = f"{filename_prefix}_{src_base}_{idx}.mp4"
|
||||
out_full = os.path.join(out_dir, base_name)
|
||||
idx += 1
|
||||
return out_full
|
||||
|
||||
def process_video(
|
||||
self,
|
||||
video_path,
|
||||
filename_prefix,
|
||||
delete_original,
|
||||
text,
|
||||
all_caps,
|
||||
font,
|
||||
font_size,
|
||||
letter_spacing,
|
||||
font_alignment,
|
||||
fill_color_hex,
|
||||
fill_alpha,
|
||||
padding,
|
||||
vertical_alignment,
|
||||
y_shift,
|
||||
horizontal_alignment,
|
||||
x_shift,
|
||||
line_spacing,
|
||||
stroke_enable,
|
||||
stroke_color_hex,
|
||||
stroke_thickness,
|
||||
stroke_alpha,
|
||||
bg_enable,
|
||||
bg_padding,
|
||||
bg_radius,
|
||||
bg_color_hex,
|
||||
bg_alpha,
|
||||
shadow_enable,
|
||||
shadow_distance,
|
||||
shadow_color_hex,
|
||||
shadow_alpha,
|
||||
animate,
|
||||
animation_kind,
|
||||
animation_frames,
|
||||
animation_ease,
|
||||
animation_opacity_target,
|
||||
pause_seconds_before_start,
|
||||
):
|
||||
|
||||
"""
|
||||
Reads the video frame by frame, applies the same text overlay logic as the
|
||||
batch TextOverlay, and writes a new video file.
|
||||
Returns the full path string to the new video.
|
||||
|
||||
Shows progress in:
|
||||
- ComfyUI (ProgressBar)
|
||||
- Console (tqdm)
|
||||
|
||||
After writing the processed video, we mux the original audio track
|
||||
from `video_path` into the output file using ffmpeg, if available.
|
||||
"""
|
||||
if not video_path or not os.path.exists(video_path):
|
||||
raise FileNotFoundError(f"Video file not found: {video_path}")
|
||||
|
||||
out_dir = self._get_output_dir()
|
||||
out_path = self._make_unique_path(out_dir, filename_prefix, video_path)
|
||||
|
||||
# Reuse your existing text overlay logic on each frame
|
||||
overlay = TextOverlay()
|
||||
|
||||
reader = imageio.get_reader(video_path)
|
||||
meta = reader.get_meta_data()
|
||||
fps = meta.get("fps", 30)
|
||||
|
||||
# Try to get a sane total frame count
|
||||
nframes_meta = meta.get("nframes", None)
|
||||
duration = meta.get("duration", None) # seconds, if available
|
||||
|
||||
# Convert pause_seconds_before_start (seconds) to frames
|
||||
try:
|
||||
pause_frames = max(0, int(round(float(pause_seconds_before_start) * float(fps))))
|
||||
except Exception:
|
||||
pause_frames = max(0, int(pause_seconds_before_start))
|
||||
|
||||
total_frames = None
|
||||
|
||||
# 1) Trust nframes only if it's a reasonable integer
|
||||
if isinstance(nframes_meta, (int, float)) and 0 < nframes_meta < 1e8:
|
||||
total_frames = int(nframes_meta)
|
||||
|
||||
# 2) Otherwise, estimate from duration * fps if we have that
|
||||
elif isinstance(duration, (int, float)) and duration > 0 and fps > 0:
|
||||
total_frames = int(duration * fps)
|
||||
|
||||
# ComfyUI progress bar
|
||||
comfy_pbar = None
|
||||
if ProgressBar is not None and isinstance(total_frames, int) and total_frames > 0:
|
||||
comfy_pbar = ProgressBar(total_frames)
|
||||
|
||||
# tqdm progress bar
|
||||
if isinstance(total_frames, int) and total_frames > 0:
|
||||
frame_iter = tqdm(reader, total=total_frames, desc="TextOverlayVideo")
|
||||
else:
|
||||
frame_iter = tqdm(reader, desc="TextOverlayVideo")
|
||||
|
||||
T = max(1, int(animation_frames)) if animate else 1
|
||||
|
||||
writer = imageio.get_writer(
|
||||
out_path,
|
||||
fps=fps,
|
||||
macro_block_size=1 # avoid auto-resizing to multiples of 16
|
||||
)
|
||||
|
||||
try:
|
||||
for i, frame in enumerate(frame_iter):
|
||||
pil_img = Image.fromarray(frame)
|
||||
|
||||
if i < pause_frames:
|
||||
# Before pause: pass the frame through with no overlay
|
||||
out_img = pil_img
|
||||
else:
|
||||
if animate:
|
||||
# Animation timing: animate on frames [pause_frames .. pause_frames+T-1],
|
||||
# then hold last pose afterwards
|
||||
eff_idx = i - pause_frames
|
||||
eff_t = min(eff_idx, T - 1)
|
||||
p = animations.progress(eff_t, max(1, T - 1), animation_ease)
|
||||
op = animations.compute_opacity(animation_kind, p, float(animation_opacity_target))
|
||||
dx, dy = animations.compute_offsets(animation_kind, p, pil_img.width, pil_img.height)
|
||||
else:
|
||||
op = 1.0
|
||||
dx = dy = 0
|
||||
|
||||
use_cache = (i > pause_frames)
|
||||
|
||||
out_img = overlay.draw_text(
|
||||
pil_img,
|
||||
text,
|
||||
all_caps,
|
||||
font_size,
|
||||
letter_spacing,
|
||||
font,
|
||||
fill_color_hex,
|
||||
fill_alpha,
|
||||
stroke_enable,
|
||||
stroke_color_hex,
|
||||
stroke_alpha,
|
||||
stroke_thickness,
|
||||
padding,
|
||||
horizontal_alignment,
|
||||
vertical_alignment,
|
||||
x_shift,
|
||||
y_shift,
|
||||
line_spacing,
|
||||
bg_enable,
|
||||
bg_color_hex,
|
||||
bg_alpha,
|
||||
bg_padding,
|
||||
bg_radius,
|
||||
shadow_enable,
|
||||
shadow_color_hex,
|
||||
shadow_alpha,
|
||||
shadow_distance,
|
||||
font_alignment,
|
||||
use_cache=use_cache,
|
||||
opacity_scale=op,
|
||||
dx=dx,
|
||||
dy=dy,
|
||||
)
|
||||
|
||||
writer.append_data(np.array(out_img))
|
||||
|
||||
# Update ComfyUI progress
|
||||
if comfy_pbar is not None:
|
||||
comfy_pbar.update(1)
|
||||
|
||||
|
||||
finally:
|
||||
writer.close()
|
||||
reader.close()
|
||||
|
||||
# ---- NEW: mux original audio into the processed video using ffmpeg ----
|
||||
try:
|
||||
# We create a temporary output file, then replace the original out_path
|
||||
tmp_out = out_path + ".tmp_audio.mp4"
|
||||
|
||||
# ffmpeg command:
|
||||
# - input 0: processed video (no audio)
|
||||
# - input 1: original video (with audio)
|
||||
# - copy streams without re-encoding: -c copy
|
||||
# - map video from 0, audio from 1
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-y", # overwrite without asking
|
||||
"-i", out_path,
|
||||
"-i", video_path,
|
||||
"-c", "copy",
|
||||
"-map", "0:v:0",
|
||||
"-map", "1:a:0",
|
||||
tmp_out,
|
||||
]
|
||||
|
||||
completed = subprocess.run(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=False,
|
||||
)
|
||||
|
||||
if completed.returncode == 0:
|
||||
os.replace(tmp_out, out_path)
|
||||
else:
|
||||
# If something goes wrong, keep the silent video and print the error
|
||||
print("[TextOverlayVideo] ffmpeg failed to mux audio, keeping silent video.")
|
||||
print(completed.stderr.decode("utf-8", errors="ignore"))
|
||||
|
||||
except Exception as e:
|
||||
# Fail gracefully: overlay still works, just no audio
|
||||
print(f"[TextOverlayVideo] Could not mux audio from original video: {e}")
|
||||
|
||||
# NEW: optionally delete the original input video after processing is finished
|
||||
if delete_original:
|
||||
try:
|
||||
os.remove(video_path)
|
||||
except Exception as e:
|
||||
print(f"[TextOverlayVideo] Failed to delete original video '{video_path}': {e}")
|
||||
|
||||
return (out_path,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Advanced Text Overlay": TextOverlay,
|
||||
"Advanced Text Overlay - Video": TextOverlayVideo,
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
[project]
|
||||
name = "advanced-textoverlay" # Unique and immutable node identifier.
|
||||
description = "Advanced Text Overlay Node for ComfyUI featuring rebuilt rendering logic, precise alignment, custom fonts, and extended styling controls."
|
||||
version = "1.3.0" # Semantic versioning
|
||||
version = "1.4.0" # Semantic versioning
|
||||
dependencies = [] # Populated from requirements.txt if present.
|
||||
|
||||
[project.urls]
|
||||
|
||||
+3
-1
@@ -1,3 +1,5 @@
|
||||
numpy
|
||||
torch
|
||||
Pillow
|
||||
Pillow
|
||||
imageio
|
||||
tqdm
|
||||
Reference in New Issue
Block a user