OctusTech/cartalogo-api
0
1import os2import sys3from pathlib import Path4from dotenv import load_dotenv5import torch6import open_clip7from PIL import Image, ImageEnhance, ImageFilter8import numpy as np9import cv210from typing import Dict, List, Tuple, Optional11import json12import colorsys13from sklearn.cluster import KMeans14import time15 16# FORÇA carregamento das variáveis de ambiente17env_file = Path(__file__).parent / ".env"18print(f"🔍 Carregando .env de: {env_file}")19print(f"🔍 Arquivo existe: {env_file.exists()}")20 21if env_file.exists():22 # Carrega com override para forçar23 load_result = load_dotenv(env_file, override=True, verbose=True)24 print(f"🔍 Load result: {load_result}")25else:26 print("❌ Arquivo .env não encontrado!")27 # Tenta carregar do diretório atual28 load_dotenv(override=True, verbose=True)29 30class JewelryImageProcessor:31 def __init__(self):32 """Inicializa o processador especializado em joias"""33 print("🤖 Inicializando processador de imagens para joias...")34 35 # Configuração de caminhos36 self.root_dir = Path(__file__).parent37 self.images_dir = self.root_dir / "imagens"38 self.thumbnails_dir = self.root_dir / "thumbnails"39 40 # Cria diretório de thumbnails41 self.thumbnails_dir.mkdir(exist_ok=True)42 43 # Configuração do dispositivo44 self.device = "cuda" if torch.cuda.is_available() else "cpu"45 print(f"🔧 Dispositivo: {self.device}")46 47 # Carrega modelo OpenCLIP48 try:49 model_name = os.getenv('EMBEDDING_MODEL', 'ViT-L-14')50 pretrained = os.getenv('EMBEDDING_PRETRAINED', 'laion2b_s32b_b82k')51 52 print(f"📥 Carregando modelo {model_name} ({pretrained})...")53 self.model, _, self.preprocess = open_clip.create_model_and_transforms(54 model_name, pretrained=pretrained, device=self.device55 )56 self.tokenizer = open_clip.get_tokenizer(model_name)57 print("✅ Modelo OpenCLIP carregado com sucesso!")58 59 except Exception as e:60 print(f"❌ Erro ao carregar modelo OpenCLIP: {e}")61 raise62 63 # DEBUG: Configuração OpenAI v1.0+64 print("\n🔍 DEBUG OpenAI:")65 66 # Tenta diferentes formas de obter a chave67 openai_api_key = None68 69 # Método 1: os.getenv70 openai_api_key = os.getenv('OPENAI_API_KEY')71 print(f"🔍 Método 1 (os.getenv): {openai_api_key[:15] if openai_api_key else 'None'}...")72 73 # Método 2: os.environ.get74 if not openai_api_key or openai_api_key.startswith('your-'):75 openai_api_key = os.environ.get('OPENAI_API_KEY')76 print(f"🔍 Método 2 (os.environ): {openai_api_key[:15] if openai_api_key else 'None'}...")77 78 # Método 3: Lê diretamente do arquivo .env79 if not openai_api_key or openai_api_key.startswith('your-'):80 try:81 env_file = self.root_dir / ".env"82 if env_file.exists():83 with open(env_file, 'r', encoding='utf-8') as f:84 for line in f:85 if line.startswith('OPENAI_API_KEY='):86 openai_api_key = line.split('=', 1)[1].strip()87 print(f"🔍 Método 3 (arquivo): {openai_api_key[:15]}...")88 break89 except Exception as e:90 print(f"🔍 Erro método 3: {e}")91 92 self.openai_client = None93 94 print(f"🔑 OpenAI API Key final: {'Sim' if openai_api_key else 'Não'}")95 if openai_api_key:96 print(f"🔑 Primeiros caracteres: {openai_api_key[:15]}...")97 print(f"🔑 Começa com sk-: {openai_api_key.startswith('sk-')}")98 99 if openai_api_key and openai_api_key.startswith('sk-'):100 try:101 from openai import OpenAI102 self.openai_client = OpenAI(api_key=openai_api_key)103 print("✅ OpenAI v1.0+ configurado para descrições IA")104 105 # Testa a conexão106 try:107 # Faz um teste simples para verificar se a API key funciona108 test_response = self.openai_client.models.list()109 print("✅ Conexão com OpenAI testada com sucesso")110 except Exception as test_error:111 print(f"⚠️ Erro ao testar conexão OpenAI: {test_error}")112 self.openai_client = None113 114 except ImportError:115 print("⚠️ OpenAI não instalado - descrições IA desabilitadas")116 self.openai_client = None117 except Exception as e:118 print(f"⚠️ Erro ao configurar OpenAI: {e}")119 self.openai_client = None120 else:121 print("⚠️ OpenAI API key inválida - descrições IA desabilitadas")122 self.openai_client = None123 124 def generate_embedding(self, image_path: Path) -> Optional[np.ndarray]:125 """Gera embedding da imagem usando OpenCLIP"""126 try:127 if not image_path.exists():128 print(f"❌ Arquivo não encontrado: {image_path}")129 return None130 131 # Carrega e processa imagem132 image = Image.open(image_path).convert('RGB')133 image_input = self.preprocess(image).unsqueeze(0).to(self.device)134 135 with torch.no_grad():136 image_features = self.model.encode_image(image_input)137 # Normaliza o vetor138 image_features = image_features / image_features.norm(dim=-1, keepdim=True)139 140 return image_features.cpu().numpy().flatten()141 142 except Exception as e:143 print(f"❌ Erro ao gerar embedding para {image_path.name}: {e}")144 return None145 146 def analyze_jewelry_technical(self, image_path: Path) -> Dict:147 """Análise técnica especializada para joias"""148 try:149 if not image_path.exists():150 return {}151 152 # Carrega imagem153 image = cv2.imread(str(image_path))154 if image is None:155 print(f"❌ Não foi possível carregar a imagem: {image_path}")156 return {}157 158 image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)159 160 print(f"🔍 Analisando {image_path.name}...")161 162 analysis = {163 "basic_metrics": self._analyze_basic_metrics(image_rgb),164 "color_analysis": self._analyze_jewelry_colors(image_rgb),165 "metal_analysis": self._analyze_metal_properties(image_rgb),166 "stone_analysis": self._analyze_stones(image_rgb),167 "shape_analysis": self._analyze_jewelry_shapes(image_rgb),168 "texture_analysis": self._analyze_textures(image_rgb),169 "reflection_analysis": self._analyze_reflections(image_rgb),170 "quality_assessment": self._assess_image_quality(image_rgb)171 }172 173 return analysis174 175 except Exception as e:176 print(f"❌ Erro na análise técnica de {image_path.name}: {e}")177 return {}178 179 def extract_visual_features(self, image_path: Path, technical_analysis: Dict) -> List[Dict]:180 """Extrai características visuais estruturadas para a tabela visual_features"""181 features = []182 183 try:184 # Características de cor185 color_analysis = technical_analysis.get('color_analysis', {})186 dominant_colors = color_analysis.get('dominant_colors', [])187 188 for i, color in enumerate(dominant_colors[:3]): # Top 3 cores189 features.append({190 'feature_type': 'dominant_color',191 'feature_value': color.get('hex', '#000000'),192 'confidence_score': min(0.99, color.get('percentage', 0) / 100)193 })194 195 # Características de metal196 metal_analysis = technical_analysis.get('metal_analysis', {})197 if metal_analysis.get('has_metallic_elements', False):198 features.append({199 'feature_type': 'material',200 'feature_value': 'metallic',201 'confidence_score': min(0.99, metal_analysis.get('metal_percentage', 0))202 })203 204 # Características de pedras205 stone_analysis = technical_analysis.get('stone_analysis', {})206 if stone_analysis.get('has_colored_stones', False):207 features.append({208 'feature_type': 'material',209 'feature_value': 'gemstone',210 'confidence_score': min(0.99, stone_analysis.get('stone_percentage', 0))211 })212 213 # Características de forma214 shape_analysis = technical_analysis.get('shape_analysis', {})215 complexity = shape_analysis.get('shape_complexity', 'unknown')216 if complexity != 'unknown':217 features.append({218 'feature_type': 'shape',219 'feature_value': complexity,220 'confidence_score': 0.8221 })222 223 # Características de textura224 texture_analysis = technical_analysis.get('texture_analysis', {})225 texture_type = texture_analysis.get('texture_type', 'unknown')226 if texture_type != 'unknown':227 features.append({228 'feature_type': 'texture',229 'feature_value': texture_type,230 'confidence_score': 0.75231 })232 233 # Características de qualidade234 quality_assessment = technical_analysis.get('quality_assessment', {})235 quality_grade = quality_assessment.get('quality_grade', 'unknown')236 if quality_grade != 'unknown':237 features.append({238 'feature_type': 'quality',239 'feature_value': quality_grade,240 'confidence_score': quality_assessment.get('quality_score', 0.5)241 })242 243 except Exception as e:244 print(f"⚠️ Erro ao extrair características visuais: {e}")245 246 return features247 248 def generate_image_tags(self, image_path: Path, piece_metadata: Dict, technical_analysis: Dict) -> List[Dict]:249 """Gera tags automáticas para a imagem"""250 tags = []251 252 try:253 # Tags baseadas nos metadados254 category = piece_metadata.get('category', '')255 if category:256 tags.append({257 'tag': category,258 'confidence': 0.95,259 'tag_type': 'category'260 })261 262 collection = piece_metadata.get('collection', '')263 if collection:264 tags.append({265 'tag': collection.lower(),266 'confidence': 0.9,267 'tag_type': 'collection'268 })269 270 stone = piece_metadata.get('stone', '')271 if stone and stone.lower() not in ['', 'nan', 'none']:272 tags.append({273 'tag': stone.lower(),274 'confidence': 0.85,275 'tag_type': 'material'276 })277 278 metal = piece_metadata.get('predominant_metal', '')279 if metal and metal.lower() not in ['', 'nan', 'none']:280 tags.append({281 'tag': metal.lower().replace(' ', '_'),282 'confidence': 0.85,283 'tag_type': 'material'284 })285 286 # Tags baseadas na análise técnica287 color_analysis = technical_analysis.get('color_analysis', {})288 dominant_colors = color_analysis.get('dominant_colors', [])289 290 for color in dominant_colors[:2]: # Top 2 cores291 if color.get('percentage', 0) > 15: # Apenas cores significativas292 color_name = self._get_color_name_from_rgb(color.get('rgb', [0, 0, 0]))293 tags.append({294 'tag': color_name,295 'confidence': min(0.8, color.get('percentage', 0) / 100),296 'tag_type': 'color'297 })298 299 # Tags de estilo baseadas no nome do arquivo300 filename = image_path.name.lower()301 if 'modelo' in filename:302 tags.append({303 'tag': 'lifestyle',304 'confidence': 0.9,305 'tag_type': 'style'306 })307 else:308 tags.append({309 'tag': 'product_shot',310 'confidence': 0.9,311 'tag_type': 'style'312 })313 314 # Tags de qualidade315 quality_assessment = technical_analysis.get('quality_assessment', {})316 quality_score = quality_assessment.get('quality_score', 0)317 if quality_score > 0.7:318 tags.append({319 'tag': 'high_quality',320 'confidence': quality_score,321 'tag_type': 'quality'322 })323 elif quality_score > 0.3:324 tags.append({325 'tag': 'medium_quality',326 'confidence': quality_score,327 'tag_type': 'quality'328 })329 330 except Exception as e:331 print(f"⚠️ Erro ao gerar tags: {e}")332 333 return tags334 335 def create_thumbnail(self, image_path: Path, size: Tuple[int, int] = None) -> Optional[Path]:336 """Cria thumbnail da imagem"""337 try:338 if size is None:339 size = (int(os.getenv('THUMBNAIL_SIZE', 200)), int(os.getenv('THUMBNAIL_SIZE', 200)))340 341 if not image_path.exists():342 return None343 344 # Define caminho do thumbnail345 thumbnail_name = f"thumb_{image_path.stem}.png"346 thumbnail_path = self.thumbnails_dir / thumbnail_name347 348 with Image.open(image_path) as img:349 # Mantém proporção350 img.thumbnail(size, Image.Resampling.LANCZOS)351 352 # Cria imagem quadrada com fundo branco353 thumbnail = Image.new('RGB', size, (255, 255, 255))354 355 # Centraliza a imagem356 x = (size[0] - img.width) // 2357 y = (size[1] - img.height) // 2358 thumbnail.paste(img, (x, y))359 360 # Salva thumbnail361 thumbnail.save(thumbnail_path, 'PNG', quality=95)362 363 print(f"✅ Thumbnail criado: {thumbnail_name}")364 return thumbnail_path365 366 except Exception as e:367 print(f"❌ Erro ao criar thumbnail de {image_path.name}: {e}")368 return None369 370 def generate_ai_description(self, image_path: Path, piece_metadata: Dict) -> str:371 """Gera descrição detalhada usando GPT-4 Vision (OpenAI v1.0+)"""372 try:373 if not self.openai_client:374 print(f" ⚠️ OpenAI não configurado, usando descrição básica")375 return self._generate_basic_description(piece_metadata)376 377 # Converte imagem para base64378 import base64379 with open(image_path, "rb") as image_file:380 base64_image = base64.b64encode(image_file.read()).decode('utf-8')381 382 prompt = f"""383 Analise esta imagem de joia e forneça uma descrição detalhada e técnica em português brasileiro.384 385 Metadados da peça:386 - SKU: {piece_metadata.get('sku', 'N/A')}387 - Nome: {piece_metadata.get('name', 'N/A')}388 - Material: {piece_metadata.get('material', 'N/A')}389 - Pedra: {piece_metadata.get('stone', 'N/A')}390 - Coleção: {piece_metadata.get('collection', 'N/A')}391 392 Descreva detalhadamente:393 1. Tipo de joia e formato geral394 2. Materiais visíveis (metais, pedras, acabamentos)395 3. Cores predominantes e tonalidades396 4. Estilo e design (moderno, clássico, vintage, etc.)397 5. Detalhes técnicos (lapidação, engastes, texturas)398 6. Qualidade aparente e acabamento399 7. Características distintivas para busca400 401 Use terminologia técnica de joalheria. Seja específico e descritivo.402 """403 404 print(f" 🤖 Gerando descrição IA para {image_path.name}...")405 406 response = self.openai_client.chat.completions.create(407 model="gpt-4o", # Modelo mais recente e estável408 messages=[409 {410 "role": "user",411 "content": [412 {"type": "text", "text": prompt},413 {414 "type": "image_url",415 "image_url": {416 "url": f"data:image/png;base64,{base64_image}",417 "detail": "low" # Reduz custo418 }419 }420 ]421 }422 ],423 max_tokens=400,424 temperature=0.3 # Mais consistente425 )426 427 description = response.choices[0].message.content428 print(f" ✅ Descrição IA gerada ({len(description)} caracteres)")429 return description430 431 except Exception as e:432 print(f" ⚠️ Erro ao gerar descrição IA para {image_path.name}: {e}")433 print(f" 🔄 Usando descrição básica como fallback")434 return self._generate_basic_description(piece_metadata)435 436 def _generate_basic_description(self, piece_metadata: Dict) -> str:437 """Gera descrição básica sem IA"""438 name = piece_metadata.get('name', 'Joia')439 collection = piece_metadata.get('collection', '')440 material = piece_metadata.get('material', '')441 stone = piece_metadata.get('stone', '')442 443 description = f"{name}"444 if collection:445 description += f" da coleção {collection}"446 if material:447 description += f", confeccionada em {material}"448 if stone:449 description += f" com {stone}"450 451 return description452 453 def find_images_from_metadata(self, piece_metadata: Dict) -> List[Dict]:454 """Encontra imagens baseado nos metadados da planilha"""455 images = []456 sku = piece_metadata.get('sku', '')457 458 # Baseado na estrutura real do CSV: apenas arquivo_peca_01459 filename = piece_metadata.get('arquivo_peca_01', '').strip()460 461 if filename and filename != 'nan' and filename != '':462 image_path = self.images_dir / filename463 464 if image_path.exists():465 # Determina tipo baseado no nome do arquivo466 filename_lower = filename.lower()467 468 if any(keyword in filename_lower for keyword in ["modelo", "person", "wearing", "lifestyle", "usando"]):469 image_type = "lifestyle"470 elif any(keyword in filename_lower for keyword in ["detalhe", "detail", "close"]):471 image_type = "detail"472 elif any(keyword in filename_lower for keyword in ["embalagem", "packaging", "box"]):473 image_type = "packaging"474 else:475 image_type = "product"476 477 images.append({478 'path': image_path,479 'filename': filename,480 'type': image_type,481 'size': image_path.stat().st_size,482 'column': 'arquivo_peca_01'483 })484 print(f" ✅ Encontrada: {filename} ({image_type})")485 else:486 print(f" ❌ Não encontrada: {filename}")487 else:488 print(f" ⚠️ Nenhum arquivo especificado para SKU: {sku}")489 490 return images491 492 def _get_color_name_from_rgb(self, rgb: List[int]) -> str:493 """Retorna nome da cor baseado em RGB"""494 if not rgb or len(rgb) != 3:495 return "indefinido"496 497 r, g, b = rgb498 499 # Cores básicas para joias500 if r > 200 and g > 200 and b > 200:501 return "branco"502 elif r < 50 and g < 50 and b < 50:503 return "preto"504 elif r > 200 and g > 180 and b < 100:505 return "dourado"506 elif r > 180 and g > 180 and b > 180:507 return "prateado"508 elif r > 150 and g < 100 and b < 100:509 return "vermelho"510 elif r < 100 and g < 100 and b > 150:511 return "azul"512 elif r < 100 and g > 150 and b < 100:513 return "verde"514 elif r > 150 and g > 100 and b > 150:515 return "roxo"516 elif r > 200 and g > 150 and b < 100:517 return "laranja"518 else:519 return "multicolorido"520 521 # Métodos de análise técnica (mantidos iguais)522 def _analyze_basic_metrics(self, image: np.ndarray) -> Dict:523 """Métricas básicas da imagem"""524 height, width = image.shape[:2]525 brightness = float(np.mean(image))526 527 gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)528 contrast = float(np.std(gray))529 530 hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)531 saturation = float(np.mean(hsv[:, :, 1]))532 533 return {534 "dimensions": {"width": width, "height": height},535 "brightness": brightness,536 "contrast": contrast,537 "saturation": saturation,538 "aspect_ratio": float(width / height)539 }540 541 def _analyze_jewelry_colors(self, image: np.ndarray) -> Dict:542 """Análise de cores para joias"""543 try:544 dominant_colors = self._extract_dominant_colors(image, k=6)545 546 return {547 "dominant_colors": dominant_colors,548 "color_count": len(dominant_colors),549 "primary_color": dominant_colors[0] if dominant_colors else None550 }551 except:552 return {"dominant_colors": [], "color_count": 0, "primary_color": None}553 554 def _extract_dominant_colors(self, image: np.ndarray, k: int = 6) -> List[Dict]:555 """Extrai cores dominantes"""556 try:557 pixels = image.reshape(-1, 3)558 559 # Reduz amostra para performance560 if len(pixels) > 10000:561 indices = np.random.choice(len(pixels), 10000, replace=False)562 pixels = pixels[indices]563 564 kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)565 kmeans.fit(pixels)566 567 colors = []568 labels = kmeans.labels_569 unique, counts = np.unique(labels, return_counts=True)570 571 for i, color in enumerate(kmeans.cluster_centers_):572 percentage = counts[i] / len(labels) * 100573 rgb = [int(c) for c in color]574 575 colors.append({576 "rgb": rgb,577 "hex": "#{:02x}{:02x}{:02x}".format(*rgb),578 "percentage": float(percentage)579 })580 581 return sorted(colors, key=lambda x: x['percentage'], reverse=True)582 583 except Exception as e:584 print(f"⚠️ Erro na extração de cores: {e}")585 return []586 587 def _analyze_metal_properties(self, image: np.ndarray) -> Dict:588 """Análise básica de propriedades metálicas"""589 try:590 hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)591 592 # Detecta áreas metálicas (brilhantes + baixa saturação)593 brightness_mask = hsv[:, :, 2] > 150594 low_saturation_mask = hsv[:, :, 1] < 80595 metal_mask = brightness_mask & low_saturation_mask596 597 metal_percentage = float(np.sum(metal_mask) / (image.shape[0] * image.shape[1]))598 599 return {600 "metal_percentage": metal_percentage,601 "has_metallic_elements": metal_percentage > 0.1602 }603 except:604 return {"metal_percentage": 0.0, "has_metallic_elements": False}605 606 def _analyze_stones(self, image: np.ndarray) -> Dict:607 """Análise básica de pedras"""608 try:609 hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)610 high_saturation_mask = hsv[:, :, 1] > 100611 stone_percentage = float(np.sum(high_saturation_mask) / (image.shape[0] * image.shape[1]))612 613 return {614 "stone_percentage": stone_percentage,615 "has_colored_stones": stone_percentage > 0.05616 }617 except:618 return {"stone_percentage": 0.0, "has_colored_stones": False}619 620 def _analyze_jewelry_shapes(self, image: np.ndarray) -> Dict:621 """Análise básica de formas"""622 try:623 gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)624 edges = cv2.Canny(gray, 50, 150)625 edge_density = float(np.sum(edges > 0) / (edges.shape[0] * edges.shape[1]))626 627 return {628 "edge_density": edge_density,629 "shape_complexity": "high" if edge_density > 0.1 else "low"630 }631 except:632 return {"edge_density": 0.0, "shape_complexity": "unknown"}633 634 def _analyze_textures(self, image: np.ndarray) -> Dict:635 """Análise básica de texturas"""636 try:637 gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)638 texture_variance = float(np.var(gray))639 640 return {641 "texture_variance": texture_variance,642 "texture_type": "smooth" if texture_variance < 1000 else "textured"643 }644 except:645 return {"texture_variance": 0.0, "texture_type": "unknown"}646 647 def _analyze_reflections(self, image: np.ndarray) -> Dict:648 """Análise básica de reflexos"""649 try:650 gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)651 bright_threshold = np.percentile(gray, 95)652 reflection_mask = gray > bright_threshold653 reflection_percentage = float(np.sum(reflection_mask) / (gray.shape[0] * gray.shape[1]))654 655 return {656 "reflection_percentage": reflection_percentage,657 "has_reflections": reflection_percentage > 0.05658 }659 except:660 return {"reflection_percentage": 0.0, "has_reflections": False}661 662 def _assess_image_quality(self, image: np.ndarray) -> Dict:663 """Avaliação básica de qualidade"""664 try:665 gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)666 667 # Nitidez usando variância do Laplaciano668 sharpness = float(cv2.Laplacian(gray, cv2.CV_64F).var())669 670 # Score de qualidade simples671 quality_score = min(1.0, sharpness / 1000)672 673 return {674 "sharpness": sharpness,675 "quality_score": quality_score,676 "quality_grade": "high" if quality_score > 0.7 else "medium" if quality_score > 0.3 else "low"677 }678 except:679 return {"sharpness": 0.0, "quality_score": 0.0, "quality_grade": "unknown"}680 