cap2 / canvas_renderer.py
ADXabhi's picture
Upload 7 files
c516bed verified
"""
Canvas Renderer - Pillow-based caption frame rendering
Replicates the canvas rendering logic from burned-clip-green.html
"""
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import os
from typing import List, Tuple
# Canvas dimensions
WIDTH = 640
HEIGHT = 240
GREEN_SCREEN = (0, 255, 0) # #00FF00
# Try to load a bold font, fallback to default
def get_font(size: int = 52):
"""Get the best available bold font"""
font_paths = [
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf",
"/System/Library/Fonts/Helvetica.ttc",
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
]
for path in font_paths:
if os.path.exists(path):
try:
return ImageFont.truetype(path, size)
except:
continue
# Fallback to default
return ImageFont.load_default()
def render_frame(words: List[str], active_index: int, style: str = "hormozi") -> Image.Image:
"""
Render a single caption frame with the specified style.
Args:
words: List of words to display
active_index: Index of the currently active (highlighted) word (-1 for none)
style: One of 'hormozi', 'cinematic', 'netflix', 'neon'
Returns:
PIL Image with green screen background and rendered captions
"""
# Create green screen image
img = Image.new('RGB', (WIDTH, HEIGHT), GREEN_SCREEN)
draw = ImageDraw.Draw(img)
font = get_font(52)
padding = 18
center_x = WIDTH // 2
center_y = HEIGHT // 2
# Measure all words to calculate layout
word_metrics = []
total_width = 0
for idx, word in enumerate(words):
text = word.upper()
bbox = draw.textbbox((0, 0), text, font=font)
w = bbox[2] - bbox[0]
word_metrics.append({
'text': text,
'width': w,
'is_active': idx == active_index
})
total_width += w + padding
total_width -= padding # Remove last padding
# Calculate starting position
current_x = center_x - (total_width // 2)
# Draw each word
for wm in word_metrics:
word_center_x = current_x + (wm['width'] // 2)
is_active = wm['is_active']
text = wm['text']
# Get text position (centered)
bbox = draw.textbbox((0, 0), text, font=font)
text_height = bbox[3] - bbox[1]
text_y = center_y - (text_height // 2)
text_x = current_x
if style == 'hormozi':
_draw_hormozi(img, draw, text, text_x, text_y, is_active, font)
elif style == 'cinematic':
_draw_cinematic(img, draw, text, text_x, text_y, is_active, font)
elif style == 'netflix':
_draw_netflix(img, draw, text, text_x, text_y, is_active, font)
elif style == 'neon':
_draw_neon(img, draw, text, text_x, text_y, is_active, font)
else:
_draw_hormozi(img, draw, text, text_x, text_y, is_active, font)
current_x += wm['width'] + padding
return img
def _draw_text_with_outline(draw, text, x, y, font, fill_color, outline_color, outline_width=3):
"""Draw text with outline (stroke)"""
# Draw outline
for dx in range(-outline_width, outline_width + 1):
for dy in range(-outline_width, outline_width + 1):
if dx != 0 or dy != 0:
draw.text((x + dx, y + dy), text, font=font, fill=outline_color)
# Draw fill
draw.text((x, y), text, font=font, fill=fill_color)
def _draw_hormozi(img, draw, text, x, y, is_active, font):
"""Hormozi style: Gold active, white inactive, pop effect"""
if is_active:
# Gold with black outline
_draw_text_with_outline(draw, text, x, y, font,
fill_color=(255, 215, 0), # Gold #FFD700
outline_color=(0, 0, 0),
outline_width=4)
else:
# White with black outline
_draw_text_with_outline(draw, text, x, y, font,
fill_color=(255, 255, 255),
outline_color=(0, 0, 0),
outline_width=3)
def _draw_cinematic(img, draw, text, x, y, is_active, font):
"""Cinematic style: Bright white active with cyan tint, dimmed inactive"""
if is_active:
# Bright white with heavy black outline
_draw_text_with_outline(draw, text, x, y, font,
fill_color=(255, 255, 255),
outline_color=(0, 0, 0),
outline_width=5)
else:
# Muted gray
_draw_text_with_outline(draw, text, x, y, font,
fill_color=(180, 180, 180),
outline_color=(0, 0, 0),
outline_width=3)
def _draw_netflix(img, draw, text, x, y, is_active, font):
"""Netflix style: Red active, white inactive"""
if is_active:
# Netflix red with black outline
_draw_text_with_outline(draw, text, x, y, font,
fill_color=(229, 9, 20), # Netflix red #E50914
outline_color=(0, 0, 0),
outline_width=5)
else:
# White with black outline
_draw_text_with_outline(draw, text, x, y, font,
fill_color=(255, 255, 255),
outline_color=(0, 0, 0),
outline_width=4)
def _draw_neon(img, draw, text, x, y, is_active, font):
"""Neon style: Magenta active, cyan inactive"""
if is_active:
# Magenta neon
_draw_text_with_outline(draw, text, x, y, font,
fill_color=(255, 0, 255), # Magenta #FF00FF
outline_color=(0, 0, 0),
outline_width=4)
else:
# Cyan
_draw_text_with_outline(draw, text, x, y, font,
fill_color=(0, 255, 255), # Cyan #00FFFF
outline_color=(0, 0, 0),
outline_width=3)
# Test function
if __name__ == "__main__":
test_words = ["WATCH", "THIS", "NOW"]
for i in range(-1, 3):
img = render_frame(test_words, i, "hormozi")
img.save(f"test_frame_{i}.png")
print(f"Saved test_frame_{i}.png")