import gradio as gr import numpy as np from PIL import Image, ImageDraw, ImageFilter, ImageOps, ImageFont import cv2 import torch import io import tempfile import traceback import time import os import random from typing import List, Tuple, Optional, Dict, Any import warnings warnings.filterwarnings("ignore") # ============================ # CONFIGURAÇÕES # ============================ MAX_IMAGES = 10 OUTPUT_MAX_SIZE_MB = 1.5 # Presets de resolução RESOLUTION_PRESETS = { "Instagram Post (1:1)": (1080, 1080), "Instagram Story (9:16)": (1080, 1920), "Facebook Post": (1200, 630), "Twitter Post": (1200, 675), "LinkedIn Post": (1200, 627), "Pinterest Pin": (1000, 1500), "YouTube Thumbnail": (1280, 720), "A4 Print (300dpi)": (2480, 3508), "A3 Print (300dpi)": (3508, 4960), "Poster 18x24": (2700, 3600), "Custom": None } # Estilos de borda BORDER_STYLES = { "Sem Borda": "none", "Sombra Natural": "natural_shadow", "Brilho Neon": "neon_glow", "Contorno Colorido": "colored_outline", "Borda Dupla": "double_border" } # Efeitos de texto TEXT_EFFECTS = { "3D Profundo": "deep_3d", "Neon Glitch": "neon_glitch", "Metal Brilhante": "shiny_metal", "Gradiente Colorido": "color_gradient", "Texto com Sombra": "shadow_text" } # Estilos de fonte FONT_STYLES = { "Moderno Bold": "modern_bold", "Futurista": "futuristic", "Elegante": "elegant", "Artístico": "artistic", "Retrô": "retro" } class ProcessingLogger: """Logger para mostrar progresso ao usuário""" def __init__(self): self.logs = [] self.start_time = time.time() def add_log(self, message: str, status: str = "info", error_details: str = ""): """Adiciona um log com timestamp""" elapsed = time.time() - self.start_time timestamp = f"[{elapsed:.1f}s]" icons = { "info": "📝", "success": "✅", "warning": "⚠️", "error": "❌", "processing": "🔄", "debug": "🔍" } icon = icons.get(status, "📝") log_entry = f"{icon} {timestamp} {message}" if error_details and status == "error": log_entry += f"\n 🔍 Detalhes: {error_details[:100]}" self.logs.append(log_entry) # Manter apenas os últimos 20 logs if len(self.logs) > 20: self.logs = self.logs[-20:] return self.get_logs() def get_logs(self) -> str: """Retorna logs formatados""" return "\n".join(self.logs) def clear(self): """Limpa logs""" self.logs = [] self.start_time = time.time() return "✅ Logs limpos" class AdvancedCollagePosterGenerator: def __init__(self): self.device = "cuda" if torch.cuda.is_available() else "cpu" self.logger = ProcessingLogger() self.image_cache = {} # Cache para imagens processadas print(f"⚙️ Dispositivo: {self.device}") def precise_background_removal(self, image: Image.Image, image_num: int, file_hash: str) -> Image.Image: """Remove fundo com precisão usando contornos de objetos""" try: self.logger.add_log(f"Imagem {image_num}: Removendo fundo...", "processing") # Converter para array numpy img_array = np.array(image.convert('RGB')) # Converter para escala de cinza gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY) # Aplicar filtro para reduzir ruído blurred = cv2.GaussianBlur(gray, (5, 5), 0) # Detectar bordas edges = cv2.Canny(blurred, 30, 100) # Dilatar bordas para fechar contornos kernel = np.ones((3, 3), np.uint8) edges = cv2.dilate(edges, kernel, iterations=2) # Encontrar contornos contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: # Encontrar o maior contorno (objeto principal) largest_contour = max(contours, key=cv2.contourArea) # Criar máscara do objeto mask = np.zeros(gray.shape, np.uint8) cv2.drawContours(mask, [largest_contour], -1, 255, -1) # Suavizar máscara mask = cv2.GaussianBlur(mask, (5, 5), 0) # Criar imagem com fundo transparente result = Image.new('RGBA', image.size, (0, 0, 0, 0)) img_rgba = image.convert('RGBA') # Aplicar máscara alpha = Image.fromarray(mask) img_rgba.putalpha(alpha) result.paste(img_rgba, (0, 0), img_rgba) self.logger.add_log(f"Imagem {image_num}: Fundo removido com precisão", "success") return result else: # Fallback para método simples return self.simple_background_removal(image, image_num) except Exception as e: error_msg = str(e) self.logger.add_log(f"Imagem {image_num}: Erro na remoção: {error_msg[:50]}", "error") return self.simple_background_removal(image, image_num) def simple_background_removal(self, image: Image.Image, image_num: int) -> Image.Image: """Método simples de remoção de fundo (fallback)""" try: # Converter para RGBA img = image.convert("RGBA") data = np.array(img) # Detectar cor dominante nos cantos (assumindo fundo) h, w, _ = data.shape corner_samples = [] sample_size = min(20, h//10, w//10) if sample_size > 0: corners = [ data[:sample_size, :sample_size], data[:sample_size, -sample_size:], data[-sample_size:, :sample_size], data[-sample_size:, -sample_size:] ] for corner in corners: if corner.size > 0: corner_samples.append(corner.mean(axis=(0, 1))) if corner_samples: bg_color = np.mean(corner_samples, axis=0) # Calcular diferença de cor color_diff = np.sqrt(np.sum((data[:, :, :3] - bg_color) ** 2, axis=2)) # Normalizar e criar máscara max_diff = np.max(color_diff) if np.max(color_diff) > 0 else 1 normalized_diff = color_diff / max_diff threshold = 0.15 mask = normalized_diff > threshold # Suavizar máscara mask = mask.astype(np.uint8) * 255 mask = cv2.GaussianBlur(mask, (7, 7), 0) # Aplicar máscara data[:, :, 3] = mask result = Image.fromarray(data, 'RGBA') self.logger.add_log(f"Imagem {image_num}: Fundo removido (método simples)", "warning") return result except Exception as e: self.logger.add_log(f"Imagem {image_num}: Fallback falhou, mantendo original", "error") return image.convert("RGBA") def apply_border_effect(self, image: Image.Image, style: str, color: str, image_num: int, outline_thickness: int = 3) -> Image.Image: """Aplica efeitos de borda avançados""" if style == "none": return image try: self.logger.add_log(f"Imagem {image_num}: Aplicando efeito '{style}'...", "processing") if image.mode != 'RGBA': image = image.convert('RGBA') # Converter cor hex para RGB try: hex_color = color.strip().lstrip('#').upper() if len(hex_color) == 3: hex_color = ''.join([c*2 for c in hex_color]) if len(hex_color) != 6: raise ValueError("Cor hexadecimal inválida") rgb = ( int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16) ) except: self.logger.add_log(f"⚠️ Cor inválida: {color}, usando azul padrão", "warning") rgb = (0, 120, 255) if style == "natural_shadow": # Sombra natural que segue o contorno shadow_size = 20 expanded = Image.new('RGBA', (image.width + shadow_size, image.height + shadow_size), (0, 0, 0, 0)) # Extrair máscara alpha alpha = np.array(image.split()[-1]) # Criar sombra kernel = np.ones((5, 5), np.uint8) dilated = cv2.dilate(alpha, kernel, iterations=2) shadow_mask = cv2.GaussianBlur(dilated, (15, 15), 5) # Criar sombra colorida shadow_layer = np.zeros((*shadow_mask.shape, 4), dtype=np.uint8) shadow_layer[..., 0] = max(0, rgb[0] - 50) shadow_layer[..., 1] = max(0, rgb[1] - 50) shadow_layer[..., 2] = max(0, rgb[2] - 50) shadow_layer[..., 3] = shadow_mask * 0.3 shadow_img = Image.fromarray(shadow_layer, 'RGBA') # Colar sombra e imagem expanded.paste(shadow_img, (8, 8), shadow_img) expanded.paste(image, (0, 0), image) result = expanded elif style == "neon_glow": # Brilho neon glow_size = 25 expanded = Image.new('RGBA', (image.width + glow_size*2, image.height + glow_size*2), (0, 0, 0, 0)) alpha = np.array(image.split()[-1]) # Criar camadas de brilho for i in range(3): layer_size = glow_size - i * 5 kernel = np.ones((layer_size, layer_size), np.uint8) dilated = cv2.dilate(alpha, kernel, iterations=1) if i > 0: prev_kernel = np.ones((layer_size + 5, layer_size + 5), np.uint8) prev_dilated = cv2.dilate(alpha, prev_kernel, iterations=1) layer_mask = dilated - prev_dilated else: layer_mask = dilated # Suavizar layer_mask = cv2.GaussianBlur(layer_mask, (5, 5), 2) # Criar camada glow_color = ( min(255, rgb[0] + i * 20), min(255, rgb[1] + i * 20), min(255, rgb[2] + i * 20) ) glow_layer = Image.new('RGBA', expanded.size, glow_color + (80 - i*20,)) glow_layer.putalpha(Image.fromarray(layer_mask)) expanded.paste(glow_layer, (glow_size, glow_size), glow_layer) expanded.paste(image, (glow_size, glow_size), image) result = expanded elif style == "colored_outline": # Contorno colorido com espessura ajustável outline_thickness = max(1, min(outline_thickness, 10)) expanded = Image.new('RGBA', (image.width + outline_thickness*2, image.height + outline_thickness*2), (0, 0, 0, 0)) alpha = np.array(image.split()[-1]) # Dilatar para criar contorno kernel_size = outline_thickness * 2 + 1 kernel = np.ones((kernel_size, kernel_size), np.uint8) dilated = cv2.dilate(alpha, kernel, iterations=1) # Subtrair original para obter contorno outline_mask = dilated - alpha # Suavizar if outline_thickness > 1: outline_mask = cv2.GaussianBlur(outline_mask, (3, 3), 1) # Criar contorno outline_img = Image.new('RGBA', expanded.size, rgb) outline_img.putalpha(Image.fromarray(outline_mask)) expanded.paste(outline_img, (0, 0), outline_img) expanded.paste(image, (outline_thickness, outline_thickness), image) result = expanded elif style == "double_border": # Borda dupla border = 15 expanded = Image.new('RGBA', (image.width + border*2, image.height + border*2), (0, 0, 0, 0)) draw = ImageDraw.Draw(expanded) # Cor complementar complement = (255 - rgb[0], 255 - rgb[1], 255 - rgb[2]) # Borda externa draw.rectangle([0, 0, expanded.width-1, expanded.height-1], outline=rgb, width=border//2) # Borda interna draw.rectangle([border//2, border//2, expanded.width-border//2-1, expanded.height-border//2-1], outline=complement, width=border//4) expanded.paste(image, (border, border), image) result = expanded self.logger.add_log(f"Imagem {image_num}: Efeito '{style}' aplicado", "success") return result except Exception as e: error_msg = str(e) self.logger.add_log(f"Imagem {image_num}: Erro na borda: {error_msg[:50]}", "error") return image def create_gradient_background(self, size: Tuple[int, int], color1: str, color2: str) -> Image.Image: """Cria fundo degradê""" try: self.logger.add_log("Criando fundo degradê...", "processing") width, height = size # Validar e converter cores def validate_color(color_str): try: hex_color = color_str.strip().lstrip('#').upper() if len(hex_color) == 3: hex_color = ''.join([c*2 for c in hex_color]) if len(hex_color) != 6: raise ValueError return ( int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16) ) except: return (102, 126, 234) # Azul padrão rgb1 = validate_color(color1) rgb2 = validate_color(color2) # Criar fundo background = Image.new('RGB', size) draw = ImageDraw.Draw(background) # Gradiente vertical for y in range(height): ratio = y / height r = int(rgb1[0] * (1 - ratio) + rgb2[0] * ratio) g = int(rgb1[1] * (1 - ratio) + rgb2[1] * ratio) b = int(rgb1[2] * (1 - ratio) + rgb2[2] * ratio) draw.line([(0, y), (width, y)], fill=(r, g, b)) # Suavizar background = background.filter(ImageFilter.GaussianBlur(radius=0.5)) self.logger.add_log("Fundo degradê criado", "success") return background except Exception as e: error_msg = str(e) self.logger.add_log(f"Erro no fundo: {error_msg[:50]}", "error") return Image.new('RGB', size, (200, 200, 200)) def apply_text_effect(self, draw, text: str, position: Tuple[int, int], font: ImageFont.FreeTypeFont, effect: str, color: str) -> None: """Aplica efeitos de texto""" # Validar cor try: hex_color = color.strip().lstrip('#').upper() if len(hex_color) == 3: hex_color = ''.join([c*2 for c in hex_color]) if len(hex_color) != 6: raise ValueError text_rgb = ( int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16) ) except: text_rgb = (255, 255, 255) x, y = position if effect == "deep_3d": # Efeito 3D for i in range(5, 0, -1): shadow = (text_rgb[0]//(i+1), text_rgb[1]//(i+1), text_rgb[2]//(i+1)) draw.text((x + i, y + i), text, font=font, fill=shadow) draw.text((x, y), text, font=font, fill=text_rgb) elif effect == "neon_glitch": # Neon glitch offsets = [(2, 0), (-2, 0), (0, 2)] colors = [(0, 255, 255), (255, 0, 255), (255, 255, 0)] for (dx, dy), glow in zip(offsets, colors): draw.text((x + dx, y + dy), text, font=font, fill=glow) draw.text((x, y), text, font=font, fill=text_rgb) elif effect == "shiny_metal": # Metal brilhante for i in range(3): shade = 180 - i * 30 draw.text((x - i, y - i), text, font=font, fill=(shade, shade, shade)) draw.text((x, y), text, font=font, fill=(255, 255, 200)) elif effect == "color_gradient": # Gradiente for i, char in enumerate(text): ratio = i / max(len(text)-1, 1) r = int(text_rgb[0] * (1 - ratio) + 255 * ratio) g = int(text_rgb[1] * ratio + 128 * (1 - ratio)) b = int(text_rgb[2] * (1 - ratio) + 255 * ratio) char_x = x + draw.textlength(text[:i], font=font) draw.text((char_x, y), char, font=font, fill=(r, g, b)) else: # shadow_text # Sombra clássica shadow = (text_rgb[0]//3, text_rgb[1]//3, text_rgb[2]//3) draw.text((x + 2, y + 2), text, font=font, fill=shadow) draw.text((x, y), text, font=font, fill=text_rgb) def add_text_with_effects(self, image: Image.Image, title: str, description: str, text_color: str, text_effect: str, font_style: str) -> Image.Image: """Adiciona texto com efeitos""" try: if not title and not description: return image self.logger.add_log("Adicionando texto...", "processing") if image.mode != 'RGBA': image = image.convert('RGBA') text_layer = Image.new('RGBA', image.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(text_layer) width, height = image.size margin = int(min(width, height) * 0.1) # Carregar fontes try: title_size = int(min(width, height) * 0.07) desc_size = int(min(width, height) * 0.035) font_paths = { "modern_bold": "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", "futuristic": "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", "elegant": "/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf", "artistic": "/usr/share/fonts/truetype/ubuntu/Ubuntu-B.ttf", "retro": "/usr/share/fonts/truetype/liberation/LiberationMono-Bold.ttf" } font_path = font_paths.get(font_style, "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf") title_font = ImageFont.truetype(font_path, title_size) desc_font = ImageFont.truetype(font_path, desc_size) except: title_font = ImageFont.load_default() desc_font = ImageFont.load_default() # Posicionar texto de baixo para cima y_pos = height - margin # Descrição if description and description.strip(): words = description.split() lines = [] current = [] for word in words: current.append(word) test = ' '.join(current) try: bbox = draw.textbbox((0, 0), test, font=desc_font) w = bbox[2] - bbox[0] except: w = len(test) * desc_size * 0.6 if w > (width - 2 * margin): if len(current) > 1: lines.append(' '.join(current[:-1])) current = [word] else: lines.append(word) current = [] if current: lines.append(' '.join(current)) lines.reverse() for line in lines: try: bbox = draw.textbbox((0, 0), line, font=desc_font) w = bbox[2] - bbox[0] h = bbox[3] - bbox[1] except: w = len(line) * desc_size * 0.6 h = desc_size x = (width - w) // 2 y_pos -= h + 10 self.apply_text_effect(draw, line, (x, y_pos), desc_font, text_effect, text_color) # Título if title and title.strip(): if description and description.strip(): y_pos -= int(title_size * 0.5) words = title.split() lines = [] current = [] for word in words: current.append(word) test = ' '.join(current) try: bbox = draw.textbbox((0, 0), test, font=title_font) w = bbox[2] - bbox[0] except: w = len(test) * title_size * 0.6 if w > (width - 2 * margin): if len(current) > 1: lines.append(' '.join(current[:-1])) current = [word] else: lines.append(word) current = [] if current: lines.append(' '.join(current)) lines.reverse() for line in lines: try: bbox = draw.textbbox((0, 0), line, font=title_font) w = bbox[2] - bbox[0] h = bbox[3] - bbox[1] except: w = len(line) * title_size * 0.6 h = title_size x = (width - w) // 2 y_pos -= h + 20 self.apply_text_effect(draw, line, (x, y_pos), title_font, text_effect, text_color) result = Image.alpha_composite(image, text_layer) self.logger.add_log("Texto adicionado", "success") return result except Exception as e: error_msg = str(e) self.logger.add_log(f"Erro no texto: {error_msg[:50]}", "error") return image def create_collage(self, images: List[Image.Image], background: Image.Image) -> Image.Image: """Cria collage harmoniosa""" try: self.logger.add_log("Criando collage...", "processing") result = background.copy().convert('RGBA') bg_width, bg_height = background.size if not images: return result.convert('RGB') num_images = len(images) if num_images == 1: img = images[0] img.thumbnail((bg_width // 2, bg_height // 2), Image.Resampling.LANCZOS) x = (bg_width - img.width) // 2 y = (bg_height - img.height) // 3 result.paste(img, (x, y), img) elif num_images == 2: for idx, img in enumerate(images): scale = 0.6 if idx == 0 else 0.5 img.thumbnail((int(bg_width * scale), int(bg_height * scale)), Image.Resampling.LANCZOS) if idx == 0: x = bg_width // 4 - img.width // 2 y = bg_height // 3 - img.height // 2 else: x = bg_width * 2 // 3 - img.width // 2 y = bg_height * 2 // 3 - img.height // 2 result.paste(img, (x, y), img) elif num_images == 3: positions = [ (bg_width // 3, bg_height // 4), (bg_width * 2 // 3, bg_height // 4), (bg_width // 2, bg_height * 2 // 3) ] for idx, (img, (px, py)) in enumerate(zip(images, positions)): scale = 0.5 - idx * 0.05 img.thumbnail((int(bg_width * scale), int(bg_height * scale)), Image.Resampling.LANCZOS) x = px - img.width // 2 y = py - img.height // 2 # Rotação leve if idx % 2 == 0: img = img.rotate(5, expand=True, fillcolor=(0, 0, 0, 0)) else: img = img.rotate(-5, expand=True, fillcolor=(0, 0, 0, 0)) result.paste(img, (x, y), img) else: cols = min(3, int(np.ceil(np.sqrt(num_images)))) rows = int(np.ceil(num_images / cols)) cell_w = bg_width // (cols + 1) cell_h = bg_height // (rows + 2) for idx, img in enumerate(images[:cols*rows]): row = idx // cols col = idx % cols scale = 0.8 - row * 0.1 img.thumbnail((int(cell_w * scale), int(cell_h * scale)), Image.Resampling.LANCZOS) x = (col + 1) * cell_w - img.width // 2 y = (row + 1) * cell_h - img.height // 2 # Pequena rotação aleatória if random.random() > 0.7: rot = random.randint(-8, 8) img = img.rotate(rot, expand=True, fillcolor=(0, 0, 0, 0)) result.paste(img, (x, y), img) self.logger.add_log("Collage criada", "success") return result.convert('RGB') except Exception as e: error_msg = str(e) self.logger.add_log(f"Erro na collage: {error_msg[:50]}", "error") return background.convert('RGB') def optimize_file_size(self, image: Image.Image) -> Image.Image: """Otimiza tamanho do arquivo""" try: self.logger.add_log("Otimizando arquivo...", "processing") if image.mode == 'RGBA': image = image.convert('RGB') # Tentar diferentes qualidades for quality in [90, 85, 80, 75, 70]: buffer = io.BytesIO() image.save(buffer, format='JPEG', quality=quality, optimize=True) size_mb = len(buffer.getvalue()) / (1024 * 1024) if size_mb <= OUTPUT_MAX_SIZE_MB: buffer.seek(0) result = Image.open(buffer).copy() self.logger.add_log(f"Arquivo: {size_mb:.2f}MB (qualidade: {quality}%)", "success") return result # Fallback buffer = io.BytesIO() image.save(buffer, format='JPEG', quality=70, optimize=True) buffer.seek(0) return Image.open(buffer).copy() except Exception as e: error_msg = str(e) self.logger.add_log(f"Erro na otimização: {error_msg[:50]}", "error") return image def generate_poster(self, image_files, resolution_preset, custom_width, custom_height, border_style, border_color, gradient_color1, gradient_color2, title, description, text_color, text_effect, font_style, outline_thickness): """Função principal para gerar o pôster""" # Limpar cache antigo self.image_cache.clear() self.logger.clear() try: self.logger.add_log("🚀 INICIANDO GERAÇÃO DE PÔSTER", "info") if not image_files: self.logger.add_log("❌ Nenhuma imagem fornecida", "error") return None, "❌ Por favor, faça upload de imagens", self.logger.get_logs() self.logger.add_log(f"📸 {len(image_files)} imagem(ns) carregada(s)", "success") # Determinar resolução if resolution_preset == "Custom": try: size = (int(custom_width), int(custom_height)) self.logger.add_log(f"📐 Custom: {size[0]}x{size[1]}px", "info") except: self.logger.add_log("❌ Dimensões inválidas", "error") return None, "❌ Use números válidos", self.logger.get_logs() else: size = RESOLUTION_PRESETS.get(resolution_preset, (1080, 1080)) self.logger.add_log(f"📐 {resolution_preset}: {size[0]}x{size[1]}px", "info") # Processar imagens processed_images = [] for idx, file_info in enumerate(image_files[:MAX_IMAGES]): try: filepath = file_info if isinstance(file_info, str) else file_info.name self.logger.add_log(f"🔄 Processando imagem {idx+1}...", "processing") # Abrir imagem img = Image.open(filepath) if img.mode != 'RGB': img = img.convert('RGB') # Gerar hash do arquivo para cache import hashlib with open(filepath, 'rb') as f: file_hash = hashlib.md5(f.read()).hexdigest()[:8] # Remover fundo bg_removed = self.precise_background_removal(img, idx+1, file_hash) # Aplicar borda if border_style != "Sem Borda": border_key = BORDER_STYLES[border_style] bordered = self.apply_border_effect(bg_removed, border_key, border_color, idx+1, outline_thickness) processed_images.append(bordered) else: processed_images.append(bg_removed) self.logger.add_log(f"✅ Imagem {idx+1} processada", "success") except Exception as e: error_msg = str(e) self.logger.add_log(f"⚠️ Imagem {idx+1} ignorada: {error_msg[:50]}", "warning") continue if not processed_images: self.logger.add_log("❌ Nenhuma imagem processada", "error") return None, "❌ Não foi possível processar imagens", self.logger.get_logs() self.logger.add_log(f"✅ {len(processed_images)} imagem(s) processada(s)", "success") # Criar fundo self.logger.add_log("🎨 Criando fundo...", "processing") background = self.create_gradient_background(size, gradient_color1, gradient_color2) # Criar collage self.logger.add_log("🖼️ Criando collage...", "processing") collage = self.create_collage(processed_images, background) # Adicionar texto if title or description: self.logger.add_log("✍️ Adicionando texto...", "processing") collage = self.add_text_with_effects(collage, title, description, text_color, text_effect, font_style) # Otimizar self.logger.add_log("🗜️ Otimizando...", "processing") final_image = self.optimize_file_size(collage) # Finalizar self.logger.add_log("🎉 PÔSTER GERADO COM SUCESSO!", "success") success_msg = f"✅ Pôster pronto! {len(processed_images)} imagem(s), {size[0]}x{size[1]}px" return final_image, success_msg, self.logger.get_logs() except Exception as e: error_msg = str(e) self.logger.add_log(f"❌ ERRO CRÍTICO: {error_msg[:100]}", "error") return None, f"❌ Erro: {error_msg[:100]}", self.logger.get_logs() # ============================ # INTERFACE GRADIO # ============================ # Inicializar gerador generator = AdvancedCollagePosterGenerator() def format_logs_for_html(log_text): """Formata logs para HTML""" lines = log_text.split('\n') formatted = [] for line in lines: if '❌' in line: formatted.append(f'{line}') elif '✅' in line: formatted.append(f'{line}') elif '⚠️' in line: formatted.append(f'{line}') elif '🔄' in line: formatted.append(f'{line}') elif '🚀' in line or '🎉' in line: formatted.append(f'{line}') elif '📸' in line or '📐' in line: formatted.append(f'{line}') elif '🎨' in line or '🖼️' in line: formatted.append(f'{line}') elif '✍️' in line: formatted.append(f'{line}') elif '🗜️' in line: formatted.append(f'{line}') else: formatted.append(f'{line}') return '
'.join(formatted) with gr.Blocks(theme=gr.themes.Soft(), title="Gerador de Pôster de Colagem") as demo: gr.Markdown(""" # 🎨 Gerador de Pôster de Colagem Crie pôsteres incríveis com múltiplas imagens! """) with gr.Row(): with gr.Column(scale=1): # Upload de imagens - SIMPLIFICADO gr.Markdown("### 📤 Upload de Imagens") image_input = gr.File( label="Selecione imagens (até 10)", file_types=["image"], file_count="multiple", type="filepath", height=100 ) # Apenas mostra quantas imagens foram carregadas image_counter = gr.Markdown("**Nenhuma imagem carregada**") # Configurações gr.Markdown("### ⚙️ Configurações") with gr.Group(): resolution_preset = gr.Dropdown( label="Tamanho do Pôster", choices=list(RESOLUTION_PRESETS.keys()), value="Instagram Post (1:1)" ) with gr.Row(visible=False) as custom_row: custom_width = gr.Number(label="Largura", value=1080) custom_height = gr.Number(label="Altura", value=1080) with gr.Group(): border_style = gr.Dropdown( label="Estilo da Borda", choices=list(BORDER_STYLES.keys()), value="Sombra Natural" ) with gr.Row(): border_color = gr.ColorPicker( label="Cor da Borda", value="#FF6B6B" ) outline_thickness = gr.Slider( label="Espessura do Contorno", minimum=1, maximum=10, value=3, step=1, visible=False ) with gr.Group(): gr.Markdown("### 🌈 Cores do Fundo") with gr.Row(): gradient_color1 = gr.ColorPicker( label="Cor Superior", value="#667EEA" ) gradient_color2 = gr.ColorPicker( label="Cor Inferior", value="#764BA2" ) with gr.Group(): gr.Markdown("### ✨ Texto") with gr.Row(): text_effect = gr.Dropdown( label="Efeito do Texto", choices=list(TEXT_EFFECTS.keys()), value="3D Profundo" ) font_style = gr.Dropdown( label="Estilo da Fonte", choices=list(FONT_STYLES.keys()), value="Moderno Bold" ) text_color = gr.ColorPicker( label="Cor do Texto", value="#FFFFFF" ) title_input = gr.Textbox( label="Título (opcional)", placeholder="Digite o título..." ) description_input = gr.Textbox( label="Descrição (opcional)", placeholder="Digite a descrição...", lines=2 ) # Botões with gr.Row(): process_btn = gr.Button( "🚀 Gerar Pôster", variant="primary", size="lg" ) clear_btn = gr.Button( "🗑️ Limpar Tudo", variant="secondary" ) with gr.Column(scale=2): # Resultado gr.Markdown("### 🖼️ Pôster Gerado") output_image = gr.Image( label="Resultado", type="pil", height=500 ) # Status status_text = gr.Markdown(""" **Status:** Aguardando imagens... **Instruções:** 1. Selecione imagens 2. Configure as opções 3. Clique em 'Gerar Pôster' """) # Logs logs_display = gr.HTML( label="📋 Logs", value='
Aguardando processamento...
' ) # Download gr.Markdown("### 💾 Download") with gr.Row(): download_jpeg = gr.DownloadButton( "⬇️ Baixar JPEG", visible=False ) download_png = gr.DownloadButton( "⬇️ Baixar PNG", visible=False ) # ===== FUNÇÕES ===== def update_image_counter(files): """Atualiza contador de imagens (NÃO processa as imagens)""" if not files: return "**Nenhuma imagem carregada**", [] # Apenas conta as imagens, não as processa count = len(files) return f"**{count} imagem(ns) carregada(s)**", files def toggle_custom_res(choice): """Mostra/oculta campos customizados""" return gr.update(visible=choice == "Custom") def toggle_outline_control(style): """Mostra/oculta controle de espessura""" return gr.update(visible=style == "Contorno Colorido") def process_images(files, *args): """Processa as imagens APENAS quando o botão é clicado""" if not files: return [ None, "**❌ ERRO:** Nenhuma imagem carregada", '
❌ Faça upload de imagens primeiro
', gr.update(visible=False), gr.update(visible=False) ] try: # Gerar pôster result_img, result_msg, logs = generator.generate_poster(files, *args) # Formatar logs html_logs = f"""
{format_logs_for_html(logs)}
""" if result_img is not None: # Salvar arquivos temporários jpeg_temp = tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') result_img.save(jpeg_temp.name, 'JPEG', quality=90, optimize=True) png_temp = tempfile.NamedTemporaryFile(delete=False, suffix='.png') if result_img.mode == 'RGBA': result_img.save(png_temp.name, 'PNG') else: result_img.convert('RGB').save(png_temp.name, 'PNG') return [ result_img, f"**✅ SUCESSO!** {result_msg}", html_logs, gr.update(visible=True, value=jpeg_temp.name), gr.update(visible=True, value=png_temp.name) ] else: return [ None, f"**❌ ERRO:** {result_msg}", html_logs, gr.update(visible=False), gr.update(visible=False) ] except Exception as e: error_html = f"""
❌ ERRO NO PROCESSAMENTO
{str(e)[:150]}
""" return [ None, "**❌ ERRO:** Ocorreu um erro no processamento", error_html, gr.update(visible=False), gr.update(visible=False) ] def clear_all(): """Limpa tudo""" generator.image_cache.clear() generator.logger.clear() return [ None, "**Nenhuma imagem carregada**", [], None, "**Status:** Aguardando imagens...", '
Sistema limpo. Aguardando novas imagens...
', gr.update(visible=False), gr.update(visible=False) ] # ===== EVENTOS ===== # Upload de imagens - APENAS contagem, NÃO processamento image_input.change( fn=update_image_counter, inputs=[image_input], outputs=[image_counter, image_input] ) # Alternar resolução customizada resolution_preset.change( fn=toggle_custom_res, inputs=[resolution_preset], outputs=[custom_row] ) # Alternar controle de espessura border_style.change( fn=toggle_outline_control, inputs=[border_style], outputs=[outline_thickness] ) # Botão Gerar - AGORA SIM processa as imagens process_btn.click( fn=process_images, inputs=[ image_input, resolution_preset, custom_width, custom_height, border_style, border_color, gradient_color1, gradient_color2, title_input, description_input, text_color, text_effect, font_style, outline_thickness ], outputs=[ output_image, status_text, logs_display, download_jpeg, download_png ] ) # Botão Limpar clear_btn.click( fn=clear_all, outputs=[ image_input, image_counter, image_input, output_image, status_text, logs_display, download_jpeg, download_png ] ) # ===== INSTRUÇÕES ===== gr.Markdown(""" --- ## 📖 Como Usar: 1. **Upload de Imagens**: Selecione até 10 imagens 2. **Configurações**: Escolha tamanho, bordas, cores e texto 3. **Gerar**: Clique no botão para processar 4. **Download**: Baixe o resultado em JPEG ou PNG ### 🎯 Recursos: - ✅ Remoção precisa de fundo - ✅ 5 estilos de borda diferentes - ✅ Controle de espessura do contorno (1-10px) - ✅ Fundo degradê personalizável - ✅ Texto com efeitos especiais - ✅ Composição harmoniosa - ✅ Logs detalhados ### ⚡ Dicas: - Para melhor remoção de fundo, use imagens com bom contraste - Teste diferentes combinações de cores - Use títulos curtos e descrições concisas - O sistema é otimizado para performance """) # ============================ # EXECUÇÃO # ============================ if __name__ == "__main__": print("🚀 Iniciando Gerador de Pôster de Colagem...") print("✅ Sistema pronto!") demo.launch( server_name="0.0.0.0", server_port=7860, share=False )