Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
# -*- coding: utf-8 -*-
|
| 2 |
import gradio as gr
|
| 3 |
-
import requests
|
|
|
|
|
|
|
| 4 |
import os
|
| 5 |
import json
|
| 6 |
import docx
|
|
@@ -30,15 +32,31 @@ import re
|
|
| 30 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - [%(funcName)s] - %(message)s')
|
| 31 |
logger = logging.getLogger(__name__)
|
| 32 |
|
|
|
|
| 33 |
load_dotenv()
|
| 34 |
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") or os.environ.get("GEMINI_API_KEY")
|
| 35 |
if not GEMINI_API_KEY:
|
| 36 |
raise ValueError("❌ A chave GEMINI_API_KEY não foi encontrada. Configure-a no arquivo .env ou como Secret no Hugging Face.")
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
TIPOS_DE_PETICAO = ["Petição Inicial", "Contestação", "Reconvenção", "Impugnação à Contestação", "Embargos de Declaração", "Recurso de Apelação", "Agravo de Instrumento", "Petição de Tutela Provisória"]
|
| 44 |
|
|
@@ -55,6 +73,8 @@ DICAS_OAB_MELHORADO = {
|
|
| 55 |
HISTORICO = []
|
| 56 |
CORRECAO_CACHE = {}
|
| 57 |
|
|
|
|
|
|
|
| 58 |
def preprocess_image_for_ocr(img_pil):
|
| 59 |
if not CV2_AVAILABLE:
|
| 60 |
logger.warning("OpenCV não disponível, usando conversão simples para OCR.")
|
|
@@ -68,113 +88,70 @@ def preprocess_image_for_ocr(img_pil):
|
|
| 68 |
return img_pil.convert('L')
|
| 69 |
|
| 70 |
def extract_text_from_pdf(file_path_or_obj):
|
| 71 |
-
text = ""
|
| 72 |
-
ocr_text_parts = []
|
| 73 |
try:
|
| 74 |
logger.info(f"Abrindo PDF: {getattr(file_path_or_obj, 'name', file_path_or_obj)}")
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
else:
|
| 90 |
-
logger.warning(f"OCR retornou vazio para a página {i+1}.")
|
| 91 |
-
ocr_text_parts.append(f"[OCR Página {i+1}: Vazio ou falhou]")
|
| 92 |
-
except Exception as ocr_e:
|
| 93 |
-
logger.error(f"Erro no OCR da página {i+1}: {str(ocr_e)}")
|
| 94 |
-
ocr_text_parts.append(f"[Erro OCR Página {i+1}: {str(ocr_e)}]")
|
| 95 |
-
pdf_reader.close()
|
| 96 |
ocr_text = "\n".join(ocr_text_parts)
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
elif
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
elif text.strip():
|
| 104 |
-
logger.info("Usando texto extraído curto como fallback.")
|
| 105 |
-
return text
|
| 106 |
-
else:
|
| 107 |
-
logger.warning("Não foi possível extrair texto significativo do PDF.")
|
| 108 |
-
return "⚠️ Não foi possível extrair texto do PDF (extração e OCR falharam)."
|
| 109 |
-
except Exception as e:
|
| 110 |
-
logger.error(f"Erro geral ao processar o PDF: {str(e)}", exc_info=True)
|
| 111 |
-
return f"❌ Erro crítico ao processar o PDF: {str(e)}"
|
| 112 |
|
| 113 |
def extract_text_from_docx(file_path_or_obj):
|
| 114 |
try:
|
| 115 |
doc = docx.Document(file_path_or_obj)
|
| 116 |
-
text = "\n".join([
|
| 117 |
-
return text if text.strip() else "⚠️
|
| 118 |
-
except Exception as e:
|
| 119 |
-
logger.error(f"Erro ao processar o DOCX: {str(e)}", exc_info=True)
|
| 120 |
-
return f"❌ Erro ao processar o DOCX: {str(e)}"
|
| 121 |
|
| 122 |
def extract_text_from_txt(file_path_or_obj):
|
| 123 |
try:
|
| 124 |
-
content = b''
|
| 125 |
if isinstance(file_path_or_obj, str):
|
| 126 |
-
with open(file_path_or_obj, 'rb') as f:
|
| 127 |
-
content = f.read()
|
| 128 |
elif hasattr(file_path_or_obj, 'read'):
|
| 129 |
-
if hasattr(file_path_or_obj, 'seek')
|
| 130 |
-
try: file_path_or_obj.seek(0)
|
| 131 |
-
except Exception as seek_e: logger.warning(f"Não foi possível resetar ponteiro do arquivo: {seek_e}")
|
| 132 |
content = file_path_or_obj.read()
|
| 133 |
-
else:
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
logger.info(f"Arquivo TXT decodificado com {encoding}.")
|
| 140 |
-
return decoded_text if decoded_text.strip() else "⚠️ Arquivo TXT vazio."
|
| 141 |
-
except UnicodeDecodeError:
|
| 142 |
-
continue
|
| 143 |
-
logger.error("Não foi possível decodificar o arquivo TXT com encodings comuns.")
|
| 144 |
-
return "❌ Não foi possível decodificar o arquivo TXT. Verifique o encoding."
|
| 145 |
-
except Exception as e:
|
| 146 |
-
logger.error(f"Erro ao processar o TXT: {str(e)}", exc_info=True)
|
| 147 |
-
return f"❌ Erro ao processar o TXT: {str(e)}"
|
| 148 |
|
| 149 |
def process_file(file_obj):
|
| 150 |
-
if file_obj is None:
|
| 151 |
-
logger.warning("process_file chamado com file_obj=None")
|
| 152 |
-
return "⚠️ Nenhum arquivo enviado."
|
| 153 |
try:
|
| 154 |
-
|
| 155 |
-
if not
|
| 156 |
-
|
| 157 |
-
return "❌ Erro: Não foi possível identificar o arquivo enviado."
|
| 158 |
-
filename = os.path.basename(file_path).lower()
|
| 159 |
-
logger.info(f"Processando arquivo: {filename}")
|
| 160 |
try:
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
else:
|
| 172 |
-
logger.warning(f"Formato de arquivo não suportado: {filename}")
|
| 173 |
-
return "⚠️ Formato de arquivo não suportado. Use PDF, DOCX ou TXT."
|
| 174 |
-
except Exception as e:
|
| 175 |
-
logger.error(f"Erro GERAL ao processar o arquivo '{getattr(file_obj, 'name', 'N/A')}': {str(e)}", exc_info=True)
|
| 176 |
-
return f"❌ Erro inesperado ao processar o arquivo: {str(e)}"
|
| 177 |
-
|
| 178 |
def get_instrucoes_gerais_avaliacao(modo_treinamento=False):
|
| 179 |
base = """
|
| 180 |
**Estrutura da Avaliação:**
|
|
@@ -191,53 +168,30 @@ def get_instrucoes_gerais_avaliacao(modo_treinamento=False):
|
|
| 191 |
|
| 192 |
def estruturar_prompt_analise(caso, peticao, tipo_peticao, modo_treinamento=False):
|
| 193 |
instrucoes_gerais = get_instrucoes_gerais_avaliacao(modo_treinamento)
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
elif tipo_peticao == "Contestação":
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
prompt_especifico += "- Endereçamento. Processo nº. Qualificação autor. Tempestividade (15d - Art. 350/351).\n- **Resposta Preliminares.**\n- **Resposta Fatos Novos** (Art. 350).\n- **Resposta Documentos** (Art. 437).\n- **Ratificação Inicial.**\n- **Especificação Provas** (contra defesa indireta/preliminares).\n- Pedidos: Rejeitar preliminares; refutar defesa; proceder inicial.\n**Critérios Adicionais:** Não responder preliminar (-1.5). Não manifestar fato novo (-2.0). Não ratificar (-0.5)."
|
| 203 |
-
elif tipo_peticao == "Embargos de Declaração":
|
| 204 |
-
prompt_especifico += "- **Contra Decisão Específica.** Endereçamento ao juízo prolator. Processo nº. Partes. Tempestividade (5d - Art. 1023).\n- **Cabimento (Art. 1022):** Apontar PRECISAMENTE: Omissão, Contradição, Obscuridade ou Erro Material.\n- **NÃO REJULGAR CAUSA.**\n- **Pedido:** Conhecimento e ACOLHIMENTO para sanar vício.\n**Critérios Adicionais:** Rediscutir mérito (-4.0). Não apontar vício (-3.0). Endereçar tribunal (-1.0)."
|
| 205 |
-
elif tipo_peticao == "Recurso de Apelação":
|
| 206 |
-
prompt_especifico += "- **Interposição:** Ao juízo a quo. Processo nº. Partes. Pressupostos (cabimento Art. 1009; tempestividade 15d; preparo/JG). Pedido recebimento/remessa.\n- **Razões:** Ao TRIBUNAL. Relato. **Fundamentos Reforma/Anulação (Art. 1010):** Error in procedendo/judicando. **Dialeticidade.**\n- **Pedidos:** Conhecimento; PROVIMENTO (anular/reformar); Inversão sucumbência.\n**Critérios Adicionais:** Falta peça (-10). Endereço razões (-1.0). Sem dialeticidade (-3 a -5). Sem pedido reforma/anulação (-1.5). Falta preparo/JG (-2.0)."
|
| 207 |
-
elif tipo_peticao == "Agravo de Instrumento":
|
| 208 |
-
prompt_especifico += "- Peça única ao TRIBUNAL. Processo origem. Partes e Advogados (endereços Art. 1016, IV). Cópia decisão agravada (Art. 1017, I).\n- **Cabimento (Art. 1015 / mitigada).** Tempestividade (15d). Preparo/JG.\n- **Peças Obrigatórias (Art. 1017, I):** Decisão, Intimação, Procurações. Facultativas (III).\n- **Razões (Art. 1016):** Motivos reforma/invalidação.\n- **Efeito Suspensivo / Tutela Antecipada Recursal (Art. 1019, I)** (se risco).\n- **Pedidos Finais:** Conhecimento; Efeito/Tutela (se aplicável); PROVIMENTO; Comunicação juiz.\n**Critérios Adicionais:** Falta peça obrigatória (-4.0). Sem cabimento (-3.0). Endereço juiz (-1.0). Sem advogados (-0.5)."
|
| 209 |
-
elif tipo_peticao == "Petição de Tutela Provisória":
|
| 210 |
-
prompt_especifico += "- Tipo (Urgência/Evidência). Caráter (Antecedente/Incidental). Endereçamento. Qualificação. Processo (se incidental).\n- **Tutela Urgência (Art. 300):** **Fumus Boni Iuris** E **Periculum in Mora**. **Reversibilidade** (Art. 300, §3º).\n- **Tutela Evidência (Art. 311):** Hipótese I a IV. **NÃO precisa periculum.**\n- **Pedido Específico:** Medida liminar CLARA.\n- **Se Antecedente (Art. 303/305):** Indicar pedido final ou aditar/apresentar inicial. Valor Causa.\n- **Pedidos:** Concessão tutela; citação/intimação; indicação ação/aditamento.\n**Critérios Adicionais:** Sem Fumus/Periculum (-3 a -5). Erro hipótese Evidência (-3.0). Pedido genérico (-2.0)."
|
| 211 |
-
else: prompt_especifico += "- Adequação ao tipo? Clareza? Fundamentação Mínima? Pedido?"
|
| 212 |
-
prompt_especifico += "\n\n**Instruções Finais:** Analise estritamente com base no caso e critérios. Seja rigoroso mas construtivo. Gere **exclusivamente** no formato solicitado."
|
| 213 |
-
return prompt_especifico
|
| 214 |
|
| 215 |
def estruturar_prompt_peticao_corrigida(caso, peticao_original, tipo_peticao):
|
| 216 |
-
prompt = f"**Caso:**\n```\n{caso}\n```\n**Peça Original ({tipo_peticao}):**\n```\n{peticao_original}\n```\n**Tarefa:** Gere versão corrigida
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
elif tipo_peticao == "Contestação":
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
elif tipo_peticao == "Impugnação à Contestação":
|
| 224 |
-
prompt += "1.Endereçamento, proc, autor, tempestividade. 2.Tópico: DA(S) PRELIMINAR(ES) - Rebater. 3.Tópico: DO MÉRITO E FATOS NOVOS - Manifestar. 4.Tópico: DA RATIFICAÇÃO INICIAL. 5.Tópico: DAS PROVAS - Especificar. 6.Pedidos: Rejeitar prelim., refutar defesa, proceder inicial. 7.Fechamento OAB."
|
| 225 |
-
elif tipo_peticao == "Embargos de Declaração":
|
| 226 |
-
prompt += "1.Endereçamento juízo prolator. Proc, Partes. Decisão embargada ID. Tempestividade (5d). 2.Tópico para cada vício (Art.1022): DA OMISSÃO / DA CONTRADIÇÃO / DA OBSCURIDADE / DO ERRO MATERIAL. 3.Dentro: Transcrever trecho + explicar vício OBJETIVAMENTE. NÃO REJULGAR. 4.Pedido: Conhecimento e ACOLHIMENTO p/ sanar vício(s). 5.Fechamento OAB."
|
| 227 |
-
elif tipo_peticao == "Recurso de Apelação":
|
| 228 |
-
prompt += "1.Interposição: Juízo a quo. Proc, Partes. Pressupostos (cabim., temp., prep./JG). Pedido receb./remessa. 2.Razões: Tribunal. Relato. FUNDAMENTOS REFORMA/ANULAÇÃO (Error in proc./judic.). DIALETICIDADE. 3.Pedidos: Conhec.; PROVIMENTO (anular/reformar); Inversão sucumb. 4.Fechamento OAB."
|
| 229 |
-
elif tipo_peticao == "Agravo de Instrumento":
|
| 230 |
-
prompt += "1.Peça única Tribunal. Proc. origem, Partes, Advogados (endereços). Decisão agravada ID (juntar cópia). 2.CABIMENTO (Art.1015/mitig.). TEMPESTIVIDADE. PREPARO/JG. PEÇAS OBRIG./FACULT. 3.RAZÕES RECURSAIS: Motivos reforma/invalidação. 4.EFEITO SUSPENSIVO/TUTELA ANTECIPADA (se cabível). 5.PEDIDOS: Conhec.; Efeito/Tutela; PROVIMENTO; Comunicação juiz. 6.Fechamento OAB."
|
| 231 |
-
elif tipo_peticao == "Petição de Tutela Provisória":
|
| 232 |
-
prompt += "1.Indicar TIPO(Urg/Evid) e CARÁTER(Antec/Incid). Endereç. Qualif. Proc(se incid). 2.REQUISITOS: Urgência=Fumus+Periculum(+Reversib.). Evidência=Hipótese Art.311. 3.PEDIDO LIMINAR: Medida CLARA. 4.Se Antec.: Indicar pedido final/aditar. Valor Causa. 5.PEDIDOS: Concessão tutela; citação/intim.; indic. ação/adit. 6.Fechamento OAB."
|
| 233 |
-
else: prompt += "1.Estrutura Lógica. 2.Argumentos Claros. 3.Pedido Claro. 4.Formalidades OAB."
|
| 234 |
-
prompt += "\n\n**Instrução Final:** Gere **APENAS** o texto completo da peça corrigida abaixo, começando pelo endereçamento e terminando com 'OAB/...'. Sem comentários extras."
|
| 235 |
return prompt
|
| 236 |
|
| 237 |
def pos_corrigir_peticao(peticao_corrigida, tipo_peticao):
|
| 238 |
if "Nestes termos, pede deferimento" in peticao_corrigida:
|
| 239 |
peticao_corrigida = peticao_corrigida.replace("Nestes termos, pede deferimento", "Nestes termos pede deferimento")
|
| 240 |
if tipo_peticao == "Petição Inicial":
|
|
|
|
| 241 |
peticao_lower = peticao_corrigida.lower()
|
| 242 |
if "juntada da guia de custas" not in peticao_lower and "gratuidade de justiça" not in peticao_lower:
|
| 243 |
partes = peticao_corrigida.split("Nestes termos pede deferimento")
|
|
@@ -248,74 +202,93 @@ def pos_corrigir_peticao(peticao_corrigida, tipo_peticao):
|
|
| 248 |
if len(partes) == 2: peticao_corrigida = partes[0].strip() + "\nOpta o Autor pela realização da audiência de conciliação ou mediação (Art. 319, VII, CPC).\nNestes termos pede deferimento" + partes[1]
|
| 249 |
else: peticao_corrigida = peticao_corrigida.replace("Local, data.", "Opta o Autor pela realização da audiência de conciliação ou mediação (Art. 319, VII, CPC).\n\nLocal, data.")
|
| 250 |
linhas = [l.strip() for l in peticao_corrigida.strip().split('\n') if l.strip()]
|
| 251 |
-
|
| 252 |
-
while linhas and any(
|
| 253 |
-
linhas.extend(
|
| 254 |
return "\n".join(linhas)
|
| 255 |
|
|
|
|
| 256 |
def chamar_api(prompt):
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
for tentativa in range(CONFIG["max_retry_attempts"]):
|
| 261 |
try:
|
| 262 |
with rate_limiter:
|
| 263 |
logger.debug(f"API Call - Tentativa {tentativa + 1}/{CONFIG['max_retry_attempts']}")
|
| 264 |
-
response =
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
if response.
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
except
|
| 309 |
-
logger.error(f"
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
logger.error("Falha API após todas as tentativas.")
|
| 317 |
return "❌ Falha API após múltiplas tentativas."
|
| 318 |
|
|
|
|
| 319 |
def corrigir_peticao(caso, tipo_peticao, peticao_texto=None, peticao_arquivo=None, modo_treinamento=False):
|
| 320 |
logger.info(f"Iniciando correção. Tipo={tipo_peticao}, Treinamento={modo_treinamento}")
|
| 321 |
start_time = time.time()
|
|
@@ -339,12 +312,14 @@ def corrigir_peticao(caso, tipo_peticao, peticao_texto=None, peticao_arquivo=Non
|
|
| 339 |
logger.info("Correção encontrada no cache.")
|
| 340 |
cached = CORRECAO_CACHE[cache_key]
|
| 341 |
return cached["analise_formatada"], cached["peticao_corrigida"], cached["resumo_diff"], cached["criticas_diff"], cached["diff_html"]
|
|
|
|
| 342 |
prompt_analise = estruturar_prompt_analise(caso, peticao_conteudo, tipo_peticao, modo_treinamento)
|
| 343 |
-
analise_bruta = chamar_api(prompt_analise)
|
| 344 |
if analise_bruta.startswith(("❌", "⏱️", "⚠️")): return analise_bruta, "", "", "", ""
|
| 345 |
analise_formatada = formatar_correcao(analise_bruta)
|
|
|
|
| 346 |
prompt_peticao = estruturar_prompt_peticao_corrigida(caso, peticao_conteudo, tipo_peticao)
|
| 347 |
-
peticao_corrigida_bruta = chamar_api(prompt_peticao)
|
| 348 |
if peticao_corrigida_bruta.startswith(("❌", "⏱️", "⚠️")):
|
| 349 |
peticao_corrigida_final = f"⚠️ Falha ao gerar versão corrigida:\n{peticao_corrigida_bruta}"
|
| 350 |
else:
|
|
@@ -356,6 +331,7 @@ def corrigir_peticao(caso, tipo_peticao, peticao_texto=None, peticao_arquivo=Non
|
|
| 356 |
if idx != -1: start_index = idx; break
|
| 357 |
peticao_extraida = peticao_corrigida_bruta[start_index:] if start_index != -1 else peticao_corrigida_bruta
|
| 358 |
peticao_corrigida_final = pos_corrigir_peticao(peticao_extraida, tipo_peticao)
|
|
|
|
| 359 |
diff_html, resumo_diff, criticas_diff = calcular_diff_interno(peticao_conteudo, peticao_corrigida_final)
|
| 360 |
resultado_cache = {"analise_formatada": analise_formatada,"peticao_corrigida": peticao_corrigida_final,"resumo_diff": resumo_diff,"criticas_diff": criticas_diff,"diff_html": diff_html}
|
| 361 |
CORRECAO_CACHE[cache_key] = resultado_cache
|
|
@@ -368,6 +344,8 @@ def corrigir_peticao(caso, tipo_peticao, peticao_texto=None, peticao_arquivo=Non
|
|
| 368 |
logger.exception("Erro GERAL inesperado dentro de corrigir_peticao:")
|
| 369 |
return f"❌ Erro inesperado correção: {str(e)}", "", "", "", ""
|
| 370 |
|
|
|
|
|
|
|
| 371 |
def formatar_correcao(texto):
|
| 372 |
texto = texto.replace("### Análise Geral", "## 📝 Análise Geral")
|
| 373 |
texto = texto.replace("### Nota Final", "## ⭐ Nota Final")
|
|
@@ -419,18 +397,13 @@ def calcular_diff_interno(texto_original, texto_corrigido):
|
|
| 419 |
kw_add = ["custas", "art. 319", "art. 337", "art. 1022", "art. 1015", "art. 300", "fumus", "periculum", "essencial", "preliminar", "tempestividade", "cabimento"]
|
| 420 |
for linha in diff:
|
| 421 |
txt = linha[2:]; txt_esc = escape(txt)
|
| 422 |
-
if linha.startswith(' '):
|
| 423 |
-
res_html.append(f"<div class='linha-igual'>{txt_esc}</div>")
|
| 424 |
elif linha.startswith('- '):
|
| 425 |
-
l_rem += 1
|
| 426 |
-
|
| 427 |
-
if txt.strip() and any(kw in txt.lower() for kw in kw_rem):
|
| 428 |
-
crit.append(f"<span style='color:red'>🔴 REMOVIDO:</span> {txt_esc}")
|
| 429 |
elif linha.startswith('+ '):
|
| 430 |
-
l_add += 1
|
| 431 |
-
|
| 432 |
-
if txt.strip() and any(kw in txt.lower() for kw in kw_add):
|
| 433 |
-
crit.append(f"<span style='color:green'>🟢 ADICIONADO:</span> {txt_esc}")
|
| 434 |
resumo = f"**Resumo:** Rem={l_rem}, Add={l_add}, Saldo={l_add - l_rem}"
|
| 435 |
crit_txt = "### 🚨 Mudanças Críticas:\n" + "\n".join(crit) if crit else "*Nenhuma mudança crítica detectada.*"
|
| 436 |
css = "<style>.diff-container{font-family:monospace;white-space:pre-wrap;border:1px solid #ccc;padding:10px;background-color:#f9f9f9;max-height:600px;overflow-y:auto}.linha-igual{color:#555}.linha-removida{background-color:#ffebee;color:#c62828}.linha-adicionada{background-color:#e8f5e9;color:#2e7d32}.linha-removida del,.linha-adicionada ins{text-decoration:none;padding:0 2px;border-radius:2px}.linha-removida del{background-color:#ffcdd2}.linha-adicionada ins{background-color:#c8e6c9}</style>"
|
|
@@ -444,12 +417,9 @@ def calcular_progresso():
|
|
| 444 |
tipos.add(entry["tipo_peticao"])
|
| 445 |
m = re.search(r"Nota Final:?\s*\**(\d{1,2}[,.]\d{1}|\d{1,2})\s*/\s*10", entry["analise_formatada"], re.I)
|
| 446 |
if m:
|
| 447 |
-
try:
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
pass # Ignora se não conseguir converter a nota
|
| 451 |
-
tot = len(HISTORICO); media = sum(notas)/len(notas) if notas else 0
|
| 452 |
-
tend = ""
|
| 453 |
if len(notas) > 1: tend = "⬆️" if notas[-1] > notas[-2] else ("⬇️" if notas[-1] < notas[-2] else "↔️")
|
| 454 |
return f"**📊 Resumo:** Corrs:{tot}, Tipos:{len(tipos)}, Média:{media:.1f}/10 ({len(notas)} val), Tend:{tend if tend else 'N/A'}"
|
| 455 |
|
|
@@ -491,18 +461,15 @@ def carregar_progresso(arquivo_entrada):
|
|
| 491 |
return tipo, caso, peticao, "✅ Progresso carregado!"
|
| 492 |
except Exception as e: logger.error(f"Erro carregar: {e}", exc_info=True); return "", "", "", f"❌ Erro carregar: {e}"
|
| 493 |
|
|
|
|
| 494 |
def estruturar_prompt_geracao_caso(tipo_peticao="Petição Inicial"):
|
| 495 |
prompt = f"Crie caso fictício **detalhado e realista** p/ peça **'{tipo_peticao}'** (simulado OAB).\n**Geral:**\n1.Complexidade adequada (15-25l).\n2.Dados Essenciais (nomes,locais,datas,valores,relação,conflito).\n3.Coerência.\n4.Foco no Tipo Peça.\n**Específico '{tipo_peticao}':**\n"
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
elif tipo_peticao == "
|
| 499 |
-
|
| 500 |
-
elif tipo_peticao == "Embargos de Declaração": prompt += "* Breve caso. * TRECHO DECISÃO JUDICIAL. * INDIQUE VÍCIO (omissão/contradição/obscuridade/erro material) NESSA DECISÃO."
|
| 501 |
-
elif tipo_peticao == "Recurso de Apelação": prompt += "* Caso original. * Resumo SENTENÇA. * ELEMENTOS indicando ERRO (in judicando/procedendo). * Indique cliente (apelante)."
|
| 502 |
-
elif tipo_peticao == "Agravo de Instrumento": prompt += "* Caso em andamento. * DECISÃO INTERLOCUTÓRIA. * Enquadrar Art.1015/mitigação. * ELEMENTOS indicando ERRO + URGÊNCIA. * Indique cliente (agravante)."
|
| 503 |
-
elif tipo_peticao == "Petição de Tutela Provisória": prompt += "* Situação URGENTE ou EVIDENTE. * ELEMENTOS: Urgência=Fumus+Periculum; Evidência=Hipótese Art.311. * Medida específica. * Antecedente ou Incidental."
|
| 504 |
else: prompt += "* Crie cenário cível com conflito claro."
|
| 505 |
-
prompt += f'\n**Finalize:**\n"**Situação-Problema:** Advogado(a) de [Nome Cliente], elabore a peça processual cabível ({tipo_peticao})."\n**Importante:** Temas comuns.
|
| 506 |
return prompt
|
| 507 |
|
| 508 |
def gerar_caso_ficticio(tipo_peticao="Petição Inicial"):
|
|
@@ -510,7 +477,7 @@ def gerar_caso_ficticio(tipo_peticao="Petição Inicial"):
|
|
| 510 |
try:
|
| 511 |
if not tipo_peticao or tipo_peticao not in TIPOS_DE_PETICAO: return "⚠️ Selecione tipo válido."
|
| 512 |
prompt = estruturar_prompt_geracao_caso(tipo_peticao)
|
| 513 |
-
caso_gerado = chamar_api(prompt)
|
| 514 |
if caso_gerado.startswith(("❌","⏱️","⚠️")): return f"⚠️ Falha gerar caso:\n{caso_gerado}"
|
| 515 |
frase_final = "**Situação-Problema:** Na qualidade de advogado(a) de"
|
| 516 |
if frase_final.lower() not in caso_gerado.lower():
|
|
@@ -522,13 +489,17 @@ def gerar_caso_ficticio(tipo_peticao="Petição Inicial"):
|
|
| 522 |
return caso_gerado
|
| 523 |
except Exception as e: logger.exception("Erro gerar_caso:"); return f"❌ Erro gerar caso: {e}"
|
| 524 |
|
|
|
|
| 525 |
def atualizar_config(temp, timeout, tokens):
|
| 526 |
CONFIG["temperatura"] = temp
|
| 527 |
-
CONFIG["
|
| 528 |
CONFIG["max_tokens"] = tokens
|
| 529 |
-
logger.info(f"Configs atualizadas: Temp={temp},
|
| 530 |
-
|
|
|
|
|
|
|
| 531 |
|
|
|
|
| 532 |
def interface_gradio():
|
| 533 |
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="orange")) as demo:
|
| 534 |
gr.Markdown("# ⚖️ Corretor Jurídico OAB Master ⚖️\nAnalise e aprimore suas peças processuais!")
|
|
@@ -539,7 +510,8 @@ def interface_gradio():
|
|
| 539 |
modo_treinamento = gr.Checkbox(label="🎓 Modo Treinamento", value=False)
|
| 540 |
with gr.Accordion("🛠️ Opções Avançadas", open=False):
|
| 541 |
temperatura_slider = gr.Slider(0.0, 1.0, value=CONFIG["temperatura"], step=0.1, label="Criatividade")
|
| 542 |
-
|
|
|
|
| 543 |
max_tokens_slider = gr.Slider(2048, 8192, value=CONFIG["max_tokens"], step=256, label="Max Tokens")
|
| 544 |
config_update_btn = gr.Button("💾 Salvar Configs")
|
| 545 |
config_status = gr.Textbox(label="Status", interactive=False, lines=1)
|
|
@@ -583,6 +555,7 @@ def interface_gradio():
|
|
| 583 |
with gr.Column(scale=3): visualizacao_detalhada_output = gr.HTML(label="Detalhes")
|
| 584 |
clear_btn = gr.Button("🗑️ Limpar Tudo")
|
| 585 |
|
|
|
|
| 586 |
caso_btn.click(lambda: gr.update(value="⏳ Gerando...", visible=True), outputs=processing_message).then(
|
| 587 |
fn=gerar_caso_ficticio, inputs=[tipo_peticao_input], outputs=[caso_input]
|
| 588 |
).then(lambda: gr.update(value="", visible=False), outputs=processing_message)
|
|
@@ -595,7 +568,7 @@ def interface_gradio():
|
|
| 595 |
fn=corrigir_peticao,
|
| 596 |
inputs=[caso_input, tipo_peticao_input, peticao_texto_input, gr.State(None), modo_treinamento],
|
| 597 |
outputs=[resultado_analise_output, resultado_peticao_corrigida_output, resumo_alteracoes_output, criticas_output, visualizacao_detalhada_output],
|
| 598 |
-
concurrency_limit=1
|
| 599 |
).then(lambda: gr.update(value="", visible=False), outputs=processing_message)
|
| 600 |
|
| 601 |
btn_corrigir_arquivo.click(lambda: gr.update(value="⏳ Processando arquivo...", visible=True), outputs=processing_message).then(
|
|
@@ -605,7 +578,8 @@ def interface_gradio():
|
|
| 605 |
concurrency_limit=1
|
| 606 |
).then(lambda: gr.update(value="", visible=False), outputs=processing_message)
|
| 607 |
|
| 608 |
-
|
|
|
|
| 609 |
progresso_btn.click(calcular_progresso, outputs=progresso_output)
|
| 610 |
historico_btn.click(mostrar_historico, outputs=historico_output)
|
| 611 |
|
|
@@ -631,6 +605,9 @@ def interface_gradio():
|
|
| 631 |
|
| 632 |
if __name__ == "__main__":
|
| 633 |
logger.info("Iniciando a aplicação Gradio...")
|
|
|
|
|
|
|
|
|
|
| 634 |
app = interface_gradio()
|
| 635 |
app.launch()
|
| 636 |
logger.info("Aplicação Gradio encerrada.")
|
|
|
|
| 1 |
# -*- coding: utf-8 -*-
|
| 2 |
import gradio as gr
|
| 3 |
+
# import requests # Não é mais necessário para a API
|
| 4 |
+
import google.generativeai as genai # Importa a biblioteca oficial
|
| 5 |
+
from google.api_core import exceptions as google_exceptions # Para tratar erros da API
|
| 6 |
import os
|
| 7 |
import json
|
| 8 |
import docx
|
|
|
|
| 32 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - [%(funcName)s] - %(message)s')
|
| 33 |
logger = logging.getLogger(__name__)
|
| 34 |
|
| 35 |
+
# Carregar Variáveis de Ambiente e Configurar API Key
|
| 36 |
load_dotenv()
|
| 37 |
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") or os.environ.get("GEMINI_API_KEY")
|
| 38 |
if not GEMINI_API_KEY:
|
| 39 |
raise ValueError("❌ A chave GEMINI_API_KEY não foi encontrada. Configure-a no arquivo .env ou como Secret no Hugging Face.")
|
| 40 |
|
| 41 |
+
try:
|
| 42 |
+
genai.configure(api_key=GEMINI_API_KEY)
|
| 43 |
+
logger.info("Biblioteca google-generativeai configurada com sucesso.")
|
| 44 |
+
except Exception as e:
|
| 45 |
+
raise ValueError(f"❌ Erro ao configurar a biblioteca google-generativeai: {e}")
|
| 46 |
+
|
| 47 |
+
# Constantes e Configurações Globais
|
| 48 |
+
GEMINI_MODEL_NAME = "gemini-1.5-flash-latest" # Modelo a ser usado pela biblioteca
|
| 49 |
+
CONFIG = {"max_retry_attempts": 3, "retry_delay": 5, "timeout_api": 120, "temperatura": 0.3, "max_tokens": 8192} # Timeout não é mais direto na chamada da lib, mas mantemos para retries
|
| 50 |
+
rate_limiter = RateLimiter(max_calls=15, period=60) # Reduzir ainda mais para segurança
|
| 51 |
+
|
| 52 |
+
# Configurações de Segurança (ajuste conforme necessário)
|
| 53 |
+
# Bloqueia conteúdo de risco ALTO em todas as categorias
|
| 54 |
+
SAFETY_SETTINGS = [
|
| 55 |
+
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
| 56 |
+
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
| 57 |
+
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
| 58 |
+
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
| 59 |
+
]
|
| 60 |
|
| 61 |
TIPOS_DE_PETICAO = ["Petição Inicial", "Contestação", "Reconvenção", "Impugnação à Contestação", "Embargos de Declaração", "Recurso de Apelação", "Agravo de Instrumento", "Petição de Tutela Provisória"]
|
| 62 |
|
|
|
|
| 73 |
HISTORICO = []
|
| 74 |
CORRECAO_CACHE = {}
|
| 75 |
|
| 76 |
+
# --- Funções Auxiliares (Extração de Texto, Preprocessamento, etc.) ---
|
| 77 |
+
# (Mantidas como na versão anterior, sem alterações significativas)
|
| 78 |
def preprocess_image_for_ocr(img_pil):
|
| 79 |
if not CV2_AVAILABLE:
|
| 80 |
logger.warning("OpenCV não disponível, usando conversão simples para OCR.")
|
|
|
|
| 88 |
return img_pil.convert('L')
|
| 89 |
|
| 90 |
def extract_text_from_pdf(file_path_or_obj):
|
| 91 |
+
text = ""; ocr_text_parts = []
|
|
|
|
| 92 |
try:
|
| 93 |
logger.info(f"Abrindo PDF: {getattr(file_path_or_obj, 'name', file_path_or_obj)}")
|
| 94 |
+
with pdfplumber_open(file_path_or_obj) as pdf:
|
| 95 |
+
logger.info(f"PDF com {len(pdf.pages)} páginas.")
|
| 96 |
+
for i, page in enumerate(pdf.pages):
|
| 97 |
+
page_text = page.extract_text(x_tolerance=2, y_tolerance=2)
|
| 98 |
+
if page_text and len(page_text.strip()) > 20: text += page_text + "\n"
|
| 99 |
+
else:
|
| 100 |
+
logger.warning(f"Página {i+1} sem texto, tentando OCR.")
|
| 101 |
+
try:
|
| 102 |
+
img = page.to_image(resolution=200)
|
| 103 |
+
preprocessed_img = preprocess_image_for_ocr(img.original)
|
| 104 |
+
page_ocr = pytesseract.image_to_string(preprocessed_img, lang='por', config='--psm 6')
|
| 105 |
+
if page_ocr and page_ocr.strip(): ocr_text_parts.append(page_ocr)
|
| 106 |
+
else: logger.warning(f"OCR vazio pág {i+1}."); ocr_text_parts.append(f"[OCR P{i+1}: Vazio]")
|
| 107 |
+
except Exception as e: logger.error(f"Erro OCR pág {i+1}: {e}"); ocr_text_parts.append(f"[Erro OCR P{i+1}: {e}]")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
ocr_text = "\n".join(ocr_text_parts)
|
| 109 |
+
len_text, len_ocr = len(text.strip()), len(ocr_text.strip())
|
| 110 |
+
if len_text > len_ocr * 0.7: logger.info(f"Usando texto extraído PDF ({len_text} chars)."); return text
|
| 111 |
+
elif len_ocr > 0: logger.info(f"Usando texto OCR PDF ({len_ocr} chars)."); return ocr_text
|
| 112 |
+
elif len_text > 0: logger.info("Usando texto extraído curto PDF."); return text
|
| 113 |
+
else: logger.warning("PDF sem texto extraível."); return "⚠️ PDF sem texto (extração e OCR falharam)."
|
| 114 |
+
except Exception as e: logger.error(f"Erro PDF: {e}", exc_info=True); return f"❌ Erro PDF: {e}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
def extract_text_from_docx(file_path_or_obj):
|
| 117 |
try:
|
| 118 |
doc = docx.Document(file_path_or_obj)
|
| 119 |
+
text = "\n".join([p.text for p in doc.paragraphs if p.text])
|
| 120 |
+
return text if text.strip() else "⚠️ DOCX vazio."
|
| 121 |
+
except Exception as e: logger.error(f"Erro DOCX: {e}", exc_info=True); return f"❌ Erro DOCX: {e}"
|
|
|
|
|
|
|
| 122 |
|
| 123 |
def extract_text_from_txt(file_path_or_obj):
|
| 124 |
try:
|
| 125 |
+
content = b''; encodings = ['utf-8', 'latin-1', 'cp1252']
|
| 126 |
if isinstance(file_path_or_obj, str):
|
| 127 |
+
with open(file_path_or_obj, 'rb') as f: content = f.read()
|
|
|
|
| 128 |
elif hasattr(file_path_or_obj, 'read'):
|
| 129 |
+
if hasattr(file_path_or_obj, 'seek'): try: file_path_or_obj.seek(0) except: pass
|
|
|
|
|
|
|
| 130 |
content = file_path_or_obj.read()
|
| 131 |
+
else: return "❌ Input TXT inválido."
|
| 132 |
+
for enc in encodings:
|
| 133 |
+
try: decoded = content.decode(enc); logger.info(f"TXT decodificado: {enc}."); return decoded if decoded.strip() else "⚠️ TXT vazio."
|
| 134 |
+
except UnicodeDecodeError: continue
|
| 135 |
+
return "❌ Não foi possível decodificar TXT."
|
| 136 |
+
except Exception as e: logger.error(f"Erro TXT: {e}", exc_info=True); return f"❌ Erro TXT: {e}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
def process_file(file_obj):
|
| 139 |
+
if file_obj is None: return "⚠️ Nenhum arquivo enviado."
|
|
|
|
|
|
|
| 140 |
try:
|
| 141 |
+
fpath = getattr(file_obj, 'name', None);
|
| 142 |
+
if not fpath: return "❌ Erro: Não identificou arquivo."
|
| 143 |
+
fname = os.path.basename(fpath).lower(); logger.info(f"Processando: {fname}")
|
|
|
|
|
|
|
|
|
|
| 144 |
try:
|
| 145 |
+
fsize = os.path.getsize(fpath); logger.info(f"Tamanho: {fsize} bytes")
|
| 146 |
+
if fsize > 15*1024*1024: return "⚠️ Arquivo grande (Máx 15MB)."
|
| 147 |
+
except OSError as e: logger.warning(f"Erro tamanho arquivo: {e}")
|
| 148 |
+
if fname.endswith('.pdf'): return extract_text_from_pdf(fpath)
|
| 149 |
+
elif fname.endswith('.docx'): return extract_text_from_docx(fpath)
|
| 150 |
+
elif fname.endswith('.txt'): return extract_text_from_txt(fpath)
|
| 151 |
+
else: return "⚠️ Formato não suportado (PDF, DOCX, TXT)."
|
| 152 |
+
except Exception as e: logger.error(f"Erro process_file: {e}", exc_info=True); return f"❌ Erro processar arquivo: {e}"
|
| 153 |
+
|
| 154 |
+
# --- Estruturação de Prompts (sem alterações) ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
def get_instrucoes_gerais_avaliacao(modo_treinamento=False):
|
| 156 |
base = """
|
| 157 |
**Estrutura da Avaliação:**
|
|
|
|
| 168 |
|
| 169 |
def estruturar_prompt_analise(caso, peticao, tipo_peticao, modo_treinamento=False):
|
| 170 |
instrucoes_gerais = get_instrucoes_gerais_avaliacao(modo_treinamento)
|
| 171 |
+
prompt = f"## Análise: {tipo_peticao}\n**Caso:**\n```\n{caso}\n```\n**Peça Aluno:**\n```\n{peticao}\n```\n{instrucoes_gerais}\n**Requisitos Específicos ({tipo_peticao}):**\n"
|
| 172 |
+
# (Adicionar requisitos específicos por tipo como antes - omitido por brevidade)
|
| 173 |
+
if tipo_peticao == "Petição Inicial": prompt += "- Endereçamento/competência...\n**Critérios Adicionais:** Usar 'XXX'..."
|
| 174 |
+
elif tipo_peticao == "Contestação": prompt += "- Endereçamento...\n**Critérios Adicionais:** Preliminar não arguida..."
|
| 175 |
+
# ... (outros tipos) ...
|
| 176 |
+
else: prompt += "- Adequação tipo? Clareza? Fundamentação? Pedido?"
|
| 177 |
+
prompt += "\n\n**Instruções Finais:** Analise estritamente. Seja rigoroso e construtivo. Gere **exclusivamente** no formato solicitado."
|
| 178 |
+
return prompt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
def estruturar_prompt_peticao_corrigida(caso, peticao_original, tipo_peticao):
|
| 181 |
+
prompt = f"**Caso:**\n```\n{caso}\n```\n**Peça Original ({tipo_peticao}):**\n```\n{peticao_original}\n```\n**Tarefa:** Gere versão corrigida ({tipo_peticao}), seguindo práticas OAB.\n**Instruções ({tipo_peticao}):**\n"
|
| 182 |
+
# (Adicionar instruções específicas por tipo como antes - omitido por brevidade)
|
| 183 |
+
if tipo_peticao == "Petição Inicial": prompt += "1.Endereçamento... 11.Fechamento ('Nestes termos...')..."
|
| 184 |
+
elif tipo_peticao == "Contestação": prompt += "1.Endereçamento... 7.Fechamento OAB."
|
| 185 |
+
# ... (outros tipos) ...
|
| 186 |
+
else: prompt += "1.Estrutura Lógica. 2.Argumentos. 3.Pedido. 4.Formalidades."
|
| 187 |
+
prompt += "\n\n**Instrução Final:** Gere **APENAS** texto da peça corrigida (endereçamento até 'OAB/...'). Sem comentários extras."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
return prompt
|
| 189 |
|
| 190 |
def pos_corrigir_peticao(peticao_corrigida, tipo_peticao):
|
| 191 |
if "Nestes termos, pede deferimento" in peticao_corrigida:
|
| 192 |
peticao_corrigida = peticao_corrigida.replace("Nestes termos, pede deferimento", "Nestes termos pede deferimento")
|
| 193 |
if tipo_peticao == "Petição Inicial":
|
| 194 |
+
# (Lógica para adicionar custas e audiência mantida)
|
| 195 |
peticao_lower = peticao_corrigida.lower()
|
| 196 |
if "juntada da guia de custas" not in peticao_lower and "gratuidade de justiça" not in peticao_lower:
|
| 197 |
partes = peticao_corrigida.split("Nestes termos pede deferimento")
|
|
|
|
| 202 |
if len(partes) == 2: peticao_corrigida = partes[0].strip() + "\nOpta o Autor pela realização da audiência de conciliação ou mediação (Art. 319, VII, CPC).\nNestes termos pede deferimento" + partes[1]
|
| 203 |
else: peticao_corrigida = peticao_corrigida.replace("Local, data.", "Opta o Autor pela realização da audiência de conciliação ou mediação (Art. 319, VII, CPC).\n\nLocal, data.")
|
| 204 |
linhas = [l.strip() for l in peticao_corrigida.strip().split('\n') if l.strip()]
|
| 205 |
+
fechamento = ["Local, data.", "ADVOGADO(A)", "OAB/..."]
|
| 206 |
+
while linhas and any(t in linhas[-1] for t in ["ADVOGADO","OAB","Local","data"]): linhas.pop()
|
| 207 |
+
linhas.extend(fechamento)
|
| 208 |
return "\n".join(linhas)
|
| 209 |
|
| 210 |
+
# --- Chamada à API Refatorada ---
|
| 211 |
def chamar_api(prompt):
|
| 212 |
+
logger.info(f"Chamando API Gemini (Modelo: {GEMINI_MODEL_NAME})...")
|
| 213 |
+
model = genai.GenerativeModel(GEMINI_MODEL_NAME, safety_settings=SAFETY_SETTINGS)
|
| 214 |
+
generation_config = genai.types.GenerationConfig(
|
| 215 |
+
temperature=CONFIG["temperatura"],
|
| 216 |
+
max_output_tokens=CONFIG["max_tokens"],
|
| 217 |
+
top_p=0.95 # Exemplo, pode ser ajustado ou removido
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
for tentativa in range(CONFIG["max_retry_attempts"]):
|
| 221 |
try:
|
| 222 |
with rate_limiter:
|
| 223 |
logger.debug(f"API Call - Tentativa {tentativa + 1}/{CONFIG['max_retry_attempts']}")
|
| 224 |
+
response = model.generate_content(
|
| 225 |
+
contents=prompt,
|
| 226 |
+
generation_config=generation_config,
|
| 227 |
+
# request_options={'timeout': CONFIG["timeout_api"]} # Timeout pode ser configurado aqui se suportado
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
# Verifica bloqueio no prompt feedback mesmo sem exceção
|
| 231 |
+
if response.prompt_feedback.block_reason:
|
| 232 |
+
logger.error(f"Prompt bloqueado pela API. Motivo: {response.prompt_feedback.block_reason}")
|
| 233 |
+
return f"❌ Prompt bloqueado: {response.prompt_feedback.block_reason}. Reformule."
|
| 234 |
+
|
| 235 |
+
# Verifica se há texto na resposta
|
| 236 |
+
if not response.text:
|
| 237 |
+
# Verifica se a geração foi interrompida por segurança ou outro motivo
|
| 238 |
+
if response.candidates and response.candidates[0].finish_reason != genai.types.FinishReason.STOP:
|
| 239 |
+
finish_reason = response.candidates[0].finish_reason.name
|
| 240 |
+
logger.error(f"Geração API interrompida (sem texto): {finish_reason}")
|
| 241 |
+
safety_ratings = response.candidates[0].safety_ratings
|
| 242 |
+
harm_cats = [r.category.name for r in safety_ratings if r.probability.name not in ['NEGLIGIBLE', 'LOW']]
|
| 243 |
+
if harm_cats: return f"❌ Bloqueado por segurança: {finish_reason} ({','.join(harm_cats)})."
|
| 244 |
+
else: return f"❌ Geração interrompida: {finish_reason}."
|
| 245 |
+
else:
|
| 246 |
+
# Resposta vazia por outro motivo
|
| 247 |
+
logger.warning("API retornou resposta sem texto e sem bloqueio aparente.")
|
| 248 |
+
return "⚠️ API retornou resposta vazia." # Pode ser token insuficiente, etc.
|
| 249 |
+
|
| 250 |
+
logger.info(f"API retornou com sucesso ({len(response.text)} chars).")
|
| 251 |
+
return response.text
|
| 252 |
+
|
| 253 |
+
# Tratamento de Erros Específicos da Biblioteca google-generativeai
|
| 254 |
+
except google_exceptions.ResourceExhausted as e: # Erro 429 (Rate Limit)
|
| 255 |
+
logger.warning(f"Erro 429 (Rate Limit) - Tentativa {tentativa + 1}: {e}")
|
| 256 |
+
if tentativa < CONFIG["max_retry_attempts"] - 1:
|
| 257 |
+
wait_time = CONFIG['retry_delay'] * (2 ** tentativa) # Backoff exponencial
|
| 258 |
+
logger.info(f"Aguardando {wait_time}s para retentativa...")
|
| 259 |
+
time.sleep(wait_time)
|
| 260 |
+
continue
|
| 261 |
+
return "❌ Erro 429: Muitas requisições. Tente mais tarde."
|
| 262 |
+
except google_exceptions.PermissionDenied as e:
|
| 263 |
+
logger.error(f"Erro de Permissão API: {e}")
|
| 264 |
+
return f"❌ Erro de Permissão: Verifique sua API Key e acesso ao modelo '{GEMINI_MODEL_NAME}'."
|
| 265 |
+
except google_exceptions.InvalidArgument as e:
|
| 266 |
+
logger.error(f"Erro de Argumento Inválido API: {e}")
|
| 267 |
+
return f"❌ Erro nos parâmetros da API: {e}"
|
| 268 |
+
except genai.types.BlockedPromptException as e:
|
| 269 |
+
logger.error(f"Prompt bloqueado pela API (exceção): {e}")
|
| 270 |
+
return f"❌ Prompt bloqueado. Reformule o conteúdo."
|
| 271 |
+
except genai.types.StopCandidateException as e:
|
| 272 |
+
logger.error(f"Geração interrompida pela API (exceção): {e}")
|
| 273 |
+
# Tenta extrair informação de segurança se disponível no erro
|
| 274 |
+
return f"❌ Geração interrompida pela API (segurança ou outro motivo)."
|
| 275 |
+
except google_exceptions.GoogleAPIError as e: # Outros erros da API Google
|
| 276 |
+
logger.error(f"Erro genérico da API Google - Tentativa {tentativa + 1}: {e}")
|
| 277 |
+
if tentativa < CONFIG["max_retry_attempts"] - 1:
|
| 278 |
+
time.sleep(CONFIG['retry_delay'] * (tentativa + 1))
|
| 279 |
+
continue
|
| 280 |
+
return f"❌ Erro da API Google: {e}"
|
| 281 |
+
except Exception as e: # Erros gerais (rede, etc.)
|
| 282 |
+
logger.exception(f"Erro inesperado - Tentativa {tentativa + 1}: {str(e)}")
|
| 283 |
+
if tentativa < CONFIG["max_retry_attempts"] - 1:
|
| 284 |
+
time.sleep(CONFIG['retry_delay'] * (tentativa + 1))
|
| 285 |
+
continue
|
| 286 |
+
return f"❌ Erro inesperado: {str(e)}"
|
| 287 |
+
|
| 288 |
logger.error("Falha API após todas as tentativas.")
|
| 289 |
return "❌ Falha API após múltiplas tentativas."
|
| 290 |
|
| 291 |
+
# --- Função Principal de Correção (Usa a nova chamar_api) ---
|
| 292 |
def corrigir_peticao(caso, tipo_peticao, peticao_texto=None, peticao_arquivo=None, modo_treinamento=False):
|
| 293 |
logger.info(f"Iniciando correção. Tipo={tipo_peticao}, Treinamento={modo_treinamento}")
|
| 294 |
start_time = time.time()
|
|
|
|
| 312 |
logger.info("Correção encontrada no cache.")
|
| 313 |
cached = CORRECAO_CACHE[cache_key]
|
| 314 |
return cached["analise_formatada"], cached["peticao_corrigida"], cached["resumo_diff"], cached["criticas_diff"], cached["diff_html"]
|
| 315 |
+
|
| 316 |
prompt_analise = estruturar_prompt_analise(caso, peticao_conteudo, tipo_peticao, modo_treinamento)
|
| 317 |
+
analise_bruta = chamar_api(prompt_analise) # Usa a nova função
|
| 318 |
if analise_bruta.startswith(("❌", "⏱️", "⚠️")): return analise_bruta, "", "", "", ""
|
| 319 |
analise_formatada = formatar_correcao(analise_bruta)
|
| 320 |
+
|
| 321 |
prompt_peticao = estruturar_prompt_peticao_corrigida(caso, peticao_conteudo, tipo_peticao)
|
| 322 |
+
peticao_corrigida_bruta = chamar_api(prompt_peticao) # Usa a nova função
|
| 323 |
if peticao_corrigida_bruta.startswith(("❌", "⏱️", "⚠️")):
|
| 324 |
peticao_corrigida_final = f"⚠️ Falha ao gerar versão corrigida:\n{peticao_corrigida_bruta}"
|
| 325 |
else:
|
|
|
|
| 331 |
if idx != -1: start_index = idx; break
|
| 332 |
peticao_extraida = peticao_corrigida_bruta[start_index:] if start_index != -1 else peticao_corrigida_bruta
|
| 333 |
peticao_corrigida_final = pos_corrigir_peticao(peticao_extraida, tipo_peticao)
|
| 334 |
+
|
| 335 |
diff_html, resumo_diff, criticas_diff = calcular_diff_interno(peticao_conteudo, peticao_corrigida_final)
|
| 336 |
resultado_cache = {"analise_formatada": analise_formatada,"peticao_corrigida": peticao_corrigida_final,"resumo_diff": resumo_diff,"criticas_diff": criticas_diff,"diff_html": diff_html}
|
| 337 |
CORRECAO_CACHE[cache_key] = resultado_cache
|
|
|
|
| 344 |
logger.exception("Erro GERAL inesperado dentro de corrigir_peticao:")
|
| 345 |
return f"❌ Erro inesperado correção: {str(e)}", "", "", "", ""
|
| 346 |
|
| 347 |
+
# --- Funções de Formatação, Dicas, Diff, Progresso, Histórico, Salvar/Carregar ---
|
| 348 |
+
# (Mantidas como na versão anterior, sem alterações lógicas necessárias)
|
| 349 |
def formatar_correcao(texto):
|
| 350 |
texto = texto.replace("### Análise Geral", "## 📝 Análise Geral")
|
| 351 |
texto = texto.replace("### Nota Final", "## ⭐ Nota Final")
|
|
|
|
| 397 |
kw_add = ["custas", "art. 319", "art. 337", "art. 1022", "art. 1015", "art. 300", "fumus", "periculum", "essencial", "preliminar", "tempestividade", "cabimento"]
|
| 398 |
for linha in diff:
|
| 399 |
txt = linha[2:]; txt_esc = escape(txt)
|
| 400 |
+
if linha.startswith(' '): res_html.append(f"<div class='linha-igual'>{txt_esc}</div>")
|
|
|
|
| 401 |
elif linha.startswith('- '):
|
| 402 |
+
l_rem += 1; res_html.append(f"<div class='linha-removida'><del>{txt_esc}</del></div>")
|
| 403 |
+
if txt.strip() and any(kw in txt.lower() for kw in kw_rem): crit.append(f"<span style='color:red'>🔴 REMOVIDO:</span> {txt_esc}")
|
|
|
|
|
|
|
| 404 |
elif linha.startswith('+ '):
|
| 405 |
+
l_add += 1; res_html.append(f"<div class='linha-adicionada'><ins>{txt_esc}</ins></div>")
|
| 406 |
+
if txt.strip() and any(kw in txt.lower() for kw in kw_add): crit.append(f"<span style='color:green'>🟢 ADICIONADO:</span> {txt_esc}")
|
|
|
|
|
|
|
| 407 |
resumo = f"**Resumo:** Rem={l_rem}, Add={l_add}, Saldo={l_add - l_rem}"
|
| 408 |
crit_txt = "### 🚨 Mudanças Críticas:\n" + "\n".join(crit) if crit else "*Nenhuma mudança crítica detectada.*"
|
| 409 |
css = "<style>.diff-container{font-family:monospace;white-space:pre-wrap;border:1px solid #ccc;padding:10px;background-color:#f9f9f9;max-height:600px;overflow-y:auto}.linha-igual{color:#555}.linha-removida{background-color:#ffebee;color:#c62828}.linha-adicionada{background-color:#e8f5e9;color:#2e7d32}.linha-removida del,.linha-adicionada ins{text-decoration:none;padding:0 2px;border-radius:2px}.linha-removida del{background-color:#ffcdd2}.linha-adicionada ins{background-color:#c8e6c9}</style>"
|
|
|
|
| 417 |
tipos.add(entry["tipo_peticao"])
|
| 418 |
m = re.search(r"Nota Final:?\s*\**(\d{1,2}[,.]\d{1}|\d{1,2})\s*/\s*10", entry["analise_formatada"], re.I)
|
| 419 |
if m:
|
| 420 |
+
try: notas.append(float(m.group(1).replace(",", ".")))
|
| 421 |
+
except ValueError: pass
|
| 422 |
+
tot = len(HISTORICO); media = sum(notas)/len(notas) if notas else 0; tend = ""
|
|
|
|
|
|
|
|
|
|
| 423 |
if len(notas) > 1: tend = "⬆️" if notas[-1] > notas[-2] else ("⬇️" if notas[-1] < notas[-2] else "↔️")
|
| 424 |
return f"**📊 Resumo:** Corrs:{tot}, Tipos:{len(tipos)}, Média:{media:.1f}/10 ({len(notas)} val), Tend:{tend if tend else 'N/A'}"
|
| 425 |
|
|
|
|
| 461 |
return tipo, caso, peticao, "✅ Progresso carregado!"
|
| 462 |
except Exception as e: logger.error(f"Erro carregar: {e}", exc_info=True); return "", "", "", f"❌ Erro carregar: {e}"
|
| 463 |
|
| 464 |
+
# --- Geração de Casos (Usa a nova chamar_api) ---
|
| 465 |
def estruturar_prompt_geracao_caso(tipo_peticao="Petição Inicial"):
|
| 466 |
prompt = f"Crie caso fictício **detalhado e realista** p/ peça **'{tipo_peticao}'** (simulado OAB).\n**Geral:**\n1.Complexidade adequada (15-25l).\n2.Dados Essenciais (nomes,locais,datas,valores,relação,conflito).\n3.Coerência.\n4.Foco no Tipo Peça.\n**Específico '{tipo_peticao}':**\n"
|
| 467 |
+
# (Adicionar instruções específicas por tipo como antes - omitido por brevidade)
|
| 468 |
+
if tipo_peticao == "Petição Inicial": prompt += "* Descreva violação direitos... * Tema: Consumo, Contrato, etc."
|
| 469 |
+
elif tipo_peticao == "Contestação": prompt += "* Resumo PI recebida... * Versão Réu..."
|
| 470 |
+
# ... (outros tipos) ...
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
else: prompt += "* Crie cenário cível com conflito claro."
|
| 472 |
+
prompt += f'\n**Finalize:**\n"**Situação-Problema:** Advogado(a) de [Nome Cliente], elabore a peça processual cabível ({tipo_peticao})."\n**Importante:** Temas comuns. Criativo!'
|
| 473 |
return prompt
|
| 474 |
|
| 475 |
def gerar_caso_ficticio(tipo_peticao="Petição Inicial"):
|
|
|
|
| 477 |
try:
|
| 478 |
if not tipo_peticao or tipo_peticao not in TIPOS_DE_PETICAO: return "⚠️ Selecione tipo válido."
|
| 479 |
prompt = estruturar_prompt_geracao_caso(tipo_peticao)
|
| 480 |
+
caso_gerado = chamar_api(prompt) # Usa a nova função
|
| 481 |
if caso_gerado.startswith(("❌","⏱️","⚠️")): return f"⚠️ Falha gerar caso:\n{caso_gerado}"
|
| 482 |
frase_final = "**Situação-Problema:** Na qualidade de advogado(a) de"
|
| 483 |
if frase_final.lower() not in caso_gerado.lower():
|
|
|
|
| 489 |
return caso_gerado
|
| 490 |
except Exception as e: logger.exception("Erro gerar_caso:"); return f"❌ Erro gerar caso: {e}"
|
| 491 |
|
| 492 |
+
# --- Função para Atualizar Config ---
|
| 493 |
def atualizar_config(temp, timeout, tokens):
|
| 494 |
CONFIG["temperatura"] = temp
|
| 495 |
+
# CONFIG["timeout_api"] = timeout # Timeout da API não é mais usado diretamente
|
| 496 |
CONFIG["max_tokens"] = tokens
|
| 497 |
+
logger.info(f"Configs atualizadas: Temp={temp}, Max Tokens={tokens}")
|
| 498 |
+
# Atualiza config do modelo se já instanciado (ou recria na próxima chamada)
|
| 499 |
+
# model.generation_config.temperature = temp # Exemplo, requer acesso à instância do modelo
|
| 500 |
+
return f"✅ Configs Atualizadas (Temp: {temp}, Tokens: {tokens})!"
|
| 501 |
|
| 502 |
+
# --- Interface Gradio ---
|
| 503 |
def interface_gradio():
|
| 504 |
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="orange")) as demo:
|
| 505 |
gr.Markdown("# ⚖️ Corretor Jurídico OAB Master ⚖️\nAnalise e aprimore suas peças processuais!")
|
|
|
|
| 510 |
modo_treinamento = gr.Checkbox(label="🎓 Modo Treinamento", value=False)
|
| 511 |
with gr.Accordion("🛠️ Opções Avançadas", open=False):
|
| 512 |
temperatura_slider = gr.Slider(0.0, 1.0, value=CONFIG["temperatura"], step=0.1, label="Criatividade")
|
| 513 |
+
# Timeout Slider removido pois não se aplica diretamente à lib google-genai
|
| 514 |
+
# timeout_slider = gr.Slider(30, 180, value=CONFIG["timeout_api"], step=10, label="Timeout (s)")
|
| 515 |
max_tokens_slider = gr.Slider(2048, 8192, value=CONFIG["max_tokens"], step=256, label="Max Tokens")
|
| 516 |
config_update_btn = gr.Button("💾 Salvar Configs")
|
| 517 |
config_status = gr.Textbox(label="Status", interactive=False, lines=1)
|
|
|
|
| 555 |
with gr.Column(scale=3): visualizacao_detalhada_output = gr.HTML(label="Detalhes")
|
| 556 |
clear_btn = gr.Button("🗑️ Limpar Tudo")
|
| 557 |
|
| 558 |
+
# --- Eventos ---
|
| 559 |
caso_btn.click(lambda: gr.update(value="⏳ Gerando...", visible=True), outputs=processing_message).then(
|
| 560 |
fn=gerar_caso_ficticio, inputs=[tipo_peticao_input], outputs=[caso_input]
|
| 561 |
).then(lambda: gr.update(value="", visible=False), outputs=processing_message)
|
|
|
|
| 568 |
fn=corrigir_peticao,
|
| 569 |
inputs=[caso_input, tipo_peticao_input, peticao_texto_input, gr.State(None), modo_treinamento],
|
| 570 |
outputs=[resultado_analise_output, resultado_peticao_corrigida_output, resumo_alteracoes_output, criticas_output, visualizacao_detalhada_output],
|
| 571 |
+
concurrency_limit=1 # Limita execuções simultâneas para evitar sobrecarga/rate limit
|
| 572 |
).then(lambda: gr.update(value="", visible=False), outputs=processing_message)
|
| 573 |
|
| 574 |
btn_corrigir_arquivo.click(lambda: gr.update(value="⏳ Processando arquivo...", visible=True), outputs=processing_message).then(
|
|
|
|
| 578 |
concurrency_limit=1
|
| 579 |
).then(lambda: gr.update(value="", visible=False), outputs=processing_message)
|
| 580 |
|
| 581 |
+
# Atualiza config - removido timeout_slider dos inputs
|
| 582 |
+
config_update_btn.click(atualizar_config, [temperatura_slider, max_tokens_slider], config_status)
|
| 583 |
progresso_btn.click(calcular_progresso, outputs=progresso_output)
|
| 584 |
historico_btn.click(mostrar_historico, outputs=historico_output)
|
| 585 |
|
|
|
|
| 605 |
|
| 606 |
if __name__ == "__main__":
|
| 607 |
logger.info("Iniciando a aplicação Gradio...")
|
| 608 |
+
# Configuração opcional Tesseract (se necessário)
|
| 609 |
+
# try: pytesseract.pytesseract.tesseract_cmd = '/usr/bin/tesseract' # Ex: Linux
|
| 610 |
+
# except Exception as e: logger.warning(f"Tesseract não configurado: {e}")
|
| 611 |
app = interface_gradio()
|
| 612 |
app.launch()
|
| 613 |
logger.info("Aplicação Gradio encerrada.")
|