GabrielGD commited on
Commit
326795c
·
verified ·
1 Parent(s): c461d1c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +142 -322
app.py CHANGED
@@ -13,6 +13,11 @@ from google.genai import types
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
  if "resultado_auditoria" not in st.session_state: st.session_state.resultado_auditoria = None
17
  if "doc_revisado_bytes" not in st.session_state: st.session_state.doc_revisado_bytes = None
18
  if "doc_formatado_bytes" not in st.session_state: st.session_state.doc_formatado_bytes = None
@@ -56,13 +61,13 @@ class AnaliseABNT(BaseModel):
56
  def get_client():
57
  api_key = os.environ.get("GEMINI_API_KEY")
58
  if not api_key:
59
- st.error("❌ ERRO: A variável de ambiente GEMINI_API_KEY não foi encontrada nos Secrets do Hugging Face.")
60
  st.stop()
61
  return genai.Client(api_key=api_key)
62
 
63
  client = get_client()
64
 
65
- # ─── 4. MANIPULAÇÃO DE DOCUMENTOS (DOCX) ──────────────────────────────────
66
  def extrair_texto_docx(arquivo_upload) -> str:
67
  doc = Document(arquivo_upload)
68
  return "\n\n".join([para.text.strip() for para in doc.paragraphs if para.text.strip()])
@@ -79,239 +84,124 @@ def estimar_paginas(doc: Document) -> int:
79
  )
80
  return max(1, palavras_totais // 350)
81
 
82
- def _set_run_font(run, nome: str, tamanho_pt: float,
83
- negrito: bool = None, italico: bool = None):
84
- """Aplica fonte, tamanho e atributos opcionais a um run."""
85
  run.font.name = nome
86
  run.font.size = Pt(tamanho_pt)
87
- if negrito is not None:
88
- run.font.bold = negrito
89
- if italico is not None:
90
- run.font.italic = italico
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
  def aplicar_formatacao_unifagoc_abnt(doc: Document):
94
- """
95
- Aplica a formatação exata do template UNIFAGOC / ABNT 6023:2018:
96
-
97
- PÁGINA
98
- • A4 (21 × 29,7 cm)
99
- • Margens: superior 3 cm | esquerda 3 cm | inferior 2 cm | direita 2 cm
100
- • Cabeçalho / rodapé: 0,5 cm das bordas
101
-
102
- CORPO DO TEXTO
103
- • Fonte: Calibri 12 pt
104
- • Alinhamento: justificado
105
- • Recuo de primeira linha: 1,25 cm
106
- • Espaçamento entre linhas: 1,5
107
- • Espaço antes/depois: 0 pt
108
-
109
- TÍTULOS DAS SEÇÕES PRIMÁRIAS (caixa-alta, estilo Heading 2)
110
- • Calibri 12 pt, negrito, centralizado
111
- • Espaço antes: 0 pt | depois: 12 pt | entre linhas: 1,5
112
-
113
- TÍTULOS SECUNDÁRIOS (estilo Heading 3)
114
- • Calibri 12 pt, negrito, justificado
115
- • Espaço antes: 12 pt | depois: 0 pt | entre linhas: 1,5
116
-
117
- TÍTULOS TERCIÁRIOS (estilo Heading 4)
118
- • Calibri 12 pt, negrito + itálico, justificado
119
- • Espaço antes: 0 pt | depois: 0 pt | entre linhas: 1,5
120
-
121
- RESUMO / ABSTRACT
122
- • Calibri 11 pt, itálico (rótulo "RESUMO" e "Palavras-chave" em negrito sem itálico)
123
- • Alinhamento: justificado | sem recuo | entre linhas: 1,5
124
-
125
- CITAÇÕES LONGAS (> 4 linhas)
126
- • Calibri 11 pt, justificado
127
- • Recuo esquerdo: 4 cm | sem recuo de primeira linha
128
- • Espaçamento simples (1,0) | 6 pt antes e depois
129
-
130
- REFERÊNCIAS (linhas individuais)
131
- • Calibri 11 pt, alinhamento esquerdo
132
- • Sem recuo | espaçamento simples (1,0) | 6 pt depois (entre entradas)
133
- • Espaço antes: 0 pt
134
- """
135
-
136
- # ── Página ───────────────────────────────────────────────────────────────
137
  for section in doc.sections:
138
- section.page_height = Cm(29.7)
139
- section.page_width = Cm(21.0)
140
- section.top_margin = Cm(3.0)
141
- section.left_margin = Cm(3.0)
142
- section.bottom_margin = Cm(2.0)
143
- section.right_margin = Cm(2.0)
144
- section.header_distance = Cm(0.5)
145
- section.footer_distance = Cm(0.5)
146
-
147
- em_referencias = False
148
- em_resumo = False
149
 
150
  for para in doc.paragraphs:
151
  texto = para.text.strip()
152
- if not texto:
153
- continue
154
 
155
- pf = para.paragraph_format
156
- style_name = (para.style.name or "").lower()
157
 
158
- # ── Detecção de seções-chave ─────────────────────────────────────────
159
  if re.match(r"^RESUMO\b", texto, re.IGNORECASE):
160
- em_resumo = True
161
- em_referencias = False
162
  elif re.match(r"^(?:INTRODU[ÇC]|ABSTRACT|1[\s\.])", texto, re.IGNORECASE):
163
  em_resumo = False
164
 
165
  is_titulo_refs = bool(re.search(r"^(?:\d+\.?\s*)?REFER[ÊE]NCIAS\b", texto, re.IGNORECASE))
166
- if is_titulo_refs:
167
- em_referencias = True
168
 
169
- # ─────────────────────────────────────────────────────────────────────
170
- # A) REFERÊNCIAS – corpo (entradas individuais)
171
- # ─────────────────────────────────────────────────────────────────────
172
  if em_referencias and not is_titulo_refs:
173
- pf.alignment = WD_ALIGN_PARAGRAPH.LEFT
174
- pf.first_line_indent = Cm(0)
175
- pf.left_indent = Cm(0)
176
- pf.space_before = Pt(0)
177
- pf.space_after = Pt(6)
178
- pf.line_spacing = 1.0 # simples
179
- for run in para.runs:
180
- _set_run_font(run, "Calibri", 11)
181
 
182
- # ─────────────────────────────────────────────────────────────────────
183
- # B) TÍTULO "REFERÊNCIAS"
184
- # ─────────────────────────────────────────────────────────────────────
185
  elif is_titulo_refs:
186
- pf.alignment = WD_ALIGN_PARAGRAPH.CENTER
187
- pf.first_line_indent = Cm(0)
188
- pf.left_indent = Cm(0)
189
- pf.space_before = Pt(12)
190
- pf.space_after = Pt(12)
191
- pf.line_spacing = 1.5
192
- for run in para.runs:
193
- _set_run_font(run, "Calibri", 12, negrito=True, italico=False)
194
 
195
- # ─────────────────────────────────────────────────────────────────────
196
- # C) RESUMO / ABSTRACT
197
- # ─────────────────────────────────────────────────────────────────────
198
  elif em_resumo:
199
- pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
200
- pf.first_line_indent = Cm(0)
201
- pf.left_indent = Cm(0)
202
- pf.space_before = Pt(0)
203
- pf.space_after = Pt(0)
204
- pf.line_spacing = 1.5
205
  for run in para.runs:
206
- is_label = bool(re.match(
207
- r"^(RESUMO|PALAVRAS.CHAVE|ABSTRACT|KEYWORDS)",
208
- run.text.strip(), re.IGNORECASE
209
- ))
210
- if is_label:
211
- _set_run_font(run, "Calibri", 11, negrito=True, italico=False)
212
- else:
213
- _set_run_font(run, "Calibri", 11, negrito=False, italico=True)
214
 
215
- # ─────────────────────────────────────────────────────────────────────
216
- # D) TÍTULOS DE SEÇÕES – detectados por estilo Word ou caixa-alta curta
217
- # ─────────────────────────────────────────────────────────────────────
218
  elif "heading" in style_name or (texto.isupper() and 3 < len(texto) < 120):
219
- # Heading 2 → seção primária (centralizado, maiúsculas)
220
- if "heading 2" in style_name or "heading2" in style_name or (
221
- texto.isupper() and "heading" not in style_name
222
- ):
223
- pf.alignment = WD_ALIGN_PARAGRAPH.CENTER
224
- pf.first_line_indent = Cm(0)
225
- pf.left_indent = Cm(0)
226
- pf.space_before = Pt(0)
227
- pf.space_after = Pt(12)
228
- pf.line_spacing = 1.5
229
- for run in para.runs:
230
- _set_run_font(run, "Calibri", 12, negrito=True, italico=False)
231
-
232
- # Heading 3 → seção secundária (justificado, negrito)
233
  elif "heading 3" in style_name or "heading3" in style_name:
234
- pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
235
- pf.first_line_indent = Cm(0)
236
- pf.left_indent = Cm(0)
237
- pf.space_before = Pt(12)
238
- pf.space_after = Pt(0)
239
- pf.line_spacing = 1.5
240
- for run in para.runs:
241
- _set_run_font(run, "Calibri", 12, negrito=True, italico=False)
242
-
243
- # Heading 4 → seção terciária (justificado, negrito + itálico)
244
  elif "heading 4" in style_name or "heading4" in style_name:
245
- pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
246
- pf.first_line_indent = Cm(0)
247
- pf.left_indent = Cm(0)
248
- pf.space_before = Pt(0)
249
- pf.space_after = Pt(0)
250
- pf.line_spacing = 1.5
251
- for run in para.runs:
252
- _set_run_font(run, "Calibri", 12, negrito=True, italico=True)
253
-
254
- # Heading 1 → título principal do artigo (centralizado, maior)
255
  else:
256
- pf.alignment = WD_ALIGN_PARAGRAPH.CENTER
257
- pf.first_line_indent = Cm(0)
258
- pf.left_indent = Cm(0)
259
- pf.space_before = Pt(6)
260
- pf.space_after = Pt(12)
261
- pf.line_spacing = 1.5
262
- for run in para.runs:
263
- _set_run_font(run, "Calibri", 14, negrito=True, italico=False)
264
-
265
- # ─────────────────────────────────────────────────────────────────────
266
- # E) CITAÇÕES LONGAS – recuo ≥ 3,5 cm (bloco com recuo existente)
267
- # ─────────────────────────────────────────────────────────────────────
268
  elif pf.left_indent is not None and pf.left_indent >= Cm(3.5):
269
- pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
270
- pf.first_line_indent = Cm(0)
271
- pf.left_indent = Cm(4.0)
272
- pf.space_before = Pt(6)
273
- pf.space_after = Pt(6)
274
- pf.line_spacing = 1.0 # simples
275
- for run in para.runs:
276
- _set_run_font(run, "Calibri", 11)
277
 
278
- # ─────────────────────────────────────────────────────────────────────
279
- # F) CORPO DO TEXTO (padrão)
280
- # ─────────────────────────────────────────────────────────────────────
281
  else:
282
- pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
283
- pf.first_line_indent = Cm(1.25)
284
- pf.left_indent = Cm(0)
285
- pf.space_before = Pt(0)
286
- pf.space_after = Pt(0)
287
- pf.line_spacing = 1.5
288
- for run in para.runs:
289
- _set_run_font(run, "Calibri", 12)
290
-
291
-
292
- def formatacao_cruzada_indices(para, texto_alvo, aplicar_italico=False, aplicar_negrito=False) -> int:
293
- aplicacoes = 0
294
- if aplicar_italico:
295
- matches = list(re.finditer(r'\b' + re.escape(texto_alvo) + r'\b', para.text, re.IGNORECASE))
296
- else:
297
- idx = para.text.find(texto_alvo)
298
- matches = [(idx, idx + len(texto_alvo))] if idx != -1 else []
299
-
300
- for match in matches:
301
- pos_ini, pos_fim = match if isinstance(match, tuple) else (match.start(), match.end())
302
- if pos_ini == -1:
303
- continue
304
- pos_atual = 0
305
- for run in para.runs:
306
- fim_run = pos_atual + len(run.text)
307
- if pos_atual < pos_fim and fim_run > pos_ini:
308
- if aplicar_italico:
309
- run.italic = True
310
- if aplicar_negrito:
311
- run.bold = True
312
- aplicacoes += 1
313
- pos_atual += len(run.text)
314
- return aplicacoes
315
 
316
  # ─── 5. INTERFACE PRINCIPAL (ABAS) ────────────────────────────────────────
317
  st.title("⚖️ Assistente Acadêmico - Direito UNIFAGOC")
@@ -326,15 +216,11 @@ aba_auditoria, aba_revisao, aba_abnt = st.tabs([
326
  # ==============================================================================
327
  with aba_auditoria:
328
  st.markdown("Cruze o **Pré-Projeto** com o **TCC** para identificar fuga do tema e gerar a **Nota da Banca (80 pontos)**.")
329
- fase_tcc = st.radio(
330
- "Fase atual:",
331
- ["TCC 1 (Apenas Introdução e Capítulo 1)", "TCC 2 (TCC Completo)"],
332
- horizontal=True
333
- )
334
 
335
  col1, col2 = st.columns(2)
336
- with col1: arq_projeto = st.file_uploader("1. Pré-Projeto (.docx)", type=["docx"], key="proj")
337
- with col2: arq_tcc = st.file_uploader("2. TCC (.docx)", type=["docx"], key="tcc_aud")
338
 
339
  if st.button("🔍 Iniciar Auditoria e Avaliação", type="primary"):
340
  if not arq_projeto or not arq_tcc:
@@ -345,15 +231,10 @@ with aba_auditoria:
345
  texto_projeto = extrair_texto_docx(arq_projeto)
346
  texto_tcc = extrair_texto_docx(arq_tcc)
347
 
348
- regra_fase = (
349
- "ATENÇÃO: O aluno está em TCC 1. NÃO penalize a ausência de Resumo e Conclusão."
350
- if "TCC 1" in fase_tcc
351
- else "O aluno está em TCC 2. Exija o trabalho completo."
352
- )
353
  prompt = (
354
  f"Como Professor Examinador titular da banca de Direito da UNIFAGOC:\n{regra_fase}\n"
355
- f"=== PRÉ-PROJETO ===\n{texto_projeto}\n"
356
- f"=== TCC ===\n{texto_tcc}\n"
357
  "TAREFA 1: Avalie o cruzamento de dados, fuga de tema e objetivos.\n"
358
  "TAREFA 2: Distribua 80 pontos rigorosamente de acordo com os critérios da ficha UNIFAGOC."
359
  )
@@ -361,13 +242,8 @@ with aba_auditoria:
361
  st.write("A submeter dados à IA do Google...")
362
  try:
363
  res = client.models.generate_content(
364
- model="gemini-3.5-flash",
365
- contents=prompt,
366
- config=types.GenerateContentConfig(
367
- temperature=0.0,
368
- response_mime_type="application/json",
369
- response_schema=AuditoriaTCC
370
- )
371
  )
372
  st.session_state.resultado_auditoria = json.loads(res.text)
373
  status.update(label="Avaliação da Banca concluída!", state="complete", expanded=False)
@@ -378,52 +254,38 @@ with aba_auditoria:
378
  if st.session_state.resultado_auditoria:
379
  dados = st.session_state.resultado_auditoria
380
  st.header("🏆 Avaliação da Banca UNIFAGOC")
381
- nota_total = sum([
382
- dados.get("nota_tema_problema", 0),
383
- dados.get("nota_introducao_metodo", 0),
384
- dados.get("nota_estruturacao", 0),
385
- dados.get("nota_formatacao_abnt", 0)
386
- ])
387
 
388
  c1, c2, c3, c4 = st.columns(4)
389
- c1.metric("Tema/Problema", f"{dados.get('nota_tema_problema')}/20")
390
- c2.metric("Intro/Método", f"{dados.get('nota_introducao_metodo')}/20")
391
- c3.metric("Estrutura/Linguagem", f"{dados.get('nota_estruturacao')}/20")
392
- c4.metric("Formatação/ABNT", f"{dados.get('nota_formatacao_abnt')}/20")
393
-
394
- st.info(
395
- f"**NOTA FINAL TEXTUAL: {nota_total}/80 pontos**\n"
396
- f"*Parecer da Banca:* {dados.get('justificativa_notas_banca')}"
397
- )
398
  st.divider()
399
 
400
  st.metric("Índice de Aderência ao Projeto", f"{dados.get('aderencia_percentual')}%")
401
  col_a, col_b = st.columns(2)
402
  with col_a:
403
  st.subheader("🎯 Problema de Pesquisa")
404
- if dados.get("tcc_respondeu_ao_problema"):
405
- st.success(f"✅ O TCC respondeu: {dados.get('justificativa_problema')}")
406
- else:
407
- st.error(f"❌ Não respondeu: {dados.get('justificativa_problema')}")
408
  with col_b:
409
  st.subheader("📋 Objetivos")
410
- for f in dados.get("objetivos_nao_cumpridos_no_tcc", []):
411
- st.error(f"Faltou: {f}")
412
- if not dados.get("objetivos_nao_cumpridos_no_tcc"):
413
- st.success("✅ Todos os objetivos foram abordados.")
414
 
415
  st.subheader("🚨 Filtro Anti-Embromação")
416
- for fuga in dados.get("desvios_de_tema_ou_linguica", []):
417
- st.warning(f"Fuga: {fuga}")
418
- for ia in dados.get("indicios_texto_generico_ia", []):
419
- st.error(f"Indício de IA/Texto Genérico: {ia}")
420
 
421
  # ==============================================================================
422
  # ABA 2: REVISÃO TEXTUAL INTELIGENTE
423
  # ==============================================================================
424
  with aba_revisao:
425
  st.markdown("A IA analisa o contexto para corrigir **Ortografia**, **CAIXA ALTA** indevida e aplica **Itálico** em termos latinos/estrangeiros.")
426
- arq_rev = st.file_uploader("Envie o TCC (.docx)", type=["docx"], key="revisao")
427
 
428
  if st.button("🧠 Realizar Revisão"):
429
  if arq_rev:
@@ -439,13 +301,8 @@ with aba_revisao:
439
  st.write("A aguardar correções da Inteligência Artificial...")
440
  try:
441
  res = client.models.generate_content(
442
- model="gemini-3.5-flash",
443
- contents=prompt_rev,
444
- config=types.GenerateContentConfig(
445
- temperature=0.1,
446
- response_mime_type="application/json",
447
- response_schema=RevisaoInteligente
448
- )
449
  )
450
  dados = json.loads(res.text)
451
 
@@ -456,11 +313,7 @@ with aba_revisao:
456
  if dados.get("correcoes"):
457
  for c in dados["correcoes"]:
458
  for para in doc_rev.paragraphs:
459
- if c["original"] in para.text:
460
- for run in para.runs:
461
- if c["original"] in run.text:
462
- run.text = run.text.replace(c["original"], c["corrigido"], 1)
463
- aplicadas_texto += 1
464
 
465
  if dados.get("termos_estrangeiros_para_italico"):
466
  for termo in dados["termos_estrangeiros_para_italico"]:
@@ -468,10 +321,8 @@ with aba_revisao:
468
  aplicadas_italico += formatacao_cruzada_indices(para, termo, aplicar_italico=True)
469
 
470
  st.session_state.doc_revisado_bytes = docx_to_bytes(doc_rev)
471
- status.update(
472
- label=f"Processo finalizado com {aplicadas_texto + aplicadas_italico} intervenções automáticas.",
473
- state="complete", expanded=False
474
- )
475
  except Exception as e:
476
  status.update(label="Erro no processamento.", state="error")
477
  st.error(f"Erro: {e}")
@@ -480,7 +331,7 @@ with aba_revisao:
480
  st.download_button(
481
  "⬇️ Baixar TCC Revisado (.docx)",
482
  data=st.session_state.doc_revisado_bytes,
483
- file_name="TCC_Revisado.docx",
484
  mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
485
  )
486
 
@@ -488,101 +339,70 @@ with aba_revisao:
488
  # ABA 3: FORMATAÇÃO TOTAL ABNT (UNIFAGOC)
489
  # ==============================================================================
490
  with aba_abnt:
491
- st.markdown(
492
- "Motor de formatação **fiel ao template UNIFAGOC**. "
493
- "Aplica: Calibri 12/11 pt · Margens 3×2 cm · Espaçamento 1,5 · "
494
- "Recuo 1,25 cm · Referências simples + 6 pt · Cabeçalho/rodapé 0,5 cm."
495
- )
496
- arq_abnt = st.file_uploader("Envie o TCC (.docx)", type=["docx"], key="abnt_file")
497
 
498
  if st.button("📐 Formatar TCC"):
499
  if arq_abnt:
500
  with st.status("A reestruturar formatação conforme template UNIFAGOC...", expanded=True) as status:
501
  doc_abnt = Document(arq_abnt)
502
 
503
- # ── Estimativa de páginas ────────────────────────────────────
504
  paginas_estimadas = estimar_paginas(doc_abnt)
505
- if paginas_estimadas > 18:
506
- st.warning(f"🚨 Extensão de {paginas_estimadas} páginas. O limite recomendado é 18.")
507
- else:
508
- st.info(f"📄 Extensão estimada: {paginas_estimadas} páginas.")
509
 
510
- # ── Aplicação das regras físicas do template ─────────────────
511
  st.write("A aplicar regras do template (Calibri, margens, recuos, espaçamentos)...")
512
  aplicar_formatacao_unifagoc_abnt(doc_abnt)
513
 
514
- # ── Extração limpa das referências bibliográficas ────────────
515
  st.write("A analisar Referências Bibliográficas com IA...")
516
- refs_limpas = []
517
- em_refs = False
518
  for i, para in enumerate(doc_abnt.paragraphs):
519
  txt = para.text.strip()
520
- if not txt:
521
- continue
522
  if re.search(r"^(?:\d+\.?\s*)?REFER[ÊE]NCIAS", txt, re.IGNORECASE):
523
- em_refs = True
524
- continue
525
  if em_refs:
526
- # Interrompe se encontrar outro título de seção em caixa-alta
527
- if len(txt) < 120 and txt.isupper() and not txt[0].isdigit():
528
- break
529
  refs_limpas.append((i, txt))
530
 
 
 
531
  if refs_limpas:
532
  texto_refs = "\n".join(f"[{idx}] {txt}" for idx, txt in refs_limpas)
533
  prompt_abnt = (
534
- "Analise as referências (ABNT 6023:2018). "
535
- "Para cada uma, devolva o índice exato entre colchetes como 'numero', "
536
- "e extraia o trecho EXATO (título do livro ou nome da revista/periódico) "
537
- "que deve receber negrito.\n"
538
  f"REFERÊNCIAS:\n{texto_refs}"
539
  )
540
  try:
541
  res = client.models.generate_content(
542
- model="gemini-3.5-flash",
543
- contents=prompt_abnt,
544
- config=types.GenerateContentConfig(
545
- temperature=0.0,
546
- response_mime_type="application/json",
547
- response_schema=AnaliseABNT
548
- )
549
  )
550
  dados_abnt = json.loads(res.text)
551
 
552
- # Constrói mapa: índice-parágrafo → objeto parágrafo
553
- mapa_idx_para = {idx: doc_abnt.paragraphs[idx] for idx, _ in refs_limpas
554
- if idx < len(doc_abnt.paragraphs)}
555
-
556
  negs_aplicados = 0
 
557
  for problema in dados_abnt.get("problemas", []):
558
- num = problema.get("numero")
559
- elem = problema.get("elemento_negrito", "").strip()
560
  if elem and num in mapa_idx_para:
561
- negs_aplicados += formatacao_cruzada_indices(
562
- mapa_idx_para[num], elem, aplicar_negrito=True
563
- )
564
 
565
  st.session_state.doc_formatado_bytes = docx_to_bytes(doc_abnt)
566
- status.update(
567
- label=f"TCC formatado! {negs_aplicados} negritos aplicados nas referências.",
568
- state="complete", expanded=False
569
- )
570
  except Exception as e:
571
- # Salva mesmo sem o negrito nas referências
572
  st.session_state.doc_formatado_bytes = docx_to_bytes(doc_abnt)
573
  status.update(label="Formatação aplicada (negrito de referências ignorado).", state="complete")
574
  st.warning(f"Aviso na análise ABNT: {e}")
575
  else:
576
  st.session_state.doc_formatado_bytes = docx_to_bytes(doc_abnt)
577
- status.update(
578
- label="Formatação aplicada (nenhuma referência encontrada para análise de negrito).",
579
- state="complete"
580
- )
581
 
582
  if st.session_state.doc_formatado_bytes:
583
  st.download_button(
584
  "⬇️ Baixar TCC Final Formatado (.docx)",
585
  data=st.session_state.doc_formatado_bytes,
586
- file_name="TCC_Final_Formatado.docx",
587
  mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
588
  )
 
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
+ # Limpa o cache se o usuário trocar de arquivo, evitando downloads cruzados
17
+ def reset_estado(chave):
18
+ if chave in st.session_state:
19
+ st.session_state[chave] = None
20
+
21
  if "resultado_auditoria" not in st.session_state: st.session_state.resultado_auditoria = None
22
  if "doc_revisado_bytes" not in st.session_state: st.session_state.doc_revisado_bytes = None
23
  if "doc_formatado_bytes" not in st.session_state: st.session_state.doc_formatado_bytes = None
 
61
  def get_client():
62
  api_key = os.environ.get("GEMINI_API_KEY")
63
  if not api_key:
64
+ st.error("❌ ERRO: A variável de ambiente GEMINI_API_KEY não foi encontrada nos Secrets.")
65
  st.stop()
66
  return genai.Client(api_key=api_key)
67
 
68
  client = get_client()
69
 
70
+ # ─── 4. MANIPULAÇÃO AVANÇADA DE DOCUMENTOS (DOCX) ─────────────────────────
71
  def extrair_texto_docx(arquivo_upload) -> str:
72
  doc = Document(arquivo_upload)
73
  return "\n\n".join([para.text.strip() for para in doc.paragraphs if para.text.strip()])
 
84
  )
85
  return max(1, palavras_totais // 350)
86
 
87
+ def _set_run_font(run, nome: str, tamanho_pt: float, negrito: bool = None, italico: bool = None):
 
 
88
  run.font.name = nome
89
  run.font.size = Pt(tamanho_pt)
90
+ if negrito is not None: run.font.bold = negrito
91
+ if italico is not None: run.font.italic = italico
92
+
93
+ def substituir_texto_cross_run(para, original, corrigido) -> int:
94
+ """Substitui textos mesmo quando fragmentados em múltiplos runs pelo Word."""
95
+ if original not in para.text: return 0
96
+
97
+ # Se está num único run (caso simples)
98
+ for run in para.runs:
99
+ if original in run.text:
100
+ run.text = run.text.replace(original, corrigido, 1)
101
+ return 1
102
+
103
+ # Se está fragmentado (cross-run) - Preserva o estilo do primeiro run afetado
104
+ estilo_base = para.runs[0].style if para.runs else None
105
+ texto_completo = para.text.replace(original, corrigido, 1)
106
+ para.clear()
107
+ novo_run = para.add_run(texto_completo)
108
+ if estilo_base: novo_run.style = estilo_base
109
+ return 1
110
 
111
+ def formatacao_cruzada_indices(para, texto_alvo, aplicar_italico=False, aplicar_negrito=False) -> int:
112
+ aplicacoes = 0
113
+ if aplicar_italico:
114
+ matches = list(re.finditer(r'\b' + re.escape(texto_alvo) + r'\b', para.text, re.IGNORECASE))
115
+ else:
116
+ idx = para.text.find(texto_alvo)
117
+ matches = [(idx, idx + len(texto_alvo))] if idx != -1 else []
118
+
119
+ for match in matches:
120
+ pos_ini, pos_fim = match if isinstance(match, tuple) else (match.start(), match.end())
121
+ if pos_ini == -1: continue
122
+ pos_atual = 0
123
+ for run in para.runs:
124
+ fim_run = pos_atual + len(run.text)
125
+ if pos_atual < pos_fim and fim_run > pos_ini:
126
+ if aplicar_italico: run.italic = True
127
+ if aplicar_negrito: run.bold = True
128
+ aplicacoes += 1
129
+ pos_atual += len(run.text)
130
+ return aplicacoes
131
 
132
  def aplicar_formatacao_unifagoc_abnt(doc: Document):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  for section in doc.sections:
134
+ section.page_height, section.page_width = Cm(29.7), Cm(21.0)
135
+ section.top_margin, section.left_margin = Cm(3.0), Cm(3.0)
136
+ section.bottom_margin, section.right_margin = Cm(2.0), Cm(2.0)
137
+ section.header_distance, section.footer_distance = Cm(0.5), Cm(0.5)
138
+
139
+ em_referencias, em_resumo = False, False
 
 
 
 
 
140
 
141
  for para in doc.paragraphs:
142
  texto = para.text.strip()
143
+ if not texto: continue
 
144
 
145
+ pf, style_name = para.paragraph_format, (para.style.name or "").lower()
 
146
 
 
147
  if re.match(r"^RESUMO\b", texto, re.IGNORECASE):
148
+ em_resumo, em_referencias = True, False
 
149
  elif re.match(r"^(?:INTRODU[ÇC]|ABSTRACT|1[\s\.])", texto, re.IGNORECASE):
150
  em_resumo = False
151
 
152
  is_titulo_refs = bool(re.search(r"^(?:\d+\.?\s*)?REFER[ÊE]NCIAS\b", texto, re.IGNORECASE))
153
+ if is_titulo_refs: em_referencias = True
 
154
 
155
+ # REFERÊNCIAS (Entradas)
 
 
156
  if em_referencias and not is_titulo_refs:
157
+ pf.alignment, pf.first_line_indent, pf.left_indent = WD_ALIGN_PARAGRAPH.LEFT, Cm(0), Cm(0)
158
+ pf.space_before, pf.space_after, pf.line_spacing = Pt(0), Pt(6), 1.0
159
+ for run in para.runs: _set_run_font(run, "Calibri", 11)
 
 
 
 
 
160
 
161
+ # TÍTULO "REFERÊNCIAS"
 
 
162
  elif is_titulo_refs:
163
+ pf.alignment, pf.first_line_indent, pf.left_indent = WD_ALIGN_PARAGRAPH.CENTER, Cm(0), Cm(0)
164
+ pf.space_before, pf.space_after, pf.line_spacing = Pt(12), Pt(12), 1.5
165
+ for run in para.runs: _set_run_font(run, "Calibri", 12, negrito=True, italico=False)
 
 
 
 
 
166
 
167
+ # RESUMO
 
 
168
  elif em_resumo:
169
+ pf.alignment, pf.first_line_indent, pf.left_indent = WD_ALIGN_PARAGRAPH.JUSTIFY, Cm(0), Cm(0)
170
+ pf.space_before, pf.space_after, pf.line_spacing = Pt(0), Pt(0), 1.5
 
 
 
 
171
  for run in para.runs:
172
+ is_label = bool(re.match(r"^(RESUMO|PALAVRAS.CHAVE|ABSTRACT|KEYWORDS)", run.text.strip(), re.IGNORECASE))
173
+ _set_run_font(run, "Calibri", 11, negrito=is_label, italico=not is_label)
 
 
 
 
 
 
174
 
175
+ # SEÇÕES (Títulos e Subtítulos)
 
 
176
  elif "heading" in style_name or (texto.isupper() and 3 < len(texto) < 120):
177
+ if "heading 2" in style_name or "heading2" in style_name or (texto.isupper() and "heading" not in style_name):
178
+ pf.alignment, pf.first_line_indent, pf.left_indent = WD_ALIGN_PARAGRAPH.CENTER, Cm(0), Cm(0)
179
+ pf.space_before, pf.space_after, pf.line_spacing = Pt(0), Pt(12), 1.5
180
+ for run in para.runs: _set_run_font(run, "Calibri", 12, negrito=True, italico=False)
 
 
 
 
 
 
 
 
 
 
181
  elif "heading 3" in style_name or "heading3" in style_name:
182
+ pf.alignment, pf.first_line_indent, pf.left_indent = WD_ALIGN_PARAGRAPH.JUSTIFY, Cm(0), Cm(0)
183
+ pf.space_before, pf.space_after, pf.line_spacing = Pt(12), Pt(0), 1.5
184
+ for run in para.runs: _set_run_font(run, "Calibri", 12, negrito=True, italico=False)
 
 
 
 
 
 
 
185
  elif "heading 4" in style_name or "heading4" in style_name:
186
+ pf.alignment, pf.first_line_indent, pf.left_indent = WD_ALIGN_PARAGRAPH.JUSTIFY, Cm(0), Cm(0)
187
+ pf.space_before, pf.space_after, pf.line_spacing = Pt(0), Pt(0), 1.5
188
+ for run in para.runs: _set_run_font(run, "Calibri", 12, negrito=True, italico=True)
 
 
 
 
 
 
 
189
  else:
190
+ pf.alignment, pf.first_line_indent, pf.left_indent = WD_ALIGN_PARAGRAPH.CENTER, Cm(0), Cm(0)
191
+ pf.space_before, pf.space_after, pf.line_spacing = Pt(6), Pt(12), 1.5
192
+ for run in para.runs: _set_run_font(run, "Calibri", 14, negrito=True, italico=False)
193
+
194
+ # CITAÇÕES LONGAS
 
 
 
 
 
 
 
195
  elif pf.left_indent is not None and pf.left_indent >= Cm(3.5):
196
+ pf.alignment, pf.first_line_indent, pf.left_indent = WD_ALIGN_PARAGRAPH.JUSTIFY, Cm(0), Cm(4.0)
197
+ pf.space_before, pf.space_after, pf.line_spacing = Pt(6), Pt(6), 1.0
198
+ for run in para.runs: _set_run_font(run, "Calibri", 11)
 
 
 
 
 
199
 
200
+ # CORPO DO TEXTO
 
 
201
  else:
202
+ pf.alignment, pf.first_line_indent, pf.left_indent = WD_ALIGN_PARAGRAPH.JUSTIFY, Cm(1.25), Cm(0)
203
+ pf.space_before, pf.space_after, pf.line_spacing = Pt(0), Pt(0), 1.5
204
+ for run in para.runs: _set_run_font(run, "Calibri", 12)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
  # ─── 5. INTERFACE PRINCIPAL (ABAS) ────────────────────────────────────────
207
  st.title("⚖️ Assistente Acadêmico - Direito UNIFAGOC")
 
216
  # ==============================================================================
217
  with aba_auditoria:
218
  st.markdown("Cruze o **Pré-Projeto** com o **TCC** para identificar fuga do tema e gerar a **Nota da Banca (80 pontos)**.")
219
+ 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",))
 
 
 
 
220
 
221
  col1, col2 = st.columns(2)
222
+ with col1: arq_projeto = st.file_uploader("1. Pré-Projeto (.docx)", type=["docx"], key="proj", on_change=reset_estado, args=("resultado_auditoria",))
223
+ with col2: arq_tcc = st.file_uploader("2. TCC (.docx)", type=["docx"], key="tcc_aud", on_change=reset_estado, args=("resultado_auditoria",))
224
 
225
  if st.button("🔍 Iniciar Auditoria e Avaliação", type="primary"):
226
  if not arq_projeto or not arq_tcc:
 
231
  texto_projeto = extrair_texto_docx(arq_projeto)
232
  texto_tcc = extrair_texto_docx(arq_tcc)
233
 
234
+ 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."
 
 
 
 
235
  prompt = (
236
  f"Como Professor Examinador titular da banca de Direito da UNIFAGOC:\n{regra_fase}\n"
237
+ f"=== PRÉ-PROJETO ===\n{texto_projeto}\n=== TCC ===\n{texto_tcc}\n"
 
238
  "TAREFA 1: Avalie o cruzamento de dados, fuga de tema e objetivos.\n"
239
  "TAREFA 2: Distribua 80 pontos rigorosamente de acordo com os critérios da ficha UNIFAGOC."
240
  )
 
242
  st.write("A submeter dados à IA do Google...")
243
  try:
244
  res = client.models.generate_content(
245
+ model="gemini-3.5-flash", contents=prompt,
246
+ config=types.GenerateContentConfig(temperature=0.0, response_mime_type="application/json", response_schema=AuditoriaTCC)
 
 
 
 
 
247
  )
248
  st.session_state.resultado_auditoria = json.loads(res.text)
249
  status.update(label="Avaliação da Banca concluída!", state="complete", expanded=False)
 
254
  if st.session_state.resultado_auditoria:
255
  dados = st.session_state.resultado_auditoria
256
  st.header("🏆 Avaliação da Banca UNIFAGOC")
257
+ nota_total = sum([dados.get("nota_tema_problema", 0), dados.get("nota_introducao_metodo", 0), dados.get("nota_estruturacao", 0), dados.get("nota_formatacao_abnt", 0)])
 
 
 
 
 
258
 
259
  c1, c2, c3, c4 = st.columns(4)
260
+ c1.metric("Tema/Problema", f"{dados.get('nota_tema_problema')}/20")
261
+ c2.metric("Intro/Método", f"{dados.get('nota_introducao_metodo')}/20")
262
+ c3.metric("Estrutura/Linguagem", f"{dados.get('nota_estruturacao')}/20")
263
+ c4.metric("Formatação/ABNT", f"{dados.get('nota_formatacao_abnt')}/20")
264
+
265
+ st.info(f"**NOTA FINAL TEXTUAL: {nota_total}/80 pontos**\n*Parecer da Banca:* {dados.get('justificativa_notas_banca')}")
 
 
 
266
  st.divider()
267
 
268
  st.metric("Índice de Aderência ao Projeto", f"{dados.get('aderencia_percentual')}%")
269
  col_a, col_b = st.columns(2)
270
  with col_a:
271
  st.subheader("🎯 Problema de Pesquisa")
272
+ if dados.get("tcc_respondeu_ao_problema"): st.success(f"✅ O TCC respondeu: {dados.get('justificativa_problema')}")
273
+ else: st.error(f"❌ Não respondeu: {dados.get('justificativa_problema')}")
 
 
274
  with col_b:
275
  st.subheader("📋 Objetivos")
276
+ for f in dados.get("objetivos_nao_cumpridos_no_tcc", []): st.error(f"Faltou: {f}")
277
+ if not dados.get("objetivos_nao_cumpridos_no_tcc"): st.success("✅ Todos os objetivos foram abordados.")
 
 
278
 
279
  st.subheader("🚨 Filtro Anti-Embromação")
280
+ for fuga in dados.get("desvios_de_tema_ou_linguica", []): st.warning(f"Fuga: {fuga}")
281
+ for ia in dados.get("indicios_texto_generico_ia", []): st.error(f"Indício de IA/Texto Genérico: {ia}")
 
 
282
 
283
  # ==============================================================================
284
  # ABA 2: REVISÃO TEXTUAL INTELIGENTE
285
  # ==============================================================================
286
  with aba_revisao:
287
  st.markdown("A IA analisa o contexto para corrigir **Ortografia**, **CAIXA ALTA** indevida e aplica **Itálico** em termos latinos/estrangeiros.")
288
+ arq_rev = st.file_uploader("Envie o TCC (.docx)", type=["docx"], key="revisao", on_change=reset_estado, args=("doc_revisado_bytes",))
289
 
290
  if st.button("🧠 Realizar Revisão"):
291
  if arq_rev:
 
301
  st.write("A aguardar correções da Inteligência Artificial...")
302
  try:
303
  res = client.models.generate_content(
304
+ model="gemini-3.5-flash", contents=prompt_rev,
305
+ config=types.GenerateContentConfig(temperature=0.1, response_mime_type="application/json", response_schema=RevisaoInteligente)
 
 
 
 
 
306
  )
307
  dados = json.loads(res.text)
308
 
 
313
  if dados.get("correcoes"):
314
  for c in dados["correcoes"]:
315
  for para in doc_rev.paragraphs:
316
+ aplicadas_texto += substituir_texto_cross_run(para, c["original"], c["corrigido"])
 
 
 
 
317
 
318
  if dados.get("termos_estrangeiros_para_italico"):
319
  for termo in dados["termos_estrangeiros_para_italico"]:
 
321
  aplicadas_italico += formatacao_cruzada_indices(para, termo, aplicar_italico=True)
322
 
323
  st.session_state.doc_revisado_bytes = docx_to_bytes(doc_rev)
324
+ st.session_state.nome_arq_rev = f"{arq_rev.name.replace('.docx', '')}_Revisado.docx"
325
+ status.update(label=f"Processo finalizado com {aplicadas_texto + aplicadas_italico} intervenções automáticas.", state="complete", expanded=False)
 
 
326
  except Exception as e:
327
  status.update(label="Erro no processamento.", state="error")
328
  st.error(f"Erro: {e}")
 
331
  st.download_button(
332
  "⬇️ Baixar TCC Revisado (.docx)",
333
  data=st.session_state.doc_revisado_bytes,
334
+ file_name=st.session_state.nome_arq_rev,
335
  mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
336
  )
337
 
 
339
  # ABA 3: FORMATAÇÃO TOTAL ABNT (UNIFAGOC)
340
  # ==============================================================================
341
  with aba_abnt:
342
+ st.markdown("Motor de formatação **fiel ao template UNIFAGOC**.")
343
+ arq_abnt = st.file_uploader("Envie o TCC (.docx)", type=["docx"], key="abnt_file", on_change=reset_estado, args=("doc_formatado_bytes",))
 
 
 
 
344
 
345
  if st.button("📐 Formatar TCC"):
346
  if arq_abnt:
347
  with st.status("A reestruturar formatação conforme template UNIFAGOC...", expanded=True) as status:
348
  doc_abnt = Document(arq_abnt)
349
 
 
350
  paginas_estimadas = estimar_paginas(doc_abnt)
351
+ if paginas_estimadas > 18: st.warning(f"🚨 Extensão de {paginas_estimadas} páginas. O limite recomendado é 18.")
352
+ else: st.info(f"📄 Extensão estimada: {paginas_estimadas} páginas.")
 
 
353
 
 
354
  st.write("A aplicar regras do template (Calibri, margens, recuos, espaçamentos)...")
355
  aplicar_formatacao_unifagoc_abnt(doc_abnt)
356
 
 
357
  st.write("A analisar Referências Bibliográficas com IA...")
358
+ refs_limpas, em_refs = [], False
 
359
  for i, para in enumerate(doc_abnt.paragraphs):
360
  txt = para.text.strip()
361
+ if not txt: continue
 
362
  if re.search(r"^(?:\d+\.?\s*)?REFER[ÊE]NCIAS", txt, re.IGNORECASE):
363
+ em_refs = True; continue
 
364
  if em_refs:
365
+ if len(txt) < 120 and txt.isupper() and not txt[0].isdigit(): break
 
 
366
  refs_limpas.append((i, txt))
367
 
368
+ st.session_state.nome_arq_abnt = f"{arq_abnt.name.replace('.docx', '')}_Formatado.docx"
369
+
370
  if refs_limpas:
371
  texto_refs = "\n".join(f"[{idx}] {txt}" for idx, txt in refs_limpas)
372
  prompt_abnt = (
373
+ "Analise as referências (ABNT 6023:2018). Para cada uma, devolva o índice exato entre colchetes como 'numero', "
374
+ "e extraia o trecho EXATO (título do livro ou nome da revista/periódico) que deve receber negrito.\n"
 
 
375
  f"REFERÊNCIAS:\n{texto_refs}"
376
  )
377
  try:
378
  res = client.models.generate_content(
379
+ model="gemini-3.5-flash", contents=prompt_abnt,
380
+ config=types.GenerateContentConfig(temperature=0.0, response_mime_type="application/json", response_schema=AnaliseABNT)
 
 
 
 
 
381
  )
382
  dados_abnt = json.loads(res.text)
383
 
384
+ mapa_idx_para = {idx: doc_abnt.paragraphs[idx] for idx, _ in refs_limpas if idx < len(doc_abnt.paragraphs)}
 
 
 
385
  negs_aplicados = 0
386
+
387
  for problema in dados_abnt.get("problemas", []):
388
+ num, elem = problema.get("numero"), problema.get("elemento_negrito", "").strip()
 
389
  if elem and num in mapa_idx_para:
390
+ negs_aplicados += formatacao_cruzada_indices(mapa_idx_para[num], elem, aplicar_negrito=True)
 
 
391
 
392
  st.session_state.doc_formatado_bytes = docx_to_bytes(doc_abnt)
393
+ status.update(label=f"TCC formatado! {negs_aplicados} negritos aplicados nas referências.", state="complete", expanded=False)
 
 
 
394
  except Exception as e:
 
395
  st.session_state.doc_formatado_bytes = docx_to_bytes(doc_abnt)
396
  status.update(label="Formatação aplicada (negrito de referências ignorado).", state="complete")
397
  st.warning(f"Aviso na análise ABNT: {e}")
398
  else:
399
  st.session_state.doc_formatado_bytes = docx_to_bytes(doc_abnt)
400
+ status.update(label="Formatação aplicada (nenhuma referência encontrada).", state="complete")
 
 
 
401
 
402
  if st.session_state.doc_formatado_bytes:
403
  st.download_button(
404
  "⬇️ Baixar TCC Final Formatado (.docx)",
405
  data=st.session_state.doc_formatado_bytes,
406
+ file_name=st.session_state.nome_arq_abnt,
407
  mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
408
  )