GabrielGD commited on
Commit
1e0e93d
·
verified ·
1 Parent(s): d6d318c

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +676 -0
app.py ADDED
@@ -0,0 +1,676 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ import gradio as gr
3
+ import requests
4
+ import os
5
+ import json
6
+ import tempfile
7
+ import PyPDF2
8
+ import docx
9
+ import io
10
+ import time
11
+ from dotenv import load_dotenv
12
+ from pdfplumber import open as pdfplumber_open
13
+ from PIL import Image
14
+ import pytesseract
15
+ import cv2
16
+ import numpy as np
17
+ from datetime import datetime
18
+
19
+ # Carrega variáveis de ambiente de um arquivo .env (opcional para dev local)
20
+ load_dotenv()
21
+
22
+ # Obtém a chave da API do ambiente (no Hugging Face, configure como Secret)
23
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
24
+ if not GEMINI_API_KEY:
25
+ raise ValueError("❌ A chave GEMINI_API_KEY não foi encontrada. Configure-a como Secret no Hugging Face Spaces.")
26
+
27
+ # URL da API Gemini
28
+ GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={GEMINI_API_KEY}"
29
+
30
+ # Configurações da aplicação
31
+ CONFIG = {
32
+ "max_retry_attempts": 3,
33
+ "retry_delay": 2,
34
+ "timeout": 60,
35
+ "temperatura": 0.2,
36
+ "max_tokens": 4096
37
+ }
38
+
39
+ # Banco de dicas contextuais
40
+ DICAS_OAB = {
41
+ "Endereçamento": "Em relações de consumo, prefira o foro do domicílio do autor (art. 101, I, CDC).",
42
+ "Qualificação": "Mencione nome, estado civil (se informado), endereço (use 'Rua XXX' se não fornecido).",
43
+ "Fatos": "Descreva os fatos cronologicamente, sem inventar informações.",
44
+ "Pedidos": "Seja específico nos pedidos (ex.: 'condenação ao pagamento de R$ X').",
45
+ "Custas": "Inclua 'Requer a juntada da guia de custas devidamente recolhida'."
46
+ }
47
+
48
+ # Histórico de correções
49
+ HISTORICO = []
50
+
51
+ def preprocess_image_for_ocr(img):
52
+ try:
53
+ gray = img.convert('L')
54
+ np_img = np.array(gray)
55
+ _, thresh = cv2.threshold(np_img, 150, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
56
+ return Image.fromarray(thresh)
57
+ except Exception as e:
58
+ return f"❌ Erro no pré-processamento: {str(e)}"
59
+
60
+ def extract_text_from_pdf(file_path_or_obj):
61
+ try:
62
+ if isinstance(file_path_or_obj, str):
63
+ with open(file_path_or_obj, 'rb') as f:
64
+ text = ""
65
+ with pdfplumber_open(f) as pdf:
66
+ for page in pdf.pages:
67
+ text += (page.extract_text() or "") + "\n"
68
+ if not text.strip():
69
+ images = pdfplumber_open(f).pages
70
+ for i, image in enumerate(images):
71
+ try:
72
+ img = image.to_image()
73
+ preprocessed_img = preprocess_image_for_ocr(img)
74
+ text += pytesseract.image_to_string(preprocessed_img) + "\n"
75
+ except Exception as e:
76
+ text += f"[Erro na página {i+1}: {str(e)}]\n"
77
+ else:
78
+ text = ""
79
+ with pdfplumber_open(file_path_or_obj) as pdf:
80
+ for page in pdf.pages:
81
+ text += (page.extract_text() or "") + "\n"
82
+ if not text.strip():
83
+ images = pdfplumber_open(file_path_or_obj).pages
84
+ for i, image in enumerate(images):
85
+ try:
86
+ img = image.to_image()
87
+ preprocessed_img = preprocess_image_for_ocr(img)
88
+ text += pytesseract.image_to_string(preprocessed_img) + "\n"
89
+ except Exception as e:
90
+ text += f"[Erro na página {i+1}: {str(e)}]\n"
91
+ return text if text.strip() else "⚠️ Não foi possível extrair texto do PDF."
92
+ except Exception as e:
93
+ return f"❌ Erro ao processar o PDF: {str(e)}"
94
+
95
+ def extract_text_from_docx(file_path_or_obj):
96
+ try:
97
+ if isinstance(file_path_or_obj, str):
98
+ with open(file_path_or_obj, 'rb') as f:
99
+ doc = docx.Document(f)
100
+ text = ""
101
+ for para in doc.paragraphs:
102
+ text += para.text + "\n"
103
+ for table in doc.tables:
104
+ for row in table.rows:
105
+ for cell in row.cells:
106
+ text += cell.text + " "
107
+ text += "\n"
108
+ else:
109
+ doc = docx.Document(file_path_or_obj)
110
+ text = ""
111
+ for para in doc.paragraphs:
112
+ text += para.text + "\n"
113
+ for table in doc.tables:
114
+ for row in table.rows:
115
+ for cell in row.cells:
116
+ text += cell.text + " "
117
+ text += "\n"
118
+ return text if text.strip() else "⚠️ Não foi possível extrair texto do DOCX."
119
+ except Exception as e:
120
+ return f"❌ Erro ao processar o DOCX: {str(e)}"
121
+
122
+ def extract_text_from_txt(file_path_or_obj):
123
+ try:
124
+ if isinstance(file_path_or_obj, str):
125
+ with open(file_path_or_obj, 'rb') as f:
126
+ content = f.read()
127
+ encodings = ['utf-8', 'latin-1', 'cp1252', 'iso-8859-1']
128
+ for encoding in encodings:
129
+ try:
130
+ return content.decode(encoding)
131
+ except UnicodeDecodeError:
132
+ continue
133
+ return "❌ Não foi possível decodificar o arquivo TXT."
134
+ else:
135
+ content = file_path_or_obj.read()
136
+ encodings = ['utf-8', 'latin-1', 'cp1252', 'iso-8859-1']
137
+ for encoding in encodings:
138
+ try:
139
+ return content.decode(encoding)
140
+ except UnicodeDecodeError:
141
+ continue
142
+ return "❌ Não foi possível decodificar o arquivo TXT."
143
+ except Exception as e:
144
+ return f"❌ Erro ao processar o TXT: {str(e)}"
145
+
146
+ def process_file(file_obj):
147
+ if file_obj is None:
148
+ return "Nenhum arquivo enviado."
149
+ try:
150
+ if hasattr(file_obj, 'name'):
151
+ file_path = file_obj.name
152
+ filename = os.path.basename(file_path).lower()
153
+ file_size = os.path.getsize(file_path)
154
+ if file_size > 10 * 1024 * 1024:
155
+ return "⚠️ O arquivo é muito grande. Máx. 10 MB."
156
+ if filename.endswith('.pdf'):
157
+ return extract_text_from_pdf(file_path)
158
+ elif filename.endswith('.docx'):
159
+ return extract_text_from_docx(file_path)
160
+ elif filename.endswith('.txt'):
161
+ return extract_text_from_txt(file_path)
162
+ else:
163
+ return "⚠️ Formato de arquivo não suportado. Use PDF, DOCX ou TXT."
164
+ else:
165
+ return "❌ Erro: Objeto de arquivo inválido."
166
+ except Exception as e:
167
+ return f"❌ Erro ao processar o arquivo: {str(e)}"
168
+
169
+ def calcular_pontuacao(enderecamento, fundamentos, pedidos, fechamento):
170
+ tabela = [
171
+ ("**Endereçamento**", ""),
172
+ ("1. Correto endereçamento ao juízo competente", f"{enderecamento[0]:.2f} / 0,10"),
173
+ ("2. Qualificação das partes conforme Art. 319, II, CPC", f"{enderecamento[1]:.2f} / 0,20"),
174
+ ("**Fundamentos**", ""),
175
+ ("3. Exposição clara dos fatos", f"{fundamentos[0]:.2f} / 0,20"),
176
+ ("4. Fundamentação jurídica adequada", f"{fundamentos[1]:.2f} / 0,25"),
177
+ ("5. Demonstração de ilícito e dano", f"{fundamentos[2]:.2f} / 0,25"),
178
+ ("**Pedidos**", ""),
179
+ ("6. Pedidos claros e específicos", f"{pedidos[0]:.2f} / 0,30"),
180
+ ("7. Valor da causa indicado", f"{pedidos[1]:.2f} / 0,10"),
181
+ ("8. Provas pretendidas especificadas", f"{pedidos[2]:.2f} / 0,10"),
182
+ ("9. Opção sobre audiência de conciliação", f"{pedidos[3]:.2f} / 0,10"),
183
+ ("10. Pedido de citação do réu", f"{pedidos[4]:.2f} / 0,10"),
184
+ ("11. Juntada da guia de custas", f"{pedidos[5]:.2f} / 0,10"),
185
+ ("**Fechamento**", ""),
186
+ ("12. Fechamento formal adequado", f"{fechamento[0]:.2f} / 0,10"),
187
+ ]
188
+ nota_final = sum(enderecamento) + sum(fundamentos) + sum(pedidos) + sum(fechamento)
189
+ tabela_formatada = "\n".join([f"| {linha[0].ljust(60)} | {linha[1].rjust(10)} |" for linha in tabela])
190
+ return f"{tabela_formatada}\n\n**Nota Final:** {nota_final:.2f} / 10,00"
191
+
192
+ def estruturar_prompt_correcao(caso, peticao, modo_treinamento=False):
193
+ base_prompt = f"""
194
+ ## Correção Rigorosa de Petição Inicial Cível (Critérios Realistas da OAB)
195
+ **Caso Fornecido:**
196
+ {caso}
197
+ **Petição do Aluno:**
198
+ {peticao}
199
+ **Estrutura da Avaliação:**
200
+ ### Análise da Petição
201
+ 1. Análise Geral (Breve visão geral sobre a petição - máximo 3 linhas)
202
+ 2. Nota Final (0 a 10 com uma casa decimal)
203
+ 3. Pontos Positivos (Liste pelo menos 3 aspectos bem executados)
204
+ 4. Pontos a Melhorar (Liste os problemas encontrados em ordem de gravidade)
205
+ 5. Análise Específica dos Requisitos:
206
+ - Endereçamento e qualificação do juízo
207
+ - Qualificação das partes (Art. 319, II, CPC)
208
+ - Fatos e fundamentos jurídicos (Art. 319, III, CPC)
209
+ - Pedidos com especificações (Art. 319, IV, CPC)
210
+ - Valor da causa (Art. 319, V, CPC)
211
+ - Provas com que o autor pretende demonstrar a verdade dos fatos (Art. 319, VI, CPC)
212
+ - Opção pela realização ou não de audiência de conciliação (Art. 319, VII, CPC)
213
+ - Pedido de citação do réu
214
+ - Juntada da guia de custas
215
+ - Fechamento formal da petição
216
+ **Critérios Fundamentais:**
217
+ 1. Identificação do candidato resulta em nota máxima de 3,0
218
+ 2. Invenção de fatos não constantes no caso resulta em penalização grave (-5,0 pontos)
219
+ 3. Falta de pedido de juntada de custas resulta em perda de 1,0 ponto
220
+ 4. Use marcadores para dados não fornecidos (ex: "Rua XXX", "CPF XXX")
221
+ 5. Não penalize por informações ausentes no caso original
222
+ **Instruções Especiais para Endereçamento:**
223
+ - CPC (art. 46): foro competente é o domicílio do réu; CDC (art. 101, I): em relação de consumo, foro do autor é correto.
224
+ - Avalie o contexto: priorize o foro do autor em casos consumeristas.
225
+ **Pontuação Detalhada:**
226
+ Inclua uma tabela com:
227
+ | ITEM | PONTUAÇÃO |
228
+ |-------------------------------------------|-----------|
229
+ **Endereçamento** | |
230
+ 1. Correto endereçamento ao juízo competente (0,10) | / 0,10 |
231
+ 2. Qualificação das partes conforme Art. 319, II, CPC (0,20) | / 0,20 |
232
+ **Fundamentos** | |
233
+ 3. Exposição clara dos fatos (0,20) | / 0,20 |
234
+ 4. Fundamentação jurídica adequada (0,25) | / 0,25 |
235
+ 5. Demonstração de ilícito e dano (0,25) | / 0,25 |
236
+ **Pedidos** | |
237
+ 6. Pedidos claros e específicos (0,30) | / 0,30 |
238
+ 7. Valor da causa indicado (0,10) | / 0,10 |
239
+ 8. Provas pretendidas especificadas (0,10) | / 0,10 |
240
+ 9. Opção sobre audiência de conciliação (0,10) | / 0,10 |
241
+ 10. Pedido de citação do réu (0,10) | / 0,10 |
242
+ 11. Juntada da guia de custas (0,10) | / 0,10 |
243
+ **Fechamento** | |
244
+ 12. Fechamento formal adequado (0,10) | / 0,10 |
245
+ **Nota Final:**
246
+ Calcule a nota final somando os pontos.
247
+ ---
248
+
249
+ ### Petição Corrigida
250
+ Forneça a petição do aluno reescrita com todas as correções aplicadas, incluindo partes omitidas (como pedidos ou custas) e ajustando erros identificados nos "Pontos a Melhorar" e "Análise Específica". Use apenas informações do caso e fundamente com leis brasileiras relevantes (ex.: CPC, CDC). Estruture a petição corrigida com:
251
+ 1. Endereçamento correto
252
+ 2. Qualificação completa (Art. 319, II, CPC)
253
+ 3. Fatos claros (Art. 319, III, CPC)
254
+ 4. Fundamentação jurídica (ex.: CPC, CDC)
255
+ 5. Pedidos específicos (Art. 319, IV, CPC)
256
+ 6. Valor da causa (Art. 319, V, CPC)
257
+ 7. Provas (Art. 319, VI, CPC)
258
+ 8. Opção por audiência (Art. 319, VII, CPC)
259
+ 9. Pedido de citação
260
+ 10. Juntada da guia de custas
261
+ 11. Fechamento formal
262
+ """
263
+ if modo_treinamento:
264
+ base_prompt += f"""
265
+ **Modo de Treinamento Interativo Ativado:**
266
+ - Para cada item com pontuação abaixo do máximo, forneça:
267
+ a) Uma explicação detalhada do erro.
268
+ b) Uma sugestão de redação corrigida baseada no caso.
269
+ c) Uma dica prática para evitar o erro no futuro (use este banco de dicas: {json.dumps(DICAS_OAB)}).
270
+ """
271
+ return base_prompt
272
+
273
+ def estruturar_prompt_geracao_caso():
274
+ return """
275
+ Crie um caso fictício detalhado para uma petição inicial cível, similar ao exemplo:
276
+ "Ana, modelo profissional, residente em Manaus, viajou para São Paulo, para o casamento de sua filha. Para lavar, pintar seus cabelos e realizar um penteado para o casamento, Ana procurou os serviços de João Macedo, cabeleireiro e dono do salão de beleza 'Hair', sediado na cidade de São Paulo, que lhe cobrou R$500,00 (quinhentos reais) pela prestação do serviço. Após lavar os cabelos de Ana, João aplicou-lhe uma tintura da marca francesa ABC, importada pela empresa Brasil Connection Ltda, sediada na cidade de Curitiba (PR). Meia hora após a aplicação da tintura, Ana sofreu uma reação alérgica, que demandou atendimento médico-hospitalar, no valor de R$1.000,00 (um mil reais), bem como dois dias de absoluto repouso que impossibilitou sua presença no casamento de sua filha. Além disso, perdeu grande parte de seu cabelo, tendo permanecido com manchas em seu rosto por dois meses, perdendo um ensaio fotográfico, para o qual já havia sido contratada, pelo valor de R$50.000,00 (cinquenta mil reais). Posteriormente constatou-se que a tintura utilizada continha substâncias químicas extremamente perigosas à vida e à saúde das pessoas e a fabricante ABC já havia sido condenada pela justiça francesa a encerrar a fabricação e comercialização do produto. Indignada com os danos sofridos, Ana procura um advogado para pleitear o devido ressarcimento."
277
+ O caso deve:
278
+ 1. Conter uma situação clara de violação de direitos que justifique uma ação cível
279
+ 2. Incluir valores específicos de danos materiais e/ou morais
280
+ 3. Mencionar nomes completos, cidades e informações suficientes para identificar as partes
281
+ 4. Estabelecer relação entre as partes (contratual, extracontratual, consumo, etc.)
282
+ 5. Ter complexidade adequada para uma petição inicial de nível OAB
283
+ 6. Ter entre 15-25 linhas de texto
284
+ 7. Focar em um dos seguintes temas (escolha APENAS UM aleatoriamente):
285
+ - Responsabilidade civil por acidente de consumo
286
+ - Inadimplemento contratual com danos
287
+ - Responsabilidade civil por dano ambiental
288
+ - Violação de direitos da personalidade
289
+ - Relação de consumo com vício de produto/serviço
290
+ - Reparação por danos patrimoniais
291
+ Termine com: "Situação problema: Como advogado(a) de [NOME DO CLIENTE], promova a demanda cabível."
292
+ """
293
+
294
+ def gerar_caso_ficticio():
295
+ prompt = estruturar_prompt_geracao_caso()
296
+ payload = {
297
+ "contents": [{"parts": [{"text": prompt}]}],
298
+ "generationConfig": {"temperature": 0.7, "maxOutputTokens": CONFIG["max_tokens"], "topP": 0.95}
299
+ }
300
+ headers = {"Content-Type": "application/json"}
301
+ for tentativa in range(CONFIG["max_retry_attempts"]):
302
+ try:
303
+ response = requests.post(GEMINI_API_URL, headers=headers, data=json.dumps(payload), timeout=CONFIG["timeout"])
304
+ if response.status_code != 200:
305
+ response_data = response.json()
306
+ error_msg = response_data.get("error", {}).get("message", "Erro desconhecido")
307
+ if tentativa < CONFIG["max_retry_attempts"] - 1:
308
+ time.sleep(CONFIG["retry_delay"] * (tentativa + 1))
309
+ continue
310
+ return f"❌ Erro da API (Tentativa {tentativa+1}/{CONFIG['max_retry_attempts']}): {error_msg}"
311
+ response_data = response.json()
312
+ if "candidates" not in response_data or not response_data["candidates"]:
313
+ if tentativa < CONFIG["max_retry_attempts"] - 1:
314
+ time.sleep(CONFIG["retry_delay"] * (tentativa + 1))
315
+ continue
316
+ return "❌ A API retornou uma resposta vazia. Tente novamente."
317
+ caso_gerado = response_data["candidates"][0]["content"]["parts"][0]["text"].strip()
318
+ return f"## 📝 Caso Fictício Gerado\n\n{caso_gerado}"
319
+ except requests.Timeout:
320
+ if tentativa < CONFIG["max_retry_attempts"] - 1:
321
+ time.sleep(CONFIG["retry_delay"] * (tentativa + 1))
322
+ continue
323
+ return "⏱️ A resposta demorou muito. Tente novamente mais tarde."
324
+ except requests.RequestException as e:
325
+ if tentativa < CONFIG["max_retry_attempts"] - 1:
326
+ time.sleep(CONFIG["retry_delay"] * (tentativa + 1))
327
+ continue
328
+ return f"❌ Erro ao chamar a API: {e}"
329
+ except Exception as e:
330
+ return f"❌ Erro inesperado: {str(e)}"
331
+
332
+ def corrigir_peticao(caso, peticao_texto=None, peticao_arquivo=None, modo_treinamento=False):
333
+ if not caso.strip():
334
+ return "⚠️ Forneça o caso da banca."
335
+ if peticao_arquivo is not None:
336
+ peticao_conteudo = process_file(peticao_arquivo)
337
+ if peticao_conteudo.startswith("❌") or peticao_conteudo.startswith("⚠️"):
338
+ return peticao_conteudo
339
+ else:
340
+ peticao_conteudo = peticao_texto or ""
341
+ if not peticao_conteudo.strip():
342
+ return "⚠️ Forneça uma petição."
343
+ prompt = estruturar_prompt_correcao(caso, peticao_conteudo, modo_treinamento)
344
+ payload = {
345
+ "contents": [{"parts": [{"text": prompt}]}],
346
+ "generationConfig": {"temperature": CONFIG["temperatura"], "maxOutputTokens": CONFIG["max_tokens"], "topP": 0.95}
347
+ }
348
+ headers = {"Content-Type": "application/json"}
349
+ for tentativa in range(CONFIG["max_retry_attempts"]):
350
+ try:
351
+ response = requests.post(GEMINI_API_URL, headers=headers, data=json.dumps(payload), timeout=CONFIG["timeout"])
352
+ if response.status_code != 200:
353
+ response_data = response.json()
354
+ error_msg = response_data.get("error", {}).get("message", "Erro desconhecido")
355
+ if tentativa < CONFIG["max_retry_attempts"] - 1:
356
+ time.sleep(CONFIG["retry_delay"] * (tentativa + 1))
357
+ continue
358
+ return f"❌ Erro da API (Tentativa {tentativa+1}/{CONFIG['max_retry_attempts']}): {error_msg}"
359
+ response_data = response.json()
360
+ if "candidates" not in response_data or not response_data["candidates"]:
361
+ if tentativa < CONFIG["max_retry_attempts"] - 1:
362
+ time.sleep(CONFIG["retry_delay"] * (tentativa + 1))
363
+ continue
364
+ return "❌ A API retornou uma resposta vazia. Tente novamente."
365
+ correção_completa = response_data["candidates"][0]["content"]["parts"][0]["text"]
366
+ if "### Petição Corrigida" in correção_completa:
367
+ analise_fim = correção_completa.index("### Petição Corrigida")
368
+ analise = correção_completa[:analise_fim].strip()
369
+ else:
370
+ analise = correção_completa.strip()
371
+ correção_formatada = formatar_correção(analise)
372
+ HISTORICO.append({
373
+ "data": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
374
+ "caso": caso,
375
+ "peticao": peticao_conteudo,
376
+ "correcao_completa": correção_completa
377
+ })
378
+ return correção_formatada
379
+ except requests.Timeout:
380
+ if tentativa < CONFIG["max_retry_attempts"] - 1:
381
+ time.sleep(CONFIG["retry_delay"] * (tentativa + 1))
382
+ continue
383
+ return "⏱️ A resposta demorou muito. Tente novamente mais tarde."
384
+ except requests.RequestException as e:
385
+ if tentativa < CONFIG["max_retry_attempts"] - 1:
386
+ time.sleep(CONFIG["retry_delay"] * (tentativa + 1))
387
+ continue
388
+ return f"❌ Erro ao chamar a API: {e}"
389
+ except Exception as e:
390
+ return f"❌ Erro inesperado: {str(e)}"
391
+
392
+ def formatar_correção(texto):
393
+ linhas = texto.split('\n')
394
+ texto_formatado = ""
395
+ for linha in linhas:
396
+ if "Análise Geral" in linha or "Nota Final" in linha or "Pontos Positivos" in linha or "Pontos a Melhorar" in linha:
397
+ texto_formatado += f"### {linha.strip()}\n\n"
398
+ elif linha.strip().startswith('-') and len(linha) > 2 and any(termo in linha for termo in ["Endereçamento", "Qualificação", "Fatos", "Pedidos", "Valor", "Provas"]):
399
+ texto_formatado += f"**{linha.strip()}**\n"
400
+ elif "Explicação" in linha or "Sugestão" in linha or "Dica" in linha:
401
+ texto_formatado += f"*{linha.strip()}*\n"
402
+ else:
403
+ texto_formatado += f"{linha}\n"
404
+ if "Nota Final" in texto_formatado:
405
+ import re
406
+ nota_pattern = r"### Nota Final.*?(\d+[.,]\d+|\d+)"
407
+ match = re.search(nota_pattern, texto_formatado, re.DOTALL)
408
+ if match:
409
+ nota = match.group(1)
410
+ texto_formatado = texto_formatado.replace(f"### Nota Final\n\n{nota}", f"### Nota Final: {nota}/10\n\n")
411
+ return texto_formatado
412
+
413
+ def extrair_peticao_corrigida(correcao_completa):
414
+ try:
415
+ if "### Petição Corrigida" in correcao_completa:
416
+ inicio = correcao_completa.index("### Petição Corrigida") + len("### Petição Corrigida")
417
+ peticao_texto = ""
418
+ for line in correcao_completa[inicio:].split('\n'):
419
+ if line.strip() and not line.strip().startswith('#') and not line.strip().startswith('*') and not any(prefix in line for prefix in ["Análise", "Nota Final", "Pontos Positivos", "Pontos a Melhorar"]):
420
+ peticao_texto += line + '\n'
421
+ return peticao_texto.strip()
422
+ return "⚠️ Petição corrigida não encontrada na resposta da API."
423
+ except Exception as e:
424
+ return f"❌ Erro ao extrair petição corrigida: {str(e)}"
425
+
426
+ def fornecer_dica_contextual(texto):
427
+ dicas = [f"💡 {chave}: {dica}" for chave, dica in DICAS_OAB.items() if chave.lower() in texto.lower()]
428
+ return "\n".join(dicas) if dicas else "💡 Escreva mais para receber dicas."
429
+
430
+ def calcular_progresso():
431
+ if not HISTORICO:
432
+ return "📊 Nenhum progresso registrado."
433
+ notas = []
434
+ for entry in HISTORICO:
435
+ for line in entry["correcao_completa"].split("\n"):
436
+ if "Nota Final" in line:
437
+ try:
438
+ nota = float(line.split()[-1].split("/")[0].replace(",", "."))
439
+ notas.append(nota)
440
+ except:
441
+ continue
442
+ media = sum(notas) / len(notas) if notas else 0
443
+ tendencia = "⬆️ Melhorando" if len(notas) > 1 and notas[-1] > notas[-2] else "⬇️ Estável ou piorando"
444
+ return f"📊 Progresso: Média {media:.1f}/10 | Tentativas: {len(notas)} | Tendência: {tendencia}"
445
+
446
+ def comparar_peticao_melhorada(caso, peticao_texto, correcao):
447
+ import difflib
448
+ from html import escape
449
+
450
+ if not caso:
451
+ return "⚠️ Forneça um caso antes de comparar.", "", ""
452
+ peticao = peticao_texto if peticao_texto else (HISTORICO[-1]["peticao"] if HISTORICO else "")
453
+ if not peticao:
454
+ return "⚠️ Forneça uma petição para comparar ou corrija uma antes.", "", ""
455
+ if not correcao:
456
+ return "⚠️ Corrija a petição antes de comparar.", "", ""
457
+ ultima_correcao = HISTORICO[-1]["correcao_completa"] if HISTORICO else ""
458
+ if not ultima_correcao:
459
+ return "⚠️ Nenhuma correção disponível no histórico.", "", ""
460
+ peticao_corrigida = extrair_peticao_corrigida(ultima_correcao)
461
+
462
+ orig_lines = peticao.split('\n')
463
+ corr_lines = peticao_corrigida.split('\n')
464
+ start_orig = next((i for i, line in enumerate(orig_lines) if "AÇÃO" in line.upper() or "EXCELENTÍSSIMO" in line.upper()), 0)
465
+ start_corr = next((i for i, line in enumerate(corr_lines) if "AÇÃO" in line.upper() or "EXCELENTÍSSIMO" in line.upper()), 0)
466
+
467
+ orig_body = "\n".join(orig_lines[start_orig:]).strip()
468
+ corr_body = "\n".join(corr_lines[start_corr:]).strip()
469
+
470
+ diff = difflib.ndiff(orig_body.splitlines(), corr_body.splitlines())
471
+ secoes = {
472
+ "ENDEREÇAMENTO": [], "QUALIFICAÇÃO": [], "FATOS": [], "FUNDAMENTOS": [], "PEDIDOS": [], "FECHAMENTO": [], "OUTROS": []
473
+ }
474
+
475
+ def identificar_secao(linha):
476
+ linha_upper = linha.upper()
477
+ if "EXCELENTÍSSIMO" in linha_upper or "JUIZ" in linha_upper or "VARA" in linha_upper:
478
+ return "ENDEREÇAMENTO"
479
+ elif "QUALIFICAÇÃO" in linha_upper or "AUTOR" in linha_upper or "RÉU" in linha_upper:
480
+ return "QUALIFICAÇÃO"
481
+ elif "DOS FATOS" in linha_upper:
482
+ return "FATOS"
483
+ elif "DIREITO" in linha_upper or "FUNDAMENTO" in linha_upper:
484
+ return "FUNDAMENTOS"
485
+ elif "PEDIDO" in linha_upper or "REQUER" in linha_upper:
486
+ return "PEDIDOS"
487
+ elif "TERMOS" in linha_upper and "DEFERIMENTO" in linha_upper:
488
+ return "FECHAMENTO"
489
+ else:
490
+ return "OUTROS"
491
+
492
+ secao_atual = "OUTROS"
493
+ resultado_diff = []
494
+ for linha in diff:
495
+ if linha.startswith(' '):
496
+ texto = linha[2:]
497
+ nova_secao = identificar_secao(texto)
498
+ if nova_secao != "OUTROS":
499
+ secao_atual = nova_secao
500
+ resultado_diff.append(f"<div class='linha-igual'>{escape(texto)}</div>")
501
+ elif linha.startswith('- '):
502
+ texto = linha[2:]
503
+ resultado_diff.append(f"<div class='linha-removida'><del>{escape(texto)}</del></div>")
504
+ secoes[secao_atual].append(f"- REMOVIDO: {texto}")
505
+ elif linha.startswith('+ '):
506
+ texto = linha[2:]
507
+ nova_secao = identificar_secao(texto)
508
+ if nova_secao != "OUTROS":
509
+ secao_atual = nova_secao
510
+ resultado_diff.append(f"<div class='linha-adicionada'><ins>{escape(texto)}</ins></div>")
511
+ secoes[secao_atual].append(f"+ ADICIONADO: {texto}")
512
+ elif linha.startswith('? '):
513
+ continue
514
+
515
+ resumo_alteracoes = []
516
+ for secao, alteracoes in secoes.items():
517
+ if alteracoes:
518
+ resumo_alteracoes.append(f"### {secao}:")
519
+ for alt in alteracoes[:5]:
520
+ resumo_alteracoes.append(f"- {alt}")
521
+ if len(alteracoes) > 5:
522
+ resumo_alteracoes.append(f"- ... e mais {len(alteracoes) - 5} alterações")
523
+
524
+ total_removidas = len([linha for linha in resultado_diff if "linha-removida" in linha])
525
+ total_adicionadas = len([linha for linha in resultado_diff if "linha-adicionada" in linha])
526
+
527
+ resumo_estatistico = f"""
528
+ ## 📊 Resumo das Alterações
529
+ - Total de linhas removidas: {total_removidas}
530
+ - Total de linhas adicionadas: {total_adicionadas}
531
+ - Diferença líquida: {total_adicionadas - total_removidas} linhas
532
+
533
+ ## 🔍 Alterações por Seção
534
+ {chr(10).join(resumo_alteracoes)}
535
+
536
+ ## 📝 Visualização Detalhada
537
+ <style>
538
+ .linha-igual {{ color: #000000; }}
539
+ .linha-removida {{ color: #ffffff; background-color: #b30000; }}
540
+ .linha-adicionada {{ color: #ffffff; background-color: #006600; }}
541
+ </style>
542
+ {"".join(resultado_diff)}
543
+ """
544
+ return peticao, peticao_corrigida, resumo_estatistico
545
+
546
+ def interface_gradio():
547
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
548
+ gr.Markdown("# 📝 Corretor Avançado de Petições Iniciais (Critérios OAB)")
549
+ with gr.Row():
550
+ with gr.Column(scale=1):
551
+ gr.Markdown("""
552
+ ### 🔍 Critérios de Avaliação
553
+ - ✅ Endereçamento correto
554
+ - ✅ Qualificação das partes
555
+ - ✅ Fatos e fundamentos
556
+ - ✅ Pedidos claros e específicos
557
+ - ✅ Valor da causa
558
+ - ✅ Provas pretendidas
559
+ - ✅ Opção sobre conciliação
560
+ - ✅ Pedido de citação adequado
561
+ - ✅ Juntada da guia de custas
562
+ - ✅ Fechamento formal
563
+ ### ⚖️ Penalizações
564
+ - ❌ Identificação do candidato: nota máxima 3,0
565
+ - ❌ Invenção de fatos: -5,0 pontos
566
+ - ❌ Sem pedido de custas: -1,0 ponto
567
+ """)
568
+ modo_treinamento = gr.Checkbox(label="Modo de Treinamento Interativo", value=False)
569
+ with gr.Accordion("⚙️ Configurações", open=False):
570
+ temperatura_slider = gr.Slider(minimum=0.0, maximum=1.0, value=CONFIG["temperatura"], step=0.1, label="Temperatura")
571
+ timeout_slider = gr.Slider(minimum=30, maximum=120, value=CONFIG["timeout"], step=5, label="Timeout (segundos)")
572
+ def atualizar_config(temp, timeout):
573
+ CONFIG["temperatura"] = temp
574
+ CONFIG["timeout"] = timeout
575
+ return f"✅ Configurações atualizadas!"
576
+ config_update_btn = gr.Button("📝 Atualizar Configurações")
577
+ config_status = gr.Textbox(label="Status", visible=True)
578
+ config_update_btn.click(atualizar_config, inputs=[temperatura_slider, timeout_slider], outputs=config_status)
579
+ with gr.Accordion("📊 Dashboard de Progresso", open=False):
580
+ progresso_output = gr.Markdown()
581
+ progresso_btn = gr.Button("🔄 Atualizar Progresso")
582
+ progresso_btn.click(fn=calcular_progresso, outputs=progresso_output)
583
+
584
+ with gr.Column(scale=2):
585
+ with gr.Row():
586
+ gr.Markdown("### 🧩 Gerar Caso Fictício")
587
+ caso_btn = gr.Button("🔄 Gerar Novo Caso Fictício", variant="secondary")
588
+ caso_input = gr.Textbox(label="📋 Caso Fornecido pela Banca", lines=8, placeholder="Insira o texto COMPLETO do caso...")
589
+ with gr.Tabs():
590
+ with gr.TabItem("✏️ Inserir Texto"):
591
+ peticao_texto = gr.Textbox(label="Texto da Petição do Aluno", lines=15, placeholder="Insira a petição completa...")
592
+ dica_contextual = gr.Textbox(label="Dicas Contextuais", interactive=False)
593
+ peticao_texto.change(fn=fornecer_dica_contextual, inputs=peticao_texto, outputs=dica_contextual)
594
+ btn_texto = gr.Button("🔍 Corrigir Petição (Texto)", variant="primary")
595
+ with gr.TabItem("📎 Enviar Arquivo"):
596
+ gr.Markdown("**Envie um arquivo PDF, DOCX ou TXT contendo a petição do aluno.**")
597
+ peticao_arquivo = gr.File(label="Arquivo da Petição", file_types=[".pdf", ".docx", ".txt"])
598
+ arquivo_preview = gr.Textbox(label="Prévia do Conteúdo do Arquivo", lines=5, interactive=False)
599
+ btn_arquivo = gr.Button("🔍 Corrigir Petição (Arquivo)", variant="primary")
600
+ peticao_arquivo.change(fn=process_file, inputs=[peticao_arquivo], outputs=[arquivo_preview])
601
+
602
+ with gr.Row():
603
+ clear_btn = gr.Button("🗑️ Limpar Todos os Campos")
604
+ comparar_btn = gr.Button("⚖️ Comparar com Correção")
605
+
606
+ resultado_output = gr.Markdown(label="📊 Correção Oficial e Nota", show_copy_button=True)
607
+
608
+ with gr.Row():
609
+ peticao_aluno_output = gr.Textbox(label="Petição do Aluno", lines=15, interactive=False)
610
+ peticao_corrigida_output = gr.Textbox(label="Petição Corrigida", lines=15, interactive=False)
611
+
612
+ with gr.Row():
613
+ comparacao_diferencas = gr.HTML(label="Diferenças Destacadas")
614
+
615
+ with gr.Accordion("📜 Histórico de Correções", open=False):
616
+ historico_output = gr.Markdown()
617
+ historico_btn = gr.Button("🔄 Ver Histórico")
618
+ historico_btn.click(
619
+ fn=lambda: "\n\n".join([f"**{h['data']}**\nCaso: {h['caso'][:100]}...\nCorreção: {h['correcao_completa'][:100]}..." for h in HISTORICO]),
620
+ outputs=historico_output
621
+ )
622
+
623
+ caso_btn.click(fn=gerar_caso_ficticio, inputs=[], outputs=[caso_input])
624
+ btn_texto.click(
625
+ fn=lambda caso, texto, treinamento: corrigir_peticao(caso, texto, None, treinamento),
626
+ inputs=[caso_input, peticao_texto, modo_treinamento],
627
+ outputs=[resultado_output]
628
+ )
629
+ btn_arquivo.click(
630
+ fn=lambda caso, arquivo, treinamento: corrigir_peticao(caso, None, arquivo, treinamento),
631
+ inputs=[caso_input, peticao_arquivo, modo_treinamento],
632
+ outputs=[resultado_output]
633
+ )
634
+ clear_btn.click(
635
+ fn=lambda: [None, None, None, None, "", "", "", ""],
636
+ inputs=None,
637
+ outputs=[caso_input, peticao_texto, peticao_arquivo, arquivo_preview, resultado_output, dica_contextual, peticao_aluno_output, peticao_corrigida_output]
638
+ )
639
+ comparar_btn.click(
640
+ fn=comparar_peticao_melhorada,
641
+ inputs=[caso_input, peticao_texto, resultado_output],
642
+ outputs=[peticao_aluno_output, peticao_corrigida_output, comparacao_diferencas]
643
+ )
644
+
645
+ with gr.Accordion("📘 Critérios Detalhados", open=False):
646
+ gr.Markdown("""
647
+ ### 📋 Critérios Realistas para Correção
648
+ #### 1. Qualificação das Partes (Art. 319, II, CPC)
649
+ - ✅ CORRETO: Usar "Rua XXXX" se endereço não for fornecido
650
+ - ❌ INCORRETO: Inventar dados
651
+ #### 2. Citação
652
+ - ✅ CORRETO: Pedir citação conforme dados do caso
653
+ #### 3. Pedidos Obrigatórios
654
+ - ✅ OBRIGATÓRIO: Incluir "a juntada da guia de custas"
655
+ #### 4. Penalização Grave
656
+ - ❌ INCORRETO: Identificação do candidato
657
+ - ❌ INCORRETO: Invenção de fatos
658
+ """)
659
+ gr.Markdown("""
660
+ ### ℹ️ Modo de Treinamento Interativo
661
+ Ative para explicações detalhadas.
662
+ ### 📋 Como usar
663
+ 1. Gere ou cole o caso
664
+ 2. Insira a petição
665
+ 3. Corrija e compare
666
+ ### 📄 Formatos suportados
667
+ - PDF (texto e escaneados com OCR)
668
+ - DOCX
669
+ - TXT
670
+ """)
671
+
672
+ return demo
673
+
674
+ # No Hugging Face, não usamos if __name__ == "__main__":
675
+ app = interface_gradio()
676
+ app.launch()