Spaces:
Sleeping
Sleeping
import gradio as gr | |
import camelot | |
import pandas as pd | |
import matplotlib.pyplot as plt | |
import numpy as np | |
from fpdf import FPDF | |
from fpdf.enums import XPos, YPos | |
import tempfile | |
import os | |
import matplotlib | |
import shutil | |
import colorsys | |
from datetime import datetime | |
from concurrent.futures import ThreadPoolExecutor | |
from typing import Dict, List, Tuple, Optional | |
from io import BytesIO | |
import logging | |
from contextlib import contextmanager | |
# Configurar matplotlib | |
matplotlib.use('Agg') | |
# Configurar logging | |
logging.basicConfig( | |
level=logging.INFO, | |
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' | |
) | |
logger = logging.getLogger(__name__) | |
# Configurações globais | |
ESCALA_MAXIMA_NOTAS = 12 | |
LIMITE_APROVACAO_NOTA = 5 | |
LIMITE_APROVACAO_FREQ = 75 | |
BIMESTRES = ['1º Bimestre', '2º Bimestre', '3º Bimestre', '4º Bimestre'] | |
CONCEITOS_VALIDOS = ['ES', 'EP', 'ET'] | |
# Cores para os gráficos | |
COR_APROVADO = '#2ECC71' # Verde suave | |
COR_REPROVADO = '#E74C3C' # Vermelho suave | |
# Context managers | |
def temp_directory(): | |
temp_dir = tempfile.mkdtemp() | |
try: | |
yield temp_dir | |
finally: | |
if os.path.exists(temp_dir): | |
shutil.rmtree(temp_dir) | |
def temp_file(suffix=None): | |
temp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix) | |
try: | |
yield temp.name | |
finally: | |
if os.path.exists(temp.name): | |
os.unlink(temp.name) | |
class PDFReport(FPDF): | |
def __init__(self): | |
super().__init__() | |
self.set_auto_page_break(auto=True, margin=15) | |
def header_footer(self): | |
self.set_y(-30) | |
self.line(10, self.get_y(), 200, self.get_y()) | |
self.ln(5) | |
self.set_font('Helvetica', 'I', 8) | |
self.cell(0, 10, | |
'Este relatório é uma análise automática e deve ser validado junto à secretaria da escola.', | |
0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C') | |
# Função de extração de tabelas do PDF | |
def extrair_tabelas_pdf(pdf_path: str) -> pd.DataFrame: | |
"""Extrai tabelas do PDF usando stream para o nome e lattice para notas.""" | |
try: | |
# Extrair nome do aluno usando stream | |
tables_header = camelot.read_pdf( | |
pdf_path, | |
pages='1', | |
flavor='stream', | |
edge_tol=500 | |
) | |
info_aluno = {} | |
# Procurar nome do aluno | |
for table in tables_header: | |
df = table.df | |
for i in range(len(df)): | |
for j in range(len(df.columns)): | |
texto = str(df.iloc[i,j]).strip() | |
if 'Nome do Aluno' in texto: | |
try: | |
if j + 1 < len(df.columns): | |
nome = str(df.iloc[i,j+1]).strip() | |
elif i + 1 < len(df): | |
nome = str(df.iloc[i+1,j]).strip() | |
if nome and nome != 'Nome do Aluno:': | |
info_aluno['nome'] = nome | |
break | |
except: | |
continue | |
# Extrair tabela de notas usando lattice | |
tables_notas = camelot.read_pdf( | |
pdf_path, | |
pages='all', | |
flavor='lattice' | |
) | |
# Encontrar tabela de notas | |
df_notas = None | |
max_rows = 0 | |
for table in tables_notas: | |
df_temp = table.df | |
if len(df_temp) > max_rows and 'Disciplina' in str(df_temp.iloc[0,0]): | |
max_rows = len(df_temp) | |
df_notas = df_temp.copy() | |
df_notas = df_notas.rename(columns={ | |
0: 'Disciplina', | |
1: 'Nota B1', 2: 'Freq B1', 3: '%Freq B1', 4: 'AC B1', | |
5: 'Nota B2', 6: 'Freq B2', 7: '%Freq B2', 8: 'AC B2', | |
9: 'Nota B3', 10: 'Freq B3', 11: '%Freq B3', 12: 'AC B3', | |
13: 'Nota B4', 14: 'Freq B4', 15: '%Freq B4', 16: 'AC B4', | |
17: 'CF', 18: 'Nota Final', 19: 'Freq Final', 20: 'AC Final' | |
}) | |
if df_notas is None: | |
raise ValueError("Tabela de notas não encontrada") | |
# Adicionar informações do aluno ao DataFrame | |
df_notas.attrs['nome'] = info_aluno.get('nome', 'Nome não encontrado') | |
return df_notas | |
except Exception as e: | |
logger.error(f"Erro na extração das tabelas: {str(e)}") | |
raise | |
# Funções de plotagem | |
def plotar_evolucao_bimestres(disciplinas_dados: List[Dict], temp_dir: str, | |
titulo: Optional[str] = None, | |
nome_arquivo: Optional[str] = None) -> str: | |
plt.style.use('seaborn') | |
fig, ax = plt.subplots(figsize=(11.69, 8.27)) | |
# (Configurações do gráfico e plotagem dos dados aqui) | |
plt.tight_layout() | |
fig.canvas.draw() # Adicionado para garantir a renderização do gráfico | |
nome_arquivo = nome_arquivo or 'evolucao_notas.png' | |
plot_path = os.path.join(temp_dir, nome_arquivo) | |
plt.savefig(plot_path, bbox_inches='tight', dpi=300, | |
facecolor='white', edgecolor='none') | |
plt.close() | |
return plot_path | |
def plotar_graficos_destacados(disciplinas_dados: List[Dict], temp_dir: str) -> str: | |
plt.style.use('seaborn') | |
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10)) | |
# (Configurações do gráfico e plotagem dos dados aqui) | |
plt.tight_layout() | |
fig.canvas.draw() # Adicionado para garantir a renderização do gráfico | |
plot_path = os.path.join(temp_dir, 'medias_frequencias.png') | |
plt.savefig(plot_path, bbox_inches='tight', dpi=300, | |
facecolor='white', edgecolor='none') | |
plt.close() | |
return plot_path | |
# Funções de processamento do PDF e geração de relatórios | |
def gerar_relatorio_pdf(df: pd.DataFrame, disciplinas_dados: List[Dict], | |
grafico_basica: str, grafico_diversificada: str, | |
grafico_medias: str) -> str: | |
pdf = PDFReport() | |
pdf.set_auto_page_break(auto=True, margin=15) | |
# Primeira página - Informações e Formação Básica | |
pdf.add_page() | |
pdf.set_font('Helvetica', 'B', 18) | |
pdf.cell(0, 10, 'Relatório de Desempenho Escolar', | |
0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C') | |
pdf.ln(15) | |
# Informações do aluno | |
pdf.set_font('Helvetica', 'B', 12) | |
pdf.cell(0, 10, 'Informações do Aluno', | |
0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L') | |
pdf.line(10, pdf.get_y(), 200, pdf.get_y()) | |
pdf.ln(5) | |
# Nome do aluno | |
if hasattr(df, 'attrs') and 'nome' in df.attrs: | |
pdf.set_font('Helvetica', 'B', 11) | |
pdf.cell(30, 7, 'Nome:', 0, 0) | |
pdf.set_font('Helvetica', '', 11) | |
pdf.cell(0, 7, df.attrs['nome'], | |
0, new_x=XPos.LMARGIN, new_y=YPos.NEXT) | |
pdf.ln(10) | |
# Data do relatório | |
data_atual = datetime.now().strftime('%d/%m/%Y') | |
pdf.set_font('Helvetica', 'I', 10) | |
pdf.cell(0, 5, f'Data de geração: {data_atual}', | |
0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='R') | |
pdf.ln(15) | |
# Gráficos de evolução | |
pdf.set_font('Helvetica', 'B', 14) | |
pdf.cell(0, 10, 'Evolução das Notas - Formação Geral Básica', | |
0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L') | |
pdf.line(10, pdf.get_y(), 200, pdf.get_y()) | |
pdf.ln(10) | |
pdf.image(grafico_basica, x=10, w=190) | |
# Segunda página - Parte Diversificada | |
pdf.add_page() | |
pdf.set_font('Helvetica', 'B', 14) | |
pdf.cell(0, 10, 'Evolução das Notas - Parte Diversificada', | |
0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L') | |
pdf.line(10, pdf.get_y(), 200, pdf.get_y()) | |
pdf.ln(10) | |
pdf.image(grafico_diversificada, x=10, w=190) | |
# Terceira página - Médias e Frequências | |
pdf.add_page() | |
pdf.set_font('Helvetica', 'B', 14) | |
pdf.cell(0, 10, 'Análise de Médias e Frequências', | |
0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L') | |
pdf.line(10, pdf.get_y(), 200, pdf.get_y()) | |
pdf.ln(10) | |
pdf.image(grafico_medias, x=10, w=190) | |
# Quarta página - Análise Detalhada | |
pdf.add_page() | |
pdf.set_font('Helvetica', 'B', 14) | |
pdf.cell(0, 10, 'Análise Detalhada', | |
0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L') | |
pdf.line(10, pdf.get_y(), 200, pdf.get_y()) | |
pdf.ln(10) | |
# Resumo geral | |
medias_notas = [d['media_notas'] for d in disciplinas_dados] | |
medias_freq = [d['media_freq'] for d in disciplinas_dados] | |
media_global = np.mean(medias_notas) | |
freq_global = np.mean(medias_freq) | |
pdf.set_font('Helvetica', 'B', 12) | |
pdf.cell(0, 7, 'Resumo Geral:', | |
0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L') | |
pdf.ln(5) | |
pdf.set_font('Helvetica', '', 11) | |
pdf.cell(0, 7, f'Média Global: {media_global:.1f}', | |
0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L') | |
pdf.cell(0, 7, f'Frequência Global: {freq_global:.1f}%', | |
0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L') | |
pdf.ln(10) | |
# Pontos de atenção | |
pdf.set_font('Helvetica', 'B', 12) | |
pdf.cell(0, 10, 'Pontos de Atenção:', | |
0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L') | |
pdf.ln(5) | |
pdf.set_font('Helvetica', '', 10) | |
disciplinas_risco = [] | |
for disc_data in disciplinas_dados: | |
avisos = [] | |
if disc_data['media_notas'] < LIMITE_APROVACAO_NOTA: | |
avisos.append( | |
f"Média de notas abaixo de {LIMITE_APROVACAO_NOTA} ({disc_data['media_notas']:.1f})" | |
) | |
if disc_data['media_freq'] < LIMITE_APROVACAO_FREQ: | |
avisos.append( | |
f"Frequência abaixo de {LIMITE_APROVACAO_FREQ}% ({disc_data['media_freq']:.1f}%)" | |
) | |
if avisos: | |
disciplinas_risco.append((disc_data['disciplina'], avisos)) | |
if disciplinas_risco: | |
for disc, avisos in disciplinas_risco: | |
pdf.set_font('Helvetica', 'B', 10) | |
pdf.cell(0, 7, f'- {disc}:', | |
0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L') | |
pdf.set_font('Helvetica', '', 10) | |
for aviso in avisos: | |
pdf.cell(10) # Indentação | |
pdf.cell(0, 7, f'- {aviso}', | |
0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L') | |
else: | |
pdf.cell(0, 7, 'Nenhum problema identificado.', | |
0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L') | |
pdf.header_footer() | |
# Salvar PDF | |
with temp_file(suffix='.pdf') as temp_pdf: | |
pdf.output(temp_pdf) | |
return temp_pdf | |
def processar_boletim(file) -> Tuple[Optional[str], str]: | |
"""Função principal que processa o boletim e gera o relatório.""" | |
try: | |
if file is None: | |
return None, "Nenhum arquivo foi fornecido." | |
with temp_directory() as temp_dir: | |
# Salvar arquivo temporário | |
temp_pdf = os.path.join(temp_dir, 'boletim.pdf') | |
with open(temp_pdf, 'wb') as f: | |
f.write(file) | |
if os.path.getsize(temp_pdf) == 0: | |
return None, "O arquivo está vazio." | |
# Extrair e processar dados | |
df = extrair_tabelas_pdf(temp_pdf) | |
if df is None or df.empty: | |
return None, "Não foi possível extrair dados do PDF." | |
disciplinas_dados = obter_disciplinas_validas(df) | |
if not disciplinas_dados: | |
return None, "Nenhuma disciplina válida encontrada no boletim." | |
# Separar disciplinas e determinar nível | |
categorias = separar_disciplinas_por_categoria(disciplinas_dados) | |
nivel_texto = "Ensino Médio" if categorias['nivel'] == "medio" else "Ensino Fundamental" | |
# Gerar gráficos em paralelo | |
with ThreadPoolExecutor() as executor: | |
futures = { | |
'basica': executor.submit( | |
plotar_evolucao_bimestres, | |
categorias['formacao_basica'], | |
temp_dir, | |
f"Evolução das Médias - Formação Geral Básica ({nivel_texto})", | |
'evolucao_basica.png' | |
), | |
'diversificada': executor.submit( | |
plotar_evolucao_bimestres, | |
categorias['diversificada'], | |
temp_dir, | |
f"Evolução das Médias - Parte Diversificada ({nivel_texto})", | |
'evolucao_diversificada.png' | |
), | |
'medias': executor.submit( | |
plotar_graficos_destacados, | |
disciplinas_dados, | |
temp_dir | |
) | |
} | |
grafico_basica = futures['basica'].result() | |
grafico_diversificada = futures['diversificada'].result() | |
grafico_medias = futures['medias'].result() | |
# Gerar relatório final | |
pdf_path = gerar_relatorio_pdf( | |
df, | |
disciplinas_dados, | |
grafico_basica, | |
grafico_diversificada, | |
grafico_medias | |
) | |
# Preparar arquivo de retorno | |
output_path = os.path.join(temp_dir, 'relatorio_final.pdf') | |
shutil.copy2(pdf_path, output_path) | |
return output_path, "Relatório gerado com sucesso!" | |
except Exception as e: | |
logger.exception("Erro durante o processamento") | |
return None, f"Erro ao processar o boletim: {str(e)}" | |
# Interface Gradio | |
iface = gr.Interface( | |
fn=processar_boletim, | |
inputs=gr.File( | |
label="Upload do Boletim (PDF)", | |
type="binary", | |
file_types=[".pdf"] | |
), | |
outputs=[ | |
gr.File(label="Relatório (PDF)"), | |
gr.Textbox(label="Status") | |
], | |
title="Análise de Boletim Escolar", | |
description="Faça upload do boletim em PDF para gerar um relatório com análises e visualizações.", | |
allow_flagging="never", | |
theme=gr.themes.Default() | |
) | |
if __name__ == "__main__": | |
iface.launch( | |
server_name="0.0.0.0", | |
share=True | |
) |