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 | |
matplotlib.use('Agg') | |
def extrair_tabelas_pdf(pdf_path): | |
"""Extrai tabelas do PDF e retorna um DataFrame processado.""" | |
try: | |
# Extrair tabelas do PDF usando o método 'lattice' | |
tables = camelot.read_pdf(pdf_path, pages='all', flavor='lattice') | |
print(f"Tabelas extraídas: {len(tables)}") | |
if len(tables) == 0: | |
raise ValueError("Nenhuma tabela foi extraída do PDF.") | |
# Processar a primeira tabela | |
df = tables[0].df | |
# Verificar se a tabela tem conteúdo | |
if df.empty: | |
raise ValueError("A tabela extraída está vazia.") | |
# Salvar todas as tabelas extraídas em CSV (para debug) | |
temp_dir = os.path.dirname(pdf_path) | |
for i, table in enumerate(tables): | |
csv_path = os.path.join(temp_dir, f'boletim_extraido_{i+1}.csv') | |
table.to_csv(csv_path) | |
print(f"Tabela {i+1} salva como CSV em {csv_path}") | |
return df | |
except Exception as e: | |
print(f"Erro na extração das tabelas: {str(e)}") | |
raise | |
def converter_nota(valor): | |
"""Converte valor de nota para float, tratando casos especiais.""" | |
if pd.isna(valor) or valor == '-' or valor == 'N' or valor == '' or valor == 'None': | |
return 0 | |
try: | |
if isinstance(valor, str): | |
# Remover possíveis espaços e substituir vírgula por ponto | |
valor_limpo = valor.strip().replace(',', '.') | |
# Se depois de limpar ainda estiver vazio, retorna 0 | |
if not valor_limpo: | |
return 0 | |
return float(valor_limpo) | |
elif isinstance(valor, (int, float)): | |
return float(valor) | |
return 0 | |
except: | |
return 0 | |
def obter_disciplinas_validas(df): | |
"""Identifica disciplinas válidas no boletim.""" | |
# Colunas de notas e frequências | |
colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4'] | |
colunas_freq = ['%Freq B1', '%Freq B2', '%Freq B3', '%Freq B4'] | |
# Converter notas para numérico, tratando valores inválidos | |
for col in colunas_notas: | |
if col in df.columns: | |
df[col] = df[col].apply(lambda x: converter_nota(x)) | |
# Converter frequências, tratando valores inválidos | |
for col in colunas_freq: | |
if col in df.columns: | |
df[col] = df[col].replace('%', '', regex=True) | |
df[col] = df[col].apply(lambda x: converter_nota(x) if pd.notna(x) else 0) | |
# Identificar disciplinas que têm pelo menos uma nota ou frequência | |
disciplinas_validas = [] | |
for _, row in df.iterrows(): | |
disciplina = row['Disciplina'] | |
if pd.isna(disciplina) or disciplina == '': | |
continue | |
notas = pd.to_numeric(row[colunas_notas], errors='coerce').fillna(0) | |
freq = pd.to_numeric(row[colunas_freq], errors='coerce').fillna(0) | |
if (notas > 0).any() or (freq > 0).any(): | |
disciplinas_validas.append(disciplina) | |
return disciplinas_validas | |
def gerar_paleta_cores(n_cores): | |
"""Gera uma paleta de cores distintas para o número de disciplinas.""" | |
cores_base = [ | |
'#DC143C', '#4169E1', '#9370DB', '#32CD32', '#FF8C00', | |
'#00CED1', '#FF69B4', '#8B4513', '#4B0082', '#556B2F', | |
'#B8860B', '#483D8B', '#008B8B', '#8B008B', '#8B0000' | |
] | |
# Se precisar de mais cores, gerar automaticamente | |
if n_cores > len(cores_base): | |
HSV_tuples = [(x/n_cores, 0.8, 0.9) for x in range(n_cores)] | |
cores_extras = ['#%02x%02x%02x' % tuple(int(x*255) for x in colorsys.hsv_to_rgb(*hsv)) | |
for hsv in HSV_tuples] | |
return cores_extras | |
return cores_base[:n_cores] | |
def plotar_evolucao_bimestres(df_filtrado, temp_dir): | |
"""Plota gráfico de evolução das notas por bimestre.""" | |
# Obter disciplinas válidas | |
disciplinas_validas = obter_disciplinas_validas(df_filtrado) | |
n_disciplinas = len(disciplinas_validas) | |
if n_disciplinas == 0: | |
raise ValueError("Nenhuma disciplina válida encontrada para plotar.") | |
# Calcular tamanho da figura baseado no número de disciplinas | |
altura_figura = max(6, n_disciplinas * 0.4) | |
plt.figure(figsize=(14, altura_figura)) | |
# Gerar cores para as disciplinas | |
cores = gerar_paleta_cores(n_disciplinas) | |
marcadores = ['o', 's', '^', 'D', 'v', '<', '>', 'p', 'h', '8', '*', 'H', '+', 'x', 'd'] | |
estilos_linha = ['-', '--', '-.', ':', '-', '--', '-.', ':', '-', '--', '-.', ':', '-', '--', '-.'] | |
plt.grid(True, linestyle='--', alpha=0.3, zorder=0) | |
colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4'] | |
for idx, disciplina in enumerate(disciplinas_validas): | |
dados_disciplina = df_filtrado[df_filtrado['Disciplina'] == disciplina] | |
if not dados_disciplina.empty: | |
notas = dados_disciplina[colunas_notas].values[0] | |
notas_validas = pd.to_numeric(notas, errors='coerce').fillna(0) > 0 | |
if any(notas_validas): | |
bimestres = np.arange(1, len(colunas_notas) + 1)[notas_validas] | |
notas_filtradas = pd.to_numeric(notas[notas_validas], errors='coerce').fillna(0) | |
plt.plot(bimestres, notas_filtradas, | |
color=cores[idx % len(cores)], | |
marker=marcadores[idx % len(marcadores)], | |
markersize=8, | |
linewidth=2, | |
label=disciplina, | |
linestyle=estilos_linha[idx % len(estilos_linha)], | |
alpha=0.8) | |
for x, y in zip(bimestres, notas_filtradas): | |
plt.annotate(f"{y:.1f}", (x, y), | |
textcoords="offset points", | |
xytext=(0, 5), | |
ha='center', | |
fontsize=8) | |
plt.title('Evolução das Médias por Disciplina ao Longo dos Bimestres') | |
plt.xlabel('Bimestres') | |
plt.ylabel('Média de Notas') | |
plt.xticks([1, 2, 3, 4], ['B1', 'B2', 'B3', 'B4']) | |
plt.ylim(0, 10) | |
# Ajustar legenda baseado no número de disciplinas | |
if n_disciplinas > 10: | |
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=8) | |
else: | |
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left') | |
plt.tight_layout() | |
plot_path = os.path.join(temp_dir, 'evolucao_notas.png') | |
plt.savefig(plot_path, bbox_inches='tight', dpi=300) | |
plt.close() | |
return plot_path | |
def plotar_graficos_destacados(df_boletim_clean, temp_dir): | |
"""Plota gráficos de médias e frequências com destaques.""" | |
# Obter disciplinas válidas | |
disciplinas_validas = obter_disciplinas_validas(df_boletim_clean) | |
if not disciplinas_validas: | |
raise ValueError("Nenhuma disciplina válida encontrada no boletim.") | |
n_disciplinas = len(disciplinas_validas) | |
# Calcular tamanho da figura baseado no número de disciplinas | |
altura_figura = max(6, n_disciplinas * 0.4) | |
plt.figure(figsize=(14, altura_figura)) | |
df_filtrado = df_boletim_clean[df_boletim_clean['Disciplina'].isin(disciplinas_validas)] | |
disciplinas = df_filtrado['Disciplina'].astype(str) | |
# Processar frequências com tratamento de erros melhorado | |
colunas_freq = ['%Freq B1', '%Freq B2', '%Freq B3', '%Freq B4'] | |
freq_data = df_filtrado[colunas_freq].replace('%', '', regex=True) | |
freq_data = freq_data.apply(pd.to_numeric, errors='coerce').fillna(0) | |
medias_frequencia = freq_data.mean(axis=1) | |
# Processar notas com tratamento de erros melhorado | |
colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4'] | |
notas_data = df_filtrado[colunas_notas].apply(pd.to_numeric, errors='coerce').fillna(0) | |
medias_notas = notas_data.mean(axis=1) | |
cores_notas = ['red' if media < 5 else 'blue' for media in medias_notas] | |
cores_frequencias = ['red' if media < 75 else 'green' for media in medias_frequencia] | |
frequencia_global_media = medias_frequencia.mean() | |
plt.subplot(1, 2, 1) | |
barras_notas = plt.bar(disciplinas, medias_notas, color=cores_notas) | |
plt.title('Média de Notas por Disciplina (Vermelho: < 5)') | |
plt.xticks(rotation=45, ha='right') | |
plt.ylim(0, 10) | |
# Adicionar valores nas barras | |
for barra in barras_notas: | |
altura = barra.get_height() | |
plt.text(barra.get_x() + barra.get_width()/2., altura, | |
f'{altura:.1f}', | |
ha='center', va='bottom') | |
plt.subplot(1, 2, 2) | |
barras_freq = plt.bar(disciplinas, medias_frequencia, color=cores_frequencias) | |
plt.title('Média de Frequência por Disciplina (Vermelho: < 75%)') | |
plt.xticks(rotation=45, ha='right') | |
plt.ylim(0, 100) | |
# Adicionar valores nas barras | |
for barra in barras_freq: | |
altura = barra.get_height() | |
plt.text(barra.get_x() + barra.get_width()/2., altura, | |
f'{altura:.1f}%', | |
ha='center', va='bottom') | |
plt.suptitle(f"Frequência Global Média: {frequencia_global_media:.2f}%") | |
if frequencia_global_media < 75: | |
plt.figtext(0.5, 0.02, "Cuidado: Risco de Reprovação por Baixa Frequência", | |
ha="center", fontsize=12, color="red") | |
plt.tight_layout() | |
plot_path = os.path.join(temp_dir, 'medias_frequencias.png') | |
plt.savefig(plot_path, bbox_inches='tight', dpi=300) | |
plt.close() | |
return plot_path | |
def gerar_relatorio_pdf(df, grafico1_path, grafico2_path): | |
"""Gera relatório PDF com os gráficos e análises.""" | |
pdf = FPDF() | |
pdf.add_page() | |
pdf.set_font('Helvetica', 'B', 16) | |
pdf.cell(0, 10, 'Relatório de Desempenho Escolar', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C') | |
pdf.ln(10) | |
# Informações do aluno se disponíveis | |
if 'Nome do Aluno' in df.columns: | |
pdf.set_font('Helvetica', '', 12) | |
pdf.cell(0, 10, f'Aluno: {df["Nome do Aluno"].iloc[0]}', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L') | |
pdf.image(grafico1_path, x=10, w=190) | |
pdf.ln(10) | |
pdf.image(grafico2_path, x=10, w=190) | |
pdf.ln(10) | |
pdf.set_font('Helvetica', 'B', 12) | |
pdf.cell(0, 10, 'Avisos Importantes:', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L') | |
pdf.set_font('Helvetica', '', 10) | |
# Obter disciplinas válidas | |
disciplinas_validas = obter_disciplinas_validas(df) | |
df_filtrado = df[df['Disciplina'].isin(disciplinas_validas)] | |
# Calcular médias | |
colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4'] | |
notas_data = df_filtrado[colunas_notas].apply(pd.to_numeric, errors='coerce').fillna(0) | |
medias_notas = notas_data.mean(axis=1) | |
# Processar frequências | |
colunas_freq = ['%Freq B1', '%Freq B2', '%Freq B3', '%Freq B4'] | |
freq_data = df_filtrado[colunas_freq].replace('%', '', regex=True) | |
freq_data = freq_data.apply(pd.to_numeric, errors='coerce').fillna(0) | |
medias_freq = freq_data.mean(axis=1) | |
# Adicionar média global | |
media_global = medias_notas.mean() | |
freq_global = medias_freq.mean() | |
pdf.cell(0, 10, f'Média Global: {media_global:.1f}', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L') | |
pdf.cell(0, 10, f'Frequência Global: {freq_global:.1f}%', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L') | |
pdf.ln(5) | |
for idx, (disciplina, media_nota, media_freq) in enumerate(zip(df_filtrado['Disciplina'], medias_notas, medias_freq)): | |
if media_nota < 5: | |
pdf.cell(0, 10, f'- {disciplina}: Média de notas abaixo de 5 ({media_nota:.1f})', 0, | |
new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L') | |
if media_freq < 75: | |
pdf.cell(0, 10, f'- {disciplina}: Frequência abaixo de 75% ({media_freq:.1f}%)', 0, | |
new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L') | |
temp_pdf = tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') | |
pdf_path = temp_pdf.name | |
pdf.output(pdf_path) | |
return pdf_path | |
def processar_boletim(file): | |
"""Função principal que processa o boletim e gera o relatório.""" | |
temp_dir = None | |
try: | |
# Verificar se o arquivo é válido | |
if file is None: | |
return None, "Nenhum arquivo foi fornecido." | |
# Criar diretório temporário | |
temp_dir = tempfile.mkdtemp() | |
print(f"Diretório temporário criado: {temp_dir}") | |
# Verificar se o arquivo tem conteúdo | |
if not hasattr(file, 'name') or not os.path.exists(file.name): | |
return None, "Arquivo inválido ou corrompido." | |
if os.path.getsize(file.name) == 0: | |
return None, "O arquivo está vazio." | |
# Copiar o arquivo para o diretório temporário | |
temp_pdf = os.path.join(temp_dir, 'boletim.pdf') | |
shutil.copy2(file.name, temp_pdf) | |
print(f"PDF copiado para: {temp_pdf}") | |
# Verificar se a cópia foi bem sucedida | |
if not os.path.exists(temp_pdf) or os.path.getsize(temp_pdf) == 0: | |
return None, "Erro ao copiar o arquivo." | |
# Extrair tabelas do PDF | |
print("Iniciando extração das tabelas...") | |
df = extrair_tabelas_pdf(temp_pdf) | |
print("Tabelas extraídas com sucesso") | |
if df is None or df.empty: | |
return None, "Não foi possível extrair dados do PDF." | |
# Renomear colunas para o formato esperado | |
try: | |
df.columns = ['Disciplina', 'Nota B1', 'Freq B1', '%Freq B1', 'AC B1', | |
'Nota B2', 'Freq B2', '%Freq B2', 'AC B2', | |
'Nota B3', 'Freq B3', '%Freq B3', 'AC B3', | |
'Nota B4', 'Freq B4', '%Freq B4', 'AC B4', | |
'CF', 'Nota Final', 'Freq Final', 'AC Final'] | |
except: | |
return None, "O formato do PDF não corresponde ao esperado." | |
# Processar notas | |
colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4'] | |
for col in colunas_notas: | |
if col in df.columns: | |
df[col] = df[col].apply(converter_nota) | |
print("Notas processadas") | |
# Gerar gráficos | |
print("Gerando gráficos...") | |
grafico1_path = plotar_evolucao_bimestres(df, temp_dir) | |
grafico2_path = plotar_graficos_destacados(df, temp_dir) | |
print("Gráficos gerados") | |
# Gerar PDF | |
print("Gerando relatório PDF...") | |
pdf_path = gerar_relatorio_pdf(df, grafico1_path, grafico2_path) | |
print("Relatório PDF gerado") | |
# Criar arquivo temporário para retorno | |
output_file = tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') | |
output_path = output_file.name | |
shutil.copy2(pdf_path, output_path) | |
return output_path, "Relatório gerado com sucesso!" | |
except Exception as e: | |
print(f"Erro durante o processamento: {str(e)}") | |
return None, f"Erro ao processar o boletim: {str(e)}" | |
finally: | |
# Limpar arquivos temporários | |
if temp_dir and os.path.exists(temp_dir): | |
try: | |
shutil.rmtree(temp_dir) | |
print("Arquivos temporários limpos") | |
except Exception as e: | |
print(f"Erro ao limpar arquivos temporários: {str(e)}") | |
# Interface Gradio | |
iface = gr.Interface( | |
fn=processar_boletim, | |
inputs=gr.File(label="Upload do Boletim (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" | |
) | |
if __name__ == "__main__": | |
iface.launch(server_name="0.0.0.0") |