from fastapi import APIRouter, HTTPException, Query from fastapi.responses import StreamingResponse from PIL import Image, ImageDraw, ImageFont from io import BytesIO from typing import Optional router = APIRouter() def render_subtitle_text(draw: ImageDraw.Draw, text: str, x: int, y: int, max_width: int) -> None: """ Renderiza subtítulo com fonte Montserrat-ExtraBold e fundo preto. Args: draw: Objeto ImageDraw para desenhar text: Texto do subtítulo x: Posição X (centro) y: Posição Y (topo do texto) max_width: Largura máxima do texto """ if not text.strip(): return # Carregar fonte Montserrat-ExtraBold try: font_path = "fonts/Montserrat-ExtraBold.ttf" font = ImageFont.truetype(font_path, 35) except (OSError, IOError): # Fallback para fonte padrão se não encontrar a fonte font = ImageFont.load_default() # Primeiro, dividir por quebras de linha explícitas (\n) text_lines = text.split('\n') # Função para quebrar texto em linhas baseado na largura def wrap_text(text, font_size, max_width): try: test_font = ImageFont.truetype(font_path, font_size) except (OSError, IOError): test_font = ImageFont.load_default() lines = [] words = text.split() current_line = [] for word in words: test_line = " ".join(current_line + [word]) bbox = draw.textbbox((0, 0), test_line, font=test_font) line_width = bbox[2] - bbox[0] if line_width <= max_width: current_line.append(word) else: if current_line: lines.append(" ".join(current_line)) current_line = [word] else: # Palavra muito longa, adiciona mesmo assim lines.append(word) if current_line: lines.append(" ".join(current_line)) return lines # Processar cada linha do texto (que pode ter quebras explícitas) lines = [] for text_line in text_lines: if text_line.strip(): # Se a linha não estiver vazia wrapped_lines = wrap_text(text_line.strip(), 35, max_width) lines.extend(wrapped_lines) else: # Linha vazia (quebra explícita) lines.append("") # Calcular altura total do texto line_heights = [] for line in lines: if line: # Se a linha não estiver vazia bbox = draw.textbbox((0, 0), line, font=font) line_height = bbox[3] - bbox[1] else: # Para linhas vazias, usar altura padrão baseada no tamanho da fonte bbox = draw.textbbox((0, 0), "A", font=font) line_height = bbox[3] - bbox[1] line_heights.append(line_height) # Calcular dimensões totais do texto para o fundo total_height = sum(int(line_heights[i] * 1.5) for i in range(len(lines) - 1)) + line_heights[-1] # Calcular largura máxima (ignorando linhas vazias) non_empty_lines = [line for line in lines if line] if non_empty_lines: max_line_width = max(draw.textbbox((0, 0), line, font=font)[2] - draw.textbbox((0, 0), line, font=font)[0] for line in non_empty_lines) else: max_line_width = 0 # Adicionar padding ao fundo padding = 30 bg_width = max_line_width + (padding * 2) bg_height = total_height + (padding * 2) # Calcular posição do fundo (centralizado) bg_x = x - (bg_width // 2) bg_y = y - padding # Desenhar fundo preto draw.rectangle([bg_x, bg_y, bg_x + bg_width, bg_y + bg_height], fill=(0, 0, 0, 255)) # Calcular altura total real do texto (incluindo espaçamento entre linhas) total_text_height = sum(int(line_heights[i] * 1.5) for i in range(len(lines) - 1)) + line_heights[-1] # Calcular posição Y centralizada para o texto # O fundo vai de bg_y até bg_y + bg_height # Centralizar o texto verticalmente no fundo text_start_y = bg_y + (bg_height - total_text_height) // 2 # Ajuste para sempre ter 6px de distância do fundo (padding bottom) text_start_y -= 6 # Desenhar cada linha (de cima para baixo) current_y = text_start_y for i, line in enumerate(lines): line_height = line_heights[i] # Se a linha não estiver vazia, desenhar o texto if line: # Calcular posição X centralizada para esta linha bbox = draw.textbbox((0, 0), line, font=font) line_width = bbox[2] - bbox[0] centered_x = x - (line_width // 2) # Desenhar texto branco draw.text((centered_x, current_y), line, fill=(255, 255, 255, 255), font=font) # Atualizar posição para próxima linha (descendo) current_y += int(line_height * 1.5) # Line height de 150% @router.get("/subtitle/video") def get_video_subtitle( text: Optional[str] = Query("On Capitol Hill right now.", description="Texto do subtítulo") ): """ Endpoint que retorna uma imagem de subtítulo com dimensões 1080x1920. """ try: # Processar sequências de escape como \n if text: text = text.replace('\\n', '\n') # Dimensões especificadas: largura 1080, altura 1920 width, height = 1080, 1920 # Criar fundo transparente background_image = Image.new("RGBA", (width, height), color=(0, 0, 0, 0)) # Criar objeto de desenho draw = ImageDraw.Draw(background_image) # Renderizar o subtítulo render_subtitle_text( draw, text, x=540, # Centro da imagem (1080/2) y=1420, # Posição Y especificada max_width=850 # Largura máxima para o texto ) # Converter para BytesIO (mantendo transparência) buffer = BytesIO() background_image.save(buffer, format="PNG") buffer.seek(0) return StreamingResponse(buffer, media_type="image/png") except Exception as e: raise HTTPException(status_code=500, detail=f"Erro ao gerar subtítulo: {str(e)}")