Spaces:
Sleeping
Sleeping
| 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'<span style="color: #ff6b6b; font-weight: bold;">{line}</span>') | |
| elif '✅' in line: | |
| formatted.append(f'<span style="color: #51cf66;">{line}</span>') | |
| elif '⚠️' in line: | |
| formatted.append(f'<span style="color: #ffd43b;">{line}</span>') | |
| elif '🔄' in line: | |
| formatted.append(f'<span style="color: #4dabf7;">{line}</span>') | |
| elif '🚀' in line or '🎉' in line: | |
| formatted.append(f'<span style="color: #cc5de8; font-weight: bold;">{line}</span>') | |
| elif '📸' in line or '📐' in line: | |
| formatted.append(f'<span style="color: #74c0fc;">{line}</span>') | |
| elif '🎨' in line or '🖼️' in line: | |
| formatted.append(f'<span style="color: #ffa94d;">{line}</span>') | |
| elif '✍️' in line: | |
| formatted.append(f'<span style="color: #ffc9c9;">{line}</span>') | |
| elif '🗜️' in line: | |
| formatted.append(f'<span style="color: #ced4da;">{line}</span>') | |
| else: | |
| formatted.append(f'<span style="color: #dee2e6;">{line}</span>') | |
| return '<br>'.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='<div style="background: #1a1a1a; padding: 15px; border-radius: 5px; color: #ccc;">Aguardando processamento...</div>' | |
| ) | |
| # 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", | |
| '<div style="background: #1a1a1a; padding: 15px; border-radius: 5px; color: #ff6b6b;">❌ Faça upload de imagens primeiro</div>', | |
| 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""" | |
| <div style=" | |
| background: #1a1a1a; | |
| padding: 15px; | |
| border-radius: 5px; | |
| font-family: monospace; | |
| font-size: 13px; | |
| line-height: 1.5; | |
| max-height: 300px; | |
| overflow-y: auto; | |
| "> | |
| {format_logs_for_html(logs)} | |
| </div> | |
| """ | |
| 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""" | |
| <div style=" | |
| background: #2a1a1a; | |
| padding: 15px; | |
| border-radius: 5px; | |
| color: #ff6b6b; | |
| "> | |
| ❌ ERRO NO PROCESSAMENTO<br> | |
| {str(e)[:150]} | |
| </div> | |
| """ | |
| 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...", | |
| '<div style="background: #1a1a1a; padding: 15px; border-radius: 5px; color: #ccc;">Sistema limpo. Aguardando novas imagens...</div>', | |
| 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 | |
| ) |