""" 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")