GabrielGD commited on
Commit
0e2d9f9
·
verified ·
1 Parent(s): 73edeed

Update app.py

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