Spaces:
Runtime error
Runtime error
| import os | |
| import io | |
| import re | |
| import json | |
| import streamlit as st | |
| from docx import Document | |
| from docx.shared import Cm, Pt, RGBColor | |
| from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK | |
| from docx.enum.section import WD_SECTION | |
| from docx.oxml.ns import qn | |
| from docx.oxml import OxmlElement | |
| from pydantic import BaseModel | |
| from typing import List, Optional, Tuple | |
| from google import genai | |
| from google.genai import types | |
| # ===================================================== | |
| # CONFIGURAÇÕES GLOBAIS E ESTADO | |
| # ===================================================== | |
| st.set_page_config(page_title="Corretor TCC - UNIFAGOC", page_icon="⚖️", layout="wide") | |
| def reset_estado(chave): | |
| if chave in st.session_state: | |
| st.session_state[chave] = None | |
| # Inicialização do estado | |
| for key in ["resultado_auditoria", "doc_revisado_bytes", "doc_formatado_bytes", "relatorio_abnt"]: | |
| if key not in st.session_state: | |
| st.session_state[key] = None | |
| MAX_CHARS_PER_CHUNK = 15000 # Limite de segurança para a janela de contexto | |
| LATINISMOS = [ | |
| "habeas corpus", "data venia", "jus puniendi", "jus postulandi", | |
| "mens legis", "ratio decidendi", "obiter dictum", "in casu", | |
| "periculum in mora", "fumus boni iuris", "a quo", "ad quem", | |
| "ex tunc", "ex nunc", "erga omnes", "vacatio legis", | |
| "mutatis mutandis", "strictu sensu", "lato sensu", "sine qua non" | |
| ] | |
| # ===================================================== | |
| # ESQUEMAS PYDANTIC | |
| # ===================================================== | |
| class AuditoriaTCC(BaseModel): | |
| aderencia_percentual: int | |
| diagnostico_geral: str | |
| problema_identificado_no_projeto: str | |
| tcc_respondeu_ao_problema: bool | |
| justificativa_problema: str | |
| objetivos_especificos_identificados: List[str] | |
| objetivos_nao_cumpridos_no_tcc: List[str] | |
| desvios_de_tema_ou_linguica: List[str] | |
| indicios_texto_generico_ia: List[str] | |
| incoerencias_internas: List[str] | |
| nota_tema_problema: float | |
| nota_introducao_metodo: float | |
| nota_estruturacao: float | |
| nota_formatacao_abnt: float | |
| justificativa_notas_banca: str | |
| class ErroTexto(BaseModel): | |
| original: str | |
| corrigido: str | |
| motivo: str | |
| categoria: str | |
| gravidade: str | |
| contexto: str | |
| class ProblemaAcademico(BaseModel): | |
| trecho: str | |
| problema: str | |
| sugestao: str | |
| gravidade: str | |
| class RevisaoInteligente(BaseModel): | |
| correcoes: List[ErroTexto] | |
| problemas_academicos: List[ProblemaAcademico] | |
| termos_estrangeiros_para_italico: List[str] | |
| class ProblemaReferencia(BaseModel): | |
| original: str | |
| erros: List[str] | |
| class ListaReferencias(BaseModel): | |
| referencias: List[ProblemaReferencia] | |
| # Novos Esquemas para o Diagnóstico ABNT (ABA 3) | |
| class AlertaCorrecao(BaseModel): | |
| trecho_original: str | |
| como_deve_ficar: str | |
| justificativa_regra: str | |
| local_aproximado: str | |
| class RelatorioDiagnostico(BaseModel): | |
| erros_estrutura_abnt: List[AlertaCorrecao] | |
| erros_texto_juridico: List[AlertaCorrecao] | |
| erros_referencias: List[AlertaCorrecao] | |
| nota_geral_formatacao: int | |
| # ===================================================== | |
| # CLASSE REVISOR IA (Responsável por interações com Gemini) | |
| # ===================================================== | |
| class RevisorIA: | |
| def __init__(self): | |
| api_key = st.secrets.get("GEMINI_API_KEY") or os.environ.get("GEMINI_API_KEY") | |
| if not api_key: | |
| st.error("❌ ERRO: Chave GEMINI_API_KEY não configurada nos secrets.") | |
| st.stop() | |
| self.client = genai.Client(api_key=api_key) | |
| def auditar_tcc(self, texto_projeto: str, texto_tcc: str, fase_tcc: str) -> Optional[AuditoriaTCC]: | |
| regra_fase = ( | |
| "ATENÇÃO: O aluno está em TCC 1. NÃO penalize a ausência de Resumo e Conclusão." | |
| if "TCC 1" in fase_tcc else "O aluno está em TCC 2. Exija o trabalho completo." | |
| ) | |
| prompt = ( | |
| f"Como Professor Examinador da UNIFAGOC:\n{regra_fase}\n" | |
| f"=== PRÉ-PROJETO ===\n{texto_projeto}\n" | |
| f"=== TCC ===\n{texto_tcc}\n" | |
| "TAREFA 1: Avalie cruzamento de dados e fuga de tema.\n" | |
| "TAREFA 2: Verifique incoerências internas.\n" | |
| "TAREFA 3: Distribua 80 pontos conforme ficha UNIFAGOC." | |
| ) | |
| try: | |
| res = self.client.models.generate_content( | |
| model="gemini-3.5-flash", | |
| contents=prompt, | |
| config=types.GenerateContentConfig( | |
| temperature=0.0, | |
| response_mime_type="application/json", | |
| response_schema=AuditoriaTCC | |
| ) | |
| ) | |
| return AuditoriaTCC(**json.loads(res.text)) | |
| except Exception as e: | |
| st.error(f"Erro na auditoria: {e}") | |
| return None | |
| def revisar_texto(self, texto: str) -> Optional[RevisaoInteligente]: | |
| prompt = ( | |
| "Você é um Professor Revisor de TCCs de Direito da UNIFAGOC. " | |
| "IMPORTANTE: O ano atual é maio de 2026. Não aponte citações ou referências de 2024, 2025 ou 2026 como sendo do futuro. " | |
| "Analise o texto e corrija: ORTOGRAFIA, GRAMÁTICA, VÍCIOS JURÍDICOS, COERÊNCIA, ABNT. " | |
| f"Texto: {texto}" | |
| ) | |
| try: | |
| res = self.client.models.generate_content( | |
| model="gemini-3.5-flash", | |
| contents=prompt, | |
| config=types.GenerateContentConfig( | |
| temperature=0.1, | |
| response_mime_type="application/json", | |
| response_schema=RevisaoInteligente | |
| ) | |
| ) | |
| return RevisaoInteligente(**json.loads(res.text)) | |
| except Exception as e: | |
| st.error(f"Erro na revisão: {e}") | |
| return None | |
| def validar_referencias(self, referencias: List[str]) -> ListaReferencias: | |
| refs_str = '\n'.join(referencias) | |
| prompt = ( | |
| "Analise as referências abaixo **APENAS PARA IDENTIFICAR ERROS** (NÃO reescreva, NÃO invente dados!).\n" | |
| "IMPORTANTE: Estamos em maio de 2026. Obras de 2024, 2025 e 2026 estão perfeitamente válidas e não são obras do futuro.\n" | |
| "Para cada uma, retorne:\n" | |
| "- original: texto original\n" | |
| "- erros: lista de erros (ex: [\"título não em negrito\", \"autor não invertido\"])\n\n" | |
| f"Referências:\n{refs_str}" | |
| ) | |
| try: | |
| res = self.client.models.generate_content( | |
| model="gemini-3.5-flash", | |
| contents=prompt, | |
| config=types.GenerateContentConfig( | |
| temperature=0.0, | |
| response_mime_type="application/json", | |
| response_schema=ListaReferencias | |
| ) | |
| ) | |
| return ListaReferencias(**json.loads(res.text)) | |
| except Exception as e: | |
| st.error(f"Erro na validação de referências: {e}") | |
| return ListaReferencias(referencias=[]) | |
| def gerar_diagnostico_abnt(self, texto: str) -> Optional[RelatorioDiagnostico]: | |
| prompt = f""" | |
| Você é um Inspetor Acadêmico Implacável de TCCs de Direito da UNIFAGOC. | |
| Sua missão não é reescrever o texto, mas gerar um laudo diagnóstico rigoroso apontando os erros. | |
| IMPORTANTE: O ano atual é maio de 2026. Nunca afirme que referências ou dados de 2024 a 2026 são do futuro. | |
| Para evitar estourar o limite, foque nos 5 piores erros de cada categoria. | |
| Analise o texto abaixo e preencha o relatório: | |
| 1. ESTRUTURA E ABNT: Procure parágrafos com formatação incorreta, citações longas sem destaque visual, falta de itálico em latinismos, etc. | |
| 2. TEXTO JURÍDICO: Aponte erros de estilo, repetições viciosas, e inadequação acadêmica. | |
| 3. REFERÊNCIAS: Aponte exatamente qual referência está fora do padrão ABNT (ex: título sem destaque, autor minúsculo). | |
| Para cada erro, cite o trecho original exato, sugira como deve ficar, explique a regra de forma clara e indique a localização. | |
| Dê uma nota geral de saúde da formatação (0 a 100). | |
| TEXTO DO ALUNO: | |
| {texto} | |
| """ | |
| try: | |
| res = self.client.models.generate_content( | |
| model="gemini-3.5-flash", | |
| contents=prompt, | |
| config=types.GenerateContentConfig( | |
| temperature=0.1, | |
| response_mime_type="application/json", | |
| response_schema=RelatorioDiagnostico | |
| ) | |
| ) | |
| return RelatorioDiagnostico(**json.loads(res.text)) | |
| except Exception as e: | |
| st.error(f"Erro no diagnóstico: {e}") | |
| return None | |
| # ===================================================== | |
| # CLASSE FORMATADOR UNIFAGOC (Responsável por formatação DOCX) | |
| # ===================================================== | |
| class FormatadorUNIFAGOC: | |
| def aplicar_margens(doc: Document): | |
| """Aplica margens oficiais UNIFAGOC (A4: 21x29.7cm)""" | |
| for section in doc.sections: | |
| section.page_height = Cm(29.7) | |
| section.page_width = Cm(21.0) | |
| section.top_margin = Cm(3.0) | |
| section.left_margin = Cm(3.0) | |
| section.bottom_margin = Cm(2.0) | |
| section.right_margin = Cm(2.0) | |
| section.header_distance = Cm(0.5) | |
| section.footer_distance = Cm(0.5) | |
| def adicionar_paginacao(doc: Document): | |
| """Adiciona numeração de páginas no rodapé (centralizado) - XML correto""" | |
| for i, section in enumerate(doc.sections): | |
| if i == 0: # Ignora capa/folha de rosto | |
| continue | |
| footer = section.footer | |
| para = footer.paragraphs[0] if footer.paragraphs else footer.add_paragraph() | |
| para.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| # Adiciona campo PAGE via XML | |
| run = para.add_run() | |
| fldChar = OxmlElement('w:fldChar') | |
| fldChar.set(qn('w:fldCharType'), 'begin') | |
| run._r.append(fldChar) | |
| instrText = OxmlElement('w:instrText') | |
| instrText.set(qn('xml:space'), 'preserve') | |
| instrText.text = 'PAGE' | |
| run._r.append(instrText) | |
| fldChar = OxmlElement('w:fldChar') | |
| fldChar.set(qn('w:fldCharType'), 'end') | |
| run._r.append(fldChar) | |
| for run in para.runs: | |
| run.font.name = 'Calibri' | |
| run.font.size = Pt(10) | |
| def formatar_figuras_tabelas(doc: Document): | |
| """Formata figuras, tabelas e quadros""" | |
| for para in doc.paragraphs: | |
| texto = para.text.strip() | |
| if re.match(r'^(Figura|Tabela|Quadro)\s+\d+', texto, re.IGNORECASE): | |
| para.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| for run in para.runs: | |
| run.font.name = 'Calibri' | |
| run.font.size = Pt(12) | |
| run.bold = True | |
| # Formata legenda | |
| idx = doc.paragraphs.index(para) | |
| if idx + 1 < len(doc.paragraphs): | |
| next_para = doc.paragraphs[idx + 1] | |
| for run in next_para.runs: | |
| run.font.name = 'Calibri' | |
| run.font.size = Pt(11) | |
| # Formata tabelas internas | |
| for table in doc.tables: | |
| for row in table.rows: | |
| for cell in row.cells: | |
| for para in cell.paragraphs: | |
| para.alignment = WD_ALIGN_PARAGRAPH.LEFT | |
| para.paragraph_format.line_spacing = 1.0 | |
| for run in para.runs: | |
| run.font.name = 'Calibri' | |
| run.font.size = Pt(11) | |
| def formatar_imagens(doc: Document): | |
| """Centraliza imagens (inline shapes)""" | |
| for para in doc.paragraphs: | |
| if para._p.xpath('.//w:drawing') or para._p.xpath('.//w:pic'): | |
| para.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| def formatar_notas_rodape(doc: Document): | |
| """Formata notas de rodapé (¹, ², ³) representadas no texto principal""" | |
| for para in doc.paragraphs: | |
| if re.search(r'[\d\^]+$', para.text.strip()): | |
| for run in para.runs: | |
| run.font.size = Pt(8) | |
| run.font.superscript = True | |
| def substituir_texto_preservando_formato(para, original: str, corrigido: str) -> int: | |
| """ | |
| Substitui texto PRESERVANDO formatação. | |
| Tenta substituir dentro do próprio run primeiro para preservar hiperlinks e estilos mistos. | |
| """ | |
| texto_completo = "".join(run.text for run in para.runs) | |
| if original not in texto_completo: | |
| return 0 | |
| # Tentativa 1: Substituição cirúrgica no mesmo run (preserva 100% da formatação) | |
| for run in para.runs: | |
| if original in run.text: | |
| run.text = run.text.replace(original, corrigido, 1) | |
| return 1 | |
| # Tentativa 2: Fallback caso a palavra esteja quebrada entre runs diferentes | |
| pos = texto_completo.find(original) | |
| if pos == -1: | |
| return 0 | |
| antes = texto_completo[:pos] | |
| depois = texto_completo[pos + len(original):] | |
| estilo_base = para.style | |
| alinhamento = para.paragraph_format.alignment | |
| para.clear() | |
| if antes: | |
| para.add_run(antes) | |
| run_corrigido = para.add_run(corrigido) | |
| run_corrigido.font.name = 'Calibri' | |
| run_corrigido.font.size = Pt(12) | |
| if depois: | |
| para.add_run(depois) | |
| if estilo_base: | |
| para.style = estilo_base | |
| if alinhamento: | |
| para.paragraph_format.alignment = alinhamento | |
| return 1 | |
| def formatar_documento(doc: Document, revisor: RevisorIA) -> List[ProblemaReferencia]: | |
| """Função principal de formatação estrutural (ABNT UNIFAGOC)""" | |
| FormatadorUNIFAGOC.aplicar_margens(doc) | |
| FormatadorUNIFAGOC.adicionar_paginacao(doc) | |
| FormatadorUNIFAGOC.formatar_figuras_tabelas(doc) | |
| FormatadorUNIFAGOC.formatar_imagens(doc) | |
| FormatadorUNIFAGOC.formatar_notas_rodape(doc) | |
| for para in doc.paragraphs: | |
| texto = para.text.strip() | |
| if not texto: | |
| continue | |
| # --- REFERÊNCIAS --- | |
| if re.search(r"REFER[ÊE]NCIAS", texto, re.IGNORECASE): | |
| para.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| para.paragraph_format.space_before = Pt(12) | |
| para.paragraph_format.space_after = Pt(6) | |
| for run in para.runs: | |
| run.font.name = 'Calibri' | |
| run.font.size = Pt(12) | |
| run.bold = True | |
| continue | |
| # --- RESUMO --- | |
| if texto.upper() == "RESUMO": | |
| para.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY | |
| para.paragraph_format.space_before = Pt(12) | |
| para.paragraph_format.space_after = Pt(0) | |
| para.paragraph_format.line_spacing = 1.5 | |
| for run in para.runs: | |
| run.font.name = 'Calibri' | |
| run.font.size = Pt(11) | |
| run.bold = True | |
| continue | |
| # --- PALAVRAS-CHAVE --- | |
| if texto.upper().startswith("PALAVRAS-CHAVE"): | |
| para.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY | |
| para.paragraph_format.space_before = Pt(6) | |
| para.paragraph_format.space_after = Pt(0) | |
| para.paragraph_format.line_spacing = 1.5 | |
| for run in para.runs: | |
| run.font.name = 'Calibri' | |
| run.font.size = Pt(11) | |
| if "PALAVRAS-CHAVE" in run.text.upper(): | |
| run.bold = True | |
| continue | |
| # --- TÍTULOS E SEÇÕES --- | |
| nivel = FormatadorUNIFAGOC.detectar_nivel_secao(texto) | |
| if nivel > 0 or FormatadorUNIFAGOC.detectar_titulo_ou_subtitulo(texto) == "titulo": | |
| para.alignment = WD_ALIGN_PARAGRAPH.LEFT | |
| para.paragraph_format.space_before = Pt(12) | |
| para.paragraph_format.space_after = Pt(6) | |
| for run in para.runs: | |
| run.font.name = 'Calibri' | |
| run.font.size = Pt(12) | |
| run.bold = True | |
| if nivel == 3: | |
| run.italic = True | |
| if nivel == 2: | |
| para.paragraph_format.left_indent = Cm(0.5) | |
| continue | |
| # --- CITAÇÕES LONGAS --- | |
| if FormatadorUNIFAGOC.detectar_citacao_longa(para): | |
| para.paragraph_format.left_indent = Cm(4.0) | |
| para.paragraph_format.line_spacing = 1.0 | |
| para.paragraph_format.space_before = Pt(6) | |
| para.paragraph_format.space_after = Pt(6) | |
| for run in para.runs: | |
| run.font.name = 'Calibri' | |
| run.font.size = Pt(11) | |
| continue | |
| # --- LISTAS --- | |
| if FormatadorUNIFAGOC.detectar_lista(texto): | |
| para.paragraph_format.left_indent = Cm(1.25) | |
| para.paragraph_format.first_line_indent = Cm(0) | |
| para.paragraph_format.space_before = Pt(0) | |
| para.paragraph_format.space_after = Pt(0) | |
| para.paragraph_format.line_spacing = 1.5 | |
| for run in para.runs: | |
| run.font.name = 'Calibri' | |
| run.font.size = Pt(12) | |
| continue | |
| # --- CORPO DO TEXTO PADRÃO --- | |
| para.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY | |
| para.paragraph_format.first_line_indent = Cm(1.25) | |
| para.paragraph_format.line_spacing = 1.5 | |
| para.paragraph_format.space_before = Pt(0) | |
| para.paragraph_format.space_after = Pt(0) | |
| for run in para.runs: | |
| run.font.name = 'Calibri' | |
| run.font.size = Pt(12) | |
| # 4. Valida referências extraídas no documento | |
| refs = FormatadorUNIFAGOC.extrair_referencias(doc) | |
| if refs: | |
| lista_refs = revisor.validar_referencias(refs) | |
| return lista_refs.referencias | |
| return [] | |
| def formatacao_cruzada_indices(para, texto_alvo: str, aplicar_italico: bool = False, aplicar_negrito: bool = False) -> int: | |
| aplicacoes = 0 | |
| if aplicar_italico: | |
| matches = list(re.finditer(r'\b' + re.escape(texto_alvo) + r'\b', para.text, re.IGNORECASE)) | |
| else: | |
| idx = para.text.find(texto_alvo) | |
| matches = [(idx, idx + len(texto_alvo))] if idx != -1 else [] | |
| for match in matches: | |
| pos_ini, pos_fim = match if isinstance(match, tuple) else (match.start(), match.end()) | |
| if pos_ini == -1: | |
| continue | |
| pos_atual = 0 | |
| for run in para.runs: | |
| fim_run = pos_atual + len(run.text) | |
| if pos_atual < pos_fim and fim_run > pos_ini: | |
| if aplicar_italico: | |
| run.italic = True | |
| if aplicar_negrito: | |
| run.bold = True | |
| aplicacoes += 1 | |
| pos_atual += len(run.text) | |
| return aplicacoes | |
| def detectar_nivel_secao(texto: str) -> int: | |
| match = re.match(r'^(\d+(?:\.\d+)*)\s+', texto) | |
| if not match: return 0 | |
| return match.group(1).count('.') + 1 | |
| def detectar_titulo_ou_subtitulo(texto: str) -> Optional[str]: | |
| SECOES_ESPECIAIS = ["INTRODUÇÃO", "CONSIDERAÇÕES FINAIS", "REFERÊNCIAS", "RESUMO", "PALAVRAS-CHAVE"] | |
| texto_upper = texto.upper() | |
| if any(sec in texto_upper for sec in SECOES_ESPECIAIS): return "titulo" | |
| if FormatadorUNIFAGOC.detectar_nivel_secao(texto) > 0: return "titulo" | |
| if texto.isupper() and len(texto) < 150 and not FormatadorUNIFAGOC.detectar_nivel_secao(texto): return "titulo" | |
| return None | |
| def detectar_citacao_longa(para) -> bool: | |
| if para.paragraph_format.left_indent and para.paragraph_format.left_indent >= Cm(3.5): | |
| return True | |
| texto = para.text.strip() | |
| if len(texto.split()) >= 40: | |
| return True | |
| return False | |
| def detectar_lista(texto: str) -> bool: | |
| return bool(re.match(r'^(\d+[\)\.]|[a-zA-Z][\)\.]|[IVXLCDM]+[\)\.]|[-–])\s', texto)) | |
| def extrair_referencias(doc: Document) -> List[str]: | |
| refs = [] | |
| em_refs = False | |
| for para in doc.paragraphs: | |
| texto = para.text.strip() | |
| if re.search(r"REFER[ÊE]NCIAS", texto, re.IGNORECASE): | |
| em_refs = True | |
| continue | |
| if em_refs and (len(texto) < 10 or texto.isupper()): | |
| break | |
| if em_refs: | |
| refs.append(texto) | |
| return refs | |
| def extrair_texto_docx(arquivo_upload) -> str: | |
| doc = Document(arquivo_upload) | |
| return "\n\n".join([para.text.strip() for para in doc.paragraphs if para.text.strip()]) | |
| def docx_to_bytes(doc: Document) -> bytes: | |
| byte_io = io.BytesIO() | |
| doc.save(byte_io) | |
| return byte_io.getvalue() | |
| def estimar_paginas(doc: Document) -> int: | |
| palavras_totais = sum( | |
| len(para.text.split()) for para in doc.paragraphs | |
| if not re.search(r"^(?:\d+\.?\s*)?REFER[ÊE]NCIAS", para.text, re.IGNORECASE) | |
| ) | |
| return max(1, palavras_totais // 350) | |
| # ===================================================== | |
| # CLASSE AUDITOR TCC (Responsável pela lógica da aplicação) | |
| # ===================================================== | |
| class AuditorTCC: | |
| def __init__(self, revisor: RevisorIA): | |
| self.revisor = revisor | |
| def auditar(self, arq_projeto, arq_tcc, fase_tcc: str) -> Optional[AuditoriaTCC]: | |
| texto_projeto = FormatadorUNIFAGOC.extrair_texto_docx(arq_projeto) | |
| texto_tcc = FormatadorUNIFAGOC.extrair_texto_docx(arq_tcc) | |
| return self.revisor.auditar_tcc(texto_projeto, texto_tcc, fase_tcc) | |
| def revisar(self, arq_rev) -> Tuple[Document, RevisaoInteligente, List[str]]: | |
| doc = Document(arq_rev) | |
| texto_completo = FormatadorUNIFAGOC.extrair_texto_docx(arq_rev) | |
| erros_locais = self.verificar_erros_locais(texto_completo) | |
| # Chunking seguro respeitando parágrafos | |
| chunks = self.dividir_em_chunks(texto_completo) | |
| resultados_revisao = [] | |
| for chunk in chunks: | |
| resultado = self.revisor.revisar_texto(chunk) | |
| if resultado: | |
| resultados_revisao.append(resultado) | |
| revisao_consolidada = self.consolidar_revisoes(resultados_revisao) | |
| self.aplicar_correcoes(doc, revisao_consolidada) | |
| return doc, revisao_consolidada, erros_locais | |
| def verificar_erros_locais(self, texto: str) -> List[str]: | |
| erros = [] | |
| if re.search(r"\s{2,}", texto): erros.append("Espaços duplos detectados no documento.") | |
| if re.search(r",\s*,", texto): erros.append("Vírgulas duplicadas detectadas (ex: ',,').") | |
| if re.search(r"[.!?]{2,}", texto) and not "..." in texto: erros.append("Pontuação repetida indevida.") | |
| if re.search(r"\b(\w+)\s+\1\b", texto, re.IGNORECASE): erros.append("Palavras repetidas sequencialmente.") | |
| if re.search(r"\([A-ZÀ-Ú\s]+,\s*\)", texto): erros.append("Citação ABNT com ano faltando.") | |
| return erros | |
| def dividir_em_chunks(self, texto: str) -> List[str]: | |
| """Divide texto em chunks respeitando a quebra de parágrafos""" | |
| paragrafos = texto.split('\n\n') | |
| chunks = [] | |
| chunk_atual = "" | |
| for p in paragrafos: | |
| if len(chunk_atual) + len(p) + 2 < MAX_CHARS_PER_CHUNK: | |
| chunk_atual += p + "\n\n" | |
| else: | |
| if chunk_atual: | |
| chunks.append(chunk_atual.strip()) | |
| chunk_atual = p + "\n\n" | |
| if chunk_atual: | |
| chunks.append(chunk_atual.strip()) | |
| return chunks | |
| def consolidar_revisoes(self, revisoes: List[RevisaoInteligente]) -> RevisaoInteligente: | |
| if not revisoes: | |
| return RevisaoInteligente(correcoes=[], problemas_academicos=[], termos_estrangeiros_para_italico=[]) | |
| todas_correcoes = [] | |
| todos_problemas = [] | |
| todos_termos = [] | |
| for revisao in revisoes: | |
| todas_correcoes.extend(revisao.correcoes) | |
| todos_problemas.extend(revisao.problemas_academicos) | |
| todos_termos.extend(revisao.termos_estrangeiros_para_italico) | |
| return RevisaoInteligente( | |
| correcoes=todas_correcoes, | |
| problemas_academicos=todos_problemas, | |
| termos_estrangeiros_para_italico=list(set(todos_termos)) | |
| ) | |
| def aplicar_correcoes(self, doc: Document, revisao: RevisaoInteligente): | |
| for para in doc.paragraphs: | |
| for correcao in revisao.correcoes: | |
| FormatadorUNIFAGOC.substituir_texto_preservando_formato(para, correcao.original, correcao.corrigido) | |
| for table in doc.tables: | |
| for row in table.rows: | |
| for cell in row.cells: | |
| for para in cell.paragraphs: | |
| for correcao in revisao.correcoes: | |
| FormatadorUNIFAGOC.substituir_texto_preservando_formato(para, correcao.original, correcao.corrigido) | |
| termos_para_italico = set(LATINISMOS + revisao.termos_estrangeiros_para_italico) | |
| for para in doc.paragraphs: | |
| for termo in termos_para_italico: | |
| FormatadorUNIFAGOC.formatacao_cruzada_indices(para, termo, aplicar_italico=True) | |
| for table in doc.tables: | |
| for row in table.rows: | |
| for cell in row.cells: | |
| for para in cell.paragraphs: | |
| for termo in termos_para_italico: | |
| FormatadorUNIFAGOC.formatacao_cruzada_indices(para, termo, aplicar_italico=True) | |
| # ===================================================== | |
| # INTERFACE STREAMLIT | |
| # ===================================================== | |
| st.title("⚖️ Assistente Acadêmico - Direito UNIFAGOC") | |
| aba_auditoria, aba_revisao, aba_abnt, aba_prompt, aba_feedback = st.tabs([ | |
| "🔍 Auditoria e Notas", | |
| "🧠 Revisão Inteligente", | |
| "📋 Diagnóstico ABNT", | |
| "🤖 Prompt Mestre", | |
| "👨🏫 Feedback Professor" | |
| ]) | |
| # Inicialização centralizada das classes | |
| revisor_ia = RevisorIA() | |
| auditor = AuditorTCC(revisor_ia) | |
| formatador = FormatadorUNIFAGOC() | |
| # ABA 1: AUDITORIA | |
| with aba_auditoria: | |
| st.markdown("Cruze o **Pré-Projeto** com o **TCC** para identificar fuga do tema e gerar a **Nota da Banca (80 pontos)**.") | |
| fase_tcc = st.radio("Fase atual:", ["TCC 1 (Apenas Introdução e Capítulo 1)", "TCC 2 (TCC Completo)"], horizontal=True) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| arq_projeto = st.file_uploader("1. Pré-Projeto (.docx)", type=["docx"], key="proj") | |
| with col2: | |
| arq_tcc = st.file_uploader("2. TCC (.docx)", type=["docx"], key="tcc_aud") | |
| if st.button("🔍 Iniciar Auditoria", type="primary"): | |
| if not arq_projeto or not arq_tcc: | |
| st.warning("⚠️ Envie os DOIS arquivos para realizar a comparação.") | |
| else: | |
| with st.status("Analisando coerência jurídica...", expanded=True) as status: | |
| resultado = auditor.auditar(arq_projeto, arq_tcc, fase_tcc) | |
| if resultado: | |
| st.session_state.resultado_auditoria = resultado | |
| status.update(label="✅ Avaliação concluída!", state="complete", expanded=False) | |
| else: | |
| status.update(label="❌ Falha na avaliação.", state="error") | |
| if st.session_state.resultado_auditoria: | |
| dados = st.session_state.resultado_auditoria | |
| st.header("🏆 Parecer Oficial da Banca UNIFAGOC") | |
| # 1. CÁLCULO DA NOTA COM FEEDBACK VISUAL | |
| nota_total = sum([ | |
| dados.nota_tema_problema, | |
| dados.nota_introducao_metodo, | |
| dados.nota_estruturacao, | |
| dados.nota_formatacao_abnt | |
| ]) | |
| # Se a nota for maior ou igual a 56 (70% de 80), fica verde. Se não, vermelho. | |
| if nota_total >= 56: | |
| st.success(f"### 🎉 NOTA FINAL ESTIMADA: {nota_total}/80 PONTOS") | |
| else: | |
| st.error(f"### ⚠️ NOTA FINAL ESTIMADA: {nota_total}/80 PONTOS (Risco de Reprovação)") | |
| st.info(f"**💡 Parecer Geral do Examinador:** {dados.justificativa_notas_banca}") | |
| st.divider() | |
| # 2. DISTRIBUIÇÃO DAS NOTAS (Métricas) | |
| st.subheader("📊 Distribuição de Pontos") | |
| c1, c2, c3, c4 = st.columns(4) | |
| c1.metric("Tema/Problema", f"{dados.nota_tema_problema}/20") | |
| c2.metric("Intro/Método", f"{dados.nota_introducao_metodo}/20") | |
| c3.metric("Estrutura/Linguagem", f"{dados.nota_estruturacao}/20") | |
| c4.metric("Formatação/ABNT", f"{dados.nota_formatacao_abnt}/20") | |
| st.divider() | |
| # 3. ANÁLISE DE ADERÊNCIA E CONTEÚDO (Verde vs Vermelho) | |
| st.subheader("🎯 Cruzamento: Projeto vs. TCC") | |
| # Barra de progresso visual para a aderência | |
| st.write(f"**Índice de Fidelidade ao Projeto:** {dados.aderencia_percentual}%") | |
| st.progress(dados.aderencia_percentual / 100) | |
| st.write("") # Espaçamento | |
| col_a, col_b = st.columns(2) | |
| with col_a: | |
| st.markdown("#### ❓ Resposta ao Problema") | |
| if dados.tcc_respondeu_ao_problema: | |
| st.success(f"**✅ TCC Respondeu:** {dados.justificativa_problema}") | |
| else: | |
| st.error(f"**❌ Fuga do Problema:** {dados.justificativa_problema}") | |
| st.markdown("#### 📋 Cumprimento de Objetivos") | |
| # Filtra os objetivos que foram cumpridos subtraindo os não cumpridos | |
| objetivos_cumpridos = [ | |
| obj for obj in dados.objetivos_especificos_identificados | |
| if obj not in dados.objetivos_nao_cumpridos_no_tcc | |
| ] | |
| # Exibe os Verdes (Cumpridos) | |
| if objetivos_cumpridos: | |
| for obj in objetivos_cumpridos: | |
| st.success(f"✅ **Atingido:** {obj}") | |
| # Exibe os Vermelhos (Falhos) | |
| if dados.objetivos_nao_cumpridos_no_tcc: | |
| for obj in dados.objetivos_nao_cumpridos_no_tcc: | |
| st.error(f"❌ **Faltou Abordar:** {obj}") | |
| elif not dados.objetivos_nao_cumpridos_no_tcc and objetivos_cumpridos: | |
| st.success("✨ **Perfeito!** Todos os objetivos específicos foram mapeados e cumpridos.") | |
| with col_b: | |
| st.markdown("#### 🚨 Filtro Anti-Embromação e Incoerências") | |
| teve_problema_grave = False | |
| if dados.incoerencias_internas: | |
| teve_problema_grave = True | |
| for incoerencia in dados.incoerencias_internas: | |
| st.error(f"**Incoerência Estrutural:** {incoerencia}") | |
| if dados.desvios_de_tema_ou_linguica: | |
| teve_problema_grave = True | |
| for desvio in dados.desvios_de_tema_ou_linguica: | |
| st.warning(f"**Fuga de Tema / Enrolação:** {desvio}") | |
| if dados.indicios_texto_generico_ia: | |
| teve_problema_grave = True | |
| for ia in dados.indicios_texto_generico_ia: | |
| st.error(f"**Indício de Texto Genérico/IA:** {ia}") | |
| if not teve_problema_grave: | |
| st.success("✅ **Excelente!** O texto está fluido, sem contradições internas, sem enrolação e com linguagem autêntica.") | |
| # ABA 2: REVISÃO INTELIGENTE | |
| with aba_revisao: | |
| st.markdown("Revisão de **Ortografia**, **Gramática**, **Coerência**, **Vícios Jurídicos** e **Anti-IA**.") | |
| arq_rev = st.file_uploader("Envie o TCC (.docx)", type=["docx"], key="revisao") | |
| if st.button("🧠 Realizar Revisão"): | |
| if arq_rev: | |
| with st.status("Processando revisão...", expanded=True) as status: | |
| doc_rev, revisao, erros_locais = auditor.revisar(arq_rev) | |
| for erro in erros_locais: | |
| st.warning(f"⚠️ {erro}") | |
| if revisao.problemas_academicos: | |
| st.subheader("🚨 Problemas de Redação Detectados") | |
| for prob in revisao.problemas_academicos: | |
| st.error(f"**[{prob.gravidade.upper()}] {prob.problema}**\n\nTrecho: '{prob.trecho}'\n\nSugestão: {prob.sugestao}") | |
| # --- NOVO BLOCO: Mostrando as alterações realizadas no texto --- | |
| if revisao.correcoes: | |
| st.subheader("📝 Registro de Alterações Automáticas") | |
| with st.expander("Ver detalhes das correções aplicadas ao texto"): | |
| for c in revisao.correcoes: | |
| st.markdown(f"❌ **Original:** `{c.original}`") | |
| st.markdown(f"✅ **Corrigido:** `{c.corrigido}`") | |
| st.caption(f"💡 *Motivo:* {c.motivo} | *Categoria:* {c.categoria}") | |
| st.divider() | |
| st.session_state.doc_revisado_bytes = formatador.docx_to_bytes(doc_rev) | |
| st.session_state.nome_arq_rev = arq_rev.name.replace('.docx', '') + "_Revisado.docx" | |
| status.update( | |
| label=f"✅ Revisão concluída: {len(revisao.correcoes)} correções automáticas registradas, {len(revisao.termos_estrangeiros_para_italico)} itálicos.", | |
| state="complete", | |
| expanded=False | |
| ) | |
| if st.session_state.doc_revisado_bytes: | |
| st.download_button( | |
| "⬇️ Baixar TCC Revisado", | |
| data=st.session_state.doc_revisado_bytes, | |
| file_name=st.session_state.nome_arq_rev, | |
| mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document" | |
| ) | |
| # ABA 3: DIAGNÓSTICO ABNT (Painel Interativo de Laudos) | |
| with aba_abnt: | |
| st.markdown("Diagnóstico Rigoroso de **Estrutura e ABNT** (Inspetor Acadêmico UNIFAGOC).") | |
| arq_diag = st.file_uploader("Envie o TCC (.docx) para Laudo", type=["docx"], key="abnt_file") | |
| if st.button("📋 Gerar Laudo Diagnóstico"): | |
| if arq_diag: | |
| with st.status("Inspecionando documento...", expanded=True) as status: | |
| texto_completo = formatador.extrair_texto_docx(arq_diag) | |
| # Para evitar limite de output JSON, a IA foca nos principais erros do arquivo | |
| relatorio = revisor_ia.gerar_diagnostico_abnt(texto_completo) | |
| if relatorio: | |
| st.session_state.relatorio_abnt = relatorio | |
| status.update(label="✅ Laudo gerado com sucesso!", state="complete", expanded=False) | |
| else: | |
| status.update(label="❌ Falha na inspeção.", state="error") | |
| if st.session_state.get("relatorio_abnt"): | |
| relatorio = st.session_state.relatorio_abnt | |
| st.header("📋 Laudo de Revisão de Formatação") | |
| # Dashboard de Saúde ABNT | |
| if relatorio.nota_geral_formatacao >= 70: | |
| st.success(f"### Saúde da Formatação: {relatorio.nota_geral_formatacao}/100") | |
| else: | |
| st.error(f"### Saúde da Formatação: {relatorio.nota_geral_formatacao}/100") | |
| st.progress(relatorio.nota_geral_formatacao / 100) | |
| st.divider() | |
| # Função para renderizar os cards do Dashboard de Erros | |
| def renderizar_cards_erro(lista_erros, titulo, icone): | |
| st.subheader(f"{icone} {titulo}") | |
| if not lista_erros: | |
| st.success("✅ Nenhum erro encontrado nesta categoria!") | |
| for erro in lista_erros: | |
| with st.expander(f"📍 Local aproximado: {erro.local_aproximado}"): | |
| st.error(f"**Trecho com erro:**\n\n{erro.trecho_original}") | |
| st.success(f"**Como deve ficar:**\n\n{erro.como_deve_ficar}") | |
| st.info(f"**Regra UNIFAGOC aplicada:** {erro.justificativa_regra}") | |
| st.write("") # Espaçamento | |
| renderizar_cards_erro(relatorio.erros_estrutura_abnt, "Problemas de ABNT e Estrutura", "📐") | |
| renderizar_cards_erro(relatorio.erros_texto_juridico, "Problemas de Texto Jurídico", "✍️") | |
| renderizar_cards_erro(relatorio.erros_referencias, "Problemas nas Referências Bibliográficas", "📚") | |
| # ABA 4: PROMPT MESTRE | |
| with aba_prompt: | |
| st.markdown("### 🤖 Prompt Mestre - Padrão UNIFAGOC") | |
| prompt_pronto = """ | |
| Você é um revisor acadêmico especializado nas normas ABNT e nas diretrizes da UNIFAGOC. | |
| Sua tarefa é receber um texto bruto e formatá-lo EXATAMENTE conforme as especificações | |
| técnicas abaixo, sem omitir nenhuma regra, sem inventar informações, e sem alterar o | |
| conteúdo original (apenas a estrutura e a formatação). | |
| Siga cada item deste guia na ordem apresentada. | |
| ════════════════════════════════════════════════════════════ | |
| BLOCO 1 — CONFIGURAÇÃO DE PÁGINA | |
| ════════════════════════════════════════════════════════════ | |
| ▸ Tamanho do papel: A4 (210 × 297 mm) | |
| ▸ Margens: | |
| - Superior: 3 cm | |
| - Esquerda: 3 cm | |
| - Inferior: 2 cm | |
| - Direita: 2 cm | |
| ▸ Cabeçalho: posicionado a 0,5 cm da borda superior da página | |
| ▸ Rodapé: posicionado a 0,5 cm da borda inferior da página | |
| ▸ Orientação: retrato (portrait) | |
| ▸ Extensão total do artigo: entre 10 e 18 páginas | |
| ════════════════════════════════════════════════════════════ | |
| BLOCO 2 — NUMERAÇÃO DE PÁGINAS | |
| ════════════════════════════════════════════════════════════ | |
| ▸ Localização: cabeçalho (não no rodapé) | |
| ▸ Alinhamento: direita (ou conforme padrão do template) | |
| ▸ Formato: arábico (1, 2, 3...) | |
| ▸ Início da contagem: página 1 | |
| ▸ IMPORTANTE: a PRIMEIRA página NÃO exibe o número (cabeçalho da | |
| primeira página fica em branco). O número aparece a partir da 2ª página. | |
| ════════════════════════════════════════════════════════════ | |
| BLOCO 3 — FONTE PADRÃO DO DOCUMENTO | |
| ════════════════════════════════════════════════════════════ | |
| ▸ Fonte exclusiva em todo o documento: Calibri | |
| ▸ Tamanho padrão do corpo do texto: 12 pt | |
| ▸ Idioma configurado: Português (Brasil) | |
| ▸ NÃO usar Times New Roman, Arial ou qualquer outra fonte. | |
| ════════════════════════════════════════════════════════════ | |
| BLOCO 4 — ESTRUTURA OBRIGATÓRIA DO ARTIGO (nesta ordem) | |
| ════════════════════════════════════════════════════════════ | |
| O artigo deve conter, obrigatoriamente, os seguintes elementos | |
| na ordem abaixo: | |
| 1. TÍTULO (e subtítulo, se houver) | |
| 2. Nome(s) do(s) autor(es) com afiliações | |
| 3. RESUMO + Palavras-chave | |
| 4. INTRODUÇÃO | |
| 5. Seções de desenvolvimento (numeradas: 1. TÍTULO, 2. TÍTULO...) | |
| 6. CONSIDERAÇÕES FINAIS | |
| 7. REFERÊNCIAS | |
| ════════════════════════════════════════════════════════════ | |
| BLOCO 5 — TÍTULO DO ARTIGO | |
| ════════════════════════════════════════════════════════════ | |
| ▸ Fonte: Calibri 12 pt | |
| ▸ Estilo: negrito | |
| ▸ Alinhamento: centralizado | |
| ▸ Capitalização: MAIÚSCULAS (caixa alta) para o título principal | |
| ▸ Subtítulo (após dois-pontos): quando houver, em caixa baixa | |
| - Exemplo: "TÍTULO DO ARTIGO: subtítulo do artigo" | |
| ▸ Espaçamento antes do título: 0 pt | |
| ▸ Espaçamento depois do título: 0 pt | |
| ▸ Entrelinhamento: simples (1,0) | |
| ▸ Recuos esquerdo e direito: 0 cm | |
| ════════════════════════════════════════════════════════════ | |
| BLOCO 6 — AUTORES E AFILIAÇÕES | |
| ════════════════════════════════════════════════════════════ | |
| ▸ FORMATO DO NOME DOS AUTORES: | |
| - SOBRENOME, Nome (em negrito, Calibri 12 pt) | |
| - Alinhamento: centralizado | |
| - Separar autores com ponto e vírgula ( ; ) | |
| - Após cada nome, inserir letra sobrescrita (a, b, c...) identificando | |
| a afiliação correspondente | |
| - Espaçamento antes: 12 pt | Espaçamento depois: 6 pt | |
| - Exemplo: SILVA, João a ; SOUZA, Maria b | |
| ▸ FORMATO DAS AFILIAÇÕES (logo abaixo dos nomes): | |
| - Uma linha por autor, com a letra sobrescrita correspondente (a, b, c...) | |
| - Conteúdo: Titulação – Principal atividade profissional – Instituição – E-mail | |
| - Fonte: Calibri 12 pt, sem negrito | |
| - Alinhamento: centralizado | |
| - Cada afiliação em parágrafo separado | |
| ▸ NOTA DE RODAPÉ (TCC Direito – opcional): | |
| - Pode ser inserida nota de rodapé ao nome do(s) autor(es) com | |
| "Informação adicional sobre o(a) autor(a) (opcional)." | |
| - Numeração por algarismos arábicos | |
| ════════════════════════════════════════════════════════════ | |
| BLOCO 7 — RESUMO | |
| ════════════════════════════════════════════════════════════ | |
| ▸ Rótulo "RESUMO": Calibri 12 pt, negrito | |
| ▸ Texto do resumo: Calibri 11 pt, itálico | |
| ▸ Alinhamento: justificado (recuos esquerdo e direito = 0 cm) | |
| ▸ Estrutura: parágrafo ÚNICO (sem quebras internas) | |
| ▸ Extensão: máximo 150 palavras | |
| ▸ Conteúdo obrigatório no resumo: | |
| 1. Problema de pesquisa | |
| 2. Objetivo | |
| 3. Método(s) utilizado(s) | |
| 4. Resultados/conclusões mais importantes | |
| ▸ Entrelinhamento do resumo: simples | |
| ▸ PALAVRAS-CHAVE (logo abaixo do resumo, no mesmo bloco): | |
| - Rótulo "Palavras-chave:" em Calibri 12 pt, negrito | |
| - Seguido das palavras-chave em Calibri 12 pt, sem negrito | |
| - Quantidade: de 3 a 5 palavras-chave | |
| - Separação: ponto ( . ) entre cada palavra-chave | |
| - Capitalização: primeira letra de cada palavra em MAIÚSCULA | |
| (as demais letras em minúscula) | |
| - Formato: Palavras-chave: Direito. Previdência. Reforma Constitucional. | |
| - Espaçamento antes das palavras-chave: 6 pt (após o resumo) | |
| ════════════════════════════════════════════════════════════ | |
| BLOCO 8 — TÍTULOS DAS SEÇÕES E SUBSEÇÕES | |
| ════════════════════════════════════════════════════════════ | |
| ▸ SEÇÕES PRIMÁRIAS (INTRODUÇÃO, 1. TÍTULO, CONSIDERAÇÕES FINAIS, REFERÊNCIAS): | |
| - Fonte: Calibri 12 pt | |
| - Estilo: negrito | |
| - Capitalização: CAIXA ALTA (todas em maiúsculas) | |
| - Numeração: INTRODUÇÃO e CONSIDERAÇÕES FINAIS sem número; | |
| seções de desenvolvimento com número arábico seguido de ponto | |
| (ex: 1. TÍTULO, 2. TÍTULO, 3. TÍTULO...) | |
| - Alinhamento: justificado à esquerda (sem recuo) | |
| - Espaçamento antes: 0 pt | |
| - Espaçamento depois: aproximadamente 6 pt | |
| - Recuos esquerdo e direito: 0 cm | |
| ▸ SEÇÕES SECUNDÁRIAS (subseções): | |
| - Fonte: Calibri 12 pt | |
| - Estilo: negrito | |
| - Capitalização: caixa baixa (apenas a primeira letra em maiúscula) | |
| - Numeração: 1.1 Título, 1.2 Título... | |
| - Alinhamento: justificado à esquerda (sem recuo) | |
| ▸ SEÇÕES TERCIÁRIAS (sub-subseções): | |
| - Fonte: Calibri 12 pt | |
| - Estilo: negrito + itálico | |
| - Capitalização: caixa baixa | |
| - Numeração: 1.1.1 Título, 1.1.2 Título... | |
| - Alinhamento: justificado à esquerda (sem recuo) | |
| ════════════════════════════════════════════════════════════ | |
| BLOCO 9 — CORPO DO TEXTO (parágrafos das seções) | |
| ════════════════════════════════════════════════════════════ | |
| ▸ Fonte: Calibri 12 pt | |
| ▸ Alinhamento: justificado (margens esquerda e direita alinhadas) | |
| ▸ Recuo de primeira linha (parágrafo): 1,25 cm | |
| ▸ Recuo esquerdo do parágrafo: 0 cm | |
| ▸ Recuo direito do parágrafo: 0 cm | |
| ▸ Entrelinhamento: 1,5 (150%) | |
| ▸ Espaçamento antes do parágrafo: 0 pt | |
| ▸ Espaçamento depois do parágrafo: 0 pt | |
| ════════════════════════════════════════════════════════════ | |
| BLOCO 10 — CITAÇÕES | |
| ════════════════════════════════════════════════════════════ | |
| ▸ SISTEMA DE CITAÇÃO: AUTOR-data, conforme NBR 10520 (ABNT, 2002) | |
| - Exemplos: (SILVA, 2020); Silva (2020); (SILVA, 2020, p. 45) | |
| ▸ CITAÇÕES CURTAS (até 4 linhas): | |
| - Inseridas no corpo do texto | |
| - Entre aspas duplas (" ") | |
| - Mesma fonte e tamanho do parágrafo (Calibri 12) | |
| - Chamada da fonte ao final, entre parênteses: (AUTOR, ano, p. x) | |
| ▸ CITAÇÕES LONGAS (mais de 4 linhas — citação em bloco): | |
| - Parágrafo separado do texto principal | |
| - Fonte: Calibri 11 pt (um ponto menor que o corpo do texto) | |
| - Entrelinhamento: SIMPLES (1,0) | |
| - Alinhamento: justificado | |
| - Recuo SOMENTE da margem ESQUERDA: 4 cm (a partir da margem esquerda | |
| da página, que já é 3 cm — portanto, 4 cm adicionais) | |
| - Recuo direito: 0 cm (sem recuo à direita) | |
| - SEM aspas na citação em bloco | |
| - Espaçamento antes e depois da citação em bloco: 1 linha em branco | |
| - A chamada da fonte deve vir ao final, após o ponto final: | |
| (AUTOR, ano, p. x) | |
| - NÃO usar recuo de primeira linha neste parágrafo | |
| ════════════════════════════════════════════════════════════ | |
| BLOCO 11 — FIGURAS, QUADROS E TABELAS | |
| ════════════════════════════════════════════════════════════ | |
| ▸ Posicionamento: centralizados na página, próximos à sua citação no texto | |
| ▸ Numeração: sequencial por tipo (Figura 1, Figura 2; Quadro 1, Quadro 2; Tabela 1...) | |
| ▸ TÍTULO (acima da figura/quadro/tabela): | |
| - Formato: "Figura X – Descrição do título" | |
| - Fonte: Calibri 12 pt | |
| - Alinhamento: centralizado | |
| - O rótulo "Figura X" em negrito; o texto do título sem negrito | |
| ▸ LEGENDA / FONTE (abaixo da figura/quadro/tabela): | |
| - Formato: "Fonte: AUTOR (ano)." ou "Fonte: elaborado pelo(a) autor(a)." | |
| - Fonte: Calibri 11 pt | |
| - Alinhamento: centralizado | |
| ▸ ESPAÇAMENTOS ao redor de figuras/quadros/tabelas: | |
| - Entre o corpo do texto e o TÍTULO: 6 pt | |
| - Entre o título e a figura/quadro/tabela: 3 pt | |
| - Entre a figura/quadro/tabela e a legenda/fonte: 3 pt | |
| - Entre a legenda/fonte e o parágrafo seguinte: 6 pt | |
| ════════════════════════════════════════════════════════════ | |
| BLOCO 12 — REFERÊNCIAS | |
| ════════════════════════════════════════════════════════════ | |
| ▸ Título da seção: "REFERÊNCIAS" (Calibri 12, negrito, caixa alta, sem numeração) | |
| ▸ Norma: ABNT NBR 6023:2018 | |
| ▸ Fonte das referências: Calibri 11 pt | |
| ▸ Entrelinhamento: simples (1,0) | |
| ▸ Espaçamento entre referências: 6 pt após cada referência (espaçamento depois) | |
| ▸ Alinhamento: à ESQUERDA (justificado à esquerda, sem recuo) | |
| ▸ Ordenação: ordem ALFABÉTICA pelo sobrenome do primeiro autor | |
| ▸ NÃO numerar as referências | |
| ▸ Incluir APENAS as referências efetivamente citadas no texto | |
| ▸ DESTAQUE EM NEGRITO: o título do livro, periódico ou veículo de publicação | |
| deve estar em NEGRITO (e somente ele, não o restante da referência) | |
| - Livro: SILVA, João. **Título do Livro**. 2. ed. São Paulo: Editora, 2020. | |
| - Artigo: SOUZA, M. Título do artigo. **Nome da Revista**, v. 2, n. 1, 2021. | |
| - Legislação: BRASIL. Lei nº X. **Diário Oficial da União**, Brasília, 2020. | |
| - Jurisprudência: ACÓRDÃO XXXXXRS. Relator: Nome. **Diário da Justiça**, data. | |
| ▸ REGRAS DE INTEGRIDADE: Nunca inventar, modificar ou completar referências, | |
| leis, jurisprudências ou doutrinas não fornecidas no texto original. | |
| ▸ EXEMPLOS DE FORMATAÇÃO POR TIPO: | |
| • Livro: | |
| SOBRENOME, Nome. **Título em negrito**: subtítulo. ed. Cidade: Editora, ano. | |
| • Artigo em periódico: | |
| SOBRENOME, Nome. Título do artigo. **Nome do Periódico**, cidade, v. X, | |
| n. X, p. XX-XX, mês abrev. ano. Disponível em: URL. Acesso em: dia mês. ano. | |
| • Dissertação/Tese: | |
| SOBRENOME, Nome. **Título da dissertação**. ano. X f. Dissertação (Mestrado | |
| em ...) – Universidade, Cidade. Disponível em: URL. Acesso em: data. | |
| • Legislação: | |
| BRASIL. [Decreto-Lei / Lei] nº X, de data. Ementa. **Diário Oficial da União**, | |
| Brasília, data. Disponível em: URL. Acesso em: data. | |
| • Jurisprudência: | |
| TRIBUNAL/ACÓRDÃO nº X. Relator: Nome. Câmara/Turma. Data de Julgamento: | |
| data. **Nome do Diário**, data. | |
| • Documento eletrônico/site: | |
| SOBRENOME, Nome ou INSTITUIÇÃO. Ano (se houver). Título. **Veículo/Site**. | |
| Disponível em: URL. Acesso em: mês. ano. | |
| ════════════════════════════════════════════════════════════ | |
| BLOCO 13 — TERMOS ESTRANGEIROS E ITÁLICO | |
| ════════════════════════════════════════════════════════════ | |
| ▸ Todo termo em língua estrangeira deve ser formatado em ITÁLICO | |
| - Exemplos: habeas corpus, fake news, data mining, marketing, in verbis, | |
| ex officio, ad hoc, ab initio, stricto sensu, lato sensu | |
| ▸ Nomes de obras, filmes, jornais e revistas citados no texto: itálico | |
| ▸ Palavras usadas com sentido especial ou neologismos: itálico ou aspas simples | |
| ════════════════════════════════════════════════════════════ | |
| BLOCO 14 — ALINHAMENTOS — RESUMO RÁPIDO | |
| ════════════════════════════════════════════════════════════ | |
| Elemento │ Alinhamento | |
| ──────────────────────────────────┼────────────────────────── | |
| Título do artigo │ Centralizado | |
| Autores e afiliações │ Centralizado | |
| RESUMO (texto) │ Justificado | |
| Palavras-chave │ Justificado à esquerda | |
| Títulos de seções (todos níveis) │ Justificado à esquerda | |
| Corpo do texto (parágrafos) │ Justificado (ambos lados) | |
| Citações longas (em bloco) │ Justificado | |
| Títulos de figuras/tabelas │ Centralizado | |
| Legendas/Fontes de figuras │ Centralizado | |
| REFERÊNCIAS (título) │ Centralizado ou esquerda* | |
| Referências (entradas) │ Esquerda (sem recuo) | |
| * O título "REFERÊNCIAS" pode ser centralizado conforme o template 2025, | |
| mas as entradas da lista são sempre alinhadas à esquerda. | |
| ════════════════════════════════════════════════════════════ | |
| BLOCO 15 — TAMANHOS DE FONTE — RESUMO RÁPIDO | |
| ════════════════════════════════════════════════════════════ | |
| Elemento │ Tamanho │ Estilo | |
| ──────────────────────────────────┼─────────┼──────────────────── | |
| Título do artigo │ 12 pt │ Negrito, MAIÚSCULA | |
| Autores │ 12 pt │ Negrito | |
| Afiliações dos autores │ 12 pt │ Normal | |
| Rótulo "RESUMO" │ 12 pt │ Negrito | |
| Texto do resumo │ 11 pt │ Itálico | |
| Rótulo "Palavras-chave:" │ 12 pt │ Negrito | |
| Palavras-chave (termos) │ 12 pt │ Normal | |
| Títulos de seções primárias │ 12 pt │ Negrito, MAIÚSCULA | |
| Títulos de seções secundárias │ 12 pt │ Negrito | |
| Títulos de seções terciárias │ 12 pt │ Negrito + Itálico | |
| Corpo do texto (parágrafos) │ 12 pt │ Normal | |
| Citações longas (bloco) │ 11 pt │ Normal | |
| Títulos de figuras/tabelas │ 12 pt │ "Figura X" em negrito | |
| Legendas/Fontes de figuras │ 11 pt │ Normal | |
| Entradas de referências │ 11 pt │ Título da obra em negrito | |
| Notas de rodapé │ 10 pt │ Normal (padrão) | |
| ════════════════════════════════════════════════════════════ | |
| BLOCO 16 — ENTRELINHAMENTOS — RESUMO RÁPIDO | |
| ════════════════════════════════════════════════════════════ | |
| Elemento │ Entrelinhamento | |
| ──────────────────────────────────┼────────────────── | |
| Corpo do texto (parágrafos) │ 1,5 (150%) | |
| Resumo │ Simples (1,0) | |
| Citações longas (bloco) │ Simples (1,0) | |
| Referências (cada entrada) │ Simples (1,0) | |
| Títulos de seções │ Simples (1,0) | |
| ════════════════════════════════════════════════════════════ | |
| BLOCO 17 — REGRAS DE INTEGRIDADE E CONDUTA | |
| ════════════════════════════════════════════════════════════ | |
| ▸ NÃO inventar, modificar ou criar: leis, artigos de lei, jurisprudências, | |
| doutrinas, autores ou referências que não estejam explicitamente no texto. | |
| ▸ NÃO alterar o conteúdo do texto — apenas a estrutura e a formatação. | |
| ▸ NÃO remover seções, parágrafos ou informações do original. | |
| ▸ SE o texto original não tiver resumo, palavras-chave ou considerações | |
| finais, sinalize que o autor precisa providenciá-los — não os crie. | |
| ▸ SE alguma referência estiver incompleta no original, mantenha-a como | |
| está e sinalize a incompletude para o autor. | |
| ════════════════════════════════════════════════════════════ | |
| BLOCO 18 — PROCESSO PASSO A PASSO (execute nesta ordem) | |
| ════════════════════════════════════════════════════════════ | |
| PASSO 1 — LEIA o texto completo antes de qualquer ação. | |
| PASSO 2 — CONFIGURE a página: A4, margens (sup 3 cm, esq 3 cm, | |
| inf 2 cm, dir 2 cm), cabeçalho/rodapé a 0,5 cm. | |
| PASSO 3 — CONFIGURE a numeração de página no cabeçalho (direita), | |
| iniciando na página 1, sem número na primeira página. | |
| PASSO 4 — FORMATE o TÍTULO: Calibri 14, negrito, centralizado, | |
| maiúsculas; subtítulo (se houver) em minúsculas. | |
| PASSO 5 — FORMATE os AUTORES e AFILIAÇÕES com letras sobrescritas, | |
| centralizado, Calibri 12, negrito nos nomes. | |
| PASSO 6 — FORMATE o RESUMO: rótulo em negrito, texto em Calibri 11 | |
| itálico, parágrafo único, máx. 150 palavras, justificado. | |
| Logo abaixo: "Palavras-chave:" negrito + 3-5 termos separados | |
| por ponto, primeira letra de cada palavra em maiúscula. | |
| PASSO 7 — VERIFIQUE a estrutura: o texto possui INTRODUÇÃO, seções | |
| numeradas (1, 2, 3...), CONSIDERAÇÕES FINAIS e REFERÊNCIAS. | |
| Se faltarem, sinalize ao autor. | |
| PASSO 8 — FORMATE todos os TÍTULOS DE SEÇÃO: | |
| Primárias → Calibri 12, negrito, MAIÚSCULA, sem recuo. | |
| Secundárias → Calibri 12, negrito, caixa baixa. | |
| Terciárias → Calibri 12, negrito + itálico, caixa baixa. | |
| PASSO 9 — FORMATE o CORPO DO TEXTO: Calibri 12, justificado, | |
| recuo de primeira linha 1,25 cm, entrelinhamento 1,5. | |
| PASSO 10 — IDENTIFIQUE e FORMATE as CITAÇÕES: | |
| Curtas (≤ 4 linhas) → no texto, entre aspas duplas. | |
| Longas (> 4 linhas) → bloco separado, Calibri 11, | |
| recuo esquerdo 4 cm, espaçamento simples, sem aspas. | |
| PASSO 11 — COLOQUE os TERMOS ESTRANGEIROS em itálico. | |
| PASSO 12 — FORMATE as REFERÊNCIAS: Calibri 11, espaçamento simples | |
| + 6 pt entre cada referência, alinhamento à esquerda, | |
| ordem alfabética, título da obra em negrito, sem numeração. | |
| PASSO 13 — REVISE TODO O DOCUMENTO verificando: | |
| □ Fonte Calibri em todo o documento (nenhuma exceção) | |
| □ Margens corretas (3/3/2/2 cm) | |
| □ Numeração de página no cabeçalho (sem número na 1ª pág.) | |
| □ Entrelinhamento 1,5 no corpo; simples no resumo e referências | |
| □ Termos estrangeiros em itálico | |
| □ Títulos de obras nas referências em negrito | |
| □ Referências em ordem alfabética | |
| □ Extensão entre 10 e 18 páginas | |
| ════════════════════════════════════════════════════════════ | |
| BLOCO 19 — DIFERENÇAS ENTRE OS DOIS TEMPLATES | |
| ════════════════════════════════════════════════════════════ | |
| ▸ TEMPLATE TCC DIREITO 2025: | |
| - Destinado a artigos do curso de Direito | |
| - Admite nota de rodapé junto ao nome dos autores com | |
| "Informação adicional sobre Autor/a (opcional)." | |
| - Numeração de notas de rodapé: algarismos arábicos | |
| ▸ TEMPLATE TRABALHO DE CURSO 2026: | |
| - Destinado a trabalhos de curso em geral (não exclusivo a Direito) | |
| - SEM nota de rodapé de informações adicionais dos autores | |
| - Estrutura de seções idêntica ao TCC Direito 2025 | |
| ▸ AMBOS OS TEMPLATES são idênticos em: | |
| - Configuração de página, margens, fontes, tamanhos, espaçamentos, | |
| entrelinhamentos, citações, referências e estrutura geral. | |
| ════════════════════════════════════════════════════════════ | |
| EXEMPLO DE ESTRUTURA FINAL ESPERADA | |
| ════════════════════════════════════════════════════════════ | |
| [CABEÇALHO com número de página — exceto na 1ª página] | |
| TÍTULO DO ARTIGO: subtítulo quando houver | |
| ← centralizado, Calibri 14, negrito, maiúsculas | |
| SOBRENOME, Nome a ; SOBRENOME, Nome b | |
| ← centralizado, Calibri 12, negrito | |
| a Titulação – Função – Instituição – email@email.com | |
| b Titulação – Função – Instituição – email@email.com | |
| ← centralizado, Calibri 12 | |
| RESUMO Texto do resumo em itálico, parágrafo único, máximo 150 palavras, | |
| contendo o problema, objetivo, método e resultados. [Calibri 11, itálico] | |
| Palavras-chave: Palavra Um. Palavra Dois. Palavra Três. | |
| INTRODUÇÃO | |
| ← Calibri 12, negrito, maiúsculas, sem recuo | |
| Texto do parágrafo com recuo de 1,25 cm na primeira linha... | |
| [Calibri 12, justificado, entrelinhamento 1,5] | |
| 1. TÍTULO DA SEÇÃO | |
| Texto do parágrafo... | |
| Citação curta: conforme autor, "texto da citação" (AUTOR, ano, p. x). | |
| Citação longa (em bloco): | |
| [4 cm de recuo à esquerda, Calibri 11, espaçamento simples] | |
| Texto da citação com mais de quatro linhas vai aqui, sem aspas, | |
| recuado 4 cm da margem esquerda, em Calibri tamanho 11, | |
| com espaçamento simples. (AUTOR, ano, p. x) | |
| 2. TÍTULO DA SEGUNDA SEÇÃO | |
| 2.1 Título de subseção em caixa baixa | |
| ← Calibri 12, negrito | |
| 2.1.1 Título de sub-subseção em caixa baixa | |
| ← Calibri 12, negrito + itálico | |
| CONSIDERAÇÕES FINAIS | |
| REFERÊNCIAS | |
| ← Calibri 11, negrito, maiúsculas | |
| SOBRENOME, Nome. **Título da Obra em Negrito**. ed. Cidade: Editora, ano. | |
| ← Calibri 11, esquerda, 6 pt após cada entrada | |
| --- | |
| AGORA APLIQUE TODAS ESTAS REGRAS AO TEXTO FORNECIDO ABAIXO: | |
| [COLE O TEXTO AQUI] | |
| """ | |
| st.code(prompt_pronto, language="markdown") | |
| # ABA 5: FEEDBACK | |
| with aba_feedback: | |
| st.markdown("### 👨🏫 Feedback do Professor Examinador") | |
| arq_feedback = st.file_uploader("Envie o TCC (.docx)", type=["docx"], key="feedback") | |
| if st.button("📊 Gerar Relatório"): | |
| if arq_feedback: | |
| with st.spinner("Analisando estilo acadêmico..."): | |
| texto = formatador.extrair_texto_docx(arq_feedback) | |
| prompt_fb = ( | |
| "Analise o TCC: ESTRUTURA, CITAÇÕES, PONTOS FORTES/ATENÇÃO, FORMAÇÃO. " | |
| "ATENÇÃO: Estamos em maio de 2026. Publicações de 2024, 2025 ou 2026 estão no prazo e NÃO pertencem ao futuro. " | |
| f"Texto: {texto}" | |
| ) | |
| try: | |
| res = revisor_ia.client.models.generate_content( | |
| model="gemini-3.5-flash", | |
| contents=prompt_fb, | |
| config=types.GenerateContentConfig(temperature=0.2) | |
| ) | |
| st.write(res.text) | |
| except Exception as e: | |
| st.error(f"❌ Erro: {e}") |