fccoelho's picture
Updated code to latest gradio
24fc9d0
import gradio as gr
import pymupdf # PyMuPDF
import pandas as pd
from pydantic_ai import Agent
from pydantic_ai.settings import ModelSettings
from pydantic import BaseModel
from typing import List, Optional
import google.generativeai as genai
import openai
import os
from dotenv import load_dotenv
import io
import json
import re
# Padrões globais de regex para extração de referências
REFERENCE_PATTERNS = [
# Padrão 0: Referências numeradas com autores múltiplos (formato: Número. Autores. Título. Journal info (ano).)
r'^\d+\.\s*([A-Z][A-Za-z\s,&.-]+?(?:\s&\s[A-Z][A-Za-z\s,&.-]+?)*)\.\s*([^.]+?)\.\s*([^.]+?)\s+(\d+),?\s*([^(]*?)\s*\((\d{4})\)',
# Padrão 1: Autor(es). (Ano). Título. Journal/Editora.
r'^([A-Z][A-Za-z\s,&.-]+?)\.\s*\((\d{4}[a-z]?)\)\.\s*([^.]+?)\.\s*([^.]+?)\.?\s*$',
# Padrão 2: Referências numeradas [1] Autor... ano Título. Journal doi:...
r'^\[\d+\]\s*([A-Z][A-Za-z\s,&.-]+?)\s+(\d{4})\s+([^.]+?)\.\s*([^.]+?)(?:\s+doi:([^\s.]+))?\.?\s*$',
# Padrão 3: Autor, A. (Ano). Título. Journal.
r'^([A-Z][A-Za-z\s,&.-]+?)\s+\((\d{4}[a-z]?)\)[.,]\s*([^.]+?)[.,]\s*([^.]+?)\.?\s*$',
# Padrão 4: Autor et al. (Ano) Título. Journal
r'^([A-Z][A-Za-z\s,&.-]*?et\s+al\.?)\s*\((\d{4}[a-z]?)\)[.,]?\s*([^.]+?)[.,]\s*([^.]+?)\.?\s*$',
# Padrão 5: Sobrenome, Nome (Ano). Título. Journal.
r'^([A-Z][a-z]+,\s*[A-Z][A-Za-z\s,&.-]*?)\.\s*\((\d{4}[a-z]?)\)\.\s*([^.]+?)\.\s*([^.]+?)\.?\s*$',
# Padrão 6: Múltiplos autores com &
r'^([A-Z][A-Za-z\s,&.-]+?&[A-Za-z\s,&.-]+?)\.\s*\((\d{4}[a-z]?)\)\.\s*([^.]+?)\.\s*([^.]+?)\.?\s*$',
# Padrão 7: Referências numeradas [número] Autor: Título, Editora (ano)
r'^\[\d+\]\s*([A-Z][A-Za-z\s,&.-]+?):\s*([^,]+?),\s*([^(]+?)\s*\((\d{4})\)',
# Padrão 8: Referências numeradas com DOI opcional
r"""
^ # início de linha (após possível marcador de ordem)
(?:\d+\.\s*)? # número da referência (opcional), seguido de ponto e espaço
(?P<autores> # grupo 'autores'
[^\.]+? # tudo antes do primeiro ponto final (não guloso)
)\.\s+
(?P<titulo> # grupo 'titulo'
[^\n\.]+ # até o próximo ponto final ou quebra de linha
)\.
\s*
(?P<journal> # grupo 'journal'
[^\n;]+ # até o próximo ponto e vírgula (ou quebra de linha)
)
[;,]?\s*
(?P<ano> # grupo 'ano'
\d{4} # 4 dígitos (ano)
)
(?:;[^\n]*?)? # volume, issue, páginas (opcional, não capturado)
(?:\n+ # nova linha(s), captura DOI opcional
(?P<doi> https?://doi\.org/[^\s]+ )
)? # DOI pode estar na linha de baixo ou ausente
"""
]
class Reference(BaseModel):
authors: List[str]
title: str
journal: Optional[str] = None
year: Optional[int] = None
volume: Optional[str] = None
pages: Optional[str] = None
doi: Optional[str] = None
class ReferencesResponse(BaseModel):
references: List[Reference]
def extract_pdf_text(pdf_file):
"""Extrai texto e metadados básicos do PDF"""
try:
# Abrir o PDF com PyMuPDF
doc = pymupdf.open(stream=pdf_file, filetype="pdf")
# Extrair texto de todas as páginas
full_text = ""
for page_num in range(len(doc)):
page = doc.load_page(page_num)
full_text += page.get_text() + "\n"
# Extrair metadados básicos
metadata_dict = doc.metadata
metadata = {
"num_pages": len(doc),
"title": metadata_dict.get('title', 'Não disponível') if metadata_dict.get('title') else 'Não disponível',
"author": metadata_dict.get('author', 'Não disponível') if metadata_dict.get('author') else 'Não disponível',
"subject": metadata_dict.get('subject', 'Não disponível') if metadata_dict.get('subject') else 'Não disponível',
"creator": metadata_dict.get('creator', 'Não disponível') if metadata_dict.get('creator') else 'Não disponível'
}
# Fechar o documento
doc.close()
return full_text, metadata
except Exception as e:
return None, {"error": f"Erro ao processar PDF: {str(e)}"}
def extract_references_with_llm(text, model_name):
"""Usa Pydantic AI com diferentes modelos para extrair e estruturar referências"""
try:
# Determinar se é modelo Google ou OpenAI
if model_name.startswith('gemini'):
# Configurar a API key do Google
genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
api_key = os.getenv("GEMINI_API_KEY")
else:
# Usar OpenAI
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
return [{"error": f"Chave da API não encontrada para o modelo {model_name}"}]
# Criar o agente Pydantic AI
agent = Agent(
model_name,
model_settings=ModelSettings(
timeout=30,
),
output_type=ReferencesResponse,
system_prompt="""
Você é um especialista em análise de artigos científicos.
Sua tarefa é identificar e extrair APENAS a seção de referências bibliográficas do texto fornecido.
Para cada referência encontrada, extraia:
- authors: lista completa de autores
- title: título completo do trabalho
- journal: nome da revista/conferência/editora
- year: ano de publicação
- volume: volume (se disponível)
- pages: páginas (se disponível)
- doi: DOI (se disponível)
Seja preciso e extraia referências completas.
"""
)
# Ajustar limite de texto baseado no modelo
if model_name.startswith('gemini'):
limited_text = text[:1500000] # Gemini tem limite maior
else:
limited_text = text[:500000] # OpenAI tem limite menor
# Executar o agente
result = agent.run_sync(f"Extraia as referências bibliográficas do seguinte texto de artigo científico:\n\n{limited_text}")
# Converter para lista de dicionários para compatibilidade com DataFrame
references_list = []
for ref in result.output.references:
references_list.append({
"authors": ", ".join(ref.authors) if ref.authors else "",
"title": ref.title,
"journal": ref.journal or "",
"year": ref.year or "",
"volume": ref.volume or "",
"pages": ref.pages or "",
"doi": ref.doi or ""
})
return references_list
except Exception as e:
return [{"error": f"Erro ao processar com LLM ({model_name}): {str(e)}"}]
def extract_references_with_regex(text):
"""Extrai referências usando expressões regulares em todo o texto"""
try:
references = []
# Processar cada padrão
for pattern_index, pattern in enumerate(REFERENCE_PATTERNS):
reflist = re.findall(pattern, text, re.MULTILINE | re.UNICODE | re.DOTALL| re.VERBOSE)
if reflist:
for ref_match in reflist:
groups = ref_match
if len(groups) >= 4:
authors = groups[0].strip()
# Para o padrão numerado especial (6 grupos)
if len(groups) == 6:
title = groups[1].strip()
journal = groups[2].strip()
volume = groups[3].strip()
pages = groups[4].strip()
year = groups[5].strip()
# Para o padrão 7 (formato [número] Autor: Título, Editora (ano))
elif pattern_index == 7:
title = groups[1].strip()
journal = groups[2].strip()
year = groups[3].strip()
volume = ""
else:
# Para outros padrões (4 grupos)
year = groups[1].strip()
title = groups[2].strip()
journal = groups[3].strip()
volume = ""
# Extrair DOI se presente
doi_match = re.search(r'doi[:\s]*([^\s,]+)', journal, re.IGNORECASE)
doi = doi_match.group(1) if doi_match else ""
# Extrair volume e páginas (se não foram extraídos pelo padrão especial)
if len(groups) != 6:
vol_pages_match = re.search(r'(\d+)\s*\(?\d*\)?\s*[,:]\s*(\d+[-–]\d+)', journal)
volume = vol_pages_match.group(1) if vol_pages_match else ""
pages = vol_pages_match.group(2) if vol_pages_match else ""
else:
# Para o padrão numerado, extrair páginas do journal
pages_match = re.search(r'(\d+[-–]\d+)', journal)
pages = pages_match.group(1) if pages_match else ""
# Limpar campos
authors = re.sub(r'\s+', ' ', authors)
title = re.sub(r'\s+', ' ', title)
journal = re.sub(r'\s+', ' ', journal)
reference = {
"authors": authors,
"title": title,
"journal": journal,
"year": year,
"volume": volume,
"pages": pages,
"doi": doi
}
references.append(reference)
return references
except Exception as e:
return [{"error": f"Erro na extração por regex: {str(e)}"}]
def create_plain_text(text, regex_references):
"""Retorna o texto extraído como texto simples"""
try:
return text
except Exception as e:
return f"Erro ao processar texto: {str(e)}"
def process_pdf(pdf_file, model_name):
"""Função principal que processa o PDF e retorna resultados"""
if pdf_file is None:
return {"error": "Nenhum arquivo enviado"}, pd.DataFrame(), pd.DataFrame(), "❌ Nenhum arquivo enviado", "Nenhum texto para exibir"
# Extrair texto do PDF
text, metadata = extract_pdf_text(pdf_file)
if text is None:
return metadata, pd.DataFrame(), pd.DataFrame(), "❌ Erro ao processar PDF", "Erro ao extrair texto"
# Adicionar modelo selecionado aos metadados
metadata["modelo_usado"] = model_name
metadata["caracteres_extraidos"] = len(text)
metadata["palavras_aproximadas"] = len(text.split())
# Extrair referências com LLM
llm_references = extract_references_with_llm(text, model_name)
# Extrair referências com Regex
regex_references = extract_references_with_regex(text)
# Criar texto simples
plain_text = create_plain_text(text, regex_references)
# Converter para DataFrames
if llm_references and not any("error" in ref for ref in llm_references):
llm_df = pd.DataFrame(llm_references)
else:
llm_df = pd.DataFrame({"Erro": ["Não foi possível extrair referências com LLM"]})
if regex_references and not any("error" in ref for ref in regex_references):
regex_df = pd.DataFrame(regex_references)
else:
regex_df = pd.DataFrame({"Erro": ["Não foi possível extrair referências com Regex"]})
# Criar status
llm_count = len(llm_references) if llm_references and not any("error" in ref for ref in llm_references) else 0
regex_count = len(regex_references) if regex_references and not any("error" in ref for ref in regex_references) else 0
status = f"📊 **Resultados da Extração:**\n- LLM ({model_name}): {llm_count} referências\n- Regex: {regex_count} referências"
return metadata, llm_df, regex_df, status, plain_text
def create_interface():
"""Cria a interface Gradio"""
with gr.Blocks(title="Extrator de Referências") as interface:
gr.Markdown("# 📚 Extrator de Referências de Artigos Científicos")
gr.Markdown("Faça upload de um PDF de artigo científico para extrair automaticamente a lista de referências usando IA e expressões regulares.")
with gr.Row():
with gr.Column():
pdf_input = gr.File(
label="📄 Upload do PDF",
file_types=[".pdf"],
type="binary"
)
with gr.Column():
model_dropdown = gr.Dropdown(
choices=[
"gemini-2.5-flash-lite",
"gemini-2.5-pro",
"gemini-2.5-flash",
"gpt-4o",
"gpt-o3-mini",
"gpt-4.1"
],
value="gemini-2.5-flash-lite",
label="🤖 Modelo de IA",
info="Selecione o modelo para extrair as referências"
)
extract_btn = gr.Button("🔍 Extrair Referências", variant="primary")
with gr.Row():
with gr.Column():
metadata_output = gr.JSON(label="📋 Metadados do Artigo")
with gr.Column():
extracted_text_output = gr.Textbox(
label="📄 Texto Extraído",
lines=20,
max_lines=20,
buttons=['copy'],
interactive=False
)
with gr.Row():
with gr.Column():
llm_references_output = gr.Dataframe(
label="🤖 Referências Extraídas por IA",
row_count=(10,'dynamic'),
buttons=['copy', 'fullscreen'],
wrap=True
)
with gr.Column():
regex_references_output = gr.Dataframe(
label="🔍 Referências Extraídas por Regex",
row_count=(10,'dynamic'),
buttons=['copy', 'fullscreen'],
wrap=True
)
status_output = gr.Markdown(label="📊 Status da Extração")
extract_btn.click(
process_pdf,
inputs=[pdf_input, model_dropdown],
outputs=[metadata_output, llm_references_output, regex_references_output, status_output, extracted_text_output]
)
return interface
def main():
load_dotenv() # Carrega variáveis de ambiente do arquivo .env
# Verificar se as chaves das APIs estão configuradas
google_key = os.getenv("GEMINI_API_KEY")
openai_key = os.getenv("OPENAI_API_KEY")
if not google_key and not openai_key:
print("⚠️ AVISO: Nenhuma chave de API encontrada!")
print("Configure pelo menos uma das seguintes no arquivo .env:")
print("- GEMINI_API_KEY=sua_chave_do_google")
print("- OPENAI_API_KEY=sua_chave_da_openai")
elif not google_key:
print("ℹ️ Apenas OpenAI configurado. Modelos Gemini não funcionarão.")
elif not openai_key:
print("ℹ️ Apenas Google configurado. Modelos OpenAI não funcionarão.")
interface = create_interface()
interface.launch(share=False)
if __name__ == "__main__":
main()