""" structure_content_ctf.py - Estruturação inteligente de conteúdo em formato problema → solução → resultado Author: Descomplicar® Crescimento Digital Link: https://descomplicar.pt Copyright: 2025 Descomplicar® """ import os import json import logging import requests import time from pathlib import Path from typing import Dict, Optional, List from dotenv import load_dotenv from concurrent.futures import ThreadPoolExecutor, as_completed # Carregar variáveis de ambiente load_dotenv() # Configurações INPUT_DIR = "/media/ealmeida/Dados/GDrive/Cloud/Clientes_360/CTF_Carstuff/KB/Scrapper/sites/output_md" OUTPUT_DIR = "/media/ealmeida/Dados/GDrive/Cloud/Clientes_360/CTF_Carstuff/KB/Scrapper/sites/formatted" API_KEY = os.getenv("OPENROUTER_API_KEY") # Configurar logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) class ContentStructurer: def __init__(self): self.api_key = API_KEY self.api_url = "https://openrouter.ai/api/v1/chat/completions" # Usar Gemini 2.5 Flash - modelo 2025 com "thinking" capabilities (~40x mais barato que Claude) self.model = "google/gemini-2.5-flash" # Prompt especializado para estofamento automotivo self.structure_prompt = """ És um especialista em estofamento automotivo e documentação técnica. Analisa o texto fornecido e extrai informação estruturada no seguinte formato JSON: { "metadata": { "titulo": "Título principal do conteúdo", "categoria": "tipo de conteúdo (tutorial, problema-tecnico, showcase, dica, recurso)", "topicos": ["lista", "de", "topicos", "principais"], "fonte": "nome do site original" }, "conteudo": [ { "tipo": "problema|solucao|resultado|info", "titulo": "Título desta secção", "descricao": "Descrição detalhada em português PT-PT", "detalhes": [ "Lista de pontos-chave", "Passos específicos", "Materiais ou ferramentas mencionados" ], "relevancia": "alta|media|baixa" } ], "keywords": ["palavras-chave", "tecnicas", "relevantes"], "aplicabilidade": ["tipos de veículos ou situações onde se aplica"] } REGRAS IMPORTANTES: 1. Se o conteúdo for um artigo sobre problema técnico: - Identifica claramente: problema → causa → solução → resultado 2. Se for showcase/galeria: - Extrai: projeto → técnicas usadas → materiais → resultado visual 3. Se for tutorial: - Extrai: objetivo → passos → dicas → precauções 4. Se for recurso/ferramenta: - Extrai: propósito → características → vantagens → aplicação 5. IGNORA completamente: - Navegação do site - Comentários genéricos - Publicidade - Links de rodapé sem contexto 6. FOCA em: - Técnicas de estofamento - Materiais e ferramentas - Processos e métodos - Problemas comuns e soluções - Casos práticos 7. Usa PORTUGUÊS DE PORTUGAL (PT-PT): - "estofamento" (não "estofado") - "automóvel" (não "carro") - "tecido" (não "tecido") Responde APENAS com o JSON, sem texto adicional. """ def chunk_content(self, content: str, max_chars: int = 15000) -> List[str]: """Divide conteúdo em chunks gerenciáveis.""" if len(content) <= max_chars: return [content] chunks = [] lines = content.split('\n') current_chunk = [] current_size = 0 for line in lines: line_size = len(line) + 1 # +1 para \n if current_size + line_size > max_chars and current_chunk: chunks.append('\n'.join(current_chunk)) current_chunk = [line] current_size = line_size else: current_chunk.append(line) current_size += line_size if current_chunk: chunks.append('\n'.join(current_chunk)) return chunks def structure_content(self, content: str, source_file: str, retries: int = 3) -> Optional[Dict]: """Estrutura conteúdo usando AI.""" for attempt in range(retries): try: headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", "HTTP-Referer": "https://descomplicar.pt", "X-Title": "CTF Carstuff Knowledge Base" } data = { "model": self.model, "messages": [ {"role": "system", "content": self.structure_prompt}, {"role": "user", "content": f"Ficheiro: {source_file}\n\n{content}"} ], "temperature": 0.3, # Baixa temperatura para respostas mais consistentes "max_tokens": 4000 } response = requests.post( self.api_url, headers=headers, json=data, timeout=90 ) if response.status_code == 200: result = response.json() if 'choices' in result and len(result['choices']) > 0: content_text = result['choices'][0]['message']['content'] # Tentar extrair JSON do texto try: # Remover markdown code blocks se existir if '```json' in content_text: content_text = content_text.split('```json')[1].split('```')[0] elif '```' in content_text: content_text = content_text.split('```')[1].split('```')[0] structured_data = json.loads(content_text.strip()) return structured_data except json.JSONDecodeError as e: logger.error(f"Erro ao parsear JSON: {e}") logger.debug(f"Conteúdo recebido: {content_text[:500]}") if attempt < retries - 1: time.sleep(2 ** attempt) continue return None else: logger.error(f"Resposta sem choices") elif response.status_code == 429: # Rate limit wait_time = int(response.headers.get('Retry-After', 20)) logger.warning(f"Rate limit. Aguardando {wait_time}s...") time.sleep(wait_time) continue else: logger.error(f"Erro API: {response.status_code} - {response.text}") if attempt < retries - 1: time.sleep(2 ** attempt) continue return None except requests.exceptions.Timeout: logger.warning(f"Timeout (tentativa {attempt + 1}/{retries})") if attempt < retries - 1: time.sleep(2 ** attempt) continue return None except Exception as e: logger.error(f"Erro ao estruturar conteúdo: {str(e)}") if attempt < retries - 1: time.sleep(2 ** attempt) continue return None return None def format_structured_md(self, structured_data: Dict, original_file: str) -> str: """Converte dados estruturados em Markdown formatado.""" md_lines = [] # Metadata meta = structured_data.get('metadata', {}) md_lines.append(f"# {meta.get('titulo', 'Sem Título')}") md_lines.append("") md_lines.append(f"**Categoria**: {meta.get('categoria', 'Geral')}") md_lines.append(f"**Fonte**: {meta.get('fonte', original_file)}") if meta.get('topicos'): md_lines.append(f"**Tópicos**: {', '.join(meta['topicos'])}") md_lines.append("") md_lines.append("---") md_lines.append("") # Conteúdo estruturado conteudo = structured_data.get('conteudo', []) # Agrupar por tipo problemas = [c for c in conteudo if c.get('tipo') == 'problema'] solucoes = [c for c in conteudo if c.get('tipo') == 'solucao'] resultados = [c for c in conteudo if c.get('tipo') == 'resultado'] info = [c for c in conteudo if c.get('tipo') == 'info'] # Problemas if problemas: md_lines.append("## 🔍 Problemas Identificados") md_lines.append("") for p in problemas: md_lines.append(f"### {p.get('titulo', 'Problema')}") md_lines.append("") md_lines.append(p.get('descricao', '')) md_lines.append("") if p.get('detalhes'): for detalhe in p['detalhes']: md_lines.append(f"- {detalhe}") md_lines.append("") # Soluções if solucoes: md_lines.append("## 💡 Soluções") md_lines.append("") for s in solucoes: md_lines.append(f"### {s.get('titulo', 'Solução')}") md_lines.append("") md_lines.append(s.get('descricao', '')) md_lines.append("") if s.get('detalhes'): for i, detalhe in enumerate(s['detalhes'], 1): md_lines.append(f"{i}. {detalhe}") md_lines.append("") # Resultados if resultados: md_lines.append("## ✅ Resultados") md_lines.append("") for r in resultados: md_lines.append(f"### {r.get('titulo', 'Resultado')}") md_lines.append("") md_lines.append(r.get('descricao', '')) md_lines.append("") if r.get('detalhes'): for detalhe in r['detalhes']: md_lines.append(f"- {detalhe}") md_lines.append("") # Informação adicional if info: md_lines.append("## 📋 Informação Adicional") md_lines.append("") for inf in info: md_lines.append(f"### {inf.get('titulo', 'Info')}") md_lines.append("") md_lines.append(inf.get('descricao', '')) md_lines.append("") if inf.get('detalhes'): for detalhe in inf['detalhes']: md_lines.append(f"- {detalhe}") md_lines.append("") # Keywords e aplicabilidade md_lines.append("---") md_lines.append("") if structured_data.get('keywords'): md_lines.append(f"**Palavras-chave**: {', '.join(structured_data['keywords'])}") md_lines.append("") if structured_data.get('aplicabilidade'): md_lines.append("**Aplicabilidade**:") for app in structured_data['aplicabilidade']: md_lines.append(f"- {app}") md_lines.append("") return '\n'.join(md_lines) def process_file(self, input_file: Path, output_dir: Path) -> bool: """Processa um ficheiro.""" try: output_file = output_dir / f"structured_{input_file.name}" # Skip se já existe if output_file.exists(): logger.info(f"Já existe: {output_file.name}") return True # Verificar tamanho file_size = input_file.stat().st_size if file_size < 500: # Muito pequeno logger.warning(f"Ficheiro muito pequeno ({file_size}B): {input_file.name}") return False if file_size > 100000: # >100KB logger.warning(f"Ficheiro grande ({file_size/1024:.1f}KB): {input_file.name}") # Ler conteúdo with open(input_file, 'r', encoding='utf-8') as f: content = f.read() logger.info(f"Processando: {input_file.name} ({file_size/1024:.1f}KB)") # Estruturar com AI structured_data = self.structure_content(content, input_file.name) if not structured_data: logger.error(f"Falha ao estruturar: {input_file.name}") return False # Converter para MD formatado formatted_md = self.format_structured_md(structured_data, input_file.name) # Guardar with open(output_file, 'w', encoding='utf-8') as f: f.write(formatted_md) # Guardar JSON também json_file = output_dir / f"structured_{input_file.stem}.json" with open(json_file, 'w', encoding='utf-8') as f: json.dump(structured_data, f, indent=2, ensure_ascii=False) logger.info(f"✓ Guardado: {output_file.name}") # Pausa para evitar rate limits time.sleep(3) return True except Exception as e: logger.error(f"Erro ao processar {input_file.name}: {e}") return False def main(): """Função principal.""" if not API_KEY: logger.error("OPENROUTER_API_KEY não configurada no .env") return input_path = Path(INPUT_DIR) output_path = Path(OUTPUT_DIR) if not input_path.exists(): logger.error(f"Diretório não existe: {INPUT_DIR}") return output_path.mkdir(parents=True, exist_ok=True) # Encontrar ficheiros (focar nos sites completos primeiro) priority_sites = ['thehogring.com', 'sailrite.com', 'relicate.com', 'thesamba.com'] all_files = [] for site in priority_sites: pattern = f"{site}_*.md" files = list(input_path.glob(pattern)) all_files.extend(files) logger.info(f"Encontrados {len(all_files)} ficheiros para processar") # Processar structurer = ContentStructurer() successful = 0 for md_file in all_files: if structurer.process_file(md_file, output_path): successful += 1 logger.info(f"Concluído: {successful}/{len(all_files)} ficheiros processados") # Estatísticas json_files = list(output_path.glob('*.json')) logger.info(f"Total ficheiros JSON criados: {len(json_files)}") if __name__ == '__main__': print("═══════════════════════════════════════════════════════════") print(" CTF CARSTUFF - ESTRUTURAÇÃO INTELIGENTE DE CONTEÚDO") print(" Formato: Problema → Solução → Resultado") print("═══════════════════════════════════════════════════════════") print("") main() print("") print("✓ Processo concluído!")