GabrielGD commited on
Commit
2ca1aca
·
verified ·
1 Parent(s): 88c2e3a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +552 -480
app.py CHANGED
@@ -6,28 +6,17 @@ 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 docx.enum.section import WD_SECTION
10
  from docx.oxml.ns import qn
 
11
  from pydantic import BaseModel
12
- from typing import List, Optional
13
  from google import genai
14
  from google.genai import types
15
 
16
  # =====================================================
17
- # 1. CONFIGURAÇÃO INICIAL E ESTADO
18
  # =====================================================
19
- st.set_page_config(page_title="Corretor TCC - UNIFAGOC", page_icon="⚖️", layout="wide")
20
-
21
- def reset_estado(chave):
22
- if chave in st.session_state:
23
- st.session_state[chave] = None
24
-
25
- # Inicialização do estado
26
- for key in ["resultado_auditoria", "doc_revisado_bytes", "doc_formatado_bytes"]:
27
- if key not in st.session_state:
28
- st.session_state[key] = None
29
-
30
- # Dicionário de Latinismos Jurídicos
31
  LATINISMOS = [
32
  "habeas corpus", "data venia", "jus puniendi", "jus postulandi",
33
  "mens legis", "ratio decidendi", "obiter dictum", "in casu",
@@ -37,7 +26,7 @@ LATINISMOS = [
37
  ]
38
 
39
  # =====================================================
40
- # 2. ESQUEMAS DE DADOS (PYDANTIC)
41
  # =====================================================
42
  class AuditoriaTCC(BaseModel):
43
  aderencia_percentual: int
@@ -76,414 +65,539 @@ class RevisaoInteligente(BaseModel):
76
  termos_estrangeiros_para_italico: List[str]
77
 
78
  class ProblemaReferencia(BaseModel):
79
- numero: int
80
- erros: List[str]
81
- elemento_negrito: str
82
-
83
- class ReferenciaCorrigida(BaseModel):
84
  original: str
85
- erros: List[str] # Apenas aponta erros, NÃO reescreve a referência
86
 
87
  class ListaReferencias(BaseModel):
88
- referencias: List[ReferenciaCorrigida]
89
 
90
  # =====================================================
91
- # 3. AUTENTICAÇÃO E CLIENTE GEMINI
92
  # =====================================================
93
- @st.cache_resource
94
- def get_client():
95
- api_key = st.secrets["GEMINI_API_KEY"] # Usando secrets do Streamlit
96
- if not api_key:
97
- st.error("❌ ERRO: Chave GEMINI_API_KEY não configurada nos secrets.")
98
- st.stop()
99
- return genai.Client(api_key=api_key)
100
-
101
- client = get_client()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
  # =====================================================
104
- # 4. FUNÇÕES AUXILIARES
105
  # =====================================================
106
- def extrair_texto_docx(arquivo_upload) -> str:
107
- doc = Document(arquivo_upload)
108
- return "\n\n".join([para.text.strip() for para in doc.paragraphs if para.text.strip()])
109
-
110
- def docx_to_bytes(doc: Document) -> bytes:
111
- byte_io = io.BytesIO()
112
- doc.save(byte_io)
113
- return byte_io.getvalue()
114
-
115
- def estimar_paginas(doc: Document) -> int:
116
- palavras_totais = sum(
117
- len(para.text.split()) for para in doc.paragraphs
118
- if not re.search(r"^(?:\d+\.?\s*)?REFER[ÊE]NCIAS", para.text, re.IGNORECASE)
119
- )
120
- return max(1, palavras_totais // 350)
121
-
122
- def substituir_texto_preservando_formato(para, original: str, corrigido: str) -> int:
123
- """
124
- Substitui texto PRESERVANDO formatação de runs (negrito, itálico, hyperlinks, etc.).
125
- Não usa para.clear() para evitar perder formatação.
126
- """
127
- texto_completo = "".join(run.text for run in para.runs)
128
- if original not in texto_completo:
129
- return 0
130
-
131
- # Localiza a posição do texto original
132
- pos = texto_completo.find(original)
133
- if pos == -1:
134
- return 0
135
-
136
- # Divide o texto em partes: antes, original, depois
137
- antes = texto_completo[:pos]
138
- depois = texto_completo[pos + len(original):]
139
-
140
- # Reconstroi o parágrafo com os runs existentes
141
- novo_texto = antes + corrigido + depois
142
- para.clear() # Limpa para reconstruir
143
-
144
- # Adiciona o texto completo de volta (preservando a formatação visual)
145
- run = para.add_run(novo_texto)
146
-
147
- # Aplica formatação padrão (Calibri 12)
148
- run.font.name = 'Calibri'
149
- run.font.size = Pt(12)
150
-
151
- return 1
152
-
153
- def formatacao_cruzada_indices(para, texto_alvo: str, aplicar_italico: bool = False, aplicar_negrito: bool = False) -> int:
154
- aplicacoes = 0
155
- if aplicar_italico:
156
- matches = list(re.finditer(r'\b' + re.escape(texto_alvo) + r'\b', para.text, re.IGNORECASE))
157
- else:
158
- idx = para.text.find(texto_alvo)
159
- matches = [(idx, idx + len(texto_alvo))] if idx != -1 else []
160
-
161
- for match in matches:
162
- pos_ini, pos_fim = match if isinstance(match, tuple) else (match.start(), match.end())
163
- if pos_ini == -1:
164
- continue
165
- pos_atual = 0
166
- for run in para.runs:
167
- fim_run = pos_atual + len(run.text)
168
- if pos_atual < pos_fim and fim_run > pos_ini:
169
- if aplicar_italico:
170
- run.italic = True
171
- if aplicar_negrito:
172
- run.bold = True
173
- aplicacoes += 1
174
- pos_atual += len(run.text)
175
- return aplicacoes
176
 
177
- # =====================================================
178
- # 5. DETECÇÃO ESTRUTURAL AVANÇADA
179
- # =====================================================
180
- def detectar_nivel_secao(texto: str) -> int:
181
- """Retorna o nível da seção (0=nenhum, 1=1, 2=1.1, 3=1.1.1)"""
182
- match = re.match(r'^(\d+(?:\.\d+)*)\s+', texto)
183
- if not match:
184
- return 0
185
- return match.group(1).count('.') + 1
186
-
187
- def detectar_titulo_ou_subtitulo(texto: str) -> Optional[str]:
188
- """Detecta títulos e subtítulos com base em seções especiais ou numeração"""
189
- SECOES_ESPECIAIS = ["INTRODUÇÃO", "CONSIDERAÇÕES FINAIS", "REFERÊNCIAS", "RESUMO", "PALAVRAS-CHAVE"]
190
- texto_upper = texto.upper()
191
-
192
- # Verifica se é uma seção especial
193
- if any(sec in texto_upper for sec in SECOES_ESPECIAIS):
194
- return "titulo"
195
-
196
- # Verifica se é uma seção numerada (1, 1.1, 1.1.1, etc.)
197
- if detectar_nivel_secao(texto) > 0:
198
- return "titulo"
199
-
200
- # Verifica se é um título em caixa alta (sem ser seção numerada)
201
- if texto.isupper() and len(texto) < 150 and not detectar_nivel_secao(texto):
202
- return "titulo"
203
-
204
- return None
205
-
206
- def detectar_citacao_longa(para) -> bool:
207
- """Detecta citações longas por recuo ou marcação"""
208
- if para.paragraph_format.left_indent and para.paragraph_format.left_indent >= Cm(3.5):
209
- return True
210
- texto = para.text.strip()
211
- if (texto.startswith(('Segundo ', 'Conforme ', 'De acordo com ')) and
212
- len(texto.split()) > 30):
213
- return True
214
- return False
215
-
216
- def detectar_lista(texto: str) -> bool:
217
- """Detecta listas numeradas ou alfabéticas (evitando falsos positivos)"""
218
- # Regex mais preciso: só detecta se começar com padrão de lista seguido de espaço ou hifen
219
- return bool(re.match(r'^(\d+[\)\.]|[a-zA-Z][\)\.]|[IVXLCDM]+[\)\.]|[-–])\s', texto))
220
 
221
- # =====================================================
222
- # 6. FUNÇÕES DE FORMATAÇÃO
223
- # =====================================================
224
- def aplicar_margens_unifagoc(doc: Document):
225
- """Aplica as margens oficiais do template UNIFAGOC (A4: 21x29.7cm)"""
226
- for section in doc.sections:
227
- section.page_height = Cm(29.7)
228
- section.page_width = Cm(21.0)
229
- section.top_margin = Cm(3.0)
230
- section.left_margin = Cm(3.0)
231
- section.bottom_margin = Cm(2.0)
232
- section.right_margin = Cm(2.0)
233
- section.header_distance = Cm(0.5)
234
- section.footer_distance = Cm(0.5)
235
-
236
- def adicionar_paginacao(doc: Document):
237
- """Adiciona numeração de páginas no rodapé (centralizado) - SOMENTE para seções de conteúdo"""
238
- for i, section in enumerate(doc.sections):
239
- # Ignora a primeira seção (geralmente capa e folha de rosto)
240
- if i == 0:
241
- continue
242
-
243
- # Adiciona parágrafo ao rodapé
244
- footer_para = section.footer.paragraphs[0] if section.footer.paragraphs else section.footer.add_paragraph()
245
- footer_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
246
-
247
- # Adiciona campo de página via XML
248
- run = footer_para.add_run()
249
- fldChar = run._r.add_fldChar()
250
- fldChar.type = 'begin'
251
- instrText = run._r.add_instrText()
252
- instrText.text = 'PAGE'
253
- fldChar = run._r.add_fldChar()
254
- fldChar.type = 'end'
255
-
256
- # Formatação do rodapé
257
- for run in footer_para.runs:
258
- run.font.name = 'Calibri'
259
- run.font.size = Pt(10)
260
-
261
- def formatar_figuras_tabelas(doc: Document):
262
- """Formata figuras, tabelas e quadros conforme template UNIFAGOC"""
263
- for para in doc.paragraphs:
264
- texto = para.text.strip()
265
- if re.match(r'^(Figura|Tabela|Quadro)\s+\d+', texto, re.IGNORECASE):
266
- pf = para.paragraph_format
267
- pf.alignment = WD_ALIGN_PARAGRAPH.CENTER
268
  for run in para.runs:
269
  run.font.name = 'Calibri'
270
- run.font.size = Pt(12)
271
- run.font.bold = True
272
-
273
- # Formata a legenda (próximo parágrafo)
274
- idx = doc.paragraphs.index(para)
275
- if idx + 1 < len(doc.paragraphs):
276
- next_para = doc.paragraphs[idx + 1]
277
- for run in next_para.runs:
 
 
278
  run.font.name = 'Calibri'
279
- run.font.size = Pt(11)
280
-
281
- # Formata tabelas
282
- for table in doc.tables:
283
- for row in table.rows:
284
- for cell in row.cells:
285
- for para in cell.paragraphs:
286
- para.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT
287
- para.paragraph_format.line_spacing = 1.0
288
- for run in para.runs:
289
  run.font.name = 'Calibri'
290
  run.font.size = Pt(11)
291
 
292
- def formatar_imagens(doc: Document):
293
- """Centraliza imagens (inline shapes) - Solução alternativa para python-docx"""
294
- for para in doc.paragraphs:
295
- if para._element.xpath('.//pic:pic'):
296
- para.alignment = WD_ALIGN_PARAGRAPH.CENTER
297
-
298
- def formatar_notas_rodape(doc: Document):
299
- """Formata notas de rodapé (¹, ², ³)"""
300
- for para in doc.paragraphs:
301
- if re.search(r'[\d\^]+$', para.text.strip()):
302
- for run in para.runs:
303
- run.font.size = Pt(8)
304
- run.font.superscript = True
305
-
306
- def validar_referencias(doc: Document) -> List[ReferenciaCorrigida]:
307
- """Valida referências ABNT usando IA - APENAS APONTA ERROS (não reescreve)"""
308
- refs = []
309
- em_refs = False
310
- for para in doc.paragraphs:
311
- texto = para.text.strip()
312
- if re.search(r"REFER[ÊE]NCIAS", texto, re.IGNORECASE):
313
- em_refs = True
314
- continue
315
- if em_refs and (len(texto) < 10 or texto.isupper()):
316
- break
317
- if em_refs:
318
- refs.append(texto)
319
-
320
- if not refs:
321
- return []
322
-
323
- refs_str = '\n'.join(refs)
324
- prompt_refs = (
325
- "Analise as referências abaixo **APENAS PARA IDENTIFICAR ERROS** (NÃO reescreva, NÃO invente dados!).\n"
326
- "Para cada uma, retorne:\n"
327
- "- original: texto original\n"
328
- "- erros: lista de erros (ex: [\"título não em negrito\", \"autor não invertido\"])\n\n"
329
- f"Referências:\n{refs_str}"
330
- )
331
-
332
- try:
333
- res = client.models.generate_content(
334
- model="gemini-3.5-flash", # Modelo válido
335
- contents=prompt_refs,
336
- config=types.GenerateContentConfig(
337
- temperature=0.0,
338
- response_mime_type="application/json",
339
- response_schema=ListaReferencias
340
- )
341
- )
342
- dados = json.loads(res.text)
343
- return dados.get("referencias", [])
344
- except Exception as e:
345
- st.error(f"Erro ao validar referências: {e}")
346
- return []
347
-
348
- def formatar_documento_unifagoc(doc: Document):
349
- """Função principal de formatação com fluxo único e sem sobreposição"""
350
- # 1. Aplica margens e paginação (somente para seções de conteúdo)
351
- aplicar_margens_unifagoc(doc)
352
- adicionar_paginacao(doc)
353
-
354
- # 2. Formata elementos específicos
355
- formatar_figuras_tabelas(doc)
356
- formatar_imagens(doc)
357
- formatar_notas_rodape(doc)
358
-
359
- # 3. Processa cada parágrafo com fluxo único
360
- for para in doc.paragraphs:
361
- texto = para.text.strip()
362
- if not texto:
363
- continue
364
-
365
- # --- REFERÊNCIAS ---
366
- if re.search(r"REFER[ÊE]NCIAS", texto, re.IGNORECASE):
367
- pf = para.paragraph_format
368
- pf.alignment = WD_ALIGN_PARAGRAPH.CENTER
369
- pf.space_before = Pt(12)
370
- pf.space_after = Pt(6)
371
- for run in para.runs:
372
- run.font.name = 'Calibri'
373
- run.font.size = Pt(12)
374
- run.font.bold = True
375
- continue
376
-
377
- # --- RESUMO ---
378
- if texto.upper() == "RESUMO":
379
- pf = para.paragraph_format
380
- pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
381
- pf.space_before = Pt(12)
382
- pf.space_after = Pt(0)
383
- pf.line_spacing = 1.5
384
- for run in para.runs:
385
- run.font.name = 'Calibri'
386
- run.font.size = Pt(11)
387
- run.font.bold = True
388
- continue
389
-
390
- # --- PALAVRAS-CHAVE ---
391
- if texto.upper().startswith("PALAVRAS-CHAVE"):
392
- pf = para.paragraph_format
393
- pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
394
- pf.space_before = Pt(6)
395
- pf.space_after = Pt(0)
396
- pf.line_spacing = 1.5
397
- for run in para.runs:
398
- run.font.name = 'Calibri'
399
- run.font.size = Pt(11)
400
- if "PALAVRAS-CHAVE" in run.text.upper():
401
- run.font.bold = True
402
- continue
403
-
404
- # --- TÍTULOS E SEÇÕES ---
405
- nivel = detectar_nivel_secao(texto)
406
- if nivel > 0 or detectar_titulo_ou_subtitulo(texto) == "titulo":
407
- pf = para.paragraph_format
408
- pf.alignment = WD_ALIGN_PARAGRAPH.LEFT
409
- pf.space_before = Pt(12)
410
- pf.space_after = Pt(6)
411
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  for run in para.runs:
413
  run.font.name = 'Calibri'
414
  run.font.size = Pt(12)
415
- run.font.bold = True
416
 
417
- # Seções de nível 3 também recebem itálico
418
- if nivel == 3:
419
- run.font.italic = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
 
421
- # Seções de nível 2 recebem recuo
422
- if nivel == 2:
423
- pf.left_indent = Cm(0.5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
 
425
- continue
 
 
 
 
 
 
 
426
 
427
- # --- CITAÇÕES LONGAS ---
428
- if detectar_citacao_longa(para):
429
- pf = para.paragraph_format
430
- pf.left_indent = Cm(4.0)
431
- pf.line_spacing = 1.0
432
- pf.space_before = Pt(6)
433
- pf.space_after = Pt(6)
434
- for run in para.runs:
435
- run.font.name = 'Calibri'
436
- run.font.size = Pt(11)
437
- continue
438
-
439
- # --- LISTAS ---
440
- if detectar_lista(texto):
441
- pf = para.paragraph_format
442
- pf.left_indent = Cm(1.25)
443
- pf.first_line_indent = Cm(0)
444
- pf.space_before = Pt(0)
445
- pf.space_after = Pt(0)
446
- pf.line_spacing = 1.5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
  for run in para.runs:
448
- run.font.name = 'Calibri'
449
- run.font.size = Pt(12)
450
- continue
451
-
452
- # --- CORPO DO TEXTO (padrão) ---
453
- pf = para.paragraph_format
454
- pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
455
- pf.first_line_indent = Cm(1.25)
456
- pf.line_spacing = 1.5
457
- pf.space_before = Pt(0)
458
- pf.space_after = Pt(0)
459
- for run in para.runs:
460
- run.font.name = 'Calibri'
461
- run.font.size = Pt(12)
462
-
463
- # 4. Valida referências (apenas aponta erros, não reescreve)
464
- refs_com_erros = validar_referencias(doc)
465
- for ref in refs_com_erros:
466
- st.warning(f"⚠️ Erros na referência: '{ref['original']}' -> {', '.join(ref['erros'])}")
467
 
468
  # =====================================================
469
- # 7. MOTOR DE VERIFICAÇÕES LOCAIS (REGEX)
470
- # =====================================================
471
- def verificacoes_locais_regex(texto: str) -> List[str]:
472
- erros = []
473
- if re.search(r"\s{2,}", texto):
474
- erros.append("Espaços duplos detectados no documento.")
475
- if re.search(r",\s*,", texto):
476
- erros.append("Vírgulas duplicadas detectadas (ex: ',,').")
477
- if re.search(r"[.!?]{2,}", texto) and not "..." in texto:
478
- erros.append("Pontuação repetida indevida (ex: '!!' ou '..').")
479
- if re.search(r"\b(\w+)\s+\1\b", texto, re.IGNORECASE):
480
- erros.append("Palavras repetidas sequencialmente (ex: 'o o', 'de de').")
481
- if re.search(r"\([A-ZÀ-Ú\s]+,\s*\)", texto):
482
- erros.append("Citação ABNT com ano faltando (ex: (SILVA, )).")
483
- return erros
484
-
485
- # =====================================================
486
- # 8. INTERFACE STREAMLIT
487
  # =====================================================
488
  st.title("⚖️ Assistente Acadêmico - Direito UNIFAGOC")
489
  aba_auditoria, aba_revisao, aba_abnt, aba_prompt, aba_feedback = st.tabs([
@@ -494,6 +608,10 @@ aba_auditoria, aba_revisao, aba_abnt, aba_prompt, aba_feedback = st.tabs([
494
  "👨‍🏫 Feedback Professor"
495
  ])
496
 
 
 
 
 
497
  # ABA 1: AUDITORIA
498
  with aba_auditoria:
499
  st.markdown("Cruze o **Pré-Projeto** com o **TCC** para identificar fuga do tema e gerar a **Nota da Banca (80 pontos)**.")
@@ -510,51 +628,28 @@ with aba_auditoria:
510
  st.warning("⚠️ Envie os DOIS arquivos para realizar a comparação.")
511
  else:
512
  with st.status("Analisando coerência jurídica...", expanded=True) as status:
513
- texto_projeto = extrair_texto_docx(arq_projeto)
514
- texto_tcc = extrair_texto_docx(arq_tcc)
515
- regra_fase = (
516
- "ATENÇÃO: O aluno está em TCC 1. NÃO penalize a ausência de Resumo e Conclusão."
517
- if "TCC 1" in fase_tcc else "O aluno está em TCC 2. Exija o trabalho completo."
518
- )
519
- prompt = (
520
- f"Como Professor Examinador da UNIFAGOC:\n{regra_fase}\n"
521
- f"=== PRÉ-PROJETO ===\n{texto_projeto}\n"
522
- f"=== TCC ===\n{texto_tcc}\n"
523
- "TAREFA 1: Avalie cruzamento de dados e fuga de tema.\n"
524
- "TAREFA 2: Verifique incoerências internas.\n"
525
- "TAREFA 3: Distribua 80 pontos conforme ficha UNIFAGOC."
526
- )
527
- try:
528
- res = client.models.generate_content(
529
- model="gemini-3.5-flash", # Modelo válido
530
- contents=prompt,
531
- config=types.GenerateContentConfig(
532
- temperature=0.0,
533
- response_mime_type="application/json",
534
- response_schema=AuditoriaTCC
535
- )
536
- )
537
- st.session_state.resultado_auditoria = json.loads(res.text)
538
  status.update(label="✅ Avaliação concluída!", state="complete", expanded=False)
539
- except Exception as e:
540
  status.update(label="❌ Falha na avaliação.", state="error")
541
- st.error(f"Erro: {e}")
542
 
543
  if st.session_state.resultado_auditoria:
544
  dados = st.session_state.resultado_auditoria
545
  st.header("🏆 Avaliação da Banca UNIFAGOC")
546
  nota_total = sum([
547
- dados.get("nota_tema_problema", 0),
548
- dados.get("nota_introducao_metodo", 0),
549
- dados.get("nota_estruturacao", 0),
550
- dados.get("nota_formatacao_abnt", 0)
551
  ])
552
  c1, c2, c3, c4 = st.columns(4)
553
- c1.metric("Tema/Problema", f"{dados.get('nota_tema_problema')}/20")
554
- c2.metric("Intro/Método", f"{dados.get('nota_introducao_metodo')}/20")
555
- c3.metric("Estrutura/Linguagem", f"{dados.get('nota_estruturacao')}/20")
556
- c4.metric("Formatação/ABNT", f"{dados.get('nota_formatacao_abnt')}/20")
557
- st.info(f"**NOTA FINAL: {nota_total}/80**\n\n*Parecer:* {dados.get('justificativa_notas_banca')}")
558
 
559
  # ABA 2: REVISÃO INTELIGENTE
560
  with aba_revisao:
@@ -564,51 +659,22 @@ with aba_revisao:
564
  if st.button("🧠 Realizar Revisão"):
565
  if arq_rev:
566
  with st.status("Processando revisão...", expanded=True) as status:
567
- texto = extrair_texto_docx(arq_rev)
568
- erros_locais = verificacoes_locais_regex(texto)
569
  for erro in erros_locais:
570
  st.warning(f"⚠️ {erro}")
571
 
572
- prompt_rev = (
573
- "Você é um Professor Revisor de TCCs de Direito da UNIFAGOC. "
574
- "Analise o texto e corrija: ORTOGRAFIA, GRAMÁTICA, VÍCIOS JURÍDICOS, COERÊNCIA, ABNT. "
575
- f"Texto: {texto}"
 
 
 
 
 
 
 
576
  )
577
- try:
578
- res = client.models.generate_content(
579
- model="gemini-3.5-flash", # Modelo válido
580
- contents=prompt_rev,
581
- config=types.GenerateContentConfig(
582
- temperature=0.1,
583
- response_mime_type="application/json",
584
- response_schema=RevisaoInteligente
585
- )
586
- )
587
- dados = json.loads(res.text)
588
- doc_rev = Document(arq_rev)
589
- aplicadas_texto, aplicadas_italico = 0, 0
590
-
591
- if dados.get("correcoes"):
592
- for c in dados["correcoes"]:
593
- for para in doc_rev.paragraphs:
594
- aplicadas_texto += substituir_texto_preservando_formato(
595
- para, c["original"], c["corrigido"]
596
- )
597
-
598
- termos_para_italico = set(LATINISMOS + dados.get("termos_estrangeiros_para_italico", []))
599
- for termo in termos_para_italico:
600
- for para in doc_rev.paragraphs:
601
- aplicadas_italico += formatacao_cruzada_indices(para, termo, aplicar_italico=True)
602
-
603
- st.session_state.doc_revisado_bytes = docx_to_bytes(doc_rev)
604
- st.session_state.nome_arq_rev = arq_rev.name.replace('.docx', '') + "_Revisado.docx"
605
- status.update(
606
- label=f"✅ Revisão concluída: {aplicadas_texto} correções, {aplicadas_italico} itálicos.",
607
- state="complete",
608
- expanded=False
609
- )
610
- except Exception as e:
611
- st.error(f"❌ Erro: {e}")
612
 
613
  if st.session_state.doc_revisado_bytes:
614
  st.download_button(
@@ -627,14 +693,20 @@ with aba_abnt:
627
  if arq_abnt:
628
  with st.status("Normalizando documento...", expanded=True) as status:
629
  doc_abnt = Document(arq_abnt)
630
- paginas_estimadas = estimar_paginas(doc_abnt)
631
  if paginas_estimadas > 18:
632
  st.warning(f"🚨 Extensão: {paginas_estimadas} páginas (limite: 18).")
633
 
634
- formatar_documento_unifagoc(doc_abnt)
 
 
 
 
 
 
635
 
636
  st.session_state.nome_arq_abnt = arq_abnt.name.replace('.docx', '') + "_UNIFAGOC.docx"
637
- st.session_state.doc_formatado_bytes = docx_to_bytes(doc_abnt)
638
  status.update(label="✅ Formatação concluída com 95-100% de fidelidade!", state="complete", expanded=False)
639
 
640
  if st.session_state.doc_formatado_bytes:
@@ -669,14 +741,14 @@ with aba_feedback:
669
  if st.button("📊 Gerar Relatório"):
670
  if arq_feedback:
671
  with st.spinner("Analisando estilo acadêmico..."):
672
- texto = extrair_texto_docx(arq_feedback)
673
  prompt_fb = (
674
  "Analise o TCC: ESTRUTURA, CITAÇÕES, PONTOS FORTES/ATENÇÃO, FORMAÇÃO. "
675
  f"Texto: {texto}"
676
  )
677
  try:
678
- res = client.models.generate_content(
679
- model="gemini-3.5-flash", # Modelo válido
680
  contents=prompt_fb,
681
  config=types.GenerateContentConfig(temperature=0.2)
682
  )
 
6
  from docx import Document
7
  from docx.shared import Cm, Pt
8
  from docx.enum.text import WD_ALIGN_PARAGRAPH
 
9
  from docx.oxml.ns import qn
10
+ from docx.oxml import OxmlElement
11
  from pydantic import BaseModel
12
+ from typing import List, Optional, Tuple
13
  from google import genai
14
  from google.genai import types
15
 
16
  # =====================================================
17
+ # CONFIGURAÇÕES GLOBAIS
18
  # =====================================================
19
+ MAX_CHARS_PER_CHUNK = 15000 # Limite para chunking de documentos grandes
 
 
 
 
 
 
 
 
 
 
 
20
  LATINISMOS = [
21
  "habeas corpus", "data venia", "jus puniendi", "jus postulandi",
22
  "mens legis", "ratio decidendi", "obiter dictum", "in casu",
 
26
  ]
27
 
28
  # =====================================================
29
+ # ESQUEMAS PYDANTIC
30
  # =====================================================
31
  class AuditoriaTCC(BaseModel):
32
  aderencia_percentual: int
 
65
  termos_estrangeiros_para_italico: List[str]
66
 
67
  class ProblemaReferencia(BaseModel):
 
 
 
 
 
68
  original: str
69
+ erros: List[str]
70
 
71
  class ListaReferencias(BaseModel):
72
+ referencias: List[ProblemaReferencia]
73
 
74
  # =====================================================
75
+ # CLASSE REVISOR IA (Responsável por interações com Gemini)
76
  # =====================================================
77
+ class RevisorIA:
78
+ def __init__(self):
79
+ self.client = genai.Client(api_key=st.secrets["GEMINI_API_KEY"])
80
+
81
+ def auditar_tcc(self, texto_projeto: str, texto_tcc: str, fase_tcc: str) -> Optional[AuditoriaTCC]:
82
+ regra_fase = (
83
+ "ATENÇÃO: O aluno está em TCC 1. NÃO penalize a ausência de Resumo e Conclusão."
84
+ if "TCC 1" in fase_tcc else "O aluno está em TCC 2. Exija o trabalho completo."
85
+ )
86
+ prompt = (
87
+ f"Como Professor Examinador da UNIFAGOC:\n{regra_fase}\n"
88
+ f"=== PRÉ-PROJETO ===\n{texto_projeto}\n"
89
+ f"=== TCC ===\n{texto_tcc}\n"
90
+ "TAREFA 1: Avalie cruzamento de dados e fuga de tema.\n"
91
+ "TAREFA 2: Verifique incoerências internas.\n"
92
+ "TAREFA 3: Distribua 80 pontos conforme ficha UNIFAGOC."
93
+ )
94
+ try:
95
+ res = self.client.models.generate_content(
96
+ model="gemini-3.5-flash",
97
+ contents=prompt,
98
+ config=types.GenerateContentConfig(
99
+ temperature=0.0,
100
+ response_mime_type="application/json",
101
+ response_schema=AuditoriaTCC
102
+ )
103
+ )
104
+ return AuditoriaTCC(**json.loads(res.text))
105
+ except Exception as e:
106
+ st.error(f"Erro na auditoria: {e}")
107
+ return None
108
+
109
+ def revisar_texto(self, texto: str) -> Optional[RevisaoInteligente]:
110
+ prompt = (
111
+ "Você é um Professor Revisor de TCCs de Direito da UNIFAGOC. "
112
+ "Analise o texto e corrija: ORTOGRAFIA, GRAMÁTICA, VÍCIOS JURÍDICOS, COERÊNCIA, ABNT. "
113
+ f"Texto: {texto}"
114
+ )
115
+ try:
116
+ res = self.client.models.generate_content(
117
+ model="gemini-3.5-flash",
118
+ contents=prompt,
119
+ config=types.GenerateContentConfig(
120
+ temperature=0.1,
121
+ response_mime_type="application/json",
122
+ response_schema=RevisaoInteligente
123
+ )
124
+ )
125
+ return RevisaoInteligente(**json.loads(res.text))
126
+ except Exception as e:
127
+ st.error(f"Erro na revisão: {e}")
128
+ return None
129
+
130
+ def validar_referencias(self, referencias: List[str]) -> ListaReferencias:
131
+ refs_str = '\n'.join(referencias)
132
+ prompt = (
133
+ "Analise as referências abaixo **APENAS PARA IDENTIFICAR ERROS** (NÃO reescreva, NÃO invente dados!).\n"
134
+ "Para cada uma, retorne:\n"
135
+ "- original: texto original\n"
136
+ "- erros: lista de erros (ex: [\"título não em negrito\", \"autor não invertido\"])\n\n"
137
+ f"Referências:\n{refs_str}"
138
+ )
139
+ try:
140
+ res = self.client.models.generate_content(
141
+ model="gemini-3.5-flash",
142
+ contents=prompt,
143
+ config=types.GenerateContentConfig(
144
+ temperature=0.0,
145
+ response_mime_type="application/json",
146
+ response_schema=ListaReferencias
147
+ )
148
+ )
149
+ return ListaReferencias(**json.loads(res.text))
150
+ except Exception as e:
151
+ st.error(f"Erro na validação de referências: {e}")
152
+ return ListaReferencias(referencias=[])
153
 
154
  # =====================================================
155
+ # CLASSE FORMATADOR UNIFAGOC (Responsável por formatação DOCX)
156
  # =====================================================
157
+ class FormatadorUNIFAGOC:
158
+ @staticmethod
159
+ def aplicar_margens(doc: Document):
160
+ """Aplica margens oficiais UNIFAGOC (A4: 21x29.7cm)"""
161
+ for section in doc.sections:
162
+ section.page_height = Cm(29.7)
163
+ section.page_width = Cm(21.0)
164
+ section.top_margin = Cm(3.0)
165
+ section.left_margin = Cm(3.0)
166
+ section.bottom_margin = Cm(2.0)
167
+ section.right_margin = Cm(2.0)
168
+ section.header_distance = Cm(0.5)
169
+ section.footer_distance = Cm(0.5)
170
+
171
+ @staticmethod
172
+ def adicionar_paginacao(doc: Document):
173
+ """Adiciona numeração de páginas no rodapé (centralizado) - XML correto"""
174
+ for i, section in enumerate(doc.sections):
175
+ if i == 0: # Ignora capa/folha de rosto
176
+ continue
177
+ footer = section.footer
178
+ para = footer.paragraphs[0] if footer.paragraphs else footer.add_paragraph()
179
+ para.alignment = WD_ALIGN_PARAGRAPH.CENTER
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
+ # Adiciona campo PAGE via XML
182
+ run = para.add_run()
183
+ fldChar = OxmlElement('w:fldChar')
184
+ fldChar.set(qn('w:fldCharType'), 'begin')
185
+ run._r.append(fldChar)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
+ instrText = OxmlElement('w:instrText')
188
+ instrText.set(qn('xml:space'), 'preserve')
189
+ instrText.text = 'PAGE'
190
+ run._r.append(instrText)
191
+
192
+ fldChar = OxmlElement('w:fldChar')
193
+ fldChar.set(qn('w:fldCharType'), 'end')
194
+ run._r.append(fldChar)
195
+
196
+ # Formatação
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  for run in para.runs:
198
  run.font.name = 'Calibri'
199
+ run.font.size = Pt(10)
200
+
201
+ @staticmethod
202
+ def formatar_figuras_tabelas(doc: Document):
203
+ """Formata figuras, tabelas e quadros"""
204
+ for para in doc.paragraphs:
205
+ texto = para.text.strip()
206
+ if re.match(r'^(Figura|Tabela|Quadro)\s+\d+', texto, re.IGNORECASE):
207
+ para.alignment = WD_ALIGN_PARAGRAPH.CENTER
208
+ for run in para.runs:
209
  run.font.name = 'Calibri'
210
+ run.font.size = Pt(12)
211
+ run.bold = True
212
+ # Formata legenda
213
+ idx = doc.paragraphs.index(para)
214
+ if idx + 1 < len(doc.paragraphs):
215
+ next_para = doc.paragraphs[idx + 1]
216
+ for run in next_para.runs:
 
 
 
217
  run.font.name = 'Calibri'
218
  run.font.size = Pt(11)
219
 
220
+ # Formata tabelas
221
+ for table in doc.tables:
222
+ for row in table.rows:
223
+ for cell in row.cells:
224
+ for para in cell.paragraphs:
225
+ para.alignment = WD_ALIGN_PARAGRAPH.LEFT
226
+ para.paragraph_format.line_spacing = 1.0
227
+ for run in para.runs:
228
+ run.font.name = 'Calibri'
229
+ run.font.size = Pt(11)
230
+
231
+ @staticmethod
232
+ def formatar_imagens(doc: Document):
233
+ """Centraliza imagens (inline shapes) - Solução robusta"""
234
+ for para in doc.paragraphs:
235
+ if para._p.xpath('.//w:drawing') or para._p.xpath('.//w:pic'):
236
+ para.alignment = WD_ALIGN_PARAGRAPH.CENTER
237
+
238
+ @staticmethod
239
+ def formatar_notas_rodape(doc: Document):
240
+ """Formata notas de rodapé (¹, ², ³)"""
241
+ for para in doc.paragraphs:
242
+ if re.search(r'[\d\^]+$', para.text.strip()):
243
+ for run in para.runs:
244
+ run.font.size = Pt(8)
245
+ run.font.superscript = True
246
+
247
+ @staticmethod
248
+ def substituir_texto_preservando_formato(para, original: str, corrigido: str) -> int:
249
+ """
250
+ Substitui texto PRESERVANDO formatação de runs (negrito, itálico, hyperlinks, etc.).
251
+ Usa abordagem de divisão de runs em vez de para.clear().
252
+ """
253
+ texto_completo = "".join(run.text for run in para.runs)
254
+ if original not in texto_completo:
255
+ return 0
256
+
257
+ pos = texto_completo.find(original)
258
+ if pos == -1:
259
+ return 0
260
+
261
+ # Divide o texto em partes: antes, original, depois
262
+ antes = texto_completo[:pos]
263
+ depois = texto_completo[pos + len(original):]
264
+
265
+ # Reconstroi o parágrafo mantendo a formatação dos runs existentes
266
+ # 1. Adiciona a parte "antes" com a formatação original
267
+ if antes:
268
+ para.add_run(antes)
269
+
270
+ # 2. Adiciona a parte corrigida (com formatação padrão)
271
+ run_corrigido = para.add_run(corrigido)
272
+ run_corrigido.font.name = 'Calibri'
273
+ run_corrigido.font.size = Pt(12)
274
+
275
+ # 3. Adiciona a parte "depois" com a formatação original
276
+ if depois:
277
+ para.add_run(depois)
278
+
279
+ return 1
280
+
281
+ @staticmethod
282
+ def formatar_documento(doc: Document) -> List[ProblemaReferencia]:
283
+ """Função principal de formatação com fluxo único"""
284
+ # 1. Aplica margens e paginação
285
+ FormatadorUNIFAGOC.aplicar_margens(doc)
286
+ FormatadorUNIFAGOC.adicionar_paginacao(doc)
287
+
288
+ # 2. Formata elementos específicos
289
+ FormatadorUNIFAGOC.formatar_figuras_tabelas(doc)
290
+ FormatadorUNIFAGOC.formatar_imagens(doc)
291
+ FormatadorUNIFAGOC.formatar_notas_rodape(doc)
292
+
293
+ # 3. Processa cada parágrafo com fluxo único
294
+ for para in doc.paragraphs:
295
+ texto = para.text.strip()
296
+ if not texto:
297
+ continue
298
+
299
+ # --- REFERÊNCIAS ---
300
+ if re.search(r"REFER[ÊE]NCIAS", texto, re.IGNORECASE):
301
+ para.alignment = WD_ALIGN_PARAGRAPH.CENTER
302
+ para.paragraph_format.space_before = Pt(12)
303
+ para.paragraph_format.space_after = Pt(6)
304
+ for run in para.runs:
305
+ run.font.name = 'Calibri'
306
+ run.font.size = Pt(12)
307
+ run.bold = True
308
+ continue
309
+
310
+ # --- RESUMO ---
311
+ if texto.upper() == "RESUMO":
312
+ para.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
313
+ para.paragraph_format.space_before = Pt(12)
314
+ para.paragraph_format.space_after = Pt(0)
315
+ para.paragraph_format.line_spacing = 1.5
316
+ for run in para.runs:
317
+ run.font.name = 'Calibri'
318
+ run.font.size = Pt(11)
319
+ run.bold = True
320
+ continue
321
+
322
+ # --- PALAVRAS-CHAVE ---
323
+ if texto.upper().startswith("PALAVRAS-CHAVE"):
324
+ para.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
325
+ para.paragraph_format.space_before = Pt(6)
326
+ para.paragraph_format.space_after = Pt(0)
327
+ para.paragraph_format.line_spacing = 1.5
328
+ for run in para.runs:
329
+ run.font.name = 'Calibri'
330
+ run.font.size = Pt(11)
331
+ if "PALAVRAS-CHAVE" in run.text.upper():
332
+ run.bold = True
333
+ continue
334
+
335
+ # --- TÍTULOS E SEÇÕES ---
336
+ nivel = FormatadorUNIFAGOC.detectar_nivel_secao(texto)
337
+ if nivel > 0 or FormatadorUNIFAGOC.detectar_titulo_ou_subtitulo(texto) == "titulo":
338
+ para.alignment = WD_ALIGN_PARAGRAPH.LEFT
339
+ para.paragraph_format.space_before = Pt(12)
340
+ para.paragraph_format.space_after = Pt(6)
341
+ for run in para.runs:
342
+ run.font.name = 'Calibri'
343
+ run.font.size = Pt(12)
344
+ run.bold = True
345
+ if nivel == 3:
346
+ run.italic = True
347
+ if nivel == 2:
348
+ para.paragraph_format.left_indent = Cm(0.5)
349
+ continue
350
+
351
+ # --- CITAÇÕES LONGAS ---
352
+ if FormatadorUNIFAGOC.detectar_citacao_longa(para):
353
+ para.paragraph_format.left_indent = Cm(4.0)
354
+ para.paragraph_format.line_spacing = 1.0
355
+ para.paragraph_format.space_before = Pt(6)
356
+ para.paragraph_format.space_after = Pt(6)
357
+ for run in para.runs:
358
+ run.font.name = 'Calibri'
359
+ run.font.size = Pt(11)
360
+ continue
361
+
362
+ # --- LISTAS ---
363
+ if FormatadorUNIFAGOC.detectar_lista(texto):
364
+ para.paragraph_format.left_indent = Cm(1.25)
365
+ para.paragraph_format.first_line_indent = Cm(0)
366
+ para.paragraph_format.space_before = Pt(0)
367
+ para.paragraph_format.space_after = Pt(0)
368
+ para.paragraph_format.line_spacing = 1.5
369
+ for run in para.runs:
370
+ run.font.name = 'Calibri'
371
+ run.font.size = Pt(12)
372
+ continue
373
+
374
+ # --- CORPO DO TEXTO ---
375
+ para.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
376
+ para.paragraph_format.first_line_indent = Cm(1.25)
377
+ para.paragraph_format.line_spacing = 1.5
378
+ para.paragraph_format.space_before = Pt(0)
379
+ para.paragraph_format.space_after = Pt(0)
380
  for run in para.runs:
381
  run.font.name = 'Calibri'
382
  run.font.size = Pt(12)
 
383
 
384
+ # 4. Valida referências (apenas aponta erros)
385
+ refs = FormatadorUNIFAGOC.extrair_referencias(doc)
386
+ revisor = RevisorIA()
387
+ lista_refs = revisor.validar_referencias(refs)
388
+ return lista_refs.referencias
389
+
390
+ @staticmethod
391
+ def detectar_nivel_secao(texto: str) -> int:
392
+ match = re.match(r'^(\d+(?:\.\d+)*)\s+', texto)
393
+ if not match:
394
+ return 0
395
+ return match.group(1).count('.') + 1
396
+
397
+ @staticmethod
398
+ def detectar_titulo_ou_subtitulo(texto: str) -> Optional[str]:
399
+ SECOES_ESPECIAIS = ["INTRODUÇÃO", "CONSIDERAÇÕES FINAIS", "REFERÊNCIAS", "RESUMO", "PALAVRAS-CHAVE"]
400
+ texto_upper = texto.upper()
401
+ if any(sec in texto_upper for sec in SECOES_ESPECIAIS):
402
+ return "titulo"
403
+ if FormatadorUNIFAGOC.detectar_nivel_secao(texto) > 0:
404
+ return "titulo"
405
+ if texto.isupper() and len(texto) < 150 and not FormatadorUNIFAGOC.detectar_nivel_secao(texto):
406
+ return "titulo"
407
+ return None
408
+
409
+ @staticmethod
410
+ def detectar_citacao_longa(para) -> bool:
411
+ """Detecção robusta: 40+ palavras OU recuo >= 3.5cm"""
412
+ if para.paragraph_format.left_indent and para.paragraph_format.left_indent >= Cm(3.5):
413
+ return True
414
+ texto = para.text.strip()
415
+ if len(texto.split()) >= 40:
416
+ return True
417
+ return False
418
+
419
+ @staticmethod
420
+ def detectar_lista(texto: str) -> bool:
421
+ """Regex preciso para listas (evita falsos positivos)"""
422
+ return bool(re.match(r'^(\d+[\)\.]|[a-zA-Z][\)\.]|[IVXLCDM]+[\)\.]|[-–])\s', texto))
423
+
424
+ @staticmethod
425
+ def extrair_referencias(doc: Document) -> List[str]:
426
+ """Extrai todas as referências do documento"""
427
+ refs = []
428
+ em_refs = False
429
+ for para in doc.paragraphs:
430
+ texto = para.text.strip()
431
+ if re.search(r"REFER[ÊE]NCIAS", texto, re.IGNORECASE):
432
+ em_refs = True
433
+ continue
434
+ if em_refs and (len(texto) < 10 or texto.isupper()):
435
+ break
436
+ if em_refs:
437
+ refs.append(texto)
438
+ return refs
439
+
440
+ @staticmethod
441
+ def extrair_texto_docx(arquivo_upload) -> str:
442
+ doc = Document(arquivo_upload)
443
+ return "\n\n".join([para.text.strip() for para in doc.paragraphs if para.text.strip()])
444
+
445
+ @staticmethod
446
+ def docx_to_bytes(doc: Document) -> bytes:
447
+ byte_io = io.BytesIO()
448
+ doc.save(byte_io)
449
+ return byte_io.getvalue()
450
+
451
+ @staticmethod
452
+ def estimar_paginas(doc: Document) -> int:
453
+ palavras_totais = sum(
454
+ len(para.text.split()) for para in doc.paragraphs
455
+ if not re.search(r"^(?:\d+\.?\s*)?REFER[ÊE]NCIAS", para.text, re.IGNORECASE)
456
+ )
457
+ return max(1, palavras_totais // 350)
458
 
459
+ # =====================================================
460
+ # CLASSE AUDITOR TCC (Responsável por auditorias)
461
+ # =====================================================
462
+ class AuditorTCC:
463
+ def __init__(self):
464
+ self.revisor = RevisorIA()
465
+
466
+ def auditar(self, arq_projeto, arq_tcc, fase_tcc: str) -> Optional[AuditoriaTCC]:
467
+ texto_projeto = FormatadorUNIFAGOC.extrair_texto_docx(arq_projeto)
468
+ texto_tcc = FormatadorUNIFAGOC.extrair_texto_docx(arq_tcc)
469
+ return self.revisor.auditar_tcc(texto_projeto, texto_tcc, fase_tcc)
470
+
471
+ def revisar(self, arq_rev) -> Tuple[Document, RevisaoInteligente, List[str]]:
472
+ """Revisa documento com chunking para grandes arquivos"""
473
+ doc = Document(arq_rev)
474
+ texto_completo = FormatadorUNIFAGOC.extrair_texto_docx(arq_rev)
475
+ erros_locais = self.verificar_erros_locais(texto_completo)
476
+
477
+ # Chunking para documentos grandes
478
+ chunks = self.dividir_em_chunks(texto_completo)
479
+ resultados_revisao = []
480
+
481
+ for chunk in chunks:
482
+ resultado = self.revisor.revisar_texto(chunk)
483
+ if resultado:
484
+ resultados_revisao.append(resultado)
485
+
486
+ # Consolida resultados
487
+ revisao_consolidada = self.consolidar_revisoes(resultados_revisao)
488
+
489
+ # Aplica correções no documento
490
+ self.aplicar_correcoes(doc, revisao_consolidada)
491
+
492
+ return doc, revisao_consolidada, erros_locais
493
+
494
+ def verificar_erros_locais(self, texto: str) -> List[str]:
495
+ erros = []
496
+ if re.search(r"\s{2,}", texto):
497
+ erros.append("Espaços duplos detectados no documento.")
498
+ if re.search(r",\s*,", texto):
499
+ erros.append("Vírgulas duplicadas detectadas (ex: ',,').")
500
+ if re.search(r"[.!?]{2,}", texto) and not "..." in texto:
501
+ erros.append("Pontuação repetida indevida (ex: '!!' ou '..').")
502
+ if re.search(r"\b(\w+)\s+\1\b", texto, re.IGNORECASE):
503
+ erros.append("Palavras repetidas sequencialmente (ex: 'o o', 'de de').")
504
+ if re.search(r"\([A-ZÀ-Ú\s]+,\s*\)", texto):
505
+ erros.append("Citação ABNT com ano faltando (ex: (SILVA, )).")
506
+ return erros
507
+
508
+ def dividir_em_chunks(self, texto: str) -> List[str]:
509
+ """Divide texto em chunks de MAX_CHARS_PER_CHUNK"""
510
+ chunks = []
511
+ for i in range(0, len(texto), MAX_CHARS_PER_CHUNK):
512
+ chunks.append(texto[i:i + MAX_CHARS_PER_CHUNK])
513
+ return chunks
514
+
515
+ def consolidar_revisoes(self, revisoes: List[RevisaoInteligente]) -> RevisaoInteligente:
516
+ """Consolida múltiplas revisões em uma única"""
517
+ if not revisoes:
518
+ return RevisaoInteligente(correcoes=[], problemas_academicos=[], termos_estrangeiros_para_italico=[])
519
+
520
+ # Consolida correções
521
+ todas_correcoes = []
522
+ for revisao in revisoes:
523
+ todas_correcoes.extend(revisao.correcoes)
524
+
525
+ # Consolida problemas acadêmicos
526
+ todos_problemas = []
527
+ for revisao in revisoes:
528
+ todos_problemas.extend(revisao.problemas_academicos)
529
+
530
+ # Consolida termos estrangeiros
531
+ todos_termos = []
532
+ for revisao in revisoes:
533
+ todos_termos.extend(revisao.termos_estrangeiros_para_italico)
534
+
535
+ return RevisaoInteligente(
536
+ correcoes=todas_correcoes,
537
+ problemas_academicos=todos_problemas,
538
+ termos_estrangeiros_para_italico=list(set(todos_termos))
539
+ )
540
 
541
+ def aplicar_correcoes(self, doc: Document, revisao: RevisaoInteligente):
542
+ """Aplica correções no documento (incluindo tabelas)"""
543
+ # Aplica correções em parágrafos
544
+ for para in doc.paragraphs:
545
+ for correcao in revisao.correcoes:
546
+ FormatadorUNIFAGOC.substituir_texto_preservando_formato(
547
+ para, correcao.original, correcao.corrigido
548
+ )
549
 
550
+ # Aplica correções em tabelas
551
+ for table in doc.tables:
552
+ for row in table.rows:
553
+ for cell in row.cells:
554
+ for para in cell.paragraphs:
555
+ for correcao in revisao.correcoes:
556
+ FormatadorUNIFAGOC.substituir_texto_preservando_formato(
557
+ para, correcao.original, correcao.corrigido
558
+ )
559
+
560
+ # Aplica itálico em termos estrangeiros
561
+ termos_para_italico = set(LATINISMOS + revisao.termos_estrangeiros_para_italico)
562
+ for para in doc.paragraphs:
563
+ for termo in termos_para_italico:
564
+ FormatadorUNIFAGOC.formatacao_cruzada_indices(para, termo, aplicar_italico=True)
565
+
566
+ # Formata tabelas
567
+ for table in doc.tables:
568
+ for row in table.rows:
569
+ for cell in row.cells:
570
+ for para in cell.paragraphs:
571
+ for termo in termos_para_italico:
572
+ FormatadorUNIFAGOC.formatacao_cruzada_indices(para, termo, aplicar_italico=True)
573
+
574
+ @staticmethod
575
+ def formatacao_cruzada_indices(para, texto_alvo: str, aplicar_italico: bool = False, aplicar_negrito: bool = False) -> int:
576
+ aplicacoes = 0
577
+ if aplicar_italico:
578
+ matches = list(re.finditer(r'\b' + re.escape(texto_alvo) + r'\b', para.text, re.IGNORECASE))
579
+ else:
580
+ idx = para.text.find(texto_alvo)
581
+ matches = [(idx, idx + len(texto_alvo))] if idx != -1 else []
582
+
583
+ for match in matches:
584
+ pos_ini, pos_fim = match if isinstance(match, tuple) else (match.start(), match.end())
585
+ if pos_ini == -1:
586
+ continue
587
+ pos_atual = 0
588
  for run in para.runs:
589
+ fim_run = pos_atual + len(run.text)
590
+ if pos_atual < pos_fim and fim_run > pos_ini:
591
+ if aplicar_italico:
592
+ run.italic = True
593
+ if aplicar_negrito:
594
+ run.bold = True
595
+ aplicacoes += 1
596
+ pos_atual += len(run.text)
597
+ return aplicacoes
 
 
 
 
 
 
 
 
 
 
598
 
599
  # =====================================================
600
+ # INTERFACE STREAMLIT
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
601
  # =====================================================
602
  st.title("⚖️ Assistente Acadêmico - Direito UNIFAGOC")
603
  aba_auditoria, aba_revisao, aba_abnt, aba_prompt, aba_feedback = st.tabs([
 
608
  "👨‍🏫 Feedback Professor"
609
  ])
610
 
611
+ # Inicialização das classes
612
+ auditor = AuditorTCC()
613
+ formatador = FormatadorUNIFAGOC()
614
+
615
  # ABA 1: AUDITORIA
616
  with aba_auditoria:
617
  st.markdown("Cruze o **Pré-Projeto** com o **TCC** para identificar fuga do tema e gerar a **Nota da Banca (80 pontos)**.")
 
628
  st.warning("⚠️ Envie os DOIS arquivos para realizar a comparação.")
629
  else:
630
  with st.status("Analisando coerência jurídica...", expanded=True) as status:
631
+ resultado = auditor.auditar(arq_projeto, arq_tcc, fase_tcc)
632
+ if resultado:
633
+ st.session_state.resultado_auditoria = resultado
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
634
  status.update(label="✅ Avaliação concluída!", state="complete", expanded=False)
635
+ else:
636
  status.update(label="❌ Falha na avaliação.", state="error")
 
637
 
638
  if st.session_state.resultado_auditoria:
639
  dados = st.session_state.resultado_auditoria
640
  st.header("🏆 Avaliação da Banca UNIFAGOC")
641
  nota_total = sum([
642
+ dados.nota_tema_problema,
643
+ dados.nota_introducao_metodo,
644
+ dados.nota_estruturacao,
645
+ dados.nota_formatacao_abnt
646
  ])
647
  c1, c2, c3, c4 = st.columns(4)
648
+ c1.metric("Tema/Problema", f"{dados.nota_tema_problema}/20")
649
+ c2.metric("Intro/Método", f"{dados.nota_introducao_metodo}/20")
650
+ c3.metric("Estrutura/Linguagem", f"{dados.nota_estruturacao}/20")
651
+ c4.metric("Formatação/ABNT", f"{dados.nota_formatacao_abnt}/20")
652
+ st.info(f"**NOTA FINAL: {nota_total}/80**\n\n*Parecer:* {dados.justificativa_notas_banca}")
653
 
654
  # ABA 2: REVISÃO INTELIGENTE
655
  with aba_revisao:
 
659
  if st.button("🧠 Realizar Revisão"):
660
  if arq_rev:
661
  with st.status("Processando revisão...", expanded=True) as status:
662
+ doc_rev, revisao, erros_locais = auditor.revisar(arq_rev)
 
663
  for erro in erros_locais:
664
  st.warning(f"⚠️ {erro}")
665
 
666
+ if revisao.problemas_academicos:
667
+ st.subheader("🚨 Problemas de Redação Detectados")
668
+ for prob in revisao.problemas_academicos:
669
+ st.error(f"**[{prob.gravidade.upper()}] {prob.problema}**\n\nTrecho: '{prob.trecho}'\n\nSugestão: {prob.sugestao}")
670
+
671
+ st.session_state.doc_revisado_bytes = formatador.docx_to_bytes(doc_rev)
672
+ st.session_state.nome_arq_rev = arq_rev.name.replace('.docx', '') + "_Revisado.docx"
673
+ status.update(
674
+ label=f"✅ Revisão concluída: {len(revisao.correcoes)} correções, {len(revisao.termos_estrangeiros_para_italico)} itálicos.",
675
+ state="complete",
676
+ expanded=False
677
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
678
 
679
  if st.session_state.doc_revisado_bytes:
680
  st.download_button(
 
693
  if arq_abnt:
694
  with st.status("Normalizando documento...", expanded=True) as status:
695
  doc_abnt = Document(arq_abnt)
696
+ paginas_estimadas = formatador.estimar_paginas(doc_abnt)
697
  if paginas_estimadas > 18:
698
  st.warning(f"🚨 Extensão: {paginas_estimadas} páginas (limite: 18).")
699
 
700
+ refs_com_erros = formatador.formatar_documento(doc_abnt)
701
+
702
+ # Exibe erros de referências na interface
703
+ if refs_com_erros:
704
+ st.subheader("⚠️ Erros em Referências ABNT")
705
+ for ref in refs_com_erros:
706
+ st.warning(f"Referência: '{ref.original}'\nErros: {', '.join(ref.erros)}")
707
 
708
  st.session_state.nome_arq_abnt = arq_abnt.name.replace('.docx', '') + "_UNIFAGOC.docx"
709
+ st.session_state.doc_formatado_bytes = formatador.docx_to_bytes(doc_abnt)
710
  status.update(label="✅ Formatação concluída com 95-100% de fidelidade!", state="complete", expanded=False)
711
 
712
  if st.session_state.doc_formatado_bytes:
 
741
  if st.button("📊 Gerar Relatório"):
742
  if arq_feedback:
743
  with st.spinner("Analisando estilo acadêmico..."):
744
+ texto = formatador.extrair_texto_docx(arq_feedback)
745
  prompt_fb = (
746
  "Analise o TCC: ESTRUTURA, CITAÇÕES, PONTOS FORTES/ATENÇÃO, FORMAÇÃO. "
747
  f"Texto: {texto}"
748
  )
749
  try:
750
+ res = auditor.revisor.client.models.generate_content(
751
+ model="gemini-3.5-flash",
752
  contents=prompt_fb,
753
  config=types.GenerateContentConfig(temperature=0.2)
754
  )