GabrielGD commited on
Commit
976e6e1
·
verified ·
1 Parent(s): fa9c4c8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +156 -354
app.py CHANGED
@@ -1,376 +1,178 @@
1
  import os
2
  import io
3
  import re
4
- import json
5
  import streamlit as st
6
  from docx import Document
7
  from docx.shared import Cm, Pt
8
  from docx.enum.text import WD_ALIGN_PARAGRAPH
9
- from pydantic import BaseModel
10
  from google import genai
11
  from google.genai import types
 
12
 
13
- # ─── CONFIGURAÇÃO DA PÁGINA ───────────────────────────────────────────────
14
- st.set_page_config(page_title="Corretor TCC - UNIFAGOC", page_icon="⚖️", layout="wide")
 
 
 
 
 
 
15
 
16
- # ─── ESQUEMAS DE DADOS (PYDANTIC) ─────────────────────────────────────────
17
  class AuditoriaTCC(BaseModel):
18
- aderencia_percentual: int
19
- diagnostico_geral: str
20
- problema_identificado_no_projeto: str
21
- tcc_respondeu_ao_problema: bool
22
- justificativa_problema: str
23
- objetivos_especificos_identificados: list[str]
24
- objetivos_nao_cumpridos_no_tcc: list[str]
25
- desvios_de_tema_ou_linguica: list[str]
26
- indicios_texto_generico_ia: list[str]
27
- nota_tema_problema: float
28
- nota_introducao_metodo: float
29
- nota_estruturacao: float
30
- nota_formatacao_abnt: float
31
- justificativa_notas_banca: str
32
 
33
  class ErroTexto(BaseModel):
34
  original: str
35
  corrigido: str
36
- motivo: str
37
 
38
  class RevisaoInteligente(BaseModel):
39
- correcoes: list[ErroTexto]
40
- termos_estrangeiros_para_italico: list[str]
41
-
42
- # ─── AUTENTICAÇÃO GEMINI ──────────────────────────────────────────────────
43
- api_key = os.environ.get("GEMINI_API_KEY")
44
- if not api_key:
45
- st.error("❌ ERRO: A variável de ambiente GEMINI_API_KEY não foi encontrada nos Secrets.")
46
- st.stop()
47
-
48
- client = genai.Client(api_key=api_key)
49
-
50
- # ─── FUNÇÕES AUXILIARES ───────────────────────────────────────────────────
51
- def extrair_texto_docx(arquivo_upload) -> str:
52
- doc = Document(arquivo_upload)
53
- linhas = [para.text.strip() for para in doc.paragraphs if para.text.strip()]
54
- return "\n\n".join(linhas)
55
-
56
- def extrair_referencias_docx(doc: Document) -> list:
57
- resultado = []
58
- em_refs = False
59
- for i, para in enumerate(doc.paragraphs):
60
- texto = para.text.strip()
61
- if not texto: continue
62
- if re.search(r"^(?:\d+\.?\s*)?REFER[ÊE]NCIAS", texto, re.IGNORECASE):
63
- em_refs = True
64
- continue
65
- if em_refs:
66
- if len(texto) < 120 and texto.isupper() and not texto[0].isdigit(): break
67
- resultado.append((i, texto))
68
- return resultado
69
 
70
  def docx_to_bytes(doc: Document) -> bytes:
71
- byte_io = io.BytesIO()
72
- doc.save(byte_io)
73
- return byte_io.getvalue()
74
 
75
  def estimar_paginas(doc: Document) -> int:
76
- palavras_totales = 0
77
- for para in doc.paragraphs:
78
- if re.search(r"^(?:\d+\.?\s*)?REFER[ÊE]NCIAS", para.text, re.IGNORECASE): break
79
- palavras_totales += len(para.text.split())
80
- return max(1, palavras_totales // 350)
81
-
82
- def aplicar_formatacao_estrutural_unifagoc(doc: Document):
83
- """Aplica o template de forma segura, sem corromper o XML original."""
84
- # 1. Margens Oficiais da Instituição
85
- for section in doc.sections:
86
- section.page_height = Cm(29.7)
87
- section.page_width = Cm(21.0)
88
- section.top_margin = Cm(3.0)
89
- section.left_margin = Cm(3.0)
90
- section.bottom_margin = Cm(2.0)
91
- section.right_margin = Cm(2.0)
92
-
93
- # 2. Configura o estilo "Normal" global para evitar quebra de runs
94
- if 'Normal' in doc.styles:
95
- style = doc.styles['Normal']
96
- if style.font:
97
- style.font.name = 'Calibri'
98
- style.font.size = Pt(12)
99
-
100
- em_referencias = False
101
- em_resumo = False
102
-
103
- # 3. Formatação Segura de Parágrafos
104
- for para in doc.paragraphs:
105
- texto = para.text.strip()
106
- if not texto: continue
107
-
108
- texto_upper = texto.upper()
109
- if texto_upper.startswith("RESUMO"): em_resumo = True
110
- elif texto_upper.startswith("INTRODUÇÃO"): em_resumo = False
111
- elif re.search(r"^(?:\d+\.?\s*)?REFER[ÊE]NCIAS", texto, re.IGNORECASE): em_referencias = True
112
-
113
- # Alinhamento e Recuos
114
- if em_referencias:
115
- para.alignment = WD_ALIGN_PARAGRAPH.LEFT
116
- para.paragraph_format.first_line_indent = Cm(0)
117
- para.paragraph_format.space_after = Pt(6)
118
- for run in para.runs:
119
- run.font.name = 'Calibri'
120
- run.font.size = Pt(11)
121
-
122
- elif em_resumo:
123
- para.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
124
- para.paragraph_format.first_line_indent = Cm(0)
125
- for run in para.runs:
126
- run.font.name = 'Calibri'
127
- run.font.size = Pt(11)
128
- if texto_upper != "RESUMO" and not texto_upper.startswith("PALAVRAS-CHAVE"):
129
- run.font.italic = True
130
-
131
- elif texto.isupper() and len(texto) < 100: # Identifica prováveis Títulos
132
- para.alignment = WD_ALIGN_PARAGRAPH.CENTER
133
- para.paragraph_format.first_line_indent = Cm(0)
134
- for run in para.runs:
135
- run.font.name = 'Calibri'
136
- run.font.size = Pt(12)
137
-
138
- else:
139
- para.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
140
- para.paragraph_format.first_line_indent = Cm(1.25)
141
- # Limpa formatações estranhas para forçar o herdeiro "Normal" (Calibri 12)
142
- for run in para.runs:
143
- run.font.name = 'Calibri'
144
- run.font.size = Pt(12)
145
-
146
- # ─── INTERFACE PRINCIPAL (ABAS) ───────────────────────────────────────────
147
- st.title("⚖️ Assistente Acadêmico - Direito UNIFAGOC")
148
- aba_auditoria, aba_revisao, aba_abnt = st.tabs(["🔍 Auditoria de Conteúdo e Notas", "🧠 Revisão Textual Inteligente", "📐 Formatação Total ABNT"])
149
-
150
- # ==============================================================================
151
- # ABA 1: AUDITORIA E PONTUAÇÃO UNIFAGOC
152
- # ==============================================================================
153
- with aba_auditoria:
154
- st.markdown("Cruze o **Pré-Projeto** com o **TCC** para identificar fuga do tema e gerar a **Nota da Banca (80 pontos)**.")
155
- fase_tcc = st.radio("Selecione a fase atual do trabalho:", ["TCC 1 (Apenas Introdução e Capítulo 1)", "TCC 2 (TCC Completo com Resumo e Conclusão)"], horizontal=True)
156
-
157
- col1, col2 = st.columns(2)
158
- with col1: arq_projeto = st.file_uploader("1. Envie o Pré-Projeto (.docx)", type=["docx"], key="proj")
159
- with col2: arq_tcc = st.file_uploader("2. Envie os Capítulos do TCC (.docx)", type=["docx"], key="tcc_aud")
160
-
161
- if st.button("🔍 Iniciar Auditoria e Avaliação", type="primary"):
162
- if not arq_projeto or not arq_tcc:
163
- st.warning("⚠️ Envie os DOIS arquivos para realizar a comparação.")
164
- else:
165
- with st.spinner("Avaliando argumentos e calculando notas da banca..."):
166
- texto_projeto = extrair_texto_docx(arq_projeto)
167
- texto_tcc = extrair_texto_docx(arq_tcc)
168
- regra_fase = "ATENÇÃO: O aluno está em TCC 1 (Apenas Introdução e Capítulo 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. O trabalho deve estar completo (Resumo, Introdução, Desenvolvimento, Conclusão e Referências)."
169
-
170
- prompt = f"""Você é um Professor Examinador de Direito da UNIFAGOC.
171
- {regra_fase}
172
-
173
- === PRÉ-PROJETO ===
174
- {texto_projeto}
175
-
176
- === TCC ===
177
- {texto_tcc}
178
-
179
- TAREFA 1: Identifique o Problema, Objetivos e procure por fugas de tema genéricas.
180
- TAREFA 2: Avalie o TCC conforme os 80 pontos da FICHA DE AVALIAÇÃO DA UNIFAGOC:
181
- - Tema, Delimitação do Problema e Objeto (0 a 20 pts)
182
- - Introdução e Método (0 a 20 pts)
183
- - Estruturação e Desenvolvimento (0 a 20 pts)
184
- - Formatação e ABNT (0 a 20 pts)
185
- Seja rigoroso e acadêmico na atribuição das notas.
186
- """
187
- config = types.GenerateContentConfig(temperature=0.0, response_mime_type="application/json", response_schema=AuditoriaTCC)
188
-
189
- try:
190
- resposta = client.models.generate_content(model="gemini-3.5-flash", contents=prompt, config=config)
191
- dados = json.loads(resposta.text)
192
-
193
- st.success(f"Auditoria e Avaliação concluídas para {fase_tcc.split(' ')[0]}!")
194
-
195
- st.header("🏆 Avaliação da Banca UNIFAGOC")
196
- st.markdown("*Distribuição de 80 pontos textuais (20 pts de apresentação não inclusos).*")
197
-
198
- nota_total = sum([
199
- dados.get('nota_tema_problema', 0),
200
- dados.get('nota_introducao_metodo', 0),
201
- dados.get('nota_estruturacao', 0),
202
- dados.get('nota_formatacao_abnt', 0)
203
- ])
204
-
205
- c_nota1, c_nota2, c_nota3, c_nota4 = st.columns(4)
206
- c_nota1.metric("Tema/Problema", f"{dados.get('nota_tema_problema')}/20")
207
- c_nota2.metric("Intro/Método", f"{dados.get('nota_introducao_metodo')}/20")
208
- c_nota3.metric("Estrutura/Linguagem", f"{dados.get('nota_estruturacao')}/20")
209
- c_nota4.metric("Formatação/ABNT", f"{dados.get('nota_formatacao_abnt')}/20")
210
-
211
- st.info(f"**NOTA TEXTUAL FINAL: {nota_total}/80 pontos** \n*Justificativa da Banca:* {dados.get('justificativa_notas_banca')}")
212
- st.divider()
213
-
214
- st.metric(label="Índice de Aderência ao Projeto", value=f"{dados.get('aderencia_percentual')}%")
215
- st.write(f"**Diagnóstico:** {dados.get('diagnostico_geral')}")
216
-
217
- c1, c2 = st.columns(2)
218
- with c1:
219
- st.subheader("🎯 Problema de Pesquisa")
220
- st.write(f"**Promessa:** {dados.get('problema_identificado_no_projeto')}")
221
- if dados.get("tcc_respondeu_ao_problema"): st.success(f"✅ O TCC respondeu: {dados.get('justificativa_problema')}")
222
- else: st.error(f"❌ Não respondeu: {dados.get('justificativa_problema')}")
223
- with c2:
224
- st.subheader("📋 Objetivos")
225
- for f in dados.get("objetivos_nao_cumpridos_no_tcc", []): st.error(f"Faltou: {f}")
226
- if not dados.get("objetivos_nao_cumpridos_no_tcc"): st.success("✅ Objetivos abordados.")
227
- st.subheader("🚨 Filtro Anti-Embromação")
228
- for fuga in dados.get("desvios_de_tema_ou_linguica", []): st.warning(f"Fuga de Tema: {fuga}")
229
- for ia in dados.get("indicios_texto_generico_ia", []): st.error(f"Texto Genérico/IA: {ia}")
230
- except Exception as e:
231
- if "429" in str(e): st.error("❌ A API está sobrecarregada. Aguarde 1 minuto.")
232
- else: st.error(f"Erro: {e}")
233
-
234
- # ==============================================================================
235
- # ABA 2: REVISÃO TEXTUAL INTELIGENTE
236
- # ==============================================================================
237
- with aba_revisao:
238
- st.markdown("A IA analisa o contexto para corrigir **Ortografia**, parágrafos indevidamente em **CAIXA ALTA** e aplica **Itálico** em termos estrangeiros/latinos.")
239
- arq_revisao = st.file_uploader("Envie o TCC (.docx) para revisão inteligente", type=["docx"], key="rev")
240
-
241
- if st.button("🧠 Realizar Revisão Inteligente"):
242
- if arq_revisao:
243
- with st.spinner("Lendo o documento para identificar jargões, caixa alta e erros..."):
244
- texto = extrair_texto_docx(arq_revisao)
245
- prompt_rev = f"""Analise o texto acadêmico de Direito abaixo e identifique:
246
- 1. Erros ortográficos e gramaticais.
247
- 2. Trechos do corpo do texto que estão indevidamente em CAIXA ALTA (retorne a versão corrigida). Ignore títulos.
248
- 3. Termos estrangeiros ou jargões em latim (ex: habeas corpus, fake news) que necessitam de itálico.
249
- TEXTO: {texto[:25000]}"""
250
-
251
- try:
252
- res = client.models.generate_content(
253
- model="gemini-3.5-flash",
254
- contents=prompt_rev,
255
- config=types.GenerateContentConfig(temperature=0.1, response_mime_type="application/json", response_schema=RevisaoInteligente)
256
- )
257
- dados_rev = json.loads(res.text)
258
- correcoes = dados_rev.get("correcoes", [])
259
- italicos = dados_rev.get("termos_estrangeiros_para_italico", [])
260
-
261
- if not correcoes and not italicos:
262
- st.success("🎉 O texto está perfeitamente revisado e sem jargões para formatar.")
263
- else:
264
- doc_rev = Document(arq_revisao)
265
- aplicadas_texto, aplicadas_italico = 0, 0
266
-
267
- if correcoes:
268
- st.subheader("📝 Correções de Texto e Caixa Alta")
269
- for c in correcoes:
270
- orig, corr = c.get("original", ""), c.get("corrigido", "")
271
- st.write(f"❌ `{orig}` ➔ ✅ `{corr}` (*{c.get('motivo')}*)")
272
- for para in doc_rev.paragraphs:
273
- if orig in para.text:
274
- for run in para.runs:
275
- if orig in run.text:
276
- run.text = run.text.replace(orig, corr, 1)
277
- aplicadas_texto += 1
278
- break
279
-
280
- if italicos:
281
- st.subheader("🪄 Termos Estrangeiros (Itálico Aplicado)")
282
- st.write(", ".join([f"*{i}*" for i in italicos]))
283
- for termo in italicos:
284
- for para in doc_rev.paragraphs:
285
- matches = list(re.finditer(r'\b' + re.escape(termo) + r'\b', para.text, re.IGNORECASE))
286
- for match in matches:
287
- pos_ini, pos_fim, pos_atual = match.start(), match.end(), 0
288
- for run in para.runs:
289
- fim_run = pos_atual + len(run.text)
290
- if pos_atual < pos_fim and fim_run > pos_ini:
291
- run.italic = True
292
- aplicadas_italico += 1
293
- pos_atual += len(run.text)
294
-
295
- st.success(f"✅ {aplicadas_texto} correções de texto e {aplicadas_italico} itálicos aplicados!")
296
- st.download_button("⬇️ Baixar Documento Revisado (.docx)", data=docx_to_bytes(doc_rev), file_name="TCC_Revisado.docx", mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document")
297
- except Exception as e:
298
- if "429" in str(e): st.error("❌ A API está sobrecarregada. Aguarde 1 minuto.")
299
- else: st.error(f"Erro ao processar: {e}")
300
-
301
- # ==============================================================================
302
- # ABA 3: FORMATAÇÃO TOTAL E REFERÊNCIAS ABNT
303
- # ==============================================================================
304
- with aba_abnt:
305
- st.markdown("Varre todo o documento forçando a fonte **Calibri**, Margens **3x2**, Alinhamento e Espaçamentos do edital.")
306
- st.warning("⏱️ **Aviso:** Se muitos alunos tentarem formatar ao mesmo tempo, aguarde 60 segundos.")
307
-
308
- arq_abnt = st.file_uploader("Envie o TCC (.docx) para formatar o layout estrutural", type=["docx"], key="abnt")
309
-
310
- if st.button("📐 Formatar Layout e Referências"):
311
- if arq_abnt:
312
- with st.spinner("Padronizando fontes, margens e referências... Isso pode levar um minuto."):
313
- doc_abnt = Document(arq_abnt)
314
-
315
- paginas_estimadas = estimar_paginas(doc_abnt)
316
- if paginas_estimadas > 18: st.error(f"🚨 **Atenção:** Seu TCC tem aprox. {paginas_estimadas} páginas. O limite da banca é 18.")
317
- else: st.info(f"📄 Extensão estimada: {paginas_estimadas} páginas.")
318
-
319
- aplicar_formatacao_estrutural_unifagoc(doc_abnt)
320
- st.success("✅ Estrutura Física (Margens, Fontes e Alinhamento) salva com sucesso!")
321
-
322
- referencias = extrair_referencias_docx(doc_abnt)
323
- if not referencias:
324
- st.warning("Aviso: Nenhuma seção com o título 'REFERÊNCIAS' foi localizada.")
325
- st.download_button("⬇️ Baixar TCC Formatado", data=docx_to_bytes(doc_abnt), file_name="TCC_Padrao_UNIFAGOC.docx", mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document")
326
- else:
327
- texto_refs = "\n".join(f"{i+1}. {texto}" for i, (_, texto) in enumerate(referencias))
328
- prompt_abnt = f"Verifique as referências abaixo seguindo a ABNT NBR 6023:2018. Retorne um JSON:\n{{\"problemas\": [{{\"numero\": int, \"erros\": [\"erro\"], \"elemento_negrito\": \"palavra destaque\"}}]}}\nREFERÊNCIAS:\n{texto_refs}"
329
-
330
- try:
331
- res = client.models.generate_content(
332
- model="gemini-3.5-flash",
333
- contents=prompt_abnt,
334
- config=types.GenerateContentConfig(temperature=0.0, response_mime_type="application/json")
335
- )
336
- problemas = json.loads(res.text).get("problemas", [])
337
-
338
- def aplicar_negrito_ref(doc, refs_problematicas):
339
- em_refs, ref_idx = False, 0
340
- for para in doc.paragraphs:
341
- texto = para.text.strip()
342
- if not texto: continue
343
- if re.search(r"^(?:\d+\.?\s*)?REFER[ÊE]NCIAS", texto, re.IGNORECASE):
344
- em_refs = True
345
- continue
346
- if not em_refs or len(texto) < 10: continue
347
-
348
- nums_prob = {p.get("numero", 0) - 1 for p in refs_problematicas}
349
- if ref_idx in nums_prob:
350
- match = re.match(r"^((?:[A-ZÁÉÍÓÚÀÂÊÔÃÕÇ][^.;]+[.;]\s*)+)\s*(.+?)(\.\s+(?:\d+\.\s+ed|[A-Z]).*|$)", para.text)
351
- if match:
352
- autoria, titulo = match.group(1).strip(), match.group(2).strip()
353
- pos_atual, pos_ini = 0, para.text.find(titulo, len(autoria))
354
- pos_fim = pos_ini + len(titulo)
355
- for run in para.runs:
356
- fim_run = pos_atual + len(run.text)
357
- if pos_atual >= pos_ini and fim_run <= pos_fim: run.bold = True
358
- elif pos_atual < pos_fim and fim_run > pos_ini: run.bold = True
359
- pos_atual += len(run.text)
360
- ref_idx += 1
361
-
362
- aplicar_negrito_ref(doc_abnt, problemas)
363
-
364
- if problemas:
365
- for p in problemas: st.write(f"⚠️ Referência #{p['numero']} ajustada: {', '.join(p['erros'])}")
366
- st.success("✅ Formatação de destaque (negrito) nas referências aplicada!")
367
-
368
- st.download_button(
369
- label="⬇️ Baixar TCC Totalmente Formatado (.docx)",
370
- data=docx_to_bytes(doc_abnt),
371
- file_name="TCC_Padrao_UNIFAGOC.docx",
372
- mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
373
- )
374
- except Exception as e:
375
- if "429" in str(e): st.error("❌ A API está sobrecarregada. Aguarde 1 minuto.")
376
- else: st.error(f"Erro na verificação pela IA: {e}")
 
1
  import os
2
  import io
3
  import re
 
4
  import streamlit as st
5
  from docx import Document
6
  from docx.shared import Cm, Pt
7
  from docx.enum.text import WD_ALIGN_PARAGRAPH
8
+ from pydantic import BaseModel, Field
9
  from google import genai
10
  from google.genai import types
11
+ from tenacity import retry, stop_after_attempt, wait_exponential
12
 
13
+ # ─── CONFIG ───────────────────────────────────────────────────────────────
14
+ st.set_page_config(page_title="Corretor TCC - UNIFAGOC", page_icon="⚖", layout="wide")
15
+ api_key = st.secrets.get("GEMINI_API_KEY", os.environ.get("GEMINI_API_KEY"))
16
+ if not api_key:
17
+ st.error("❌ Defina GEMINI_API_KEY nos Secrets do Streamlit.")
18
+ st.stop()
19
+ client = genai.Client(api_key=api_key)
20
+ MODEL = "gemini-3.5-flash"
21
 
22
+ # ─── MODELOS ──────────────────────────────────────────────────────────────
23
  class AuditoriaTCC(BaseModel):
24
+ aderencia_percentual: int = Field(ge=0, le=100)
25
+ diagnostico_geral: str = ""
26
+ problema_identificado_no_projeto: str = ""
27
+ tcc_respondeu_ao_problema: bool = False
28
+ justificativa_problema: str = ""
29
+ objetivos_especificos_identificados: list[str] = Field(default_factory=list)
30
+ objetivos_nao_cumpridos_no_tcc: list[str] = Field(default_factory=list)
31
+ desvios_de_tema_ou_linguica: list[str] = Field(default_factory=list)
32
+ indicios_texto_generico_ia: list[str] = Field(default_factory=list)
33
+ nota_tema_problema: float = 0.0
34
+ nota_introducao_metodo: float = 0.0
35
+ nota_estruturacao: float = 0.0
36
+ nota_formatacao_abnt: float = 0.0
37
+ justificativa_notas_banca: str = ""
38
 
39
  class ErroTexto(BaseModel):
40
  original: str
41
  corrigido: str
42
+ motivo: str = ""
43
 
44
  class RevisaoInteligente(BaseModel):
45
+ correcoes: list[ErroTexto] = Field(default_factory=list)
46
+ termos_estrangeiros_para_italico: list[str] = Field(default_factory=list)
47
+
48
+ # ─── HELPERS ──────────────────────────────────────────────────────────────
49
+ @retry(wait=wait_exponential(2, 30), stop=stop_after_attempt(3))
50
+ def chamar_gemini(prompt, schema, temp=0.0):
51
+ return client.models.generate_content(
52
+ model=MODEL,
53
+ contents=prompt,
54
+ config=types.GenerateContentConfig(
55
+ temperature=temp,
56
+ response_mime_type="application/json",
57
+ response_schema=schema
58
+ )
59
+ )
60
+
61
+ @st.cache_data(show_spinner=False)
62
+ def extrair_texto_docx(_bytes) -> str:
63
+ doc = Document(io.BytesIO(_bytes))
64
+ return "\n\n".join(p.text.strip() for p in doc.paragraphs if p.text.strip())
 
 
 
 
 
 
 
 
 
 
65
 
66
  def docx_to_bytes(doc: Document) -> bytes:
67
+ bio = io.BytesIO()
68
+ doc.save(bio)
69
+ return bio.getvalue()
70
 
71
  def estimar_paginas(doc: Document) -> int:
72
+ chars = 0
73
+ for p in doc.paragraphs:
74
+ if re.search(r"REFER[ÊE]NCIAS", p.text, re.I): break
75
+ chars += len(p.text)
76
+ return max(1, chars // 1800)
77
+
78
+ def extrair_referencias_docx(doc: Document):
79
+ refs, em_refs = [], False
80
+ for i, p in enumerate(doc.paragraphs):
81
+ t = p.text.strip()
82
+ if not t: continue
83
+ if re.match(r"^(?:\d+\.?\s*)?REFER[ÊE]NCIAS", t, re.I):
84
+ em_refs = True; continue
85
+ if em_refs:
86
+ if re.match(r"^(ANEXOS?|AP[ÊE]NDICES?|APENDICE)$", t, re.I): break
87
+ if len(t) < 80 and t.isupper(): break
88
+ refs.append((i, t))
89
+ return refs
90
+
91
+ def aplicar_formatacao_unifagoc(doc: Document):
92
+ for s in doc.sections:
93
+ s.top_margin = s.left_margin = Cm(3)
94
+ s.bottom_margin = s.right_margin = Cm(2)
95
+ normal = doc.styles['Normal']
96
+ normal.font.name = 'Calibri'
97
+ normal.font.size = Pt(12)
98
+ normal.paragraph_format.line_spacing = 1.5
99
+ normal.paragraph_format.first_line_indent = Cm(1.25)
100
+
101
+ em_ref, em_resumo = False, False
102
+ for p in doc.paragraphs:
103
+ txt = p.text.strip()
104
+ if not txt: continue
105
+ up = txt.upper()
106
+ if up.startswith("RESUMO"): em_resumo = True
107
+ elif up.startswith("INTRODUÇÃO"): em_resumo = False
108
+ elif re.match(r"REFER[ÊE]NCIAS", txt, re.I): em_ref = True
109
+
110
+ if em_ref:
111
+ p.alignment = WD_ALIGN_PARAGRAPH.LEFT
112
+ p.paragraph_format.first_line_indent = Cm(0)
113
+ for r in p.runs: r.font.size = Pt(11)
114
+ elif em_resumo and up not in ("RESUMO",) and not up.startswith("PALAVRAS"):
115
+ p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
116
+ for r in p.runs: r.font.italic = True; r.font.size = Pt(11)
117
+ elif txt.isupper() and len(txt) < 120:
118
+ p.alignment = WD_ALIGN_PARAGRAPH.CENTER
119
+ p.paragraph_format.first_line_indent = Cm(0)
120
+
121
+ def aplicar_italico_seguro(paragrafo, termo):
122
+ texto = paragrafo.text
123
+ if not re.search(rf'\b{re.escape(termo)}\b', texto, re.I): return False
124
+ partes = re.split(rf'(\b{re.escape(termo)}\b)', texto, flags=re.I)
125
+ paragrafo.clear()
126
+ for parte in partes:
127
+ run = paragrafo.add_run(parte)
128
+ if parte.lower() == termo.lower(): run.italic = True
129
+ return True
130
+
131
+ # ─── UI ───────────────────────────────────────────────────────────────────
132
+ st.title("⚖ Assistente Acadêmico - Direito UNIFAGOC")
133
+ aba1, aba2, aba3 = st.tabs(["🔍 Auditoria", "🧠 Revisão", "📐 ABNT"])
134
+
135
+ with aba1:
136
+ fase = st.radio("Fase:", ["TCC 1", "TCC 2"], horizontal=True)
137
+ c1, c2 = st.columns(2)
138
+ with c1: proj = st.file_uploader("Pré-Projeto", type="docx", key="p1")
139
+ with c2: tcc = st.file_uploader("TCC", type="docx", key="t1")
140
+ if st.button("Auditar", type="primary", disabled=not(proj and tcc)):
141
+ with st.status("Auditando...", expanded=True) as s:
142
+ txt_p = extrair_texto_docx(proj.getvalue())
143
+ txt_t = extrair_texto_docx(tcc.getvalue())
144
+ regra = "TCC 1: não penalize ausência de Resumo/Conclusão." if fase=="TCC 1" else "TCC 2: trabalho completo."
145
+ prompt = f"Você é examinador UNIFAGOC. {regra}\nPROJETO:\n{txt_p}\n\nTCC:\n{txt_t}\nAvalie 80 pontos."
146
+ resp = chamar_gemini(prompt, AuditoriaTCC, 0.0)
147
+ dados: AuditoriaTCC = resp.parsed
148
+ s.update(label="Concluído", state="complete")
149
+ st.metric("Aderência", f"{dados.aderencia_percentual}%")
150
+ total = sum([dados.nota_tema_problema, dados.nota_introducao_metodo, dados.nota_estruturacao, dados.nota_formatacao_abnt])
151
+ st.info(f"Nota textual: {total}/80 — {dados.justificativa_notas_banca}")
152
+
153
+ with aba2:
154
+ arq = st.file_uploader("TCC para revisão", type="docx", key="r1")
155
+ if st.button("Revisar", disabled=not arq):
156
+ with st.spinner("Revisando texto integral..."):
157
+ txt = extrair_texto_docx(arq.getvalue())
158
+ prompt = f"Revise ortografia, CAIXA ALTA indevida e liste termos latinos/estrangeiros para itálico. Texto:\n{txt}"
159
+ resp = chamar_gemini(prompt, RevisaoInteligente, 0.1)
160
+ rev: RevisaoInteligente = resp.parsed
161
+ doc = Document(io.BytesIO(arq.getvalue()))
162
+ mapa = {e.original: e.corrigido for e in rev.correcoes}
163
+ for p in doc.paragraphs:
164
+ for orig, corr in mapa.items():
165
+ if orig in p.text:
166
+ for r in p.runs: r.text = r.text.replace(orig, corr)
167
+ for termo in rev.termos_estrangeiros_para_italico:
168
+ for p in doc.paragraphs: aplicar_italico_seguro(p, termo)
169
+ st.download_button("Baixar revisado", docx_to_bytes(doc), "TCC_Revisado.docx")
170
+
171
+ with aba3:
172
+ arq3 = st.file_uploader("TCC para ABNT", type="docx", key="a1")
173
+ if st.button("Formatar", disabled=not arq3):
174
+ doc = Document(io.BytesIO(arq3.getvalue()))
175
+ st.write(f"Páginas estimadas: {estimar_paginas(doc)}")
176
+ aplicar_formatacao_unifagoc(doc)
177
+ refs = extrair_referencias_docx(doc)
178
+ st.download_button("Baixar formatado", docx_to_bytes(doc), "TCC_ABNT.docx")