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 | |
matplotlib.use('Agg') | |
# 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'] | |
# Definição das disciplinas de formação básica | |
FORMACAO_BASICA = { | |
'fundamental': { | |
'LINGUA PORTUGUESA', | |
'MATEMATICA', | |
'HISTORIA', | |
'GEOGRAFIA', | |
'CIENCIAS', | |
'LINGUA ESTRANGEIRA INGLES', | |
'ARTE', | |
'EDUCACAO FISICA' | |
}, | |
'medio': { | |
'LINGUA PORTUGUESA', | |
'MATEMATICA', | |
'HISTORIA', | |
'GEOGRAFIA', | |
'BIOLOGIA', | |
'FISICA', | |
'QUIMICA', | |
'INGLÊS', | |
'FILOSOFIA', | |
'SOCIOLOGIA', | |
'ARTE', | |
'EDUCACAO FISICA' | |
} | |
} | |
def detectar_nivel_ensino(disciplinas): | |
"""Detecta se é ensino fundamental ou médio baseado nas disciplinas presentes.""" | |
disciplinas_set = set(disciplinas) | |
disciplinas_exclusivas_medio = {'BIOLOGIA', 'FISICA', 'QUIMICA', 'FILOSOFIA', 'SOCIOLOGIA'} | |
return 'medio' if any(d in disciplinas_set for d in disciplinas_exclusivas_medio) else 'fundamental' | |
def separar_disciplinas_por_categoria(disciplinas_dados): | |
"""Separa as disciplinas em formação básica e diversificada.""" | |
disciplinas = [d['disciplina'] for d in disciplinas_dados] | |
nivel = detectar_nivel_ensino(disciplinas) | |
formacao_basica = [] | |
diversificada = [] | |
for disc_data in disciplinas_dados: | |
if disc_data['disciplina'] in FORMACAO_BASICA[nivel]: | |
formacao_basica.append(disc_data) | |
else: | |
diversificada.append(disc_data) | |
return { | |
'nivel': nivel, | |
'formacao_basica': formacao_basica, | |
'diversificada': diversificada | |
} | |
def converter_nota(valor): | |
"""Converte valor de nota para float, tratando casos especiais e conceitos.""" | |
if pd.isna(valor) or valor == '-' or valor == 'N' or valor == '' or valor == 'None': | |
return None | |
if isinstance(valor, str): | |
valor_limpo = valor.strip().upper() | |
if valor_limpo in CONCEITOS_VALIDOS: | |
conceitos_map = {'ET': 10, 'ES': 8, 'EP': 6} | |
return conceitos_map.get(valor_limpo) | |
try: | |
return float(valor_limpo.replace(',', '.')) | |
except: | |
return None | |
if isinstance(valor, (int, float)): | |
return float(valor) | |
return None | |
def calcular_media_bimestres(notas): | |
"""Calcula média considerando apenas bimestres com notas válidas.""" | |
notas_validas = [nota for nota in notas if nota is not None] | |
if not notas_validas: | |
return 0 | |
return sum(notas_validas) / len(notas_validas) | |
def calcular_frequencia_media(frequencias): | |
"""Calcula média de frequência considerando apenas bimestres cursados.""" | |
freq_validas = [] | |
for freq in frequencias: | |
try: | |
if isinstance(freq, str): | |
freq = freq.strip().replace('%', '').replace(',', '.') | |
if freq and freq != '-': | |
valor = float(freq) | |
if valor > 0: | |
freq_validas.append(valor) | |
except: | |
continue | |
if not freq_validas: | |
return 0 | |
return sum(freq_validas) / len(freq_validas) | |
def extrair_tabelas_pdf(pdf_path): | |
"""Extrai tabelas do PDF usando stream apenas 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 apenas o 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 (procurar a maior tabela com 'Disciplina') | |
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 apenas o nome ao DataFrame | |
df_notas.attrs['nome'] = info_aluno.get('nome', 'Nome não encontrado') | |
return df_notas | |
except Exception as e: | |
print(f"Erro na extração das tabelas: {str(e)}") | |
raise | |
def obter_disciplinas_validas(df): | |
"""Identifica disciplinas válidas no boletim com seus dados.""" | |
colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4'] | |
colunas_freq = ['%Freq B1', '%Freq B2', '%Freq B3', '%Freq B4'] | |
disciplinas_dados = [] | |
for _, row in df.iterrows(): | |
disciplina = row['Disciplina'] | |
if pd.isna(disciplina) or disciplina == '': | |
continue | |
notas = [] | |
freqs = [] | |
bimestres_cursados = [] | |
for i, (col_nota, col_freq) in enumerate(zip(colunas_notas, colunas_freq), 1): | |
nota = converter_nota(row[col_nota]) | |
freq = row[col_freq] if col_freq in row else None | |
if nota is not None or (freq and freq != '-'): | |
bimestres_cursados.append(i) | |
notas.append(nota if nota is not None else 0) | |
freqs.append(freq) | |
else: | |
notas.append(None) | |
freqs.append(None) | |
if bimestres_cursados: | |
media_notas = calcular_media_bimestres(notas) | |
media_freq = calcular_frequencia_media(freqs) | |
disciplinas_dados.append({ | |
'disciplina': disciplina, | |
'notas': notas, | |
'frequencias': freqs, | |
'media_notas': media_notas, | |
'media_freq': media_freq, | |
'bimestres_cursados': bimestres_cursados | |
}) | |
return disciplinas_dados | |
def gerar_paleta_cores(n_cores): | |
"""Gera uma paleta de cores distintas para o número de disciplinas.""" | |
cores_base = [ | |
'#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', | |
'#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf', | |
'#393b79', '#637939', '#8c6d31', '#843c39', '#7b4173' | |
] | |
if n_cores > len(cores_base): | |
HSV_tuples = [(x/n_cores, 0.7, 0.85) 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(disciplinas_dados, temp_dir, titulo=None, nome_arquivo=None): | |
"""Plota gráfico de evolução das notas por bimestre com visualização refinada.""" | |
n_disciplinas = len(disciplinas_dados) | |
if n_disciplinas == 0: | |
raise ValueError("Nenhuma disciplina válida encontrada para plotar.") | |
plt.figure(figsize=(11.69, 8.27)) | |
cores = gerar_paleta_cores(n_disciplinas) | |
marcadores = ['o', 's', '^', 'D', 'v', '<', '>', 'p', 'h', '*'] | |
estilos_linha = ['-', '--', '-.', ':', '-', '--', '-.', ':', '-', '--'] | |
plt.grid(True, linestyle='--', alpha=0.3, zorder=0) | |
# Deslocamento ainda menor e mais refinado | |
deslocamentos = np.linspace(-0.03, 0.03, n_disciplinas) | |
# Estrutura para armazenar as posições das anotações já utilizadas | |
anotacoes_usadas = {} # formato: {bimestre: [(y, texto)]} | |
# Primeira passagem: coletar todos os valores e determinar grupos | |
grupos_notas = {} # {bimestre: {nota: [índices]}} | |
for idx, disc_data in enumerate(disciplinas_dados): | |
notas = pd.Series(disc_data['notas']) | |
bimestres_cursados = disc_data['bimestres_cursados'] | |
if bimestres_cursados: | |
notas_validas = [nota for i, nota in enumerate(notas, 1) if i in bimestres_cursados and nota is not None] | |
bimestres = [bim for bim in bimestres_cursados if notas[bim-1] is not None] | |
for bim, nota in zip(bimestres, notas_validas): | |
if nota is not None: | |
if bim not in grupos_notas: | |
grupos_notas[bim] = {} | |
if nota not in grupos_notas[bim]: | |
grupos_notas[bim][nota] = [] | |
grupos_notas[bim][nota].append(idx) | |
# Segunda passagem: plotar e anotar | |
for idx, disc_data in enumerate(disciplinas_dados): | |
notas = pd.Series(disc_data['notas']) | |
bimestres_cursados = disc_data['bimestres_cursados'] | |
desloc = deslocamentos[idx] | |
if bimestres_cursados: | |
notas_validas = [nota for i, nota in enumerate(notas, 1) if i in bimestres_cursados and nota is not None] | |
bimestres = [bim for bim in bimestres_cursados if notas[bim-1] is not None] | |
bimestres_deslocados = [bim + desloc for bim in bimestres] | |
if notas_validas: | |
# Plotar linha e pontos | |
plt.plot(bimestres_deslocados, notas_validas, | |
color=cores[idx % len(cores)], | |
marker=marcadores[idx % len(marcadores)], | |
markersize=7, | |
linewidth=1.5, | |
label=disc_data['disciplina'], | |
linestyle=estilos_linha[idx % len(estilos_linha)], | |
alpha=0.8) | |
# Adicionar anotações com posicionamento otimizado | |
for bim_orig, bim_desloc, nota in zip(bimestres, bimestres_deslocados, notas_validas): | |
if nota is not None: | |
# Verificar se é o primeiro índice para esta nota neste bimestre | |
if grupos_notas[bim_orig][nota][0] == idx: | |
# Determinar posição vertical da anotação | |
if bim_orig not in anotacoes_usadas: | |
anotacoes_usadas[bim_orig] = [] | |
# Encontrar posição vertical disponível | |
y_base = nota | |
y_offset = 10 | |
texto = f"{nota:.1f}" | |
# Verificar sobreposição com anotações existentes | |
while any(abs(y - (y_base + y_offset/20)) < 0.4 for y, _ in anotacoes_usadas.get(bim_orig, [])): | |
y_offset += 5 | |
# Adicionar anotação | |
plt.annotate(texto, | |
(bim_orig, nota), | |
textcoords="offset points", | |
xytext=(0, y_offset), | |
ha='center', | |
va='bottom', | |
fontsize=8, | |
bbox=dict(facecolor='white', | |
edgecolor='none', | |
alpha=0.8, | |
pad=0.5)) | |
anotacoes_usadas[bim_orig].append((nota + y_offset/20, texto)) | |
# Usar título personalizado se fornecido | |
titulo_grafico = titulo or 'Evolução das Médias por Disciplina ao Longo dos Bimestres' | |
plt.title(titulo_grafico, pad=20, fontsize=12, fontweight='bold') | |
plt.xlabel('Bimestres', fontsize=10) | |
plt.ylabel('Notas', fontsize=10) | |
plt.xticks([1, 2, 3, 4], ['1º Bim', '2º Bim', '3º Bim', '4º Bim']) | |
plt.ylim(0, ESCALA_MAXIMA_NOTAS) | |
# Adicionar linha de aprovação | |
plt.axhline(y=LIMITE_APROVACAO_NOTA, color='r', linestyle='--', alpha=0.3) | |
plt.text(0.02, LIMITE_APROVACAO_NOTA + 0.1, 'Média mínima para aprovação', | |
transform=plt.gca().get_yaxis_transform(), color='r', alpha=0.5) | |
# Ajustar legenda | |
if n_disciplinas > 8: | |
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=8, | |
ncol=max(1, n_disciplinas // 12)) | |
else: | |
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', ncol=1) | |
plt.tight_layout() | |
# Usar nome de arquivo personalizado se fornecido | |
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) | |
plt.close() | |
return plot_path | |
def plotar_graficos_destacados(disciplinas_dados, temp_dir): | |
"""Plota gráficos de médias e frequências com destaques.""" | |
n_disciplinas = len(disciplinas_dados) | |
if not n_disciplinas: | |
raise ValueError("Nenhuma disciplina válida encontrada no boletim.") | |
# Criar figura | |
plt.figure(figsize=(12, 10)) | |
disciplinas = [d['disciplina'] for d in disciplinas_dados] | |
medias_notas = [d['media_notas'] for d in disciplinas_dados] | |
medias_freq = [d['media_freq'] for d in disciplinas_dados] | |
# Criar subplot com mais espaço entre os gráficos | |
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10), height_ratios=[1, 1]) | |
plt.subplots_adjust(hspace=0.5) # Aumentar espaço entre os gráficos | |
# Definir cores baseadas nos limites de aprovação | |
cores_notas = ['red' if media < LIMITE_APROVACAO_NOTA else '#2ecc71' for media in medias_notas] | |
cores_freq = ['red' if media < LIMITE_APROVACAO_FREQ else '#2ecc71' for media in medias_freq] | |
# Calcular médias globais | |
media_global = np.mean(medias_notas) | |
freq_global = np.mean(medias_freq) | |
# Gráfico de notas | |
barras_notas = ax1.bar(disciplinas, medias_notas, color=cores_notas) | |
ax1.set_title('Média de Notas por Disciplina', pad=20, fontsize=12, fontweight='bold') | |
ax1.set_ylim(0, ESCALA_MAXIMA_NOTAS) | |
ax1.grid(True, axis='y', alpha=0.3, linestyle='--') | |
# Melhorar a apresentação dos rótulos | |
ax1.set_xticklabels(disciplinas, rotation=45, ha='right', va='top') | |
ax1.set_ylabel('Notas', fontsize=10, labelpad=10) | |
# Adicionar linha de média mínima | |
ax1.axhline(y=LIMITE_APROVACAO_NOTA, color='r', linestyle='--', alpha=0.3) | |
ax1.text(0.02, LIMITE_APROVACAO_NOTA + 0.1, 'Média mínima (5,0)', | |
transform=ax1.get_yaxis_transform(), color='r', alpha=0.7) | |
# Valores nas barras de notas | |
for barra in barras_notas: | |
altura = barra.get_height() | |
ax1.text(barra.get_x() + barra.get_width()/2., altura, | |
f'{altura:.1f}', | |
ha='center', va='bottom', fontsize=8) | |
# Gráfico de frequências | |
barras_freq = ax2.bar(disciplinas, medias_freq, color=cores_freq) | |
ax2.set_title('Frequência Média por Disciplina', pad=20, fontsize=12, fontweight='bold') | |
ax2.set_ylim(0, 110) | |
ax2.grid(True, axis='y', alpha=0.3, linestyle='--') | |
# Melhorar a apresentação dos rótulos | |
ax2.set_xticklabels(disciplinas, rotation=45, ha='right', va='top') | |
ax2.set_ylabel('Frequência (%)', fontsize=10, labelpad=10) | |
# Adicionar linha de frequência mínima | |
ax2.axhline(y=LIMITE_APROVACAO_FREQ, color='r', linestyle='--', alpha=0.3) | |
ax2.text(0.02, LIMITE_APROVACAO_FREQ + 1, 'Frequência mínima (75%)', | |
transform=ax2.get_yaxis_transform(), color='r', alpha=0.7) | |
# Valores nas barras de frequência | |
for barra in barras_freq: | |
altura = barra.get_height() | |
ax2.text(barra.get_x() + barra.get_width()/2., altura, | |
f'{altura:.1f}%', | |
ha='center', va='bottom', fontsize=8) | |
# Título global com informações de média | |
plt.suptitle( | |
f'Desempenho Geral\nMédia Global: {media_global:.1f} | Frequência Global: {freq_global:.1f}%', | |
y=0.98, fontsize=14, fontweight='bold' | |
) | |
# Aviso de risco de reprovação se necessário | |
if freq_global < LIMITE_APROVACAO_FREQ: | |
plt.figtext(0.5, 0.02, | |
"Atenção: Risco de Reprovação por Baixa Frequência", | |
ha="center", fontsize=11, color="red", weight='bold') | |
plt.tight_layout() | |
# Salvar o gráfico | |
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, disciplinas_dados, grafico_basica, grafico_diversificada, grafico_medias): | |
"""Gera relatório PDF com os gráficos e análises.""" | |
pdf = FPDF() | |
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) | |
# Mostrar apenas o nome | |
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áfico de evolução da formação básica | |
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) | |
# Calcular médias globais | |
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) | |
# Resumo geral | |
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) | |
# Avisos Importantes | |
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 com baixo desempenho | |
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') | |
# Rodapé | |
pdf.set_y(-30) | |
pdf.line(10, pdf.get_y(), 200, pdf.get_y()) | |
pdf.ln(5) | |
pdf.set_font('Helvetica', 'I', 8) | |
pdf.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') | |
# Salvar PDF | |
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: | |
if file is None: | |
return None, "Nenhum arquivo foi fornecido." | |
temp_dir = tempfile.mkdtemp() | |
print(f"Diretório temporário criado: {temp_dir}") | |
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." | |
temp_pdf = os.path.join(temp_dir, 'boletim.pdf') | |
shutil.copy2(file.name, temp_pdf) | |
print(f"PDF copiado para: {temp_pdf}") | |
if not os.path.exists(temp_pdf) or os.path.getsize(temp_pdf) == 0: | |
return None, "Erro ao copiar o arquivo." | |
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." | |
try: | |
# Processar disciplinas | |
disciplinas_dados = obter_disciplinas_validas(df) | |
if not disciplinas_dados: | |
return None, "Nenhuma disciplina válida encontrada no boletim." | |
# Separar disciplinas por categoria | |
categorias = separar_disciplinas_por_categoria(disciplinas_dados) | |
nivel = categorias['nivel'] | |
nivel_texto = "Ensino Médio" if nivel == "medio" else "Ensino Fundamental" | |
# Gerar gráficos | |
print("Gerando gráficos...") | |
grafico_basica = plotar_evolucao_bimestres( | |
categorias['formacao_basica'], | |
temp_dir, | |
titulo=f"Evolução das Médias - Formação Geral Básica ({nivel_texto})", | |
nome_arquivo='evolucao_basica.png' | |
) | |
grafico_diversificada = plotar_evolucao_bimestres( | |
categorias['diversificada'], | |
temp_dir, | |
titulo=f"Evolução das Médias - Parte Diversificada ({nivel_texto})", | |
nome_arquivo='evolucao_diversificada.png' | |
) | |
grafico_medias = plotar_graficos_destacados(disciplinas_dados, temp_dir) | |
print("Gráficos gerados") | |
# Gerar PDF | |
print("Gerando relatório PDF...") | |
pdf_path = gerar_relatorio_pdf(df, disciplinas_dados, grafico_basica, grafico_diversificada, grafico_medias) | |
print("Relatório PDF gerado") | |
# Criar arquivo de 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: | |
return None, f"Erro ao processar os dados: {str(e)}" | |
except Exception as e: | |
print(f"Erro durante o processamento: {str(e)}") | |
return None, f"Erro ao processar o boletim: {str(e)}" | |
finally: | |
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 com suporte mobile | |
iface = gr.Interface( | |
fn=processar_boletim, | |
inputs=gr.File( | |
label="Upload do Boletim (PDF)", | |
type="file", | |
file_types=[".pdf"], # Especifica que só aceita PDFs | |
file_count="single" # Aceita apenas um arquivo | |
), | |
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", | |
examples=None, | |
cache_examples=False, | |
theme="default", | |
css="", # Pode adicionar CSS customizado se necessário | |
elem_id="boletim_analyzer" | |
) | |
# Interface Gradio corrigida | |
iface = gr.Interface( | |
fn=processar_boletim, | |
inputs=gr.File( | |
label="Upload do Boletim (PDF)", | |
file_types=[".pdf"], | |
type="binary" # Corrigido: usar 'binary' em vez de 'file' | |
), | |
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.Base() | |
) | |
if __name__ == "__main__": | |
iface.launch( | |
server_name="0.0.0.0", | |
share=True, | |
enable_queue=True | |
) |