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': | |
| return 0 | |
| try: | |
| return float(str(valor).replace(',', '.')) # Tratar decimal com vírgula | |
| except: | |
| return 0 | |
| def plotar_evolucao_bimestres(df_filtrado, temp_dir): | |
| """Plota gráfico de evolução das notas por bimestre.""" | |
| plt.figure(figsize=(12, 6)) | |
| disciplinas_basicas = ['LINGUA PORTUGUESA', 'ARTE', 'LINGUA ESTRANGEIRA INGLES', | |
| 'GEOGRAFIA', 'CIENCIAS', 'HISTORIA', 'MATEMATICA'] | |
| estilos = { | |
| 'LINGUA PORTUGUESA': {'cor': '#DC143C', 'marcador': 'p', 'zorder': 1, 'linestyle': '-', 'desloc': 0.1}, | |
| 'ARTE': {'cor': '#4169E1', 'marcador': 'D', 'zorder': 2, 'linestyle': '--', 'desloc': 0.08}, | |
| 'LINGUA ESTRANGEIRA INGLES': {'cor': '#9370DB', 'marcador': 'h', 'zorder': 3, 'linestyle': '-.', 'desloc': 0.06}, | |
| 'GEOGRAFIA': {'cor': '#32CD32', 'marcador': '^', 'zorder': 4, 'linestyle': ':', 'desloc': 0.04}, | |
| 'CIENCIAS': {'cor': '#FF8C00', 'marcador': 's', 'zorder': 5, 'linestyle': '-', 'desloc': 0.02}, | |
| 'HISTORIA': {'cor': '#00CED1', 'marcador': '*', 'zorder': 6, 'linestyle': '--', 'desloc': -0.02}, | |
| 'MATEMATICA': {'cor': '#FF69B4', 'marcador': 'o', 'zorder': 7, 'linestyle': '-.', 'desloc': -0.04} | |
| } | |
| plt.grid(True, linestyle='--', alpha=0.3, zorder=0) | |
| colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4'] | |
| for disciplina in disciplinas_basicas: | |
| dados_disciplina = df_filtrado[df_filtrado['Disciplina'] == disciplina] | |
| if not dados_disciplina.empty: | |
| notas = dados_disciplina[colunas_notas].values[0] | |
| notas_validas = notas > 0 | |
| if any(notas_validas): | |
| bimestres = np.arange(1, len(colunas_notas) + 1)[notas_validas] | |
| notas_filtradas = notas[notas_validas] | |
| estilo = estilos[disciplina] | |
| notas_deslocadas = notas_filtradas + estilo['desloc'] | |
| plt.plot(bimestres, notas_deslocadas, | |
| color=estilo['cor'], | |
| marker=estilo['marcador'], | |
| markersize=10, | |
| linewidth=2.5, | |
| label=disciplina, | |
| zorder=estilo['zorder'], | |
| linestyle=estilo['linestyle'], | |
| alpha=0.8) | |
| for x, y in zip(bimestres, notas_filtradas): | |
| plt.annotate(f"{y:.1f}", (x, y), textcoords="offset points", xytext=(0, 10), ha='center') | |
| 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) | |
| 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.""" | |
| plt.figure(figsize=(12, 6)) | |
| disciplinas = df_boletim_clean['Disciplina'].astype(str) | |
| # Processar frequências (remover % e converter para número) | |
| colunas_freq = ['%Freq B1', '%Freq B2', '%Freq B3', '%Freq B4'] | |
| freq_data = df_boletim_clean[colunas_freq].replace('%', '', regex=True) | |
| medias_frequencia = freq_data.apply(pd.to_numeric, errors='coerce').mean(axis=1) | |
| medias_notas = df_boletim_clean[['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4']].apply(pd.to_numeric, errors='coerce').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) | |
| plt.bar(disciplinas, medias_notas, color=cores_notas) | |
| plt.title('Média de Notas por Disciplina (Vermelho: < 5)') | |
| plt.xticks(rotation=90) | |
| plt.ylim(0, 10) | |
| plt.subplot(1, 2, 2) | |
| plt.bar(disciplinas, medias_frequencia, color=cores_frequencias) | |
| plt.title('Média de Frequência por Disciplina (Vermelho: < 75%)') | |
| plt.xticks(rotation=90) | |
| plt.ylim(0, 100) | |
| plt.suptitle(f"Frequência Global Média: {frequencia_global_media:.2f}%") | |
| if frequencia_global_media < 75: | |
| plt.figtext(0.5, 0.01, "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) | |
| 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) | |
| # Calcular médias | |
| medias_notas = df[['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4']].apply(pd.to_numeric, errors='coerce').mean(axis=1) | |
| # Processar frequências | |
| colunas_freq = ['%Freq B1', '%Freq B2', '%Freq B3', '%Freq B4'] | |
| freq_data = df[colunas_freq].replace('%', '', regex=True) | |
| medias_freq = freq_data.apply(pd.to_numeric, errors='coerce').mean(axis=1) | |
| for idx, (disciplina, media_nota, media_freq) in enumerate(zip(df['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: | |
| 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)"), # Removido o type="filepath" | |
| 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") |