Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -86,11 +86,13 @@ def temp_file(suffix=None):
|
|
86 |
os.unlink(temp.name)
|
87 |
|
88 |
class PDFReport(FPDF):
|
|
|
89 |
def __init__(self):
|
90 |
super().__init__()
|
91 |
self.set_auto_page_break(auto=True, margin=15)
|
92 |
|
93 |
def header_footer(self):
|
|
|
94 |
self.set_y(-30)
|
95 |
self.line(10, self.get_y(), 200, self.get_y())
|
96 |
self.ln(5)
|
@@ -99,9 +101,52 @@ class PDFReport(FPDF):
|
|
99 |
'Este relatório é uma análise automática e deve ser validado junto à secretaria da escola.',
|
100 |
0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
|
101 |
|
102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
103 |
def extrair_tabelas_pdf(pdf_path: str) -> pd.DataFrame:
|
|
|
104 |
try:
|
|
|
105 |
tables_header = camelot.read_pdf(
|
106 |
pdf_path,
|
107 |
pages='1',
|
@@ -110,6 +155,8 @@ def extrair_tabelas_pdf(pdf_path: str) -> pd.DataFrame:
|
|
110 |
)
|
111 |
|
112 |
info_aluno = {}
|
|
|
|
|
113 |
for table in tables_header:
|
114 |
df = table.df
|
115 |
for i in range(len(df)):
|
@@ -127,12 +174,14 @@ def extrair_tabelas_pdf(pdf_path: str) -> pd.DataFrame:
|
|
127 |
except:
|
128 |
continue
|
129 |
|
|
|
130 |
tables_notas = camelot.read_pdf(
|
131 |
pdf_path,
|
132 |
pages='all',
|
133 |
flavor='lattice'
|
134 |
)
|
135 |
|
|
|
136 |
df_notas = None
|
137 |
max_rows = 0
|
138 |
|
@@ -153,6 +202,7 @@ def extrair_tabelas_pdf(pdf_path: str) -> pd.DataFrame:
|
|
153 |
if df_notas is None:
|
154 |
raise ValueError("Tabela de notas não encontrada")
|
155 |
|
|
|
156 |
df_notas.attrs['nome'] = info_aluno.get('nome', 'Nome não encontrado')
|
157 |
|
158 |
return df_notas
|
@@ -161,8 +211,14 @@ def extrair_tabelas_pdf(pdf_path: str) -> pd.DataFrame:
|
|
161 |
logger.error(f"Erro na extração das tabelas: {str(e)}")
|
162 |
raise
|
163 |
|
164 |
-
|
|
|
|
|
|
|
|
|
|
|
165 |
def obter_disciplinas_validas(df: pd.DataFrame) -> List[Dict]:
|
|
|
166 |
colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4']
|
167 |
colunas_freq = ['%Freq B1', '%Freq B2', '%Freq B3', '%Freq B4']
|
168 |
|
@@ -204,48 +260,47 @@ def obter_disciplinas_validas(df: pd.DataFrame) -> List[Dict]:
|
|
204 |
|
205 |
return disciplinas_dados
|
206 |
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
|
212 |
-
|
213 |
-
|
214 |
-
if valor_limpo in CONCEITOS_VALIDOS:
|
215 |
-
conceitos_map = {'ET': 10, 'ES': 8, 'EP': 6}
|
216 |
-
return conceitos_map.get(valor_limpo)
|
217 |
-
|
218 |
-
try:
|
219 |
-
return float(valor_limpo.replace(',', '.'))
|
220 |
-
except:
|
221 |
-
return None
|
222 |
|
223 |
-
|
224 |
-
|
|
|
|
|
|
|
225 |
|
226 |
-
return
|
|
|
|
|
|
|
227 |
|
228 |
-
# Funções de
|
229 |
-
def
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
if valor > 0:
|
242 |
-
freq_validas.append(valor)
|
243 |
-
except:
|
244 |
-
continue
|
245 |
|
246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
247 |
|
248 |
-
# Funções de plotagem
|
249 |
def plotar_evolucao_bimestres(disciplinas_dados: List[Dict], temp_dir: str,
|
250 |
titulo: Optional[str] = None,
|
251 |
nome_arquivo: Optional[str] = None) -> str:
|
|
|
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)
|
|
|
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
|
108 |
+
|
109 |
+
if isinstance(valor, str):
|
110 |
+
valor_limpo = valor.strip().upper()
|
111 |
+
if valor_limpo in CONCEITOS_VALIDOS:
|
112 |
+
conceitos_map = {'ET': 10, 'ES': 8, 'EP': 6}
|
113 |
+
return conceitos_map.get(valor_limpo)
|
114 |
+
|
115 |
+
try:
|
116 |
+
return float(valor_limpo.replace(',', '.'))
|
117 |
+
except:
|
118 |
+
return None
|
119 |
+
|
120 |
+
if isinstance(valor, (int, float)):
|
121 |
+
return float(valor)
|
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:
|
134 |
+
try:
|
135 |
+
if isinstance(freq, str):
|
136 |
+
freq = freq.strip().replace('%', '').replace(',', '.')
|
137 |
+
if freq and freq != '-':
|
138 |
+
valor = float(freq)
|
139 |
+
if valor > 0:
|
140 |
+
freq_validas.append(valor)
|
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(
|
151 |
pdf_path,
|
152 |
pages='1',
|
|
|
155 |
)
|
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)):
|
|
|
174 |
except:
|
175 |
continue
|
176 |
|
177 |
+
# Extrair tabela de notas usando lattice
|
178 |
tables_notas = camelot.read_pdf(
|
179 |
pdf_path,
|
180 |
pages='all',
|
181 |
flavor='lattice'
|
182 |
)
|
183 |
|
184 |
+
# Encontrar tabela de notas
|
185 |
df_notas = None
|
186 |
max_rows = 0
|
187 |
|
|
|
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
|
|
|
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']
|
224 |
|
|
|
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:
|