BoletimSed / app.py
histlearn's picture
Update app.py
75c163e verified
raw
history blame
14.6 kB
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
@contextmanager
def temp_directory():
temp_dir = tempfile.mkdtemp()
try:
yield temp_dir
finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
@contextmanager
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
)