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 # grupo 'autores' [^\.]+? # tudo antes do primeiro ponto final (não guloso) )\.\s+ (?P # grupo 'titulo' [^\n\.]+ # até o próximo ponto final ou quebra de linha )\. \s* (?P # grupo 'journal' [^\n;]+ # até o próximo ponto e vírgula (ou quebra de linha) ) [;,]?\s* (?P # 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 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()