272 lines
9.1 KiB
Python
Executable File
272 lines
9.1 KiB
Python
Executable File
"""
|
|
pdfmd.py
|
|
|
|
Author: Descomplicar® Crescimento Digital
|
|
Link: https://descomplicar.pt
|
|
Copyright: 2025 Descomplicar®
|
|
"""
|
|
|
|
import re
|
|
import pypdf
|
|
import requests
|
|
import time
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
|
|
# Configurações da API
|
|
import os
|
|
from dotenv import load_dotenv
|
|
load_dotenv()
|
|
|
|
BASE_URL = "https://openrouter.ai/api/v1/chat/completions"
|
|
API_KEY = os.environ.get("OPENROUTER_API_KEY", "")
|
|
if not API_KEY:
|
|
print("ERRO: OPENROUTER_API_KEY não configurada! Criar ficheiro .env com OPENROUTER_API_KEY=...")
|
|
exit(1)
|
|
|
|
def extrair_nome_capitulo(texto):
|
|
"""Extrai o nome do capítulo do texto."""
|
|
padrao = r'(?:Chapter|Capítulo|Parte|Section)\s*\d*\s*[:\-]?\s*([^\n]+)'
|
|
match = re.search(padrao, texto, re.IGNORECASE)
|
|
if match:
|
|
return match.group(1).strip()
|
|
return "Sem título"
|
|
|
|
def processar_texto(texto, titulo, max_retries=3, retry_delay=5):
|
|
# Remover a linha que redefine retry_delay
|
|
"""Processa o texto extraído do PDF e formata em Markdown."""
|
|
prompt = f"""Analise este texto de '{titulo}' e crie:
|
|
|
|
1. Um resumo do capítulo (3-4 parágrafos)
|
|
2. 10 tags técnicas relevantes para o conteúdo
|
|
3. O texto completo, mantendo a estrutura e formatação original
|
|
4. 10 perguntas e respostas (FAQs) detalhadas sobre o conteúdo
|
|
|
|
Mantenha o texto no idioma original do documento.
|
|
|
|
Texto para processar:
|
|
{texto}"""
|
|
|
|
headers = {
|
|
"Authorization": f"Bearer {API_KEY}",
|
|
"Content-Type": "application/json"
|
|
}
|
|
|
|
data = {
|
|
"model": "mistralai/ministral-3b",
|
|
"messages": [{"role": "user", "content": prompt}],
|
|
"temperature": 0.3
|
|
}
|
|
|
|
for attempt in range(max_retries):
|
|
try:
|
|
response = requests.post(
|
|
BASE_URL,
|
|
json=data,
|
|
headers=headers,
|
|
timeout=30
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
return response.json()['choices'][0]['message']['content'].strip()
|
|
elif response.status_code == 429:
|
|
wait_time = retry_delay * (attempt + 1)
|
|
print(f"Rate limit atingido. A aguardar {wait_time}s...")
|
|
time.sleep(wait_time)
|
|
else:
|
|
print(f"Erro API: {response.status_code}")
|
|
print(f"Resposta: {response.text}")
|
|
time.sleep(retry_delay)
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
print(f"Erro na requisição: {str(e)}")
|
|
time.sleep(retry_delay)
|
|
|
|
return None
|
|
|
|
def extrair_capitulos(pdf_path):
|
|
"""Extrai capítulos do PDF com seus títulos."""
|
|
reader = pypdf.PdfReader(pdf_path)
|
|
texto_completo = []
|
|
|
|
# Primeiro, extrair todo o texto do PDF e formatar em Markdown
|
|
print("\nExtraindo texto do PDF...")
|
|
for i, page in enumerate(reader.pages):
|
|
try:
|
|
texto = page.extract_text()
|
|
if texto:
|
|
# Formatação básica para Markdown
|
|
texto = texto.replace('\n', ' \n') # Adiciona quebra de linha em Markdown
|
|
texto_completo.append(texto)
|
|
print(f" Página {i+1}: {len(texto)} caracteres")
|
|
else:
|
|
print(f" Página {i+1}: sem texto")
|
|
except Exception as e:
|
|
print(f" Erro na página {i+1}: {str(e)}")
|
|
continue
|
|
|
|
# Verifica se conseguiu extrair algum texto
|
|
if not texto_completo:
|
|
print("AVISO: Não foi possível extrair texto do PDF.")
|
|
return [{'numero': 1, 'titulo': 'Documento Completo', 'texto': ''}]
|
|
|
|
texto_documento = '\n'.join(texto_completo)
|
|
print(f"\nTotal extraído: {len(texto_documento)} caracteres")
|
|
|
|
# Padrões para identificar capítulos
|
|
padroes_capitulo = [
|
|
r'(?:Chapter|Capítulo|Parte|Section)\s*\d+',
|
|
r'\d+\.\s+[A-Z]',
|
|
r'[A-Z]+\s*\d+\s*[:\-]',
|
|
r'(?:Introdução|Introduction)',
|
|
r'(?:Conclusão|Conclusion)',
|
|
r'(?:Anexo|Appendix)\s*[A-Z]',
|
|
r'(?:Bibliografia|References)',
|
|
r'(?:Índice|Index)'
|
|
]
|
|
|
|
# Tenta identificar capítulos
|
|
print("\nProcurando divisões de capítulos...")
|
|
padrao_combinado = '|'.join(f'({p})' for p in padroes_capitulo)
|
|
matches = list(re.finditer(padrao_combinado, texto_documento, re.IGNORECASE | re.MULTILINE))
|
|
|
|
# Se não encontrou divisões, retorna documento completo
|
|
if not matches:
|
|
print("Nenhuma divisão de capítulo encontrada - tratando como documento único")
|
|
return [{'numero': 1, 'titulo': 'Documento Completo', 'texto': texto_documento}]
|
|
|
|
# Processa cada capítulo encontrado
|
|
capitulos = []
|
|
print("\nProcessando capítulos...")
|
|
|
|
# Adiciona o texto inicial antes do primeiro capítulo, se houver
|
|
if matches[0].start() > 0:
|
|
texto_inicial = texto_documento[:matches[0].start()].strip()
|
|
if texto_inicial:
|
|
capitulos.append({
|
|
'numero': 1,
|
|
'titulo': 'Conteúdo Inicial',
|
|
'texto': texto_inicial
|
|
})
|
|
print(f" Adicionado conteúdo inicial: {len(texto_inicial)} caracteres")
|
|
|
|
# Processa os capítulos identificados
|
|
for i, match in enumerate(matches):
|
|
inicio = match.start()
|
|
if i < len(matches) - 1:
|
|
fim = matches[i+1].start()
|
|
else:
|
|
fim = len(texto_documento)
|
|
|
|
texto_capitulo = texto_documento[inicio:fim].strip()
|
|
# Pega a primeira linha como título
|
|
linhas = texto_capitulo.split('\n')
|
|
titulo = linhas[0].strip()
|
|
|
|
numero = len(capitulos) + 1
|
|
capitulos.append({
|
|
'numero': numero,
|
|
'titulo': titulo,
|
|
'texto': texto_capitulo
|
|
})
|
|
print(f" Capítulo {numero}: {titulo} ({len(texto_capitulo)} caracteres)")
|
|
|
|
# Verifica se todo o texto foi incluído
|
|
texto_capitulos = ''.join(cap['texto'] for cap in capitulos)
|
|
if len(texto_capitulos) < len(texto_documento) * 0.9: # Margem de 10% para diferenças de formatação
|
|
print("\nAVISO: Possível perda de conteúdo detectada!")
|
|
print(f" Texto original: {len(texto_documento)} caracteres")
|
|
print(f" Texto nos capítulos: {len(texto_capitulos)} caracteres")
|
|
print("Retornando documento completo para garantir preservação...")
|
|
return [{'numero': 1, 'titulo': 'Documento Completo', 'texto': texto_documento}]
|
|
|
|
print(f"\nProcessamento concluído: {len(capitulos)} capítulos identificados")
|
|
return capitulos
|
|
|
|
def processar_pdf(arquivo_pdf):
|
|
"""Processa um arquivo PDF."""
|
|
print(f"\nProcessando: {arquivo_pdf}")
|
|
|
|
try:
|
|
capitulos = extrair_capitulos(arquivo_pdf)
|
|
print(f"Encontrados {len(capitulos)} capítulos")
|
|
|
|
md_content = f"""# {arquivo_pdf.stem}
|
|
|
|
Data de processamento: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
|
|
|
"""
|
|
|
|
for capitulo in capitulos:
|
|
print(f"Processando capítulo {capitulo['numero']}: {capitulo['titulo']}")
|
|
conteudo_processado = processar_texto(
|
|
capitulo['texto'],
|
|
f"Capítulo {capitulo['numero']} - {capitulo['titulo']}"
|
|
)
|
|
|
|
if conteudo_processado:
|
|
md_content += f"\n{conteudo_processado}\n\n---\n"
|
|
else:
|
|
print(f"Erro ao processar capítulo {capitulo['numero']}")
|
|
|
|
# Remover o delay entre capítulos para evitar interrupções
|
|
|
|
# Salvar arquivo
|
|
caminho_saida = Path('output') / arquivo_pdf.parent.relative_to('input')
|
|
caminho_saida.mkdir(parents=True, exist_ok=True)
|
|
arquivo_saida = caminho_saida / f"{arquivo_pdf.stem}.md"
|
|
|
|
with open(arquivo_saida, 'w', encoding='utf-8') as f:
|
|
f.write(md_content)
|
|
|
|
print(f"Arquivo salvo: {arquivo_saida}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"Erro ao processar {arquivo_pdf}: {str(e)}")
|
|
return False
|
|
|
|
def main():
|
|
print("Iniciando o processamento...")
|
|
start_time = datetime.now()
|
|
|
|
Path('input').mkdir(exist_ok=True)
|
|
Path('output').mkdir(exist_ok=True)
|
|
|
|
pdfs = list(Path('input').rglob('*.pdf'))
|
|
total = len(pdfs)
|
|
print(f"Encontrados {total} PDFs")
|
|
|
|
falhas = []
|
|
sucessos = []
|
|
|
|
for i, pdf in enumerate(pdfs, 1):
|
|
print(f"\nProcessando {i}/{total}: {pdf}")
|
|
try:
|
|
if processar_pdf(pdf):
|
|
sucessos.append(pdf)
|
|
else:
|
|
falhas.append(pdf)
|
|
except Exception as e:
|
|
print(f"Erro ao processar {pdf}: {str(e)}")
|
|
falhas.append(pdf)
|
|
sucessos.append(pdf)
|
|
else:
|
|
falhas.append(pdf)
|
|
time.sleep(5)
|
|
|
|
print(f"\n=== Relatório Final ===")
|
|
print(f"Data de início: {start_time}")
|
|
print(f"Data de término: {datetime.now()}")
|
|
print(f"Duração: {datetime.now() - start_time}")
|
|
print(f"PDFs processados com sucesso: {len(sucessos)}")
|
|
print(f"PDFs com falha: {len(falhas)}")
|
|
|
|
if falhas:
|
|
print("\nFalhas:")
|
|
for f in falhas:
|
|
print(f"- {f}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|