histlearn commited on
Commit
6b9ba8d
·
verified ·
1 Parent(s): fe67c45

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +12 -772
app.py CHANGED
@@ -1,567 +1,3 @@
1
- import gradio as gr
2
- import camelot
3
- import pandas as pd
4
- import matplotlib.pyplot as plt
5
- import numpy as np
6
- from fpdf import FPDF
7
- from fpdf.enums import XPos, YPos
8
- import tempfile
9
- import os
10
- import matplotlib
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
19
- LIMITE_APROVACAO_FREQ = 75
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': {
26
- 'LINGUA PORTUGUESA',
27
- 'MATEMATICA',
28
- 'HISTORIA',
29
- 'GEOGRAFIA',
30
- 'CIENCIAS',
31
- 'LINGUA ESTRANGEIRA INGLES',
32
- 'ARTE',
33
- 'EDUCACAO FISICA'
34
- },
35
- 'medio': {
36
- 'LINGUA PORTUGUESA',
37
- 'MATEMATICA',
38
- 'HISTORIA',
39
- 'GEOGRAFIA',
40
- 'BIOLOGIA',
41
- 'FISICA',
42
- 'QUIMICA',
43
- 'INGLÊS',
44
- 'FILOSOFIA',
45
- 'SOCIOLOGIA',
46
- 'ARTE',
47
- 'EDUCACAO FISICA'
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
81
-
82
- if isinstance(valor, str):
83
- valor_limpo = valor.strip().upper()
84
- if valor_limpo in CONCEITOS_VALIDOS:
85
- conceitos_map = {'ET': 10, 'ES': 8, 'EP': 6}
86
- return conceitos_map.get(valor_limpo)
87
-
88
- try:
89
- return float(valor_limpo.replace(',', '.'))
90
- except:
91
- return None
92
-
93
- if isinstance(valor, (int, float)):
94
- return float(valor)
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:
109
- try:
110
- if isinstance(freq, str):
111
- freq = freq.strip().replace('%', '').replace(',', '.')
112
- if freq and freq != '-':
113
- valor = float(freq)
114
- if valor > 0:
115
- freq_validas.append(valor)
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(
128
- pdf_path,
129
- pages='1',
130
- flavor='stream',
131
- edge_tol=500
132
- )
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)):
140
- for j in range(len(df.columns)):
141
- texto = str(df.iloc[i,j]).strip()
142
- if 'Nome do Aluno' in texto:
143
- try:
144
- if j + 1 < len(df.columns):
145
- nome = str(df.iloc[i,j+1]).strip()
146
- elif i + 1 < len(df):
147
- nome = str(df.iloc[i+1,j]).strip()
148
- if nome and nome != 'Nome do Aluno:':
149
- info_aluno['nome'] = nome
150
- break
151
- except:
152
- continue
153
-
154
- # Extrair tabela de notas usando lattice
155
- tables_notas = camelot.read_pdf(
156
- pdf_path,
157
- pages='all',
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
-
165
- for table in tables_notas:
166
- df_temp = table.df
167
- if len(df_temp) > max_rows and 'Disciplina' in str(df_temp.iloc[0,0]):
168
- max_rows = len(df_temp)
169
- df_notas = df_temp.copy()
170
- df_notas = df_notas.rename(columns={
171
- 0: 'Disciplina',
172
- 1: 'Nota B1', 2: 'Freq B1', 3: '%Freq B1', 4: 'AC B1',
173
- 5: 'Nota B2', 6: 'Freq B2', 7: '%Freq B2', 8: 'AC B2',
174
- 9: 'Nota B3', 10: 'Freq B3', 11: '%Freq B3', 12: 'AC B3',
175
- 13: 'Nota B4', 14: 'Freq B4', 15: '%Freq B4', 16: 'AC B4',
176
- 17: 'CF', 18: 'Nota Final', 19: 'Freq Final', 20: 'AC Final'
177
- })
178
-
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']
195
-
196
- disciplinas_dados = []
197
-
198
- for _, row in df.iterrows():
199
- disciplina = row['Disciplina']
200
- if pd.isna(disciplina) or disciplina == '':
201
- continue
202
-
203
- notas = []
204
- freqs = []
205
- bimestres_cursados = []
206
-
207
- for i, (col_nota, col_freq) in enumerate(zip(colunas_notas, colunas_freq), 1):
208
- nota = converter_nota(row[col_nota])
209
- freq = row[col_freq] if col_freq in row else None
210
-
211
- if nota is not None or (freq and freq != '-'):
212
- bimestres_cursados.append(i)
213
- notas.append(nota if nota is not None else 0)
214
- freqs.append(freq)
215
- else:
216
- notas.append(None)
217
- freqs.append(None)
218
-
219
- if bimestres_cursados:
220
- media_notas = calcular_media_bimestres(notas)
221
- media_freq = calcular_frequencia_media(freqs)
222
-
223
- disciplinas_dados.append({
224
- 'disciplina': disciplina,
225
- 'notas': notas,
226
- 'frequencias': freqs,
227
- 'media_notas': media_notas,
228
- 'media_freq': media_freq,
229
- 'bimestres_cursados': bimestres_cursados
230
- })
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']
293
- desloc = deslocamentos[idx]
294
-
295
- if bimestres_cursados:
296
- notas_validas = [nota for i, nota in enumerate(notas, 1) if i in bimestres_cursados and nota is not None]
297
- bimestres = [bim for bim in bimestres_cursados if notas[bim-1] is not None]
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:
@@ -588,53 +24,28 @@ def processar_boletim(file):
588
  """Função principal que processa o boletim e gera o relatório."""
589
  temp_dir = None
590
  try:
591
- print("Iniciando processamento do arquivo...")
592
-
593
  if file is None:
594
  return None, "Nenhum arquivo foi fornecido."
595
 
596
- # Criar diretório temporário
597
  temp_dir = tempfile.mkdtemp()
598
  print(f"Diretório temporário criado: {temp_dir}")
599
 
600
- # Salvar o conteúdo bytes em um arquivo temporário
 
 
 
 
 
601
  temp_pdf = os.path.join(temp_dir, 'boletim.pdf')
602
- try:
603
- with open(temp_pdf, 'wb') as f:
604
- # Se o arquivo já é bytes, escrever diretamente
605
- if isinstance(file, bytes):
606
- f.write(file)
607
- # Se é um arquivo do Gradio, ler o conteúdo primeiro
608
- else:
609
- f.write(file.read())
610
- print(f"Arquivo temporário criado: {temp_pdf}")
611
- except Exception as e:
612
- print(f"Erro ao salvar arquivo temporário: {str(e)}")
613
- return None, f"Erro ao processar arquivo: {str(e)}"
614
 
615
- # Verificar se o arquivo foi criado corretamente
616
  if not os.path.exists(temp_pdf) or os.path.getsize(temp_pdf) == 0:
617
- return None, "Erro ao salvar o arquivo."
618
-
619
- # Validar o PDF
620
- try:
621
- import PyPDF2
622
- with open(temp_pdf, 'rb') as pdf_file:
623
- pdf_reader = PyPDF2.PdfReader(pdf_file)
624
- if len(pdf_reader.pages) == 0:
625
- return None, "O PDF está vazio ou corrompido."
626
- print(f"PDF válido com {len(pdf_reader.pages)} páginas")
627
- except Exception as e:
628
- print(f"Erro ao validar PDF: {str(e)}")
629
- return None, "O arquivo não é um PDF válido."
630
 
631
  print("Iniciando extração das tabelas...")
632
- try:
633
- df = extrair_tabelas_pdf(temp_pdf)
634
- print("Tabelas extraídas com sucesso")
635
- except Exception as e:
636
- print(f"Erro na extração das tabelas: {str(e)}")
637
- return None, f"Erro ao extrair dados do PDF: {str(e)}"
638
 
639
  if df is None or df.empty:
640
  return None, "Não foi possível extrair dados do PDF."
@@ -696,181 +107,10 @@ def processar_boletim(file):
696
  except Exception as e:
697
  print(f"Erro ao limpar arquivos temporários: {str(e)}")
698
 
699
- # Interface Gradio com suporte mobile
700
- iface = gr.Interface(
701
- fn=processar_boletim,
702
- inputs=gr.File(
703
- label="Upload do Boletim (PDF)",
704
- type="file",
705
- file_types=[".pdf"], # Especifica que só aceita PDFs
706
- file_count="single" # Aceita apenas um arquivo
707
- ),
708
- outputs=[
709
- gr.File(label="Relatório (PDF)"),
710
- gr.Textbox(label="Status")
711
- ],
712
- title="Análise de Boletim Escolar",
713
- description="Faça upload do boletim em PDF para gerar um relatório com análises e visualizações.",
714
- allow_flagging="never",
715
- def extrair_dados_direto(conteudo):
716
- """Extrai dados diretamente do texto."""
717
- linhas = conteudo.split('\n')
718
- info_aluno = {}
719
- dados_disciplinas = []
720
-
721
- # Extrair informações do aluno
722
- for i, linha in enumerate(linhas):
723
- if 'Nome do Aluno:' in linha and i + 1 < len(linhas):
724
- info_aluno['nome'] = linhas[i + 1].strip()
725
-
726
- # Encontrar o início das disciplinas
727
- inicio_disciplinas = None
728
- for i, linha in enumerate(linhas):
729
- if 'Disciplina' in linha and 'Bimestre' in linha:
730
- inicio_disciplinas = i + 1
731
- break
732
-
733
- if inicio_disciplinas:
734
- # Processar disciplinas até encontrar "Resultado Final" ou "Siglas"
735
- for linha in linhas[inicio_disciplinas:]:
736
- if 'Resultado Final' in linha or 'Siglas' in linha:
737
- break
738
-
739
- partes = linha.split()
740
- if not partes or 'Disciplina' in linha:
741
- continue
742
-
743
- try:
744
- disciplina = []
745
- notas = []
746
- freqs = []
747
- current_value = ''
748
-
749
- # Primeiro item é sempre o nome da disciplina
750
- for parte in partes:
751
- if parte.replace('.', '').replace('-', '').isdigit() or parte in ['ET', 'ES', 'EP']:
752
- if current_value:
753
- disciplina.append(current_value.strip())
754
- current_value = ''
755
- notas.append(parte)
756
- elif '%' in parte:
757
- freqs.append(parte.replace('%', ''))
758
- else:
759
- if current_value:
760
- current_value += ' '
761
- current_value += parte
762
-
763
- if current_value:
764
- disciplina.append(current_value.strip())
765
-
766
- if disciplina:
767
- nome_disciplina = ' '.join(disciplina)
768
- dados_disciplinas.append({
769
- 'disciplina': nome_disciplina,
770
- 'notas': notas[:4], # Primeiros 4 valores são notas dos bimestres
771
- 'frequencias': freqs[:4], # 4 valores de frequência
772
- 'media_notas': sum([float(n) if n.replace('.', '').isdigit() else 0 for n in notas[:4]]) / len([n for n in notas[:4] if n.replace('.', '').isdigit() or n in ['ET', 'ES', 'EP']]) if any(n.replace('.', '').isdigit() or n in ['ET', 'ES', 'EP'] for n in notas[:4]) else 0,
773
- 'media_freq': sum([float(f) for f in freqs if f and f != '-']) / len([f for f in freqs if f and f != '-']) if freqs else 0,
774
- 'bimestres_cursados': [i+1 for i, (n, f) in enumerate(zip(notas[:4], freqs[:4])) if (n.replace('.', '').isdigit() or n in ['ET', 'ES', 'EP']) or (f and f != '-')]
775
- })
776
-
777
- except Exception as e:
778
- print(f"Erro ao processar linha: {linha}")
779
- print(f"Erro: {str(e)}")
780
- continue
781
-
782
- return info_aluno, dados_disciplinas
783
-
784
- def processar_boletim(file):
785
- """Função principal que processa o boletim e gera o relatório."""
786
- temp_dir = None
787
- try:
788
- print("Iniciando processamento do arquivo...")
789
-
790
- if file is None:
791
- return None, "Nenhum arquivo foi fornecido."
792
-
793
- # Criar diretório temporário
794
- temp_dir = tempfile.mkdtemp()
795
- print(f"Diretório temporário criado: {temp_dir}")
796
-
797
- # Tentar ler o conteúdo do arquivo
798
- try:
799
- if isinstance(file, bytes):
800
- conteudo = file.decode('utf-8')
801
- else:
802
- conteudo = file.read().decode('utf-8')
803
-
804
- # Extrair dados diretamente do texto
805
- info_aluno, disciplinas_dados = extrair_dados_direto(conteudo)
806
-
807
- if not disciplinas_dados:
808
- return None, "Não foi possível extrair dados do arquivo."
809
-
810
- # Separar disciplinas por categoria
811
- categorias = separar_disciplinas_por_categoria(disciplinas_dados)
812
- nivel = categorias['nivel']
813
- nivel_texto = "Ensino Médio" if nivel == "medio" else "Ensino Fundamental"
814
-
815
- # Gerar gráficos
816
- print("Gerando gráficos...")
817
- grafico_basica = plotar_evolucao_bimestres(
818
- categorias['formacao_basica'],
819
- temp_dir,
820
- titulo=f"Evolução das Médias - Formação Geral Básica ({nivel_texto})",
821
- nome_arquivo='evolucao_basica.png'
822
- )
823
-
824
- grafico_diversificada = plotar_evolucao_bimestres(
825
- categorias['diversificada'],
826
- temp_dir,
827
- titulo=f"Evolução das Médias - Parte Diversificada ({nivel_texto})",
828
- nome_arquivo='evolucao_diversificada.png'
829
- )
830
-
831
- grafico_medias = plotar_graficos_destacados(disciplinas_dados, temp_dir)
832
- print("Gráficos gerados")
833
-
834
- # Criar DataFrame simulado para manter compatibilidade
835
- df = pd.DataFrame(disciplinas_dados)
836
- df.attrs.update(info_aluno)
837
-
838
- # Gerar PDF
839
- print("Gerando relatório PDF...")
840
- pdf_path = gerar_relatorio_pdf(df, disciplinas_dados, grafico_basica, grafico_diversificada, grafico_medias)
841
- print("Relatório PDF gerado")
842
-
843
- # Criar arquivo de retorno
844
- output_file = tempfile.NamedTemporaryFile(delete=False, suffix='.pdf')
845
- output_path = output_file.name
846
- shutil.copy2(pdf_path, output_path)
847
-
848
- return output_path, "Relatório gerado com sucesso!"
849
-
850
- except Exception as e:
851
- print(f"Erro no processamento dos dados: {str(e)}")
852
- return None, f"Erro ao processar os dados: {str(e)}"
853
-
854
- except Exception as e:
855
- print(f"Erro durante o processamento: {str(e)}")
856
- return None, f"Erro ao processar o arquivo: {str(e)}"
857
-
858
- finally:
859
- if temp_dir and os.path.exists(temp_dir):
860
- try:
861
- shutil.rmtree(temp_dir)
862
- print("Arquivos temporários limpos")
863
- except Exception as e:
864
- print(f"Erro ao limpar arquivos temporários: {str(e)}")
865
-
866
  # Interface Gradio
867
  iface = gr.Interface(
868
  fn=processar_boletim,
869
- inputs=gr.File(
870
- label="Upload do Boletim (PDF)",
871
- type="binary", # Garante que receberemos os bytes
872
- file_types=[".pdf"]
873
- ),
874
  outputs=[
875
  gr.File(label="Relatório (PDF)"),
876
  gr.Textbox(label="Status")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  pdf.cell(0, 7, f'- {disc}:', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
2
  pdf.set_font('Helvetica', '', 10)
3
  for aviso in avisos:
 
24
  """Função principal que processa o boletim e gera o relatório."""
25
  temp_dir = None
26
  try:
 
 
27
  if file is None:
28
  return None, "Nenhum arquivo foi fornecido."
29
 
 
30
  temp_dir = tempfile.mkdtemp()
31
  print(f"Diretório temporário criado: {temp_dir}")
32
 
33
+ if not hasattr(file, 'name') or not os.path.exists(file.name):
34
+ return None, "Arquivo inválido ou corrompido."
35
+
36
+ if os.path.getsize(file.name) == 0:
37
+ return None, "O arquivo está vazio."
38
+
39
  temp_pdf = os.path.join(temp_dir, 'boletim.pdf')
40
+ shutil.copy2(file.name, temp_pdf)
41
+ print(f"PDF copiado para: {temp_pdf}")
 
 
 
 
 
 
 
 
 
 
42
 
 
43
  if not os.path.exists(temp_pdf) or os.path.getsize(temp_pdf) == 0:
44
+ return None, "Erro ao copiar o arquivo."
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
  print("Iniciando extração das tabelas...")
47
+ df = extrair_tabelas_pdf(temp_pdf)
48
+ print("Tabelas extraídas com sucesso")
 
 
 
 
49
 
50
  if df is None or df.empty:
51
  return None, "Não foi possível extrair dados do PDF."
 
107
  except Exception as e:
108
  print(f"Erro ao limpar arquivos temporários: {str(e)}")
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  # Interface Gradio
111
  iface = gr.Interface(
112
  fn=processar_boletim,
113
+ inputs=gr.File(label="Upload do Boletim (PDF)"),
 
 
 
 
114
  outputs=[
115
  gr.File(label="Relatório (PDF)"),
116
  gr.Textbox(label="Status")