histlearn commited on
Commit
1ada9df
·
verified ·
1 Parent(s): 163e857

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +342 -348
app.py CHANGED
@@ -11,22 +11,8 @@ import matplotlib
11
  import shutil
12
  import colorsys
13
  from datetime import datetime
14
- from concurrent.futures import ThreadPoolExecutor
15
- from typing import Dict, List, Tuple, Optional
16
- from io import BytesIO
17
- import logging
18
- from contextlib import contextmanager
19
-
20
- # Configurar matplotlib
21
  matplotlib.use('Agg')
22
 
23
- # Configurar logging
24
- logging.basicConfig(
25
- level=logging.INFO,
26
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
27
- )
28
- logger = logging.getLogger(__name__)
29
-
30
  # Configurações globais
31
  ESCALA_MAXIMA_NOTAS = 12
32
  LIMITE_APROVACAO_NOTA = 5
@@ -34,10 +20,6 @@ LIMITE_APROVACAO_FREQ = 75
34
  BIMESTRES = ['1º Bimestre', '2º Bimestre', '3º Bimestre', '4º Bimestre']
35
  CONCEITOS_VALIDOS = ['ES', 'EP', 'ET']
36
 
37
- # Cores para os gráficos
38
- COR_APROVADO = '#2ECC71' # Verde suave
39
- COR_REPROVADO = '#E74C3C' # Vermelho suave
40
-
41
  # Definição das disciplinas de formação básica
42
  FORMACAO_BASICA = {
43
  'fundamental': {
@@ -58,7 +40,7 @@ FORMACAO_BASICA = {
58
  'BIOLOGIA',
59
  'FISICA',
60
  'QUIMICA',
61
- 'INGLES',
62
  'FILOSOFIA',
63
  'SOCIOLOGIA',
64
  'ARTE',
@@ -66,42 +48,33 @@ FORMACAO_BASICA = {
66
  }
67
  }
68
 
69
- # Context managers
70
- @contextmanager
71
- def temp_directory():
72
- temp_dir = tempfile.mkdtemp()
73
- try:
74
- yield temp_dir
75
- finally:
76
- if os.path.exists(temp_dir):
77
- shutil.rmtree(temp_dir)
78
-
79
- @contextmanager
80
- def temp_file(suffix=None):
81
- temp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
82
- try:
83
- yield temp.name
84
- finally:
85
- if os.path.exists(temp.name):
86
- os.unlink(temp.name)
87
 
88
- class PDFReport(FPDF):
89
- """Classe personalizada para geração do relatório PDF."""
90
- def __init__(self):
91
- super().__init__()
92
- self.set_auto_page_break(auto=True, margin=15)
93
-
94
- def header_footer(self):
95
- """Adiciona header e footer padrão nas páginas."""
96
- self.set_y(-30)
97
- self.line(10, self.get_y(), 200, self.get_y())
98
- self.ln(5)
99
- self.set_font('Helvetica', 'I', 8)
100
- self.cell(0, 10,
101
- 'Este relatório é uma análise automática e deve ser validado junto à secretaria da escola.',
102
- 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
 
 
 
 
103
 
104
- def converter_nota(valor) -> Optional[float]:
105
  """Converte valor de nota para float, tratando casos especiais e conceitos."""
106
  if pd.isna(valor) or valor == '-' or valor == 'N' or valor == '' or valor == 'None':
107
  return None
@@ -122,12 +95,14 @@ def converter_nota(valor) -> Optional[float]:
122
 
123
  return None
124
 
125
- def calcular_media_bimestres(notas: List[float]) -> float:
126
  """Calcula média considerando apenas bimestres com notas válidas."""
127
  notas_validas = [nota for nota in notas if nota is not None]
128
- return sum(notas_validas) / len(notas_validas) if notas_validas else 0
 
 
129
 
130
- def calcular_frequencia_media(frequencias: List[str]) -> float:
131
  """Calcula média de frequência considerando apenas bimestres cursados."""
132
  freq_validas = []
133
  for freq in frequencias:
@@ -141,10 +116,12 @@ def calcular_frequencia_media(frequencias: List[str]) -> float:
141
  except:
142
  continue
143
 
144
- return sum(freq_validas) / len(freq_validas) if freq_validas else 0
 
 
145
 
146
- def extrair_tabelas_pdf(pdf_path: str) -> pd.DataFrame:
147
- """Extrai tabelas do PDF usando stream para o nome e lattice para notas."""
148
  try:
149
  # Extrair nome do aluno usando stream
150
  tables_header = camelot.read_pdf(
@@ -156,7 +133,7 @@ def extrair_tabelas_pdf(pdf_path: str) -> pd.DataFrame:
156
 
157
  info_aluno = {}
158
 
159
- # Procurar nome do aluno
160
  for table in tables_header:
161
  df = table.df
162
  for i in range(len(df)):
@@ -181,7 +158,7 @@ def extrair_tabelas_pdf(pdf_path: str) -> pd.DataFrame:
181
  flavor='lattice'
182
  )
183
 
184
- # Encontrar tabela de notas
185
  df_notas = None
186
  max_rows = 0
187
 
@@ -202,22 +179,16 @@ def extrair_tabelas_pdf(pdf_path: str) -> pd.DataFrame:
202
  if df_notas is None:
203
  raise ValueError("Tabela de notas não encontrada")
204
 
205
- # Adicionar informações do aluno ao DataFrame
206
  df_notas.attrs['nome'] = info_aluno.get('nome', 'Nome não encontrado')
207
 
208
  return df_notas
209
 
210
  except Exception as e:
211
- logger.error(f"Erro na extração das tabelas: {str(e)}")
212
  raise
213
-
214
- def detectar_nivel_ensino(disciplinas: List[str]) -> str:
215
- """Detecta se é ensino fundamental ou médio baseado nas disciplinas."""
216
- disciplinas_set = set(disciplinas)
217
- disciplinas_exclusivas_medio = {'BIOLOGIA', 'FISICA', 'QUIMICA', 'FILOSOFIA', 'SOCIOLOGIA'}
218
- return 'medio' if any(d in disciplinas_set for d in disciplinas_exclusivas_medio) else 'fundamental'
219
-
220
- def obter_disciplinas_validas(df: pd.DataFrame) -> List[Dict]:
221
  """Identifica disciplinas válidas no boletim com seus dados."""
222
  colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4']
223
  colunas_freq = ['%Freq B1', '%Freq B2', '%Freq B3', '%Freq B4']
@@ -260,71 +231,62 @@ def obter_disciplinas_validas(df: pd.DataFrame) -> List[Dict]:
260
 
261
  return disciplinas_dados
262
 
263
- def separar_disciplinas_por_categoria(disciplinas_dados: List[Dict]) -> Dict:
264
- """Separa as disciplinas em formação básica e diversificada."""
265
- disciplinas = [d['disciplina'] for d in disciplinas_dados]
266
- nivel = detectar_nivel_ensino(disciplinas)
267
-
268
- formacao_basica = []
269
- diversificada = []
270
-
271
- for disc_data in disciplinas_dados:
272
- if disc_data['disciplina'] in FORMACAO_BASICA[nivel]:
273
- formacao_basica.append(disc_data)
274
- else:
275
- diversificada.append(disc_data)
276
-
277
- return {
278
- 'nivel': nivel,
279
- 'formacao_basica': formacao_basica,
280
- 'diversificada': diversificada
281
- }
282
- # Funções de plotagem
283
- def gerar_paleta_cores(n_cores: int) -> List[str]:
284
- """Gera uma paleta de cores harmoniosa."""
285
- cores_formacao_basica = [
286
- '#2E86C1', # Azul royal
287
- '#2ECC71', # Verde esmeralda
288
- '#E74C3C', # Vermelho coral
289
- '#F1C40F', # Amarelo ouro
290
- '#8E44AD', # Roxo médio
291
- '#E67E22', # Laranja escuro
292
- '#16A085', # Verde-água
293
- '#D35400' # Laranja queimado
294
  ]
295
 
296
- if n_cores <= len(cores_formacao_basica):
297
- return cores_formacao_basica[:n_cores]
 
 
 
298
 
299
- # Gerar cores adicionais se necessário
300
- HSV_tuples = [(x/n_cores, 0.8, 0.9) for x in range(n_cores)]
301
- return ['#%02x%02x%02x' % tuple(int(x*255) for x in colorsys.hsv_to_rgb(*hsv))
302
- for hsv in HSV_tuples]
303
 
304
- def plotar_evolucao_bimestres(disciplinas_dados: List[Dict], temp_dir: str,
305
- titulo: Optional[str] = None,
306
- nome_arquivo: Optional[str] = None) -> str:
307
- """Plota gráfico de evolução das notas com visual aprimorado."""
308
  n_disciplinas = len(disciplinas_dados)
309
 
310
  if n_disciplinas == 0:
311
  raise ValueError("Nenhuma disciplina válida encontrada para plotar.")
312
 
313
- # Configuração do estilo
314
- plt.style.use('seaborn-v0_8-darkgrid')
315
- fig, ax = plt.subplots(figsize=(11.69, 8.27))
316
-
317
- # Configurar grid mais suave
318
- ax.grid(True, linestyle='--', alpha=0.2, color='gray')
319
- ax.set_axisbelow(True)
320
 
321
  cores = gerar_paleta_cores(n_disciplinas)
322
- marcadores = ['o', 's', '^', 'D', 'v', '<', '>', 'p']
323
- estilos_linha = ['-', '--', '-.', ':']
324
 
325
- deslocamentos = np.linspace(-0.02, 0.02, n_disciplinas)
326
- anotacoes_usadas = {}
327
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  for idx, disc_data in enumerate(disciplinas_dados):
329
  notas = pd.Series(disc_data['notas'])
330
  bimestres_cursados = disc_data['bimestres_cursados']
@@ -336,341 +298,374 @@ def plotar_evolucao_bimestres(disciplinas_dados: List[Dict], temp_dir: str,
336
  bimestres_deslocados = [bim + desloc for bim in bimestres]
337
 
338
  if notas_validas:
339
- ax.plot(bimestres_deslocados, notas_validas,
 
340
  color=cores[idx % len(cores)],
341
  marker=marcadores[idx % len(marcadores)],
342
- markersize=8,
343
- linewidth=2.5,
344
  label=disc_data['disciplina'],
345
  linestyle=estilos_linha[idx % len(estilos_linha)],
346
- alpha=0.8,
347
- zorder=3)
348
-
349
- ax.fill_between(bimestres_deslocados, 0, notas_validas,
350
- color=cores[idx % len(cores)], alpha=0.1)
351
 
352
- for bim, nota in zip(bimestres_deslocados, notas_validas):
 
353
  if nota is not None:
354
- y_offset = 10
355
- while any(abs(y - (nota + y_offset/20)) < 0.4 for y, _ in anotacoes_usadas.get(bim, [])):
356
- y_offset += 5
357
-
358
- ax.annotate(f"{nota:.1f}",
359
- (bim, nota),
360
- xytext=(0, y_offset),
361
- textcoords="offset points",
362
- ha='center',
363
- va='bottom',
364
- fontsize=9,
365
- bbox=dict(facecolor='white',
366
- edgecolor=cores[idx % len(cores)],
367
- alpha=0.8,
368
- pad=2,
369
- boxstyle='round,pad=0.5'))
370
-
371
- if bim not in anotacoes_usadas:
372
- anotacoes_usadas[bim] = []
373
- anotacoes_usadas[bim].append((nota + y_offset/20, nota))
374
-
375
- titulo_grafico = titulo or 'Evolução das Médias por Disciplina'
376
- ax.set_title(titulo_grafico, pad=20, fontsize=14, fontweight='bold')
377
- ax.set_xlabel('Bimestres', fontsize=12, labelpad=10)
378
- ax.set_ylabel('Notas', fontsize=12, labelpad=10)
379
-
380
- ax.spines['top'].set_visible(False)
381
- ax.spines['right'].set_visible(False)
382
-
383
- ax.set_xticks([1, 2, 3, 4])
384
- ax.set_xticklabels(['1º Bim', '2º Bim', '3º Bim', '4º Bim'], fontsize=10)
385
- ax.set_ylim(0, ESCALA_MAXIMA_NOTAS)
386
-
387
- ax.axhline(y=LIMITE_APROVACAO_NOTA, color=COR_REPROVADO, linestyle='--', alpha=0.3, linewidth=2)
388
- ax.text(0.02, LIMITE_APROVACAO_NOTA + 0.1, 'Média mínima para aprovação',
389
- transform=ax.get_yaxis_transform(), color=COR_REPROVADO, alpha=0.7)
390
-
 
 
 
 
 
 
 
 
391
  if n_disciplinas > 8:
392
- ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=9, framealpha=0.8, fancybox=True, shadow=True,
393
  ncol=max(1, n_disciplinas // 12))
394
  else:
395
- ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=10, framealpha=0.8, fancybox=True, shadow=True)
396
 
397
  plt.tight_layout()
398
-
399
- # Força a renderização para evitar o erro de renderizador
400
- fig.canvas.draw()
401
 
402
- # Salvar com alta qualidade
403
  nome_arquivo = nome_arquivo or 'evolucao_notas.png'
404
  plot_path = os.path.join(temp_dir, nome_arquivo)
405
- fig.savefig(plot_path, bbox_inches='tight', dpi=300, facecolor='white', edgecolor='none')
406
- plt.close(fig)
407
-
408
  return plot_path
409
-
410
- def plotar_graficos_destacados(disciplinas_dados: List[Dict], temp_dir: str) -> str:
411
- """Plota gráficos de médias e frequências com visual aprimorado."""
412
  n_disciplinas = len(disciplinas_dados)
413
 
414
  if not n_disciplinas:
415
  raise ValueError("Nenhuma disciplina válida encontrada no boletim.")
416
 
417
- # Configuração do estilo
418
- plt.style.use('seaborn-v0_8-darkgrid')
419
- fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10),
420
- height_ratios=[1, 1])
421
- plt.subplots_adjust(hspace=0.4)
422
 
423
  disciplinas = [d['disciplina'] for d in disciplinas_dados]
424
  medias_notas = [d['media_notas'] for d in disciplinas_dados]
425
  medias_freq = [d['media_freq'] for d in disciplinas_dados]
426
 
427
- # Definir cores baseadas nos limites
428
- cores_notas = [COR_REPROVADO if media < LIMITE_APROVACAO_NOTA
429
- else COR_APROVADO for media in medias_notas]
430
- cores_freq = [COR_REPROVADO if media < LIMITE_APROVACAO_FREQ
431
- else COR_APROVADO for media in medias_freq]
 
 
432
 
433
  # Calcular médias globais
434
  media_global = np.mean(medias_notas)
435
  freq_global = np.mean(medias_freq)
436
 
437
- # Configurações comuns para os eixos
438
- for ax in [ax1, ax2]:
439
- ax.grid(True, axis='y', alpha=0.2, linestyle='--')
440
- ax.set_axisbelow(True)
441
- ax.spines['top'].set_visible(False)
442
- ax.spines['right'].set_visible(False)
443
-
444
- # Definir os ticks do eixo x para corresponder ao número de disciplinas
445
- ax1.set_xticks(range(n_disciplinas))
446
- ax2.set_xticks(range(n_disciplinas))
447
-
448
  # Gráfico de notas
449
  barras_notas = ax1.bar(disciplinas, medias_notas, color=cores_notas)
450
- ax1.set_title('Média de Notas por Disciplina',
451
- pad=20, fontsize=14, fontweight='bold')
452
  ax1.set_ylim(0, ESCALA_MAXIMA_NOTAS)
453
- ax1.set_xticklabels(disciplinas, rotation=45, ha='right', va='top', fontsize=10)
454
- ax1.set_ylabel('Notas', fontsize=12, labelpad=10)
455
 
456
- # Linha de média mínima
457
- ax1.axhline(y=LIMITE_APROVACAO_NOTA, color=COR_REPROVADO, linestyle='--', alpha=0.3, linewidth=2)
 
 
 
 
458
  ax1.text(0.02, LIMITE_APROVACAO_NOTA + 0.1, 'Média mínima (5,0)',
459
- transform=ax1.get_yaxis_transform(), color=COR_REPROVADO, alpha=0.7, fontsize=10)
460
 
461
  # Valores nas barras de notas
462
  for barra in barras_notas:
463
  altura = barra.get_height()
464
- cor_texto = 'white' if altura >= LIMITE_APROVACAO_NOTA else 'black'
465
  ax1.text(barra.get_x() + barra.get_width()/2., altura,
466
- f'{altura:.1f}', ha='center', va='bottom', fontsize=10,
467
- bbox=dict(facecolor='white', edgecolor='none', alpha=0.7, pad=1),
468
- color=cor_texto if altura >= 8 else 'black')
469
 
470
  # Gráfico de frequências
471
  barras_freq = ax2.bar(disciplinas, medias_freq, color=cores_freq)
472
- ax2.set_title('Frequência Média por Disciplina', pad=20, fontsize=14, fontweight='bold')
473
  ax2.set_ylim(0, 110)
474
- ax2.set_xticklabels(disciplinas, rotation=45, ha='right', va='top', fontsize=10)
475
- ax2.set_ylabel('Frequência (%)', fontsize=12, labelpad=10)
 
 
 
476
 
477
- # Linha de frequência mínima
478
- ax2.axhline(y=LIMITE_APROVACAO_FREQ, color=COR_REPROVADO, linestyle='--', alpha=0.3, linewidth=2)
479
  ax2.text(0.02, LIMITE_APROVACAO_FREQ + 1, 'Frequência mínima (75%)',
480
- transform=ax2.get_yaxis_transform(), color=COR_REPROVADO, alpha=0.7, fontsize=10)
481
 
482
  # Valores nas barras de frequência
483
  for barra in barras_freq:
484
  altura = barra.get_height()
485
- cor_texto = 'white' if altura >= LIMITE_APROVACAO_FREQ else 'black'
486
  ax2.text(barra.get_x() + barra.get_width()/2., altura,
487
- f'{altura:.1f}%', ha='center', va='bottom', fontsize=10,
488
- bbox=dict(facecolor='white', edgecolor='none', alpha=0.7, pad=1),
489
- color=cor_texto if altura >= 90 else 'black')
490
 
491
- # Título global com estilo
492
  plt.suptitle(
493
  f'Desempenho Geral\nMédia Global: {media_global:.1f} | Frequência Global: {freq_global:.1f}%',
494
- y=0.98, fontsize=16, fontweight='bold',
495
- bbox=dict(facecolor='white', edgecolor='none', alpha=0.8, pad=5, boxstyle='round,pad=0.5')
496
  )
497
 
498
- # Aviso de reprovação estilizado
499
  if freq_global < LIMITE_APROVACAO_FREQ:
500
- plt.figtext(0.5, 0.02, "Atenção: Risco de Reprovação por Baixa Frequência",
501
- ha="center", fontsize=12, color=COR_REPROVADO, weight='bold',
502
- bbox=dict(facecolor='#FFEBEE', edgecolor=COR_REPROVADO, alpha=0.9, pad=5, boxstyle='round,pad=0.5'))
503
 
504
  plt.tight_layout()
505
 
506
- # Salvar com alta qualidade
507
  plot_path = os.path.join(temp_dir, 'medias_frequencias.png')
508
- plt.savefig(plot_path, bbox_inches='tight', dpi=300, facecolor='white', edgecolor='none')
509
  plt.close()
510
 
511
  return plot_path
512
 
513
- # Funções de processamento do PDF e geração de relatórios
514
- def gerar_relatorio_pdf(df: pd.DataFrame, disciplinas_dados: List[Dict],
515
- grafico_basica: str, grafico_diversificada: str,
516
- grafico_medias: str) -> str:
517
- """Gera relatório PDF com análise completa."""
518
- pdf = PDFReport()
519
  pdf.set_auto_page_break(auto=True, margin=15)
520
 
521
  # Primeira página - Informações e Formação Básica
522
  pdf.add_page()
523
  pdf.set_font('Helvetica', 'B', 18)
524
- pdf.cell(0, 10, 'Relatório de Desempenho Escolar',
525
- 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
526
  pdf.ln(15)
527
 
528
  # Informações do aluno
529
  pdf.set_font('Helvetica', 'B', 12)
530
- pdf.cell(0, 10, 'Informações do Aluno',
531
- 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
532
  pdf.line(10, pdf.get_y(), 200, pdf.get_y())
533
  pdf.ln(5)
534
 
535
- # Nome do aluno
536
  if hasattr(df, 'attrs') and 'nome' in df.attrs:
537
  pdf.set_font('Helvetica', 'B', 11)
538
- pdf.cell(30, 7, 'Nome:', new_x=XPos.RIGHT, new_y=YPos.TOP)
539
  pdf.set_font('Helvetica', '', 11)
540
- pdf.cell(0, 7, df.attrs['nome'],
541
- 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT)
542
 
543
  pdf.ln(10)
544
 
545
  # Data do relatório
546
  data_atual = datetime.now().strftime('%d/%m/%Y')
547
  pdf.set_font('Helvetica', 'I', 10)
548
- pdf.cell(0, 5, f'Data de geração: {data_atual}',
549
- 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='R')
550
  pdf.ln(15)
551
 
552
- # Inserir gráficos com verificação
553
- if os.path.exists(grafico_basica):
554
- pdf.set_font('Helvetica', 'B', 14)
555
- pdf.cell(0, 10, 'Evolução das Notas - Formação Geral Básica',
556
- 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
557
- pdf.line(10, pdf.get_y(), 200, pdf.get_y())
558
- pdf.ln(10)
559
- pdf.image(grafico_basica, x=10, w=190)
560
- else:
561
- logger.error("Gráfico de formação básica não encontrado.")
562
 
 
563
  pdf.add_page()
564
- if os.path.exists(grafico_diversificada):
565
- pdf.set_font('Helvetica', 'B', 14)
566
- pdf.cell(0, 10, 'Evolução das Notas - Parte Diversificada',
567
- 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
568
- pdf.line(10, pdf.get_y(), 200, pdf.get_y())
569
- pdf.ln(10)
570
- pdf.image(grafico_diversificada, x=10, w=190)
571
- else:
572
- logger.error("Gráfico de parte diversificada não encontrado.")
573
 
 
574
  pdf.add_page()
575
- if os.path.exists(grafico_medias):
576
- pdf.set_font('Helvetica', 'B', 14)
577
- pdf.cell(0, 10, 'Análise de Médias e Frequências',
578
- 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
579
- pdf.line(10, pdf.get_y(), 200, pdf.get_y())
580
- pdf.ln(10)
581
- pdf.image(grafico_medias, x=10, w=190)
582
- else:
583
- logger.error("Gráfico de médias e frequências não encontrado.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
584
 
585
- # Salvar PDF em um arquivo temporário
586
- with temp_file(suffix='.pdf') as temp_pdf:
587
- pdf.output(temp_pdf)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
588
 
589
- # Verifique se o arquivo PDF foi realmente salvo
590
- if os.path.exists(temp_pdf):
591
- logger.info("Relatório PDF gerado com sucesso.")
592
- return temp_pdf
593
- else:
594
- logger.error("Falha ao salvar o relatório PDF.")
595
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
596
 
597
- def processar_boletim(file) -> Tuple[Optional[str], str]:
598
  """Função principal que processa o boletim e gera o relatório."""
 
599
  try:
600
  if file is None:
601
  return None, "Nenhum arquivo foi fornecido."
602
 
603
- with temp_directory() as temp_dir:
604
- # Salvar arquivo temporário
605
- temp_pdf = os.path.join(temp_dir, 'boletim.pdf')
606
- with open(temp_pdf, 'wb') as f:
607
- f.write(file)
608
-
609
- if os.path.getsize(temp_pdf) == 0:
610
- return None, "O arquivo está vazio."
611
-
612
- # Extrair e processar dados
613
- df = extrair_tabelas_pdf(temp_pdf)
614
- if df is None or df.empty:
615
- return None, "Não foi possível extrair dados do PDF."
616
-
 
 
 
 
 
 
 
617
  disciplinas_dados = obter_disciplinas_validas(df)
618
  if not disciplinas_dados:
619
  return None, "Nenhuma disciplina válida encontrada no boletim."
620
 
621
- # Separar disciplinas e determinar nível
622
  categorias = separar_disciplinas_por_categoria(disciplinas_dados)
623
- nivel_texto = "Ensino Médio" if categorias['nivel'] == "medio" else "Ensino Fundamental"
 
624
 
625
- # Gerar gráficos em paralelo
626
- with ThreadPoolExecutor() as executor:
627
- futures = {
628
- 'basica': executor.submit(
629
- plotar_evolucao_bimestres,
630
- categorias['formacao_basica'],
631
- temp_dir,
632
- f"Evolução das Médias - Formação Geral Básica ({nivel_texto})",
633
- 'evolucao_basica.png'
634
- ),
635
- 'diversificada': executor.submit(
636
- plotar_evolucao_bimestres,
637
- categorias['diversificada'],
638
- temp_dir,
639
- f"Evolução das Médias - Parte Diversificada ({nivel_texto})",
640
- 'evolucao_diversificada.png'
641
- ),
642
- 'medias': executor.submit(
643
- plotar_graficos_destacados,
644
- disciplinas_dados,
645
- temp_dir
646
- )
647
- }
648
-
649
- grafico_basica = futures['basica'].result()
650
- grafico_diversificada = futures['diversificada'].result()
651
- grafico_medias = futures['medias'].result()
652
 
653
- # Gerar relatório final
654
- pdf_path = gerar_relatorio_pdf(
655
- df,
656
- disciplinas_dados,
657
- grafico_basica,
658
- grafico_diversificada,
659
- grafico_medias
660
  )
661
 
662
- # Verificar se o arquivo PDF foi criado corretamente
663
- if pdf_path and os.path.exists(pdf_path):
664
- output_path = os.path.join(temp_dir, 'relatorio_final.pdf')
665
- shutil.copy2(pdf_path, output_path)
666
- return output_path, "Relatório gerado com sucesso!"
667
- else:
668
- logger.error("Falha ao gerar o PDF do relatório.")
669
- return None, "Erro ao gerar o relatório PDF final."
 
 
 
 
 
 
670
 
 
 
 
671
  except Exception as e:
672
- logger.exception("Erro durante o processamento")
673
  return None, f"Erro ao processar o boletim: {str(e)}"
 
 
 
 
 
 
 
 
674
 
675
  # Interface Gradio
676
  iface = gr.Interface(
@@ -686,8 +681,7 @@ iface = gr.Interface(
686
  ],
687
  title="Análise de Boletim Escolar",
688
  description="Faça upload do boletim em PDF para gerar um relatório com análises e visualizações.",
689
- allow_flagging="never",
690
- theme=gr.themes.Default()
691
  )
692
 
693
  if __name__ == "__main__":
 
11
  import shutil
12
  import colorsys
13
  from datetime import datetime
 
 
 
 
 
 
 
14
  matplotlib.use('Agg')
15
 
 
 
 
 
 
 
 
16
  # Configurações globais
17
  ESCALA_MAXIMA_NOTAS = 12
18
  LIMITE_APROVACAO_NOTA = 5
 
20
  BIMESTRES = ['1º Bimestre', '2º Bimestre', '3º Bimestre', '4º Bimestre']
21
  CONCEITOS_VALIDOS = ['ES', 'EP', 'ET']
22
 
 
 
 
 
23
  # Definição das disciplinas de formação básica
24
  FORMACAO_BASICA = {
25
  'fundamental': {
 
40
  'BIOLOGIA',
41
  'FISICA',
42
  'QUIMICA',
43
+ 'INGLÊS',
44
  'FILOSOFIA',
45
  'SOCIOLOGIA',
46
  'ARTE',
 
48
  }
49
  }
50
 
51
+ def detectar_nivel_ensino(disciplinas):
52
+ """Detecta se é ensino fundamental ou médio baseado nas disciplinas presentes."""
53
+ disciplinas_set = set(disciplinas)
54
+ disciplinas_exclusivas_medio = {'BIOLOGIA', 'FISICA', 'QUIMICA', 'FILOSOFIA', 'SOCIOLOGIA'}
55
+ return 'medio' if any(d in disciplinas_set for d in disciplinas_exclusivas_medio) else 'fundamental'
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
+ def separar_disciplinas_por_categoria(disciplinas_dados):
58
+ """Separa as disciplinas em formação básica e diversificada."""
59
+ disciplinas = [d['disciplina'] for d in disciplinas_dados]
60
+ nivel = detectar_nivel_ensino(disciplinas)
61
+
62
+ formacao_basica = []
63
+ diversificada = []
64
+
65
+ for disc_data in disciplinas_dados:
66
+ if disc_data['disciplina'] in FORMACAO_BASICA[nivel]:
67
+ formacao_basica.append(disc_data)
68
+ else:
69
+ diversificada.append(disc_data)
70
+
71
+ return {
72
+ 'nivel': nivel,
73
+ 'formacao_basica': formacao_basica,
74
+ 'diversificada': diversificada
75
+ }
76
 
77
+ def converter_nota(valor):
78
  """Converte valor de nota para float, tratando casos especiais e conceitos."""
79
  if pd.isna(valor) or valor == '-' or valor == 'N' or valor == '' or valor == 'None':
80
  return None
 
95
 
96
  return None
97
 
98
+ def calcular_media_bimestres(notas):
99
  """Calcula média considerando apenas bimestres com notas válidas."""
100
  notas_validas = [nota for nota in notas if nota is not None]
101
+ if not notas_validas:
102
+ return 0
103
+ return sum(notas_validas) / len(notas_validas)
104
 
105
+ def calcular_frequencia_media(frequencias):
106
  """Calcula média de frequência considerando apenas bimestres cursados."""
107
  freq_validas = []
108
  for freq in frequencias:
 
116
  except:
117
  continue
118
 
119
+ if not freq_validas:
120
+ return 0
121
+ return sum(freq_validas) / len(freq_validas)
122
 
123
+ def extrair_tabelas_pdf(pdf_path):
124
+ """Extrai tabelas do PDF usando stream apenas para o nome e lattice para notas."""
125
  try:
126
  # Extrair nome do aluno usando stream
127
  tables_header = camelot.read_pdf(
 
133
 
134
  info_aluno = {}
135
 
136
+ # Procurar apenas o nome do aluno
137
  for table in tables_header:
138
  df = table.df
139
  for i in range(len(df)):
 
158
  flavor='lattice'
159
  )
160
 
161
+ # Encontrar tabela de notas (procurar a maior tabela com 'Disciplina')
162
  df_notas = None
163
  max_rows = 0
164
 
 
179
  if df_notas is None:
180
  raise ValueError("Tabela de notas não encontrada")
181
 
182
+ # Adicionar apenas o nome ao DataFrame
183
  df_notas.attrs['nome'] = info_aluno.get('nome', 'Nome não encontrado')
184
 
185
  return df_notas
186
 
187
  except Exception as e:
188
+ print(f"Erro na extração das tabelas: {str(e)}")
189
  raise
190
+
191
+ def obter_disciplinas_validas(df):
 
 
 
 
 
 
192
  """Identifica disciplinas válidas no boletim com seus dados."""
193
  colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4']
194
  colunas_freq = ['%Freq B1', '%Freq B2', '%Freq B3', '%Freq B4']
 
231
 
232
  return disciplinas_dados
233
 
234
+ def gerar_paleta_cores(n_cores):
235
+ """Gera uma paleta de cores distintas para o número de disciplinas."""
236
+ cores_base = [
237
+ '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
238
+ '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf',
239
+ '#393b79', '#637939', '#8c6d31', '#843c39', '#7b4173'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  ]
241
 
242
+ if n_cores > len(cores_base):
243
+ HSV_tuples = [(x/n_cores, 0.7, 0.85) for x in range(n_cores)]
244
+ cores_extras = ['#%02x%02x%02x' % tuple(int(x*255) for x in colorsys.hsv_to_rgb(*hsv))
245
+ for hsv in HSV_tuples]
246
+ return cores_extras
247
 
248
+ return cores_base[:n_cores]
 
 
 
249
 
250
+ def plotar_evolucao_bimestres(disciplinas_dados, temp_dir, titulo=None, nome_arquivo=None):
251
+ """Plota gráfico de evolução das notas por bimestre com visualização refinada."""
 
 
252
  n_disciplinas = len(disciplinas_dados)
253
 
254
  if n_disciplinas == 0:
255
  raise ValueError("Nenhuma disciplina válida encontrada para plotar.")
256
 
257
+ plt.figure(figsize=(11.69, 8.27))
 
 
 
 
 
 
258
 
259
  cores = gerar_paleta_cores(n_disciplinas)
260
+ marcadores = ['o', 's', '^', 'D', 'v', '<', '>', 'p', 'h', '*']
261
+ estilos_linha = ['-', '--', '-.', ':', '-', '--', '-.', ':', '-', '--']
262
 
263
+ plt.grid(True, linestyle='--', alpha=0.3, zorder=0)
 
264
 
265
+ # Deslocamento ainda menor e mais refinado
266
+ deslocamentos = np.linspace(-0.03, 0.03, n_disciplinas)
267
+
268
+ # Estrutura para armazenar as posições das anotações já utilizadas
269
+ anotacoes_usadas = {} # formato: {bimestre: [(y, texto)]}
270
+
271
+ # Primeira passagem: coletar todos os valores e determinar grupos
272
+ grupos_notas = {} # {bimestre: {nota: [índices]}}
273
+ for idx, disc_data in enumerate(disciplinas_dados):
274
+ notas = pd.Series(disc_data['notas'])
275
+ bimestres_cursados = disc_data['bimestres_cursados']
276
+
277
+ if bimestres_cursados:
278
+ notas_validas = [nota for i, nota in enumerate(notas, 1) if i in bimestres_cursados and nota is not None]
279
+ bimestres = [bim for bim in bimestres_cursados if notas[bim-1] is not None]
280
+
281
+ for bim, nota in zip(bimestres, notas_validas):
282
+ if nota is not None:
283
+ if bim not in grupos_notas:
284
+ grupos_notas[bim] = {}
285
+ if nota not in grupos_notas[bim]:
286
+ grupos_notas[bim][nota] = []
287
+ grupos_notas[bim][nota].append(idx)
288
+
289
+ # Segunda passagem: plotar e anotar
290
  for idx, disc_data in enumerate(disciplinas_dados):
291
  notas = pd.Series(disc_data['notas'])
292
  bimestres_cursados = disc_data['bimestres_cursados']
 
298
  bimestres_deslocados = [bim + desloc for bim in bimestres]
299
 
300
  if notas_validas:
301
+ # Plotar linha e pontos
302
+ plt.plot(bimestres_deslocados, notas_validas,
303
  color=cores[idx % len(cores)],
304
  marker=marcadores[idx % len(marcadores)],
305
+ markersize=7,
306
+ linewidth=1.5,
307
  label=disc_data['disciplina'],
308
  linestyle=estilos_linha[idx % len(estilos_linha)],
309
+ alpha=0.8)
 
 
 
 
310
 
311
+ # Adicionar anotações com posicionamento otimizado
312
+ for bim_orig, bim_desloc, nota in zip(bimestres, bimestres_deslocados, notas_validas):
313
  if nota is not None:
314
+ # Verificar se é o primeiro índice para esta nota neste bimestre
315
+ if grupos_notas[bim_orig][nota][0] == idx:
316
+ # Determinar posição vertical da anotação
317
+ if bim_orig not in anotacoes_usadas:
318
+ anotacoes_usadas[bim_orig] = []
319
+
320
+ # Encontrar posição vertical disponível
321
+ y_base = nota
322
+ y_offset = 10
323
+ texto = f"{nota:.1f}"
324
+
325
+ # Verificar sobreposição com anotações existentes
326
+ while any(abs(y - (y_base + y_offset/20)) < 0.4 for y, _ in anotacoes_usadas.get(bim_orig, [])):
327
+ y_offset += 5
328
+
329
+ # Adicionar anotação
330
+ plt.annotate(texto,
331
+ (bim_orig, nota),
332
+ textcoords="offset points",
333
+ xytext=(0, y_offset),
334
+ ha='center',
335
+ va='bottom',
336
+ fontsize=8,
337
+ bbox=dict(facecolor='white',
338
+ edgecolor='none',
339
+ alpha=0.8,
340
+ pad=0.5))
341
+
342
+ anotacoes_usadas[bim_orig].append((nota + y_offset/20, texto))
343
+
344
+ # Usar título personalizado se fornecido
345
+ titulo_grafico = titulo or 'Evolução das Médias por Disciplina ao Longo dos Bimestres'
346
+ plt.title(titulo_grafico, pad=20, fontsize=12, fontweight='bold')
347
+
348
+ plt.xlabel('Bimestres', fontsize=10)
349
+ plt.ylabel('Notas', fontsize=10)
350
+ plt.xticks([1, 2, 3, 4], ['1º Bim', '2º Bim', '3º Bim', '4º Bim'])
351
+ plt.ylim(0, ESCALA_MAXIMA_NOTAS)
352
+
353
+ # Adicionar linha de aprovação
354
+ plt.axhline(y=LIMITE_APROVACAO_NOTA, color='r', linestyle='--', alpha=0.3)
355
+ plt.text(0.02, LIMITE_APROVACAO_NOTA + 0.1, 'Média mínima para aprovação',
356
+ transform=plt.gca().get_yaxis_transform(), color='r', alpha=0.5)
357
+
358
+ # Ajustar legenda
359
  if n_disciplinas > 8:
360
+ plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=8,
361
  ncol=max(1, n_disciplinas // 12))
362
  else:
363
+ plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', ncol=1)
364
 
365
  plt.tight_layout()
 
 
 
366
 
367
+ # Usar nome de arquivo personalizado se fornecido
368
  nome_arquivo = nome_arquivo or 'evolucao_notas.png'
369
  plot_path = os.path.join(temp_dir, nome_arquivo)
370
+ plt.savefig(plot_path, bbox_inches='tight', dpi=300)
371
+ plt.close()
 
372
  return plot_path
373
+
374
+ def plotar_graficos_destacados(disciplinas_dados, temp_dir):
375
+ """Plota gráficos de médias e frequências com destaques."""
376
  n_disciplinas = len(disciplinas_dados)
377
 
378
  if not n_disciplinas:
379
  raise ValueError("Nenhuma disciplina válida encontrada no boletim.")
380
 
381
+ # Criar figura
382
+ plt.figure(figsize=(12, 10))
 
 
 
383
 
384
  disciplinas = [d['disciplina'] for d in disciplinas_dados]
385
  medias_notas = [d['media_notas'] for d in disciplinas_dados]
386
  medias_freq = [d['media_freq'] for d in disciplinas_dados]
387
 
388
+ # Criar subplot com mais espaço entre os gráficos
389
+ fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10), height_ratios=[1, 1])
390
+ plt.subplots_adjust(hspace=0.5) # Aumentar espaço entre os gráficos
391
+
392
+ # Definir cores baseadas nos limites de aprovação
393
+ cores_notas = ['red' if media < LIMITE_APROVACAO_NOTA else '#2ecc71' for media in medias_notas]
394
+ cores_freq = ['red' if media < LIMITE_APROVACAO_FREQ else '#2ecc71' for media in medias_freq]
395
 
396
  # Calcular médias globais
397
  media_global = np.mean(medias_notas)
398
  freq_global = np.mean(medias_freq)
399
 
 
 
 
 
 
 
 
 
 
 
 
400
  # Gráfico de notas
401
  barras_notas = ax1.bar(disciplinas, medias_notas, color=cores_notas)
402
+ ax1.set_title('Média de Notas por Disciplina', pad=20, fontsize=12, fontweight='bold')
 
403
  ax1.set_ylim(0, ESCALA_MAXIMA_NOTAS)
404
+ ax1.grid(True, axis='y', alpha=0.3, linestyle='--')
 
405
 
406
+ # Melhorar a apresentação dos rótulos
407
+ ax1.set_xticklabels(disciplinas, rotation=45, ha='right', va='top')
408
+ ax1.set_ylabel('Notas', fontsize=10, labelpad=10)
409
+
410
+ # Adicionar linha de média mínima
411
+ ax1.axhline(y=LIMITE_APROVACAO_NOTA, color='r', linestyle='--', alpha=0.3)
412
  ax1.text(0.02, LIMITE_APROVACAO_NOTA + 0.1, 'Média mínima (5,0)',
413
+ transform=ax1.get_yaxis_transform(), color='r', alpha=0.7)
414
 
415
  # Valores nas barras de notas
416
  for barra in barras_notas:
417
  altura = barra.get_height()
 
418
  ax1.text(barra.get_x() + barra.get_width()/2., altura,
419
+ f'{altura:.1f}',
420
+ ha='center', va='bottom', fontsize=8)
 
421
 
422
  # Gráfico de frequências
423
  barras_freq = ax2.bar(disciplinas, medias_freq, color=cores_freq)
424
+ ax2.set_title('Frequência Média por Disciplina', pad=20, fontsize=12, fontweight='bold')
425
  ax2.set_ylim(0, 110)
426
+ ax2.grid(True, axis='y', alpha=0.3, linestyle='--')
427
+
428
+ # Melhorar a apresentação dos rótulos
429
+ ax2.set_xticklabels(disciplinas, rotation=45, ha='right', va='top')
430
+ ax2.set_ylabel('Frequência (%)', fontsize=10, labelpad=10)
431
 
432
+ # Adicionar linha de frequência mínima
433
+ ax2.axhline(y=LIMITE_APROVACAO_FREQ, color='r', linestyle='--', alpha=0.3)
434
  ax2.text(0.02, LIMITE_APROVACAO_FREQ + 1, 'Frequência mínima (75%)',
435
+ transform=ax2.get_yaxis_transform(), color='r', alpha=0.7)
436
 
437
  # Valores nas barras de frequência
438
  for barra in barras_freq:
439
  altura = barra.get_height()
 
440
  ax2.text(barra.get_x() + barra.get_width()/2., altura,
441
+ f'{altura:.1f}%',
442
+ ha='center', va='bottom', fontsize=8)
 
443
 
444
+ # Título global com informações de média
445
  plt.suptitle(
446
  f'Desempenho Geral\nMédia Global: {media_global:.1f} | Frequência Global: {freq_global:.1f}%',
447
+ y=0.98, fontsize=14, fontweight='bold'
 
448
  )
449
 
450
+ # Aviso de risco de reprovação se necessário
451
  if freq_global < LIMITE_APROVACAO_FREQ:
452
+ plt.figtext(0.5, 0.02,
453
+ "Atenção: Risco de Reprovação por Baixa Frequência",
454
+ ha="center", fontsize=11, color="red", weight='bold')
455
 
456
  plt.tight_layout()
457
 
458
+ # Salvar o gráfico
459
  plot_path = os.path.join(temp_dir, 'medias_frequencias.png')
460
+ plt.savefig(plot_path, bbox_inches='tight', dpi=300)
461
  plt.close()
462
 
463
  return plot_path
464
 
465
+ def gerar_relatorio_pdf(df, disciplinas_dados, grafico_basica, grafico_diversificada, grafico_medias):
466
+ """Gera relatório PDF com os gráficos e análises."""
467
+ pdf = FPDF()
 
 
 
468
  pdf.set_auto_page_break(auto=True, margin=15)
469
 
470
  # Primeira página - Informações e Formação Básica
471
  pdf.add_page()
472
  pdf.set_font('Helvetica', 'B', 18)
473
+ pdf.cell(0, 10, 'Relatório de Desempenho Escolar', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
 
474
  pdf.ln(15)
475
 
476
  # Informações do aluno
477
  pdf.set_font('Helvetica', 'B', 12)
478
+ pdf.cell(0, 10, 'Informações do Aluno', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
 
479
  pdf.line(10, pdf.get_y(), 200, pdf.get_y())
480
  pdf.ln(5)
481
 
482
+ # Mostrar apenas o nome
483
  if hasattr(df, 'attrs') and 'nome' in df.attrs:
484
  pdf.set_font('Helvetica', 'B', 11)
485
+ pdf.cell(30, 7, 'Nome:', 0, 0)
486
  pdf.set_font('Helvetica', '', 11)
487
+ pdf.cell(0, 7, df.attrs['nome'], 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT)
 
488
 
489
  pdf.ln(10)
490
 
491
  # Data do relatório
492
  data_atual = datetime.now().strftime('%d/%m/%Y')
493
  pdf.set_font('Helvetica', 'I', 10)
494
+ pdf.cell(0, 5, f'Data de geração: {data_atual}', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='R')
 
495
  pdf.ln(15)
496
 
497
+ # Gráfico de evolução da formação básica
498
+ pdf.set_font('Helvetica', 'B', 14)
499
+ pdf.cell(0, 10, 'Evolução das Notas - Formação Geral Básica', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
500
+ pdf.line(10, pdf.get_y(), 200, pdf.get_y())
501
+ pdf.ln(10)
502
+ pdf.image(grafico_basica, x=10, w=190)
 
 
 
 
503
 
504
+ # Segunda página - Parte Diversificada
505
  pdf.add_page()
506
+ pdf.set_font('Helvetica', 'B', 14)
507
+ pdf.cell(0, 10, 'Evolução das Notas - Parte Diversificada', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
508
+ pdf.line(10, pdf.get_y(), 200, pdf.get_y())
509
+ pdf.ln(10)
510
+ pdf.image(grafico_diversificada, x=10, w=190)
 
 
 
 
511
 
512
+ # Terceira página - Médias e Frequências
513
  pdf.add_page()
514
+ pdf.set_font('Helvetica', 'B', 14)
515
+ pdf.cell(0, 10, 'Análise de Médias e Frequências', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
516
+ pdf.line(10, pdf.get_y(), 200, pdf.get_y())
517
+ pdf.ln(10)
518
+ pdf.image(grafico_medias, x=10, w=190)
519
+
520
+ # Quarta página - Análise Detalhada
521
+ pdf.add_page()
522
+ pdf.set_font('Helvetica', 'B', 14)
523
+ pdf.cell(0, 10, 'Análise Detalhada', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
524
+ pdf.line(10, pdf.get_y(), 200, pdf.get_y())
525
+ pdf.ln(10)
526
+
527
+ # Calcular médias globais
528
+ medias_notas = [d['media_notas'] for d in disciplinas_dados]
529
+ medias_freq = [d['media_freq'] for d in disciplinas_dados]
530
+ media_global = np.mean(medias_notas)
531
+ freq_global = np.mean(medias_freq)
532
+
533
+ # Resumo geral
534
+ pdf.set_font('Helvetica', 'B', 12)
535
+ pdf.cell(0, 7, 'Resumo Geral:', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
536
+ pdf.ln(5)
537
 
538
+ pdf.set_font('Helvetica', '', 11)
539
+ pdf.cell(0, 7, f'Média Global: {media_global:.1f}', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
540
+ pdf.cell(0, 7, f'Frequência Global: {freq_global:.1f}%', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
541
+ pdf.ln(10)
542
+
543
+ # Avisos Importantes
544
+ pdf.set_font('Helvetica', 'B', 12)
545
+ pdf.cell(0, 10, 'Pontos de Atenção:', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
546
+ pdf.ln(5)
547
+
548
+ pdf.set_font('Helvetica', '', 10)
549
+
550
+ # Disciplinas com baixo desempenho
551
+ disciplinas_risco = []
552
+ for disc_data in disciplinas_dados:
553
+ avisos = []
554
+ if disc_data['media_notas'] < LIMITE_APROVACAO_NOTA:
555
+ avisos.append(f"Média de notas abaixo de {LIMITE_APROVACAO_NOTA} ({disc_data['media_notas']:.1f})")
556
+ if disc_data['media_freq'] < LIMITE_APROVACAO_FREQ:
557
+ avisos.append(f"Frequência abaixo de {LIMITE_APROVACAO_FREQ}% ({disc_data['media_freq']:.1f}%)")
558
 
559
+ if avisos:
560
+ disciplinas_risco.append((disc_data['disciplina'], avisos))
561
+
562
+ if disciplinas_risco:
563
+ for disc, avisos in disciplinas_risco:
564
+ pdf.set_font('Helvetica', 'B', 10)
565
+ pdf.cell(0, 7, f'- {disc}:', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
566
+ pdf.set_font('Helvetica', '', 10)
567
+ for aviso in avisos:
568
+ pdf.cell(10) # Indentação
569
+ pdf.cell(0, 7, f'- {aviso}', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
570
+ else:
571
+ pdf.cell(0, 7, 'Nenhum problema identificado.', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
572
+
573
+ # Rodapé
574
+ pdf.set_y(-30)
575
+ pdf.line(10, pdf.get_y(), 200, pdf.get_y())
576
+ pdf.ln(5)
577
+ pdf.set_font('Helvetica', 'I', 8)
578
+ pdf.cell(0, 10, 'Este relatório é uma análise automática e deve ser validado junto à secretaria da escola.',
579
+ 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
580
+
581
+ # Salvar PDF
582
+ temp_pdf = tempfile.NamedTemporaryFile(delete=False, suffix='.pdf')
583
+ pdf_path = temp_pdf.name
584
+ pdf.output(pdf_path)
585
+ return pdf_path
586
 
587
+ def processar_boletim(file):
588
  """Função principal que processa o boletim e gera o relatório."""
589
+ temp_dir = None
590
  try:
591
  if file is None:
592
  return None, "Nenhum arquivo foi fornecido."
593
 
594
+ temp_dir = tempfile.mkdtemp()
595
+ print(f"Diretório temporário criado: {temp_dir}")
596
+
597
+ # Salvar o arquivo binário como um arquivo PDF temporário
598
+ temp_pdf = os.path.join(temp_dir, 'boletim.pdf')
599
+ with open(temp_pdf, 'wb') as f:
600
+ f.write(file) # Salva os bytes do arquivo no disco
601
+ print(f"PDF salvo temporariamente em: {temp_pdf}")
602
+
603
+ if os.path.getsize(temp_pdf) == 0:
604
+ return None, "O arquivo está vazio."
605
+
606
+ print("Iniciando extração das tabelas...")
607
+ df = extrair_tabelas_pdf(temp_pdf)
608
+ print("Tabelas extraídas com sucesso")
609
+
610
+ if df is None or df.empty:
611
+ return None, "Não foi possível extrair dados do PDF."
612
+
613
+ try:
614
+ # Processar disciplinas
615
  disciplinas_dados = obter_disciplinas_validas(df)
616
  if not disciplinas_dados:
617
  return None, "Nenhuma disciplina válida encontrada no boletim."
618
 
619
+ # Separar disciplinas por categoria
620
  categorias = separar_disciplinas_por_categoria(disciplinas_dados)
621
+ nivel = categorias['nivel']
622
+ nivel_texto = "Ensino Médio" if nivel == "medio" else "Ensino Fundamental"
623
 
624
+ # Gerar gráficos
625
+ print("Gerando gráficos...")
626
+ grafico_basica = plotar_evolucao_bimestres(
627
+ categorias['formacao_basica'],
628
+ temp_dir,
629
+ titulo=f"Evolução das Médias - Formação Geral Básica ({nivel_texto})",
630
+ nome_arquivo='evolucao_basica.png'
631
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
 
633
+ grafico_diversificada = plotar_evolucao_bimestres(
634
+ categorias['diversificada'],
635
+ temp_dir,
636
+ titulo=f"Evolução das Médias - Parte Diversificada ({nivel_texto})",
637
+ nome_arquivo='evolucao_diversificada.png'
 
 
638
  )
639
 
640
+ grafico_medias = plotar_graficos_destacados(disciplinas_dados, temp_dir)
641
+ print("Gráficos gerados")
642
+
643
+ # Gerar PDF
644
+ print("Gerando relatório PDF...")
645
+ pdf_path = gerar_relatorio_pdf(df, disciplinas_dados, grafico_basica, grafico_diversificada, grafico_medias)
646
+ print("Relatório PDF gerado")
647
+
648
+ # Criar arquivo de retorno
649
+ output_file = tempfile.NamedTemporaryFile(delete=False, suffix='.pdf')
650
+ output_path = output_file.name
651
+ shutil.copy2(pdf_path, output_path)
652
+
653
+ return output_path, "Relatório gerado com sucesso!"
654
 
655
+ except Exception as e:
656
+ return None, f"Erro ao processar os dados: {str(e)}"
657
+
658
  except Exception as e:
659
+ print(f"Erro durante o processamento: {str(e)}")
660
  return None, f"Erro ao processar o boletim: {str(e)}"
661
+
662
+ finally:
663
+ if temp_dir and os.path.exists(temp_dir):
664
+ try:
665
+ shutil.rmtree(temp_dir)
666
+ print("Arquivos temporários limpos")
667
+ except Exception as e:
668
+ print(f"Erro ao limpar arquivos temporários: {str(e)}")
669
 
670
  # Interface Gradio
671
  iface = gr.Interface(
 
681
  ],
682
  title="Análise de Boletim Escolar",
683
  description="Faça upload do boletim em PDF para gerar um relatório com análises e visualizações.",
684
+ allow_flagging="never"
 
685
  )
686
 
687
  if __name__ == "__main__":