""" modelos_loader.py Módulo para carregar conteúdo de metodologia e anexos dos modelos de avaliação. Extrai o conteúdo estruturado dos documentos para reconstrução com formatação padrão. """ import os from docx import Document from docx.shared import Pt, Inches from typing import Optional, List, Dict, Any, Tuple # Diretório base dos modelos (relativo ao script principal) SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) MODELOS_DIR = os.path.join(SCRIPT_DIR, "modelos") def modelo_tem_arquivos(nome_modelo: str) -> bool: """Verifica se um modelo possui pasta com arquivos de metodologia e anexos.""" if not nome_modelo or nome_modelo == "Outros": return False pasta_modelo = os.path.join(MODELOS_DIR, nome_modelo) if not os.path.isdir(pasta_modelo): return False metodologia_path = os.path.join(pasta_modelo, "METODOLOGIA.docx") anexos_path = os.path.join(pasta_modelo, "ANEXOS.docx") return os.path.exists(metodologia_path) or os.path.exists(anexos_path) def get_status_modelo(nome_modelo: str) -> str: """Retorna uma string descrevendo o status dos arquivos do modelo.""" if not nome_modelo or nome_modelo == "Outros": return "Modelo genérico (sem arquivos específicos)" pasta_modelo = os.path.join(MODELOS_DIR, nome_modelo) if not os.path.isdir(pasta_modelo): return "⚠️ Pasta do modelo não encontrada" metodologia = os.path.exists(os.path.join(pasta_modelo, "METODOLOGIA.docx")) anexos = os.path.exists(os.path.join(pasta_modelo, "ANEXOS.docx")) if metodologia and anexos: return "✅ Metodologia e Anexos disponíveis" elif metodologia: return "✅ Metodologia disponível | ⚠️ Anexos não encontrados" elif anexos: return "⚠️ Metodologia não encontrada | ✅ Anexos disponíveis" else: return "⚠️ Nenhum arquivo encontrado para este modelo" def extrair_texto_tabela(table) -> List[List[Dict]]: """ Extrai o conteúdo de uma tabela como lista de listas. Cada célula é um dict com 'texto' e 'cor' (RGB tuple ou None). """ from docx.shared import RGBColor dados = [] for row in table.rows: linha = [] for cell in row.cells: texto = cell.text.strip() cor = None # Verificar cor do primeiro run com texto for p in cell.paragraphs: for run in p.runs: if run.text.strip() and run.font.color and run.font.color.rgb: rgb = run.font.color.rgb # Verificar se é vermelho if rgb[0] > 200 and rgb[1] < 100 and rgb[2] < 100: cor = (rgb[0], rgb[1], rgb[2]) break if cor: break linha.append({'texto': texto, 'cor': cor}) dados.append(linha) return dados def extrair_conteudo_documento(filepath: str) -> List[Dict[str, Any]]: """ Extrai o conteúdo estruturado de um documento DOCX. Retorna uma lista de elementos, cada um com: - tipo: 'secao', 'subsecao', 'subsubsecao', 'subsubsubsecao', 'texto', 'variavel', 'tabela', 'vazio', 'imagem', 'anexo_titulo' - conteudo: texto ou dados da tabela - negrito: bool (para texto) - alinhamento: 'left', 'center', 'right', 'justify' - cor: tuple RGB ou None (para texto vermelho) """ if not os.path.exists(filepath): return [] try: doc = Document(filepath) except Exception as e: print(f"Erro ao carregar {filepath}: {e}") return [] elementos = [] # Mapeamento de estilos para tipos mapa_estilos = { 'LA_X': 'secao', # Seção principal (ex: 4) 'LA_X.X': 'subsecao', # Subseção (ex: 3.2) 'LA_X.X.X': 'subsubsecao', # Sub-subseção (ex: 3.2.1) 'LA_X.X.X.X': 'subsubsubsecao', # Sub-sub-subseção (ex: 3.2.1.1) 'LA_txt': 'texto', 'LA_Vars': 'variavel', 'LA_lb': 'vazio', 'LA_ANEX': 'anexo_titulo', # Título de anexo 'Item N.': 'subtitulo_centralizado', # Subtítulo centralizado } # Mapeamento de alinhamento from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.shared import RGBColor def get_alinhamento(p): if p.alignment == WD_ALIGN_PARAGRAPH.CENTER: return 'center' elif p.alignment == WD_ALIGN_PARAGRAPH.RIGHT: return 'right' elif p.alignment == WD_ALIGN_PARAGRAPH.JUSTIFY: return 'justify' return 'left' def get_cor_predominante(p): """Retorna a cor predominante do parágrafo (se for vermelho).""" for run in p.runs: if run.font.color and run.font.color.rgb: cor = run.font.color.rgb # Verificar se é vermelho (vários tons) r, g, b = cor[0], cor[1], cor[2] if r > 200 and g < 100 and b < 100: # É vermelho return (r, g, b) return None # Criar um iterador que intercala parágrafos e tabelas na ordem do documento body = doc.element.body tabela_idx = 0 para_idx = 0 for elemento in body: tag = elemento.tag.split('}')[-1] if '}' in elemento.tag else elemento.tag if tag == 'p': # É um parágrafo if para_idx < len(doc.paragraphs): p = doc.paragraphs[para_idx] para_idx += 1 estilo = p.style.name if p.style else 'Normal' texto = p.text.strip() alinhamento = get_alinhamento(p) cor = get_cor_predominante(p) # Verificar se tem negrito nos runs tem_negrito = any(run.bold for run in p.runs if run.text.strip()) # Verificar se o parágrafo contém imagem tem_imagem = False imagem_data = None # Procurar por drawing/blip no XML do parágrafo drawings = elemento.findall('.//{http://schemas.openxmlformats.org/wordprocessingml/2006/main}drawing') if drawings: tem_imagem = True # Guardar referência ao elemento XML para copiar depois imagem_data = { 'paragraph_element': elemento, 'doc': doc } tipo = mapa_estilos.get(estilo, 'texto') if tem_imagem: elementos.append({ 'tipo': 'imagem', 'conteudo': texto, 'imagem_data': imagem_data, 'alinhamento': alinhamento, 'estilo_original': estilo, 'cor': cor }) elif not texto: tipo = 'vazio' elementos.append({ 'tipo': tipo, 'conteudo': '', 'alinhamento': alinhamento, 'estilo_original': estilo, 'cor': None }) else: elementos.append({ 'tipo': tipo, 'conteudo': texto, 'negrito': tem_negrito, 'alinhamento': alinhamento, 'estilo_original': estilo, 'cor': cor }) elif tag == 'tbl': # É uma tabela if tabela_idx < len(doc.tables): tabela = doc.tables[tabela_idx] tabela_idx += 1 dados_tabela = extrair_texto_tabela(tabela) elementos.append({ 'tipo': 'tabela', 'conteudo': dados_tabela, 'linhas': len(dados_tabela), 'colunas': len(dados_tabela[0]) if dados_tabela else 0 }) return elementos def carregar_conteudo_metodologia(nome_modelo: str) -> List[Dict[str, Any]]: """Carrega o conteúdo estruturado do arquivo METODOLOGIA.docx.""" if not nome_modelo or nome_modelo == "Outros": return [] filepath = os.path.join(MODELOS_DIR, nome_modelo, "METODOLOGIA.docx") return extrair_conteudo_documento(filepath) def carregar_conteudo_anexos(nome_modelo: str) -> List[Dict[str, Any]]: """Carrega o conteúdo estruturado do arquivo ANEXOS.docx.""" if not nome_modelo or nome_modelo == "Outros": return [] filepath = os.path.join(MODELOS_DIR, nome_modelo, "ANEXOS.docx") return extrair_conteudo_documento(filepath) def listar_modelos_com_arquivos() -> list: """Lista todos os modelos que possuem arquivos de metodologia/anexos.""" if not os.path.isdir(MODELOS_DIR): return [] modelos = [] for nome in os.listdir(MODELOS_DIR): pasta = os.path.join(MODELOS_DIR, nome) if os.path.isdir(pasta): metodologia = os.path.exists(os.path.join(pasta, "METODOLOGIA.docx")) anexos = os.path.exists(os.path.join(pasta, "ANEXOS.docx")) if metodologia or anexos: modelos.append({ "nome": nome, "tem_metodologia": metodologia, "tem_anexos": anexos }) return modelos # Para teste direto do módulo if __name__ == "__main__": print("=== Teste do Módulo de Carregamento de Modelos ===\n") modelos = listar_modelos_com_arquivos() print(f"Modelos com arquivos: {len(modelos)}") for modelo in modelos: print(f"\n--- {modelo['nome']} ---") print(f" Status: {get_status_modelo(modelo['nome'])}") if modelo['tem_metodologia']: elementos = carregar_conteudo_metodologia(modelo['nome']) print(f"\n Elementos extraídos da metodologia: {len(elementos)}") # Contar por tipo tipos = {} for elem in elementos: t = elem['tipo'] tipos[t] = tipos.get(t, 0) + 1 print(f" Por tipo: {tipos}") # Mostrar estrutura de títulos print("\n Estrutura de títulos:") for elem in elementos: if elem['tipo'] in ['secao', 'subsecao', 'subsubsecao', 'subsubsubsecao']: indent = {'secao': '', 'subsecao': ' ', 'subsubsecao': ' ', 'subsubsubsecao': ' '} print(f" {indent[elem['tipo']]}{elem['tipo'].upper()}: {elem['conteudo'][:50]}...") elif elem['tipo'] == 'tabela': print(f" TABELA: {elem['linhas']}x{elem['colunas']}")