GabrielGD commited on
Commit
7d2afc7
·
verified ·
1 Parent(s): 3ba9ebd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +437 -405
app.py CHANGED
@@ -4,24 +4,31 @@ 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
- # ─── 1. CONFIGURAÇÃO DA PÁGINA E ESTADO ───────────────────────────────────
 
 
14
  st.set_page_config(page_title="Corretor TCC - UNIFAGOC", page_icon="⚖️", layout="wide")
15
 
16
  def reset_estado(chave):
17
  if chave in st.session_state:
18
  st.session_state[chave] = None
19
 
20
- if "resultado_auditoria" not in st.session_state: st.session_state.resultado_auditoria = None
21
- if "doc_revisado_bytes" not in st.session_state: st.session_state.doc_revisado_bytes = None
22
- if "doc_formatado_bytes" not in st.session_state: st.session_state.doc_formatado_bytes = None
 
23
 
24
- # Dicionário Fixo de Latinismos Jurídicos
25
  LATINISMOS = [
26
  "habeas corpus", "data venia", "jus puniendi", "jus postulandi",
27
  "mens legis", "ratio decidendi", "obiter dictum", "in casu",
@@ -30,18 +37,20 @@ LATINISMOS = [
30
  "mutatis mutandis", "strictu sensu", "lato sensu", "sine qua non"
31
  ]
32
 
33
- # ─── 2. ESQUEMAS DE DADOS ESTRITOS (PYDANTIC) ─────────────────────────────
 
 
34
  class AuditoriaTCC(BaseModel):
35
  aderencia_percentual: int
36
  diagnostico_geral: str
37
  problema_identificado_no_projeto: str
38
  tcc_respondeu_ao_problema: bool
39
  justificativa_problema: str
40
- objetivos_especificos_identificados: list[str]
41
- objetivos_nao_cumpridos_no_tcc: list[str]
42
- desvios_de_tema_ou_linguica: list[str]
43
- indicios_texto_generico_ia: list[str]
44
- incoerencias_internas: list[str]
45
  nota_tema_problema: float
46
  nota_introducao_metodo: float
47
  nota_estruturacao: float
@@ -52,41 +61,50 @@ class ErroTexto(BaseModel):
52
  original: str
53
  corrigido: str
54
  motivo: str
55
- categoria: str
56
- gravidade: str
57
- contexto: str
58
 
59
  class ProblemaAcademico(BaseModel):
60
  trecho: str
61
- problema: str
62
  sugestao: str
63
  gravidade: str
64
 
65
  class RevisaoInteligente(BaseModel):
66
- correcoes: list[ErroTexto]
67
- problemas_academicos: list[ProblemaAcademico]
68
- termos_estrangeiros_para_italico: list[str]
69
 
70
  class ProblemaReferencia(BaseModel):
71
  numero: int
72
- erros: list[str]
73
  elemento_negrito: str
74
 
75
- class AnaliseABNT(BaseModel):
76
- problemas: list[ProblemaReferencia]
 
 
 
 
 
77
 
78
- # ─── 3. AUTENTICAÇÃO E CACHE ───────────────────────────────────────────────
 
 
79
  @st.cache_resource
80
  def get_client():
81
- api_key = os.environ.get("GEMINI_API_KEY")
82
  if not api_key:
83
- st.error("❌ ERRO: A variável de ambiente GEMINI_API_KEY não foi encontrada nos Secrets.")
84
  st.stop()
85
  return genai.Client(api_key=api_key)
86
 
87
  client = get_client()
88
 
89
- # ─── 4. MANIPULAÇÃO AVANÇADA DE DOCUMENTOS (DOCX) ─────────────────────────
 
 
90
  def extrair_texto_docx(arquivo_upload) -> str:
91
  doc = Document(arquivo_upload)
92
  return "\n\n".join([para.text.strip() for para in doc.paragraphs if para.text.strip()])
@@ -103,29 +121,34 @@ def estimar_paginas(doc: Document) -> int:
103
  )
104
  return max(1, palavras_totais // 350)
105
 
106
- def substituir_texto_cross_run(para, original, corrigido) -> int:
 
107
  texto_completo = "".join(run.text for run in para.runs)
108
- if original not in texto_completo:
109
  return 0
110
-
111
- for run in para.runs:
112
- if original in run.text:
113
- run.text = run.text.replace(original, corrigido, 1)
114
- return 1
115
-
116
- novo_texto = texto_completo.replace(original, corrigido, 1)
117
- estilo_base = para.style
118
- alinhamento_base = para.paragraph_format.alignment
119
-
120
  para.clear()
121
- para.add_run(novo_texto)
122
- para.style = estilo_base
123
- if alinhamento_base:
124
- para.paragraph_format.alignment = alinhamento_base
125
-
 
 
 
 
 
 
 
126
  return 1
127
 
128
- def formatacao_cruzada_indices(para, texto_alvo, aplicar_italico=False, aplicar_negrito=False) -> int:
129
  aplicacoes = 0
130
  if aplicar_italico:
131
  matches = list(re.finditer(r'\b' + re.escape(texto_alvo) + r'\b', para.text, re.IGNORECASE))
@@ -135,36 +158,68 @@ def formatacao_cruzada_indices(para, texto_alvo, aplicar_italico=False, aplicar_
135
 
136
  for match in matches:
137
  pos_ini, pos_fim = match if isinstance(match, tuple) else (match.start(), match.end())
138
- if pos_ini == -1: continue
 
139
  pos_atual = 0
140
  for run in para.runs:
141
  fim_run = pos_atual + len(run.text)
142
  if pos_atual < pos_fim and fim_run > pos_ini:
143
- if aplicar_italico: run.italic = True
144
- if aplicar_negrito: run.bold = True
 
 
145
  aplicacoes += 1
146
  pos_atual += len(run.text)
147
  return aplicacoes
148
 
149
- # ─── MOTOR DE VERIFICAÇÕES LOCAIS (REGEX) ─────────────────────────────────
150
- def verificacoes_locais_regex(texto: str) -> list[str]:
151
- erros = []
152
- if re.search(r"\s{2,}", texto):
153
- erros.append("Espaços duplos detectados no documento.")
154
- if re.search(r",\s*,", texto):
155
- erros.append("Vírgulas duplicadas detectadas (ex: ',,').")
156
- if re.search(r"[.!?]{2,}", texto) and not "..." in texto:
157
- erros.append("Pontuação repetida indevida (ex: '!!' ou '..').")
158
- if re.search(r"\b(\w+)\s+\1\b", texto, re.IGNORECASE):
159
- erros.append("Palavras repetidas sequencialmente (ex: 'o o', 'de de').")
160
-
161
- if re.search(r"\([A-ZÀ-Ú\s]+,\s*\)", texto):
162
- erros.append("Citação ABNT com ano faltando (ex: (SILVA, )).")
163
-
164
- return erros
165
-
166
- # ─── MOTOR ABNT CORRIGIDO COM BASE NO RELATÓRIO DO UTILIZADOR ─────────────
167
- def aplicar_formatacao_unifagoc_abnt(doc: Document):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  for section in doc.sections:
169
  section.page_height = Cm(29.7)
170
  section.page_width = Cm(21.0)
@@ -175,85 +230,175 @@ def aplicar_formatacao_unifagoc_abnt(doc: Document):
175
  section.header_distance = Cm(0.5)
176
  section.footer_distance = Cm(0.5)
177
 
178
- fase_atual = "PRE_RESUMO"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
- for i, para in enumerate(doc.paragraphs):
 
 
181
  texto = para.text.strip()
182
- if not texto: continue
183
-
184
- texto_upper = texto.upper()
185
- pf = para.paragraph_format
 
 
 
186
 
187
- if texto_upper == "RESUMO":
188
- fase_atual = "RESUMO_LBL"
189
- elif fase_atual == "RESUMO_LBL":
190
- fase_atual = "RESUMO_BODY"
191
-
192
- if texto_upper.startswith("PALAVRAS-CHAVE"):
193
- fase_atual = "PALAVRAS_CH"
194
- elif re.match(r"^(?:INTRODU[ÇC]ÃO|1[\s\.])", texto_upper):
195
- fase_atual = "CORPO"
196
-
197
- is_titulo_refs = bool(re.search(r"^(?:\d+\.?\s*)?REFER[ÊE]NCIAS\b", texto_upper))
198
- if is_titulo_refs:
199
- fase_atual = "REF_TITLE"
200
- elif fase_atual == "REF_TITLE":
201
- fase_atual = "REF_ENTRY"
202
-
203
- if fase_atual == "PRE_RESUMO":
204
- if i == 0 or (texto.isupper() and len(texto) < 150):
205
- pf.alignment = WD_ALIGN_PARAGRAPH.CENTER
206
- pf.first_line_indent = None
207
- pf.left_indent = None
208
- pf.space_before = Pt(0)
209
- pf.space_after = Pt(0)
210
- pf.line_spacing = 1.5
211
- for run in para.runs:
212
  run.font.name = 'Calibri'
213
- run.font.size = Pt(14)
214
- run.font.bold = True
215
- else:
216
- pf.alignment = WD_ALIGN_PARAGRAPH.CENTER
217
- pf.first_line_indent = None
218
- pf.left_indent = None
219
- pf.space_before = Pt(0)
220
- pf.space_after = Pt(0)
221
- pf.line_spacing = None
222
- for run in para.runs:
223
- run.font.name = 'Calibri'
224
- run.font.size = Pt(12)
225
- run.font.bold = False
226
 
227
- elif fase_atual == "RESUMO_LBL":
228
- pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
229
- pf.first_line_indent = None
230
- pf.left_indent = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  pf.space_before = Pt(12)
232
- pf.space_after = Pt(0)
233
- pf.line_spacing = 1.5
234
  for run in para.runs:
235
  run.font.name = 'Calibri'
236
- run.font.size = Pt(11)
237
  run.font.bold = True
238
- run.font.italic = False
239
 
240
- elif fase_atual == "RESUMO_BODY":
 
 
241
  pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
242
- pf.first_line_indent = None
243
- pf.left_indent = None
244
- pf.space_before = Pt(0)
245
  pf.space_after = Pt(0)
246
  pf.line_spacing = 1.5
247
  for run in para.runs:
248
  run.font.name = 'Calibri'
249
  run.font.size = Pt(11)
250
- run.font.italic = True
251
- run.font.bold = False
252
 
253
- elif fase_atual == "PALAVRAS_CH":
 
 
254
  pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
255
- pf.first_line_indent = None
256
- pf.left_indent = None
257
  pf.space_before = Pt(6)
258
  pf.space_after = Pt(0)
259
  pf.line_spacing = 1.5
@@ -262,68 +407,98 @@ def aplicar_formatacao_unifagoc_abnt(doc: Document):
262
  run.font.size = Pt(11)
263
  if "PALAVRAS-CHAVE" in run.text.upper():
264
  run.font.bold = True
265
- run.font.italic = False
266
- else:
267
- run.font.bold = False
268
- run.font.italic = False
269
-
270
- elif fase_atual == "CORPO":
271
- if texto.isupper() and len(texto) < 120:
272
- pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
273
- pf.first_line_indent = None
274
- pf.left_indent = None
275
- pf.space_before = Pt(0)
276
- pf.space_after = Pt(12)
277
- pf.line_spacing = 1.5
278
- for run in para.runs:
279
- run.font.name = 'Calibri'
280
- run.font.size = Pt(12)
281
- run.font.bold = True
282
- elif pf.left_indent is not None and pf.left_indent >= Cm(3.5):
283
- pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
284
- pf.first_line_indent = None
285
- pf.left_indent = Cm(4.0)
286
- pf.space_before = Pt(6)
287
- pf.space_after = Pt(6)
288
- pf.line_spacing = 1.0
289
- for run in para.runs:
290
- run.font.name = 'Calibri'
291
- run.font.size = Pt(11)
292
- else:
293
- pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
294
- pf.first_line_indent = Cm(1.25)
295
- pf.left_indent = None
296
- pf.space_before = Pt(0)
297
- pf.space_after = Pt(0)
298
- pf.line_spacing = 1.5
299
- for run in para.runs:
300
- run.font.name = 'Calibri'
301
- run.font.size = Pt(12)
302
 
303
- elif fase_atual == "REF_TITLE":
304
- pf.alignment = WD_ALIGN_PARAGRAPH.CENTER
305
- pf.first_line_indent = None
306
- pf.left_indent = None
307
- pf.space_before = Pt(0)
308
- pf.space_after = Pt(0)
309
- pf.line_spacing = 1.5
310
  for run in para.runs:
311
  run.font.name = 'Calibri'
312
  run.font.size = Pt(12)
313
  run.font.bold = True
314
 
315
- elif fase_atual == "REF_ENTRY":
316
- pf.alignment = WD_ALIGN_PARAGRAPH.LEFT
317
- pf.first_line_indent = None
318
- pf.left_indent = None
319
- pf.space_before = Pt(6)
320
- pf.space_after = Pt(0)
 
 
 
 
 
 
 
 
321
  pf.line_spacing = 1.0
 
 
322
  for run in para.runs:
323
  run.font.name = 'Calibri'
324
  run.font.size = Pt(11)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
 
326
- # ─── 5. INTERFACE PRINCIPAL (ABAS) ────────────────────────────────────────
 
 
327
  st.title("⚖️ Assistente Acadêmico - Direito UNIFAGOC")
328
  aba_auditoria, aba_revisao, aba_abnt, aba_prompt, aba_feedback = st.tabs([
329
  "🔍 Auditoria e Notas",
@@ -333,43 +508,39 @@ aba_auditoria, aba_revisao, aba_abnt, aba_prompt, aba_feedback = st.tabs([
333
  "👨‍🏫 Feedback Professor"
334
  ])
335
 
336
- # ==============================================================================
337
- # ABA 1: AUDITORIA E PONTUAÇÃO UNIFAGOC
338
- # ==============================================================================
339
  with aba_auditoria:
340
- st.markdown("Cruze o **Pré-Projeto** com o **TCC** para identificar fuga do tema, coerência e gerar a **Nota da Banca (80 pontos)**.")
341
- fase_tcc = st.radio("Fase atual:", ["TCC 1 (Apenas Introdução e Capítulo 1)", "TCC 2 (TCC Completo)"], horizontal=True, on_change=reset_estado, args=("resultado_auditoria",))
342
 
343
  col1, col2 = st.columns(2)
344
- with col1: arq_projeto = st.file_uploader("1. Pré-Projeto (.docx)", type=["docx"], key="proj", on_change=reset_estado, args=("resultado_auditoria",))
345
- with col2: arq_tcc = st.file_uploader("2. TCC (.docx)", type=["docx"], key="tcc_aud", on_change=reset_estado, args=("resultado_auditoria",))
 
 
346
 
347
- if st.button("🔍 Iniciar Auditoria e Avaliação", type="primary"):
348
  if not arq_projeto or not arq_tcc:
349
- st.warning("⚠️ Envie os DOIS ficheiros para realizar a comparação.")
350
  else:
351
- with st.status("A analisar argumentação e coerência jurídica...", expanded=True) as status:
352
- st.write("A extrair textos dos documentos...")
353
  texto_projeto = extrair_texto_docx(arq_projeto)
354
- texto_tcc = extrair_texto_docx(arq_tcc)
355
-
356
  regra_fase = (
357
  "ATENÇÃO: O aluno está em TCC 1. NÃO penalize a ausência de Resumo e Conclusão."
358
  if "TCC 1" in fase_tcc else "O aluno está em TCC 2. Exija o trabalho completo."
359
  )
360
  prompt = (
361
- f"Como Professor Examinador titular da banca de Direito da UNIFAGOC:\n{regra_fase}\n"
362
  f"=== PRÉ-PROJETO ===\n{texto_projeto}\n"
363
  f"=== TCC ===\n{texto_tcc}\n"
364
- "TAREFA 1: Avalie o cruzamento de dados, fuga de tema e objetivos.\n"
365
- "TAREFA 2: Verifique incoerências internas severas (ex: Introdução foca em um tema, mas capítulos focam em outro).\n"
366
- "TAREFA 3: Distribua 80 pontos rigorosamente de acordo com os critérios da ficha UNIFAGOC."
367
  )
368
-
369
- st.write("A submeter dados à IA do Google...")
370
  try:
371
  res = client.models.generate_content(
372
- model="gemini-3.5-flash",
373
  contents=prompt,
374
  config=types.GenerateContentConfig(
375
  temperature=0.0,
@@ -378,97 +549,48 @@ with aba_auditoria:
378
  )
379
  )
380
  st.session_state.resultado_auditoria = json.loads(res.text)
381
- status.update(label="Avaliação da Banca concluída!", state="complete", expanded=False)
382
  except Exception as e:
383
- status.update(label="Falha na avaliação.", state="error")
384
  st.error(f"Erro: {e}")
385
 
386
  if st.session_state.resultado_auditoria:
387
  dados = st.session_state.resultado_auditoria
388
  st.header("🏆 Avaliação da Banca UNIFAGOC")
389
  nota_total = sum([
390
- dados.get("nota_tema_problema", 0), dados.get("nota_introducao_metodo", 0),
391
- dados.get("nota_estruturacao", 0), dados.get("nota_formatacao_abnt", 0)
 
 
392
  ])
393
-
394
  c1, c2, c3, c4 = st.columns(4)
395
- c1.metric("Tema/Problema", f"{dados.get('nota_tema_problema')}/20")
396
- c2.metric("Intro/Método", f"{dados.get('nota_introducao_metodo')}/20")
397
- c3.metric("Estrutura/Linguagem", f"{dados.get('nota_estruturacao')}/20")
398
- c4.metric("Formatação/ABNT", f"{dados.get('nota_formatacao_abnt')}/20")
399
-
400
- st.info(f"**NOTA FINAL TEXTUAL: {nota_total}/80 pontos**\n\n*Parecer da Banca:* {dados.get('justificativa_notas_banca')}")
401
- st.divider()
402
-
403
- st.metric("Índice de Aderência ao Projeto", f"{dados.get('aderencia_percentual')}%")
404
-
405
- if dados.get("incoerencias_internas"):
406
- st.error("🚨 Incoerências Internas Detectadas:")
407
- for inc in dados.get("incoerencias_internas"):
408
- st.write(f"- {inc}")
409
-
410
- col_a, col_b = st.columns(2)
411
- with col_a:
412
- st.subheader("🎯 Problema de Pesquisa")
413
- if dados.get("tcc_respondeu_ao_problema"):
414
- st.success(f"✅ O TCC respondeu: {dados.get('justificativa_problema')}")
415
- else:
416
- st.error(f"❌ Não respondeu: {dados.get('justificativa_problema')}")
417
- with col_b:
418
- st.subheader("📋 Objetivos")
419
- for f in dados.get("objetivos_nao_cumpridos_no_tcc", []):
420
- st.error(f"Faltou: {f}")
421
- if not dados.get("objetivos_nao_cumpridos_no_tcc"):
422
- st.success("✅ Todos os objetivos abordados.")
423
-
424
- st.subheader("🚨 Filtro Anti-Embromação")
425
- for fuga in dados.get("desvios_de_tema_ou_linguica", []):
426
- st.warning(f"Fuga: {fuga}")
427
- for ia in dados.get("indicios_texto_generico_ia", []):
428
- st.error(f"Indício de IA/Texto Genérico: {ia}")
429
-
430
- # ==============================================================================
431
- # ABA 2: REVISÃO TEXTUAL INTELIGENTE
432
- # ==============================================================================
433
  with aba_revisao:
434
- st.markdown("Revisão de múltiplas camadas: **Ortografia**, **Gramática**, **Coerência**, **Vícios Jurídicos** e motor anti-IA.")
435
- arq_rev = st.file_uploader("Envie o TCC (.docx)", type=["docx"], key="revisao", on_change=reset_estado, args=("doc_revisado_bytes",))
436
 
437
- if st.button("🧠 Realizar Revisão Acadêmica"):
438
  if arq_rev:
439
- with st.status("A processar Revisão em Múltiplas Camadas...", expanded=True) as status:
440
- st.write("1. A executar verificações locais (Regex)...")
441
  texto = extrair_texto_docx(arq_rev)
442
-
443
  erros_locais = verificacoes_locais_regex(texto)
444
  for erro in erros_locais:
445
- st.warning(f"⚠️ Verificação Rápida: {erro}")
446
-
447
- st.write("2. A submeter texto à Revisão Mestre do Google Gemini...")
448
-
449
- prompt_rev = f"""
450
- Você é um Professor e Revisor Rigoroso de TCCs de Direito da UNIFAGOC.
451
- Analise o texto linha por linha.
452
-
453
- Localize e corrija:
454
- 1. ORTOGRAFIA E GRAMÁTICA: Concordância verbal e nominal, regência, crase, pontuação.
455
- 2. VÍCIOS DE ESCRITA JURÍDICA: Repetição excessiva de palavras, períodos excessivamente longos (mais de 4 linhas sem ponto final), uso excessivo de voz passiva, parágrafos de uma frase só, repetição da expressão "Diante do exposto", linguagem coloquial.
456
- 3. TEXTO "CARA DE IA": Identifique generalizações sem fonte ("A tecnologia vem transformando..."), conclusões óbvias, repetição de estrutura sintática.
457
- 4. COESÃO E COERÊNCIA: Furos no argumento ou parágrafos desconexos.
458
- 5. ABNT E RIGOR ACADÊMICO: Identifique afirmações sem fundamento legal, jurisprudência jogada sem fonte, excesso de "apud".
459
- 6. FORMATAÇÃO: Caixa alta inadequada no corpo do texto.
460
-
461
- NÃO IGNORE NENHUM ERRO. Retorne TODOS os erros encontrados.
462
-
463
- No objeto de retorno 'termos_estrangeiros_para_italico', não inclua os termos básicos se não houverem.
464
-
465
- Texto para revisar:
466
- {texto}
467
- """
468
 
 
 
 
 
 
469
  try:
470
  res = client.models.generate_content(
471
- model="gemini-3.5-flash",
472
  contents=prompt_rev,
473
  config=types.GenerateContentConfig(
474
  temperature=0.1,
@@ -477,191 +599,101 @@ with aba_revisao:
477
  )
478
  )
479
  dados = json.loads(res.text)
480
-
481
- st.write("3. A aplicar correções cirúrgicas no Word (Cross-Run ativado)...")
482
  doc_rev = Document(arq_rev)
483
  aplicadas_texto, aplicadas_italico = 0, 0
484
 
485
  if dados.get("correcoes"):
486
  for c in dados["correcoes"]:
487
  for para in doc_rev.paragraphs:
488
- aplicadas_texto += substituir_texto_cross_run(para, c["original"], c["corrigido"])
 
 
489
 
490
- st.write("4. A aplicar Itálico Automático em Latinismos e Termos Estrangeiros...")
491
  termos_para_italico = set(LATINISMOS + dados.get("termos_estrangeiros_para_italico", []))
492
-
493
  for termo in termos_para_italico:
494
  for para in doc_rev.paragraphs:
495
  aplicadas_italico += formatacao_cruzada_indices(para, termo, aplicar_italico=True)
496
 
497
  st.session_state.doc_revisado_bytes = docx_to_bytes(doc_rev)
498
- st.session_state.nome_arq_rev = f"{arq_rev.name.replace('.docx', '')}_Revisado.docx"
499
-
500
- if dados.get("problemas_academicos"):
501
- st.subheader("🚨 Problemas de Redação e Rigor Acadêmico Detectados")
502
- for prob in dados["problemas_academicos"]:
503
- st.error(f"**[{prob['gravidade'].upper()}] {prob['problema']}**\n\nTrecho: '{prob['trecho']}'\n\nSugestão: {prob['sugestao']}")
504
-
505
  status.update(
506
- label=f"Processo finalizado: {aplicadas_texto} textos corrigidos, {aplicadas_italico} itálicos aplicados.",
507
- state="complete", expanded=False
 
508
  )
509
  except Exception as e:
510
- status.update(label="Erro no processamento.", state="error")
511
- st.error(f"Erro: {e}")
512
 
513
  if st.session_state.doc_revisado_bytes:
514
  st.download_button(
515
- "⬇️ Baixar TCC Revisado (.docx)",
516
  data=st.session_state.doc_revisado_bytes,
517
  file_name=st.session_state.nome_arq_rev,
518
  mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
519
  )
520
 
521
- # ==============================================================================
522
- # ABA 3: FORMATAÇÃO TOTAL ABNT (UNIFAGOC)
523
- # ==============================================================================
524
  with aba_abnt:
525
- st.markdown(
526
- "Motor de formatação **fiel ao template UNIFAGOC**. "
527
- "Aplica as regras corretas de Calibri, margens, espaçamentos e estilos corrigidos."
528
- )
529
- arq_abnt = st.file_uploader("Envie o TCC (.docx)", type=["docx"], key="abnt_file", on_change=reset_estado, args=("doc_formatado_bytes",))
530
 
531
  if st.button("📐 Formatar TCC"):
532
  if arq_abnt:
533
- with st.status("A reestruturar formatação conforme template UNIFAGOC...", expanded=True) as status:
534
  doc_abnt = Document(arq_abnt)
535
-
536
  paginas_estimadas = estimar_paginas(doc_abnt)
537
  if paginas_estimadas > 18:
538
- st.warning(f"🚨 Extensão de {paginas_estimadas} páginas. O limite recomendado é 18.")
539
- else:
540
- st.info(f"📄 Extensão estimada: {paginas_estimadas} páginas.")
541
-
542
- st.write("A aplicar regras rigorosas do template corrigido...")
543
- aplicar_formatacao_unifagoc_abnt(doc_abnt)
544
-
545
- st.write("A analisar Referências Bibliográficas com IA...")
546
- refs_limpas = []
547
- em_refs = False
548
- for i, para in enumerate(doc_abnt.paragraphs):
549
- txt = para.text.strip()
550
- if not txt: continue
551
- if re.search(r"^(?:\d+\.?\s*)?REFER[ÊE]NCIAS\b", txt, re.IGNORECASE):
552
- em_refs = True; continue
553
- if em_refs:
554
- if len(txt) < 120 and txt.isupper() and not txt[0].isdigit(): break
555
- refs_limpas.append((i, txt))
556
-
557
- st.session_state.nome_arq_abnt = f"{arq_abnt.name.replace('.docx', '')}_Formatado.docx"
558
-
559
- if refs_limpas:
560
- texto_refs = "\n".join(f"[{idx}] {txt}" for idx, txt in refs_limpas)
561
- prompt_abnt = (
562
- "Analise as referências (ABNT 6023:2018). "
563
- "Para cada uma, devolva o índice exato entre colchetes como 'numero', "
564
- "e extraia o trecho EXATO (título do livro ou nome da revista/periódico) "
565
- "que deve receber negrito.\n"
566
- f"REFERÊNCIAS:\n{texto_refs}"
567
- )
568
- try:
569
- res = client.models.generate_content(
570
- model="gemini-3.5-flash",
571
- contents=prompt_abnt,
572
- config=types.GenerateContentConfig(
573
- temperature=0.0,
574
- response_mime_type="application/json",
575
- response_schema=AnaliseABNT
576
- )
577
- )
578
- dados_abnt = json.loads(res.text)
579
-
580
- mapa_idx_para = {idx: doc_abnt.paragraphs[idx] for idx, _ in refs_limpas if idx < len(doc_abnt.paragraphs)}
581
- negs_aplicados = 0
582
-
583
- for problema in dados_abnt.get("problemas", []):
584
- num = problema.get("numero")
585
- elem = problema.get("elemento_negrito", "").strip()
586
- if elem and num in mapa_idx_para:
587
- negs_aplicados += formatacao_cruzada_indices(mapa_idx_para[num], elem, aplicar_negrito=True)
588
-
589
- st.session_state.doc_formatado_bytes = docx_to_bytes(doc_abnt)
590
- status.update(
591
- label=f"TCC formatado! {negs_aplicados} negritos aplicados nas referências.",
592
- state="complete", expanded=False
593
- )
594
- except Exception as e:
595
- st.session_state.doc_formatado_bytes = docx_to_bytes(doc_abnt)
596
- status.update(label="Formatação aplicada (negrito de referências ignorado).", state="complete")
597
- st.warning(f"Aviso na análise ABNT: {e}")
598
- else:
599
- st.session_state.doc_formatado_bytes = docx_to_bytes(doc_abnt)
600
- status.update(label="Formatação aplicada (nenhuma referência encontrada).", state="complete")
601
 
602
  if st.session_state.doc_formatado_bytes:
603
  st.download_button(
604
- "⬇️ Baixar TCC Final Formatado (.docx)",
605
  data=st.session_state.doc_formatado_bytes,
606
  file_name=st.session_state.nome_arq_abnt,
607
  mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
608
  )
609
 
610
- # ==============================================================================
611
  # ABA 4: PROMPT MESTRE
612
- # ==============================================================================
613
  with aba_prompt:
614
  st.markdown("### 🤖 Prompt Mestre - Padrão UNIFAGOC")
615
- st.write("Vai usar o ChatGPT, Gemini ou Claude para gerar ou rever o seu texto? Copie o prompt abaixo e cole na IA antes de enviar o seu rascunho.")
616
-
617
- prompt_pronto = """Atue como um revisor académico especialista em formatação ABNT e nas diretrizes institucionais do curso de Direito da UNIFAGOC.
618
-
619
- Vou-lhe enviar o rascunho do meu texto. A sua tarefa é revisá-lo e reescrevê-lo garantindo a linguagem formal, impessoal e o rigor do vocabulário jurídico.
620
-
621
- Siga ESTRITAMENTE estas regras estruturais ao gerar a resposta:
622
- 1. ESTRUTURA DE SECÇÕES: Organize o texto utilizando os títulos padrão exigidos: INTRODUÇÃO, secções numeradas (ex: 1. TÍTULO), CONSIDERAÇÕES FINAIS e REFERÊNCIAS.
623
- 2. RESUMO (se aplicável): Deve ser redigido em parágrafo único, contendo problema de pesquisa, objetivo, método e resultados, com limite máximo de 150 palavras. Logo abaixo, inclua de 3 a 5 palavras-chave separadas por ponto e com a primeira letra de cada palavra em maiúscula.
624
- 3. TERMOS ESTRANGEIROS: Identifique termos em latim ou língua estrangeira (ex: habeas corpus, fake news) e coloque-os em *itálico*.
625
- 4. CITAÇÕES CURTAS: Citações diretas com até 3 linhas devem vir entre aspas duplas no corpo do texto.
626
- 5. CITAÇÕES LONGAS: Citações diretas com mais de 3 linhas devem vir em parágrafo separado. Inicie o parágrafo com a tag [CITAÇÃO LONGA AQUI - RECUO] para posterior formatação.
627
- 6. REFERÊNCIAS: Formate no padrão ABNT NBR 6023:2018. Gere as referências alinhadas à esquerda e sem recuos laterais. Aplique **negrito** apenas no elemento de destaque (título do livro ou nome do periódico).
628
- 7. INTEGRIDADE: Não invente leis, jurisprudências ou doutrinas. Baseie-se exclusivamente no conteúdo e contexto fornecidos.
629
-
630
- Aqui está o meu texto:
631
- [COLE O SEU TEXTO AQUI]"""
632
-
633
  st.code(prompt_pronto, language="markdown")
634
 
635
- # ==============================================================================
636
- # ABA 5: FEEDBACK ACADÉMICO (O Olhar do Professor)
637
- # ==============================================================================
638
  with aba_feedback:
639
  st.markdown("### 👨‍🏫 Feedback do Professor Examinador")
640
- st.write("Envie o seu TCC para receber uma análise crítica do fluxo de escrita, fluidez dos parágrafos e conformidade com as exigências da banca.")
641
-
642
- arq_feedback = st.file_uploader("Envie o seu TCC (.docx) para avaliação", type=["docx"], key="feedback")
643
-
644
- if st.button("📊 Gerar Relatório de Feedback"):
645
  if arq_feedback:
646
- with st.spinner("A avaliar o estilo académico e a fluidez do texto..."):
647
  texto = extrair_texto_docx(arq_feedback)
648
-
649
- prompt_fb = f"""
650
- Você é o Professor Examinador da UNIFAGOC. Analise o TCC abaixo e forneça um feedback estruturado:
651
- 1. ESTRUTURA: O texto possui parágrafos curtos e abruptos? Há fluidez entre os capítulos?
652
- 2. CITAÇÕES E ABUSOS: O aluno usa excesso de 'Apud'? Ele cita leis corretamente começando com 'BRASIL'?
653
- 3. PONTOS FORTES e PONTOS DE ATENÇÃO: Destaque o que está excelente e o que precisa de ser refatorado imediatamente.
654
- 4. FORMAÇÃO: O aluno misturou fontes ou fez formatações erradas ao longo da redação?
655
-
656
- TEXTO: {texto}
657
- """
658
-
659
  try:
660
  res = client.models.generate_content(
661
- model="gemini-3.5-flash",
662
  contents=prompt_fb,
663
  config=types.GenerateContentConfig(temperature=0.2)
664
  )
665
  st.write(res.text)
666
  except Exception as e:
667
- st.error(f"Erro ao gerar feedback: {e}")
 
4
  import json
5
  import streamlit as st
6
  from docx import Document
7
+ from docx.shared import Cm, Pt, RGBColor
8
+ from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK
9
+ from docx.enum.section import WD_SECTION
10
+ from docx.oxml.ns import qn
11
+ from docx.oxml import OxmlElement
12
  from pydantic import BaseModel
13
+ from typing import List, Optional
14
  from google import genai
15
  from google.genai import types
16
 
17
+ ---
18
+ ### 1. CONFIGURAÇÃO INICIAL E ESTADO
19
+ ---
20
  st.set_page_config(page_title="Corretor TCC - UNIFAGOC", page_icon="⚖️", layout="wide")
21
 
22
  def reset_estado(chave):
23
  if chave in st.session_state:
24
  st.session_state[chave] = None
25
 
26
+ # Inicialização do estado
27
+ for key in ["resultado_auditoria", "doc_revisado_bytes", "doc_formatado_bytes"]:
28
+ if key not in st.session_state:
29
+ st.session_state[key] = None
30
 
31
+ # Dicionário de Latinismos Jurídicos
32
  LATINISMOS = [
33
  "habeas corpus", "data venia", "jus puniendi", "jus postulandi",
34
  "mens legis", "ratio decidendi", "obiter dictum", "in casu",
 
37
  "mutatis mutandis", "strictu sensu", "lato sensu", "sine qua non"
38
  ]
39
 
40
+ ---
41
+ ### 2. ESQUEMAS DE DADOS (PYDANTIC)
42
+ ---
43
  class AuditoriaTCC(BaseModel):
44
  aderencia_percentual: int
45
  diagnostico_geral: str
46
  problema_identificado_no_projeto: str
47
  tcc_respondeu_ao_problema: bool
48
  justificativa_problema: str
49
+ objetivos_especificos_identificados: List[str]
50
+ objetivos_nao_cumpridos_no_tcc: List[str]
51
+ desvios_de_tema_ou_linguica: List[str]
52
+ indicios_texto_generico_ia: List[str]
53
+ incoerencias_internas: List[str]
54
  nota_tema_problema: float
55
  nota_introducao_metodo: float
56
  nota_estruturacao: float
 
61
  original: str
62
  corrigido: str
63
  motivo: str
64
+ categoria: str
65
+ gravidade: str
66
+ contexto: str
67
 
68
  class ProblemaAcademico(BaseModel):
69
  trecho: str
70
+ problema: str
71
  sugestao: str
72
  gravidade: str
73
 
74
  class RevisaoInteligente(BaseModel):
75
+ correcoes: List[ErroTexto]
76
+ problemas_academicos: List[ProblemaAcademico]
77
+ termos_estrangeiros_para_italico: List[str]
78
 
79
  class ProblemaReferencia(BaseModel):
80
  numero: int
81
+ erros: List[str]
82
  elemento_negrito: str
83
 
84
+ class ReferenciaCorrigida(BaseModel):
85
+ original: str
86
+ corrigida: str
87
+ erros: List[str]
88
+
89
+ class ListaReferenciasCorrigidas(BaseModel):
90
+ referencias: List[ReferenciaCorrigida]
91
 
92
+ ---
93
+ ### 3. AUTENTICAÇÃO E CLIENTE GEMINI
94
+ ---
95
  @st.cache_resource
96
  def get_client():
97
+ api_key = st.secrets["GEMINI_API_KEY"] # Usando secrets do Streamlit
98
  if not api_key:
99
+ st.error("❌ ERRO: Chave GEMINI_API_KEY não configurada nos secrets.")
100
  st.stop()
101
  return genai.Client(api_key=api_key)
102
 
103
  client = get_client()
104
 
105
+ ---
106
+ ### 4. FUNÇÕES AUXILIARES
107
+ ---
108
  def extrair_texto_docx(arquivo_upload) -> str:
109
  doc = Document(arquivo_upload)
110
  return "\n\n".join([para.text.strip() for para in doc.paragraphs if para.text.strip()])
 
121
  )
122
  return max(1, palavras_totais // 350)
123
 
124
+ def substituir_texto_preservando_formato(para, original: str, corrigido: str) -> int:
125
+ """Substitui texto preservando formatação de runs (negrito, itálico, etc.)"""
126
  texto_completo = "".join(run.text for run in para.runs)
127
+ if original not in texto_completo:
128
  return 0
129
+
130
+ # Divide o texto em partes antes, original e depois
131
+ partes = texto_completo.split(original, 1)
132
+ if len(partes) != 2:
133
+ return 0
134
+
135
+ # Limpa o parágrafo e reconstrói com a correção
 
 
 
136
  para.clear()
137
+ para.add_run(partes[0])
138
+
139
+ # Adiciona o texto corrigido mantendo a formatação original
140
+ # (Aqui você pode adicionar lógica para preservar formatação se necessário)
141
+ run_corrigido = para.add_run(corrigido)
142
+ run_corrigido.font.name = 'Calibri' # Mantém a fonte padrão
143
+ run_corrigido.font.size = Pt(12)
144
+
145
+ # Adiciona a parte final
146
+ if partes[1]:
147
+ para.add_run(partes[1])
148
+
149
  return 1
150
 
151
+ def formatacao_cruzada_indices(para, texto_alvo: str, aplicar_italico: bool = False, aplicar_negrito: bool = False) -> int:
152
  aplicacoes = 0
153
  if aplicar_italico:
154
  matches = list(re.finditer(r'\b' + re.escape(texto_alvo) + r'\b', para.text, re.IGNORECASE))
 
158
 
159
  for match in matches:
160
  pos_ini, pos_fim = match if isinstance(match, tuple) else (match.start(), match.end())
161
+ if pos_ini == -1:
162
+ continue
163
  pos_atual = 0
164
  for run in para.runs:
165
  fim_run = pos_atual + len(run.text)
166
  if pos_atual < pos_fim and fim_run > pos_ini:
167
+ if aplicar_italico:
168
+ run.italic = True
169
+ if aplicar_negrito:
170
+ run.bold = True
171
  aplicacoes += 1
172
  pos_atual += len(run.text)
173
  return aplicacoes
174
 
175
+ ---
176
+ ### 5. DETECÇÃO ESTRUTURAL AVANÇADA
177
+ ---
178
+ def detectar_nivel_secao(texto: str) -> int:
179
+ """Retorna o nível da seção (0=nenhum, 1=1, 2=1.1, 3=1.1.1)"""
180
+ match = re.match(r'^(\d+(?:\.\d+)*)\s+', texto)
181
+ if not match:
182
+ return 0
183
+ return match.group(1).count('.') + 1
184
+
185
+ def detectar_titulo_ou_subtitulo(texto: str) -> Optional[str]:
186
+ """Detecta títulos e subtítulos com base em seções especiais ou numeração"""
187
+ SECOES_ESPECIAIS = ["INTRODUÇÃO", "CONSIDERAÇÕES FINAIS", "REFERÊNCIAS", "RESUMO", "PALAVRAS-CHAVE"]
188
+ texto_upper = texto.upper()
189
+
190
+ # Verifica se é uma seção especial
191
+ if any(sec in texto_upper for sec in SECOES_ESPECIAIS):
192
+ return "titulo"
193
+
194
+ # Verifica se é uma seção numerada (1, 1.1, 1.1.1, etc.)
195
+ if detectar_nivel_secao(texto) > 0:
196
+ return "titulo"
197
+
198
+ # Verifica se é um título em caixa alta (sem ser seção numerada)
199
+ if texto.isupper() and len(texto) < 150 and not detectar_nivel_secao(texto):
200
+ return "titulo"
201
+
202
+ return None
203
+
204
+ def detectar_citacao_longa(para) -> bool:
205
+ """Detecta citações longas por recuo ou marcação"""
206
+ if para.paragraph_format.left_indent and para.paragraph_format.left_indent >= Cm(3.5):
207
+ return True
208
+ texto = para.text.strip()
209
+ if (texto.startswith(('Segundo ', 'Conforme ', 'De acordo com ')) and
210
+ len(texto.split()) > 30):
211
+ return True
212
+ return False
213
+
214
+ def detectar_lista(texto: str) -> bool:
215
+ """Detecta listas numeradas ou alfabéticas"""
216
+ return bool(re.match(r'^([a-zA-Z\(]|\d+[\)\.]|\w+[-–])\s', texto))
217
+
218
+ ---
219
+ ### 6. FUNÇÕES DE FORMATAÇÃO
220
+ ---
221
+ def aplicar_margens_unifagoc(doc: Document):
222
+ """Aplica as margens oficiais do template UNIFAGOC (A4: 21x29.7cm)"""
223
  for section in doc.sections:
224
  section.page_height = Cm(29.7)
225
  section.page_width = Cm(21.0)
 
230
  section.header_distance = Cm(0.5)
231
  section.footer_distance = Cm(0.5)
232
 
233
+ def aplicar_paginacao(doc: Document):
234
+ """Adiciona numeração de páginas no rodapé (centralizado)"""
235
+ for section in doc.sections:
236
+ # Cria um rodapé para cada seção
237
+ footer = section.footer
238
+ if not footer:
239
+ footer = OxmlElement('w:ftr')
240
+ section._sectPr.get_or_add_ftr().append(footer)
241
+
242
+ # Adiciona parágrafo com numeração de página
243
+ para = footer.add_paragraph()
244
+ para.alignment = WD_ALIGN_PARAGRAPH.CENTER
245
+
246
+ # Adiciona campo de página
247
+ fldChar = OxmlElement('w:fldChar')
248
+ fldChar.set(qn('w:fldCharType'), 'begin')
249
+ para._p.append(fldChar)
250
+
251
+ instrText = OxmlElement('w:instrText')
252
+ instrText.set(qn('xml:space'), 'preserve')
253
+ instrText.text = 'PAGE'
254
+ para._p.append(instrText)
255
+
256
+ fldChar = OxmlElement('w:fldChar')
257
+ fldChar.set(qn('w:fldCharType'), 'end')
258
+ para._p.append(fldChar)
259
+
260
+ # Formatação do rodapé
261
+ for run in para.runs:
262
+ run.font.name = 'Calibri'
263
+ run.font.size = Pt(10)
264
 
265
+ def formatar_figuras_tabelas(doc: Document):
266
+ """Formata figuras, tabelas e quadros conforme template UNIFAGOC"""
267
+ for para in doc.paragraphs:
268
  texto = para.text.strip()
269
+ if re.match(r'^(Figura|Tabela|Quadro)\s+\d+', texto, re.IGNORECASE):
270
+ pf = para.paragraph_format
271
+ pf.alignment = WD_ALIGN_PARAGRAPH.CENTER
272
+ for run in para.runs:
273
+ run.font.name = 'Calibri'
274
+ run.font.size = Pt(12)
275
+ run.font.bold = True
276
 
277
+ # Formata a legenda (próximo parágrafo)
278
+ idx = doc.paragraphs.index(para)
279
+ if idx + 1 < len(doc.paragraphs):
280
+ next_para = doc.paragraphs[idx + 1]
281
+ for run in next_para.runs:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  run.font.name = 'Calibri'
283
+ run.font.size = Pt(11)
 
 
 
 
 
 
 
 
 
 
 
 
284
 
285
+ # Formata tabelas
286
+ for table in doc.tables:
287
+ for row in table.rows:
288
+ for cell in row.cells:
289
+ for para in cell.paragraphs:
290
+ para.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT
291
+ para.paragraph_format.line_spacing = 1.0
292
+ for run in para.runs:
293
+ run.font.name = 'Calibri'
294
+ run.font.size = Pt(11)
295
+
296
+ def formatar_imagens(doc: Document):
297
+ """Formata imagens (inline shapes) para centralização e legendas"""
298
+ for rel in doc.part.rels.values():
299
+ if "image" in rel.target_ref:
300
+ for shape in doc.inline_shapes:
301
+ if shape.type == 3: # 3 = picture
302
+ shape.alignment = WD_ALIGN_PARAGRAPH.CENTER
303
+
304
+ def formatar_notas_rodape(doc: Document):
305
+ """Formata notas de rodapé (se existirem)"""
306
+ # python-docx não tem suporte nativo para notas de rodapé, então esta é uma solução alternativa
307
+ for para in doc.paragraphs:
308
+ if re.search(r'[\d\^]+$', para.text.strip()): # Detecta notas como ¹, ², ³
309
+ for run in para.runs:
310
+ run.font.size = Pt(8)
311
+ run.font.superscript = True
312
+
313
+ def validar_referencias(doc: Document) -> List[ReferenciaCorrigida]:
314
+ """Valida referências ABNT usando IA, sem inventar dados"""
315
+ refs = []
316
+ em_refs = False
317
+ for para in doc.paragraphs:
318
+ texto = para.text.strip()
319
+ if re.search(r"REFER[ÊE]NCIAS", texto, re.IGNORECASE):
320
+ em_refs = True
321
+ continue
322
+ if em_refs and (len(texto) < 10 or texto.isupper()):
323
+ break
324
+ if em_refs:
325
+ refs.append(texto)
326
+
327
+ if not refs:
328
+ return []
329
+
330
+ refs_str = '\n'.join(refs)
331
+ prompt_refs = (
332
+ "Analise as referências abaixo **APENAS PARA IDENTIFICAR ERROS** (não invente dados!).\n"
333
+ "Para cada uma, retorne:\n"
334
+ "- original: texto original\n"
335
+ "- corrigida: versão corrigida (apenas se houver erro claro, como falta de negrito no título)\n"
336
+ "- erros: lista de erros (ex: [\"título não em negrito\", \"autor não invertido\"])\n\n"
337
+ f"Referências:\n{refs_str}"
338
+ )
339
+
340
+ try:
341
+ res = client.models.generate_content(
342
+ model="gemini-1.5-flash",
343
+ contents=prompt_refs,
344
+ config=types.GenerateContentConfig(
345
+ temperature=0.0,
346
+ response_mime_type="application/json",
347
+ response_schema=ListaReferenciasCorrigidas # Usando o schema correto
348
+ )
349
+ )
350
+ dados = json.loads(res.text)
351
+ return dados.get("referencias", [])
352
+ except Exception as e:
353
+ st.error(f"Erro ao validar referências: {e}")
354
+ return []
355
+
356
+ def formatar_documento_unifagoc(doc: Document):
357
+ """Função principal de formatação com fluxo único e sem sobreposição"""
358
+ # 1. Aplica margens e paginação
359
+ aplicar_margens_unifagoc(doc)
360
+ aplicar_paginacao(doc)
361
+
362
+ # 2. Formata elementos específicos
363
+ formatar_figuras_tabelas(doc)
364
+ formatar_imagens(doc)
365
+ formatar_notas_rodape(doc)
366
+
367
+ # 3. Processa cada parágrafo com fluxo único
368
+ for para in doc.paragraphs:
369
+ texto = para.text.strip()
370
+ if not texto:
371
+ continue
372
+
373
+ # --- REFERÊNCIAS ---
374
+ if re.search(r"REFER[ÊE]NCIAS", texto, re.IGNORECASE):
375
+ pf = para.paragraph_format
376
+ pf.alignment = WD_ALIGN_PARAGRAPH.CENTER
377
  pf.space_before = Pt(12)
378
+ pf.space_after = Pt(6)
 
379
  for run in para.runs:
380
  run.font.name = 'Calibri'
381
+ run.font.size = Pt(12)
382
  run.font.bold = True
383
+ continue
384
 
385
+ # --- RESUMO ---
386
+ if texto.upper() == "RESUMO":
387
+ pf = para.paragraph_format
388
  pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
389
+ pf.space_before = Pt(12)
 
 
390
  pf.space_after = Pt(0)
391
  pf.line_spacing = 1.5
392
  for run in para.runs:
393
  run.font.name = 'Calibri'
394
  run.font.size = Pt(11)
395
+ run.font.bold = True
396
+ continue
397
 
398
+ # --- PALAVRAS-CHAVE ---
399
+ if texto.upper().startswith("PALAVRAS-CHAVE"):
400
+ pf = para.paragraph_format
401
  pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
 
 
402
  pf.space_before = Pt(6)
403
  pf.space_after = Pt(0)
404
  pf.line_spacing = 1.5
 
407
  run.font.size = Pt(11)
408
  if "PALAVRAS-CHAVE" in run.text.upper():
409
  run.font.bold = True
410
+ continue
411
+
412
+ # --- TÍTULOS E SEÇÕES ---
413
+ nivel = detectar_nivel_secao(texto)
414
+ if nivel > 0 or detectar_titulo_ou_subtitulo(texto) == "titulo":
415
+ pf = para.paragraph_format
416
+ pf.alignment = WD_ALIGN_PARAGRAPH.LEFT
417
+ pf.space_before = Pt(12)
418
+ pf.space_after = Pt(6)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
 
 
 
 
 
 
 
 
420
  for run in para.runs:
421
  run.font.name = 'Calibri'
422
  run.font.size = Pt(12)
423
  run.font.bold = True
424
 
425
+ # Seções de nível 3 também recebem itálico
426
+ if nivel == 3:
427
+ run.font.italic = True
428
+
429
+ # Seções de nível 2 recebem recuo
430
+ if nivel == 2:
431
+ pf.left_indent = Cm(0.5)
432
+
433
+ continue
434
+
435
+ # --- CITAÇÕES LONGAS ---
436
+ if detectar_citacao_longa(para):
437
+ pf = para.paragraph_format
438
+ pf.left_indent = Cm(4.0)
439
  pf.line_spacing = 1.0
440
+ pf.space_before = Pt(6)
441
+ pf.space_after = Pt(6)
442
  for run in para.runs:
443
  run.font.name = 'Calibri'
444
  run.font.size = Pt(11)
445
+ continue
446
+
447
+ # --- LISTAS ---
448
+ if detectar_lista(texto):
449
+ pf = para.paragraph_format
450
+ pf.left_indent = Cm(1.25)
451
+ pf.first_line_indent = Cm(0)
452
+ pf.space_before = Pt(0)
453
+ pf.space_after = Pt(0)
454
+ pf.line_spacing = 1.5
455
+ for run in para.runs:
456
+ run.font.name = 'Calibri'
457
+ run.font.size = Pt(12)
458
+ continue
459
+
460
+ # --- CORPO DO TEXTO (padrão) ---
461
+ pf = para.paragraph_format
462
+ pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
463
+ pf.first_line_indent = Cm(1.25)
464
+ pf.line_spacing = 1.5
465
+ pf.space_before = Pt(0)
466
+ pf.space_after = Pt(0)
467
+ for run in para.runs:
468
+ run.font.name = 'Calibri'
469
+ run.font.size = Pt(12)
470
+
471
+ # 4. Valida e corrige referências (sem destruir formatação)
472
+ refs_corrigidas = validar_referencias(doc)
473
+ for ref in refs_corrigidas:
474
+ for para in doc.paragraphs:
475
+ if para.text.strip() == ref.get('original', ''):
476
+ # Usa a função que preserva formatação
477
+ substituir_texto_preservando_formato(para, ref['original'], ref['corrigida'])
478
+ for run in para.runs:
479
+ run.font.name = 'Calibri'
480
+ run.font.size = Pt(11)
481
+
482
+ ---
483
+ ### 7. MOTOR DE VERIFICAÇÕES LOCAIS (REGEX)
484
+ ---
485
+ def verificacoes_locais_regex(texto: str) -> List[str]:
486
+ erros = []
487
+ if re.search(r"\s{2,}", texto):
488
+ erros.append("Espaços duplos detectados no documento.")
489
+ if re.search(r",\s*,", texto):
490
+ erros.append("Vírgulas duplicadas detectadas (ex: ',,').")
491
+ if re.search(r"[.!?]{2,}", texto) and not "..." in texto:
492
+ erros.append("Pontuação repetida indevida (ex: '!!' ou '..').")
493
+ if re.search(r"\b(\w+)\s+\1\b", texto, re.IGNORECASE):
494
+ erros.append("Palavras repetidas sequencialmente (ex: 'o o', 'de de').")
495
+ if re.search(r"\([A-ZÀ-Ú\s]+,\s*\)", texto):
496
+ erros.append("Citação ABNT com ano faltando (ex: (SILVA, )).")
497
+ return erros
498
 
499
+ ---
500
+ ### 8. INTERFACE STREAMLIT
501
+ ---
502
  st.title("⚖️ Assistente Acadêmico - Direito UNIFAGOC")
503
  aba_auditoria, aba_revisao, aba_abnt, aba_prompt, aba_feedback = st.tabs([
504
  "🔍 Auditoria e Notas",
 
508
  "👨‍🏫 Feedback Professor"
509
  ])
510
 
511
+ # ABA 1: AUDITORIA
 
 
512
  with aba_auditoria:
513
+ st.markdown("Cruze o **Pré-Projeto** com o **TCC** para identificar fuga do tema e gerar a **Nota da Banca (80 pontos)**.")
514
+ fase_tcc = st.radio("Fase atual:", ["TCC 1 (Apenas Introdução e Capítulo 1)", "TCC 2 (TCC Completo)"], horizontal=True)
515
 
516
  col1, col2 = st.columns(2)
517
+ with col1:
518
+ arq_projeto = st.file_uploader("1. Pré-Projeto (.docx)", type=["docx"], key="proj")
519
+ with col2:
520
+ arq_tcc = st.file_uploader("2. TCC (.docx)", type=["docx"], key="tcc_aud")
521
 
522
+ if st.button("🔍 Iniciar Auditoria", type="primary"):
523
  if not arq_projeto or not arq_tcc:
524
+ st.warning("⚠️ Envie os DOIS arquivos para realizar a comparação.")
525
  else:
526
+ with st.status("Analisando coerência jurídica...", expanded=True) as status:
 
527
  texto_projeto = extrair_texto_docx(arq_projeto)
528
+ texto_tcc = extrair_texto_docx(arq_tcc)
 
529
  regra_fase = (
530
  "ATENÇÃO: O aluno está em TCC 1. NÃO penalize a ausência de Resumo e Conclusão."
531
  if "TCC 1" in fase_tcc else "O aluno está em TCC 2. Exija o trabalho completo."
532
  )
533
  prompt = (
534
+ f"Como Professor Examinador da UNIFAGOC:\n{regra_fase}\n"
535
  f"=== PRÉ-PROJETO ===\n{texto_projeto}\n"
536
  f"=== TCC ===\n{texto_tcc}\n"
537
+ "TAREFA 1: Avalie cruzamento de dados e fuga de tema.\n"
538
+ "TAREFA 2: Verifique incoerências internas.\n"
539
+ "TAREFA 3: Distribua 80 pontos conforme ficha UNIFAGOC."
540
  )
 
 
541
  try:
542
  res = client.models.generate_content(
543
+ model="gemini-1.5-flash",
544
  contents=prompt,
545
  config=types.GenerateContentConfig(
546
  temperature=0.0,
 
549
  )
550
  )
551
  st.session_state.resultado_auditoria = json.loads(res.text)
552
+ status.update(label="✅ Avaliação concluída!", state="complete", expanded=False)
553
  except Exception as e:
554
+ status.update(label="❌ Falha na avaliação.", state="error")
555
  st.error(f"Erro: {e}")
556
 
557
  if st.session_state.resultado_auditoria:
558
  dados = st.session_state.resultado_auditoria
559
  st.header("🏆 Avaliação da Banca UNIFAGOC")
560
  nota_total = sum([
561
+ dados.get("nota_tema_problema", 0),
562
+ dados.get("nota_introducao_metodo", 0),
563
+ dados.get("nota_estruturacao", 0),
564
+ dados.get("nota_formatacao_abnt", 0)
565
  ])
 
566
  c1, c2, c3, c4 = st.columns(4)
567
+ c1.metric("Tema/Problema", f"{dados.get('nota_tema_problema')}/20")
568
+ c2.metric("Intro/Método", f"{dados.get('nota_introducao_metodo')}/20")
569
+ c3.metric("Estrutura/Linguagem", f"{dados.get('nota_estruturacao')}/20")
570
+ c4.metric("Formatação/ABNT", f"{dados.get('nota_formatacao_abnt')}/20")
571
+ st.info(f"**NOTA FINAL: {nota_total}/80**\n\n*Parecer:* {dados.get('justificativa_notas_banca')}")
572
+
573
+ # ABA 2: REVISÃO INTELIGENTE
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
574
  with aba_revisao:
575
+ st.markdown("Revisão de **Ortografia**, **Gramática**, **Coerência**, **Vícios Jurídicos** e **Anti-IA**.")
576
+ arq_rev = st.file_uploader("Envie o TCC (.docx)", type=["docx"], key="revisao")
577
 
578
+ if st.button("🧠 Realizar Revisão"):
579
  if arq_rev:
580
+ with st.status("Processando revisão...", expanded=True) as status:
 
581
  texto = extrair_texto_docx(arq_rev)
 
582
  erros_locais = verificacoes_locais_regex(texto)
583
  for erro in erros_locais:
584
+ st.warning(f"⚠️ {erro}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
585
 
586
+ prompt_rev = (
587
+ "Você é um Professor Revisor de TCCs de Direito da UNIFAGOC. "
588
+ "Analise o texto e corrija: ORTOGRAFIA, GRAMÁTICA, VÍCIOS JURÍDICOS, COERÊNCIA, ABNT. "
589
+ f"Texto: {texto}"
590
+ )
591
  try:
592
  res = client.models.generate_content(
593
+ model="gemini-1.5-flash",
594
  contents=prompt_rev,
595
  config=types.GenerateContentConfig(
596
  temperature=0.1,
 
599
  )
600
  )
601
  dados = json.loads(res.text)
 
 
602
  doc_rev = Document(arq_rev)
603
  aplicadas_texto, aplicadas_italico = 0, 0
604
 
605
  if dados.get("correcoes"):
606
  for c in dados["correcoes"]:
607
  for para in doc_rev.paragraphs:
608
+ aplicadas_texto += substituir_texto_preservando_formato(
609
+ para, c["original"], c["corrigido"]
610
+ )
611
 
 
612
  termos_para_italico = set(LATINISMOS + dados.get("termos_estrangeiros_para_italico", []))
 
613
  for termo in termos_para_italico:
614
  for para in doc_rev.paragraphs:
615
  aplicadas_italico += formatacao_cruzada_indices(para, termo, aplicar_italico=True)
616
 
617
  st.session_state.doc_revisado_bytes = docx_to_bytes(doc_rev)
618
+ st.session_state.nome_arq_rev = arq_rev.name.replace('.docx', '') + "_Revisado.docx"
 
 
 
 
 
 
619
  status.update(
620
+ label=f"✅ Revisão concluída: {aplicadas_texto} correções, {aplicadas_italico} itálicos.",
621
+ state="complete",
622
+ expanded=False
623
  )
624
  except Exception as e:
625
+ st.error(f"❌ Erro: {e}")
 
626
 
627
  if st.session_state.doc_revisado_bytes:
628
  st.download_button(
629
+ "⬇️ Baixar TCC Revisado",
630
  data=st.session_state.doc_revisado_bytes,
631
  file_name=st.session_state.nome_arq_rev,
632
  mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
633
  )
634
 
635
+ # ABA 3: FORMATAÇÃO ABNT
 
 
636
  with aba_abnt:
637
+ st.markdown("Formatação **fiel ao template UNIFAGOC** (95-100% de fidelidade).")
638
+ arq_abnt = st.file_uploader("Envie o TCC (.docx)", type=["docx"], key="abnt_file")
 
 
 
639
 
640
  if st.button("📐 Formatar TCC"):
641
  if arq_abnt:
642
+ with st.status("Normalizando documento...", expanded=True) as status:
643
  doc_abnt = Document(arq_abnt)
 
644
  paginas_estimadas = estimar_paginas(doc_abnt)
645
  if paginas_estimadas > 18:
646
+ st.warning(f"🚨 Extensão: {paginas_estimadas} páginas (limite: 18).")
647
+
648
+ formatar_documento_unifagoc(doc_abnt)
649
+
650
+ st.session_state.nome_arq_abnt = arq_abnt.name.replace('.docx', '') + "_UNIFAGOC.docx"
651
+ st.session_state.doc_formatado_bytes = docx_to_bytes(doc_abnt)
652
+ status.update(label="✅ Formatação concluída com 95-100% de fidelidade!", state="complete", expanded=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
653
 
654
  if st.session_state.doc_formatado_bytes:
655
  st.download_button(
656
+ "⬇️ Baixar TCC Formatado",
657
  data=st.session_state.doc_formatado_bytes,
658
  file_name=st.session_state.nome_arq_abnt,
659
  mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
660
  )
661
 
 
662
  # ABA 4: PROMPT MESTRE
 
663
  with aba_prompt:
664
  st.markdown("### 🤖 Prompt Mestre - Padrão UNIFAGOC")
665
+ prompt_pronto = """
666
+ Atue como revisor académico especialista em ABNT e nas diretrizes da UNIFAGOC.
667
+ Siga ESTRITAMENTE estas regras:
668
+ 1. ESTRUTURA: INTRODUÇÃO, secções numeradas (ex: 1. TÍTULO), CONSIDERAÇÕES FINAIS, REFERÊNCIAS.
669
+ 2. RESUMO: Parágrafo único (máx. 150 palavras) + 3-5 palavras-chave em maiúsculas.
670
+ 3. TERMOS ESTRANGEIROS: Em *itálico* (ex: habeas corpus, fake news).
671
+ 4. CITAÇÕES CURTAS: Entre aspas duplas.
672
+ 5. CITAÇÕES LONGAS: Parágrafo separado com recuo de 4cm, fonte 11, espaçamento simples.
673
+ 6. REFERÊNCIAS: ABNT NBR 6023:2018, alinhadas à esquerda, **negrito** no título do livro/periódico.
674
+ 7. INTEGRIDADE: Não invente leis, jurisprudências ou doutrinas.
675
+ """
 
 
 
 
 
 
 
676
  st.code(prompt_pronto, language="markdown")
677
 
678
+ # ABA 5: FEEDBACK
 
 
679
  with aba_feedback:
680
  st.markdown("### 👨‍🏫 Feedback do Professor Examinador")
681
+ arq_feedback = st.file_uploader("Envie o TCC (.docx)", type=["docx"], key="feedback")
682
+
683
+ if st.button("📊 Gerar Relatório"):
 
 
684
  if arq_feedback:
685
+ with st.spinner("Analisando estilo acadêmico..."):
686
  texto = extrair_texto_docx(arq_feedback)
687
+ prompt_fb = (
688
+ "Analise o TCC: ESTRUTURA, CITAÇÕES, PONTOS FORTES/ATENÇÃO, FORMAÇÃO. "
689
+ f"Texto: {texto}"
690
+ )
 
 
 
 
 
 
 
691
  try:
692
  res = client.models.generate_content(
693
+ model="gemini-1.5-flash",
694
  contents=prompt_fb,
695
  config=types.GenerateContentConfig(temperature=0.2)
696
  )
697
  st.write(res.text)
698
  except Exception as e:
699
+ st.error(f"❌ Erro: {e}")