Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -58,16 +58,16 @@ def extraer_texto(pdf_path: str) -> str:
|
|
58 |
def split_secciones(texto: str) -> (str, str):
|
59 |
"""
|
60 |
Separa el texto en dos partes: la sección 'Preguntas' y la sección 'RESPUESTAS'.
|
61 |
-
Busca
|
62 |
"""
|
63 |
-
match_preg = re.search(r'(?
|
64 |
-
match_resp = re.search(r'(?
|
65 |
|
66 |
if not match_preg or not match_resp:
|
67 |
return (texto, "")
|
68 |
|
69 |
-
start_preg = match_preg.end() #
|
70 |
-
start_resp = match_resp.start()
|
71 |
|
72 |
texto_preguntas = texto[start_preg:start_resp].strip()
|
73 |
texto_respuestas = texto[match_resp.end():].strip()
|
@@ -75,25 +75,31 @@ def split_secciones(texto: str) -> (str, str):
|
|
75 |
|
76 |
def parsear_enumeraciones(texto: str) -> dict:
|
77 |
"""
|
78 |
-
Dado un texto
|
79 |
separa cada número y su contenido.
|
80 |
Retorna un dict: {"Pregunta 1": "contenido", "Pregunta 2": "contenido", ...}.
|
|
|
81 |
"""
|
82 |
-
|
|
|
|
|
83 |
resultado = {}
|
84 |
for bloque in bloques:
|
85 |
-
|
86 |
-
if not
|
87 |
continue
|
88 |
-
|
89 |
-
|
90 |
-
if
|
91 |
-
numero =
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
|
|
|
|
|
|
97 |
return resultado
|
98 |
|
99 |
# ------------
|
@@ -114,13 +120,11 @@ def comparar_preguntas_respuestas(dict_docente: dict, dict_alumno: dict) -> (str
|
|
114 |
* Incorrecta: ratio < 0.5
|
115 |
Devuelve:
|
116 |
- Un string con la retroalimentación por pregunta.
|
117 |
-
- Una lista de diccionarios con el análisis por pregunta (para
|
118 |
-
Solo se incluyen las preguntas que fueron asignadas al alumno.
|
119 |
"""
|
120 |
feedback = []
|
121 |
analisis = []
|
122 |
for pregunta, resp_correcta in dict_docente.items():
|
123 |
-
# Se “limpian” los textos para eliminar saltos de línea y espacios de más.
|
124 |
correct_clean = " ".join(resp_correcta.split())
|
125 |
resp_alumno_raw = dict_alumno.get(pregunta, "").strip()
|
126 |
|
@@ -130,7 +134,6 @@ def comparar_preguntas_respuestas(dict_docente: dict, dict_alumno: dict) -> (str
|
|
130 |
f"Respuesta del alumno: No fue asignada.\n"
|
131 |
f"Respuesta correcta: {correct_clean}\n"
|
132 |
)
|
133 |
-
# Se agrega al análisis, pero marcando que no fue asignada.
|
134 |
analisis.append({"pregunta": pregunta, "asignada": False})
|
135 |
else:
|
136 |
alumno_clean = " ".join(resp_alumno_raw.split())
|
@@ -161,12 +164,10 @@ def revisar_examen(json_cred, pdf_docente, pdf_alumno):
|
|
161 |
Función generadora que:
|
162 |
1. Configura credenciales.
|
163 |
2. Extrae y parsea el contenido de los PDFs.
|
164 |
-
3.
|
165 |
-
4.
|
166 |
-
5.
|
167 |
-
|
168 |
-
- Puntos a reforzar (respuestas incompletas o incorrectas).
|
169 |
-
- Recomendación general (solo considerando las preguntas asignadas).
|
170 |
"""
|
171 |
yield "Cargando credenciales..."
|
172 |
try:
|
@@ -190,7 +191,6 @@ def revisar_examen(json_cred, pdf_docente, pdf_alumno):
|
|
190 |
yield "Parseando enumeraciones (docente)..."
|
191 |
dict_preg_doc = parsear_enumeraciones(preguntas_doc)
|
192 |
dict_resp_doc = parsear_enumeraciones(respuestas_doc)
|
193 |
-
|
194 |
# Unir las respuestas del docente (correctas)
|
195 |
dict_docente = {}
|
196 |
for key in dict_preg_doc:
|
@@ -199,7 +199,6 @@ def revisar_examen(json_cred, pdf_docente, pdf_alumno):
|
|
199 |
yield "Parseando enumeraciones (alumno)..."
|
200 |
dict_preg_alum = parsear_enumeraciones(preguntas_alum)
|
201 |
dict_resp_alum = parsear_enumeraciones(respuestas_alum)
|
202 |
-
|
203 |
# Unir las respuestas del alumno
|
204 |
dict_alumno = {}
|
205 |
for key in dict_preg_alum:
|
@@ -207,24 +206,19 @@ def revisar_examen(json_cred, pdf_docente, pdf_alumno):
|
|
207 |
|
208 |
yield "Comparando preguntas y respuestas..."
|
209 |
feedback_text, analisis = comparar_preguntas_respuestas(dict_docente, dict_alumno)
|
210 |
-
|
211 |
if len(feedback_text.strip()) < 5:
|
212 |
yield "No se encontraron preguntas o respuestas válidas."
|
213 |
return
|
214 |
|
215 |
-
# Generar resumen global utilizando el LLM
|
216 |
-
# Se filtran solo las preguntas asignadas (se omiten las que no fueron asignadas)
|
217 |
analisis_asignadas = [a for a in analisis if a.get("asignada")]
|
218 |
resumen_prompt = f"""
|
219 |
A continuación se presenta el análisis por pregunta de un examen sobre la regulación del colesterol, considerando solo las preguntas asignadas al alumno:
|
220 |
-
|
221 |
{analisis_asignadas}
|
222 |
-
|
223 |
Con base en este análisis, genera un resumen del desempeño del alumno en el examen que incluya:
|
224 |
- Puntos fuertes: conceptos que el alumno ha comprendido correctamente.
|
225 |
- Puntos a reforzar: preguntas en las que la respuesta fue incompleta o incorrecta, indicando qué conceptos clave faltaron o se confundieron.
|
226 |
- Una recomendación general sobre si el alumno demuestra comprender los fundamentos o si necesita repasar el tema.
|
227 |
-
|
228 |
No incluyas en el análisis las preguntas que no fueron asignadas.
|
229 |
"""
|
230 |
yield "Generando resumen final con LLM..."
|
@@ -240,10 +234,8 @@ No incluyas en el análisis las preguntas que no fueron asignadas.
|
|
240 |
stream=False
|
241 |
)
|
242 |
resumen_final = summary_resp.text.strip()
|
243 |
-
|
244 |
final_result = f"{feedback_text}\n\n**Resumen del desempeño:**\n{resumen_final}"
|
245 |
yield final_result
|
246 |
-
|
247 |
except Exception as e:
|
248 |
yield f"Error al procesar: {str(e)}"
|
249 |
|
@@ -261,11 +253,10 @@ interface = gr.Interface(
|
|
261 |
title="Revisión de Exámenes (Preguntas/Respuestas enumeradas)",
|
262 |
description=(
|
263 |
"Sube las credenciales, el PDF del docente (con las preguntas y respuestas correctas) y el PDF del alumno. "
|
264 |
-
"El sistema separa las secciones 'Preguntas' y 'RESPUESTAS', parsea las enumeraciones
|
265 |
-
"Se evalúa si el alumno comprende los conceptos fundamentales: si la respuesta está incompleta se indica qué falta, "
|
266 |
"si es incorrecta se comenta por qué, y se omiten las preguntas no asignadas. Finalmente, se genera un resumen con recomendaciones."
|
267 |
)
|
268 |
)
|
269 |
|
270 |
interface.launch(debug=True)
|
271 |
-
|
|
|
58 |
def split_secciones(texto: str) -> (str, str):
|
59 |
"""
|
60 |
Separa el texto en dos partes: la sección 'Preguntas' y la sección 'RESPUESTAS'.
|
61 |
+
Busca las palabras 'Preguntas' y 'RESPUESTAS' ignorando espacios al inicio y mayúsculas.
|
62 |
"""
|
63 |
+
match_preg = re.search(r'(?im)^\s*preguntas', texto)
|
64 |
+
match_resp = re.search(r'(?im)^\s*respuestas', texto)
|
65 |
|
66 |
if not match_preg or not match_resp:
|
67 |
return (texto, "")
|
68 |
|
69 |
+
start_preg = match_preg.end() # donde termina "Preguntas"
|
70 |
+
start_resp = match_resp.start() # donde empieza "RESPUESTAS"
|
71 |
|
72 |
texto_preguntas = texto[start_preg:start_resp].strip()
|
73 |
texto_respuestas = texto[match_resp.end():].strip()
|
|
|
75 |
|
76 |
def parsear_enumeraciones(texto: str) -> dict:
|
77 |
"""
|
78 |
+
Dado un texto que contiene enumeraciones de preguntas (por ejemplo, "1. 1- RTA1" o "2- RTA2"),
|
79 |
separa cada número y su contenido.
|
80 |
Retorna un dict: {"Pregunta 1": "contenido", "Pregunta 2": "contenido", ...}.
|
81 |
+
Este patrón es flexible y tolera espacios al inicio y formatos creativos.
|
82 |
"""
|
83 |
+
# El patrón usa lookahead para dividir cada bloque cuando se encuentre una línea que comience con un número,
|
84 |
+
# un punto o guión y opcionalmente otro número seguido de un punto o guión.
|
85 |
+
bloques = re.split(r'(?=^\s*\d+[\.\-]\s*(?:\d+[\.\-])?\s*)', texto, flags=re.MULTILINE)
|
86 |
resultado = {}
|
87 |
for bloque in bloques:
|
88 |
+
bloque = bloque.strip()
|
89 |
+
if not bloque:
|
90 |
continue
|
91 |
+
# El patrón extrae el primer número (que identificará la pregunta) y el contenido.
|
92 |
+
match = re.match(r'^\s*(\d+)[\.\-]\s*(?:\d+[\.\-])?\s*(.*)', bloque)
|
93 |
+
if match:
|
94 |
+
numero = match.group(1)
|
95 |
+
contenido = match.group(2)
|
96 |
+
# Si el bloque tiene múltiples líneas, se unen las líneas siguientes
|
97 |
+
lineas = bloque.split("\n")
|
98 |
+
if len(lineas) > 1:
|
99 |
+
contenido_completo = " ".join([linea.strip() for linea in lineas[1:]])
|
100 |
+
if contenido_completo:
|
101 |
+
contenido += " " + contenido_completo
|
102 |
+
resultado[f"Pregunta {numero}"] = contenido.strip()
|
103 |
return resultado
|
104 |
|
105 |
# ------------
|
|
|
120 |
* Incorrecta: ratio < 0.5
|
121 |
Devuelve:
|
122 |
- Un string con la retroalimentación por pregunta.
|
123 |
+
- Una lista de diccionarios con el análisis por pregunta (solo para las asignadas).
|
|
|
124 |
"""
|
125 |
feedback = []
|
126 |
analisis = []
|
127 |
for pregunta, resp_correcta in dict_docente.items():
|
|
|
128 |
correct_clean = " ".join(resp_correcta.split())
|
129 |
resp_alumno_raw = dict_alumno.get(pregunta, "").strip()
|
130 |
|
|
|
134 |
f"Respuesta del alumno: No fue asignada.\n"
|
135 |
f"Respuesta correcta: {correct_clean}\n"
|
136 |
)
|
|
|
137 |
analisis.append({"pregunta": pregunta, "asignada": False})
|
138 |
else:
|
139 |
alumno_clean = " ".join(resp_alumno_raw.split())
|
|
|
164 |
Función generadora que:
|
165 |
1. Configura credenciales.
|
166 |
2. Extrae y parsea el contenido de los PDFs.
|
167 |
+
3. Separa las secciones 'Preguntas' y 'RESPUESTAS'.
|
168 |
+
4. Parsea las enumeraciones de cada sección (permitiendo formatos creativos).
|
169 |
+
5. Compara las respuestas del alumno con las correctas.
|
170 |
+
6. Llama a un LLM para generar un resumen final con retroalimentación.
|
|
|
|
|
171 |
"""
|
172 |
yield "Cargando credenciales..."
|
173 |
try:
|
|
|
191 |
yield "Parseando enumeraciones (docente)..."
|
192 |
dict_preg_doc = parsear_enumeraciones(preguntas_doc)
|
193 |
dict_resp_doc = parsear_enumeraciones(respuestas_doc)
|
|
|
194 |
# Unir las respuestas del docente (correctas)
|
195 |
dict_docente = {}
|
196 |
for key in dict_preg_doc:
|
|
|
199 |
yield "Parseando enumeraciones (alumno)..."
|
200 |
dict_preg_alum = parsear_enumeraciones(preguntas_alum)
|
201 |
dict_resp_alum = parsear_enumeraciones(respuestas_alum)
|
|
|
202 |
# Unir las respuestas del alumno
|
203 |
dict_alumno = {}
|
204 |
for key in dict_preg_alum:
|
|
|
206 |
|
207 |
yield "Comparando preguntas y respuestas..."
|
208 |
feedback_text, analisis = comparar_preguntas_respuestas(dict_docente, dict_alumno)
|
|
|
209 |
if len(feedback_text.strip()) < 5:
|
210 |
yield "No se encontraron preguntas o respuestas válidas."
|
211 |
return
|
212 |
|
213 |
+
# Generar resumen global utilizando el LLM (solo para preguntas asignadas)
|
|
|
214 |
analisis_asignadas = [a for a in analisis if a.get("asignada")]
|
215 |
resumen_prompt = f"""
|
216 |
A continuación se presenta el análisis por pregunta de un examen sobre la regulación del colesterol, considerando solo las preguntas asignadas al alumno:
|
|
|
217 |
{analisis_asignadas}
|
|
|
218 |
Con base en este análisis, genera un resumen del desempeño del alumno en el examen que incluya:
|
219 |
- Puntos fuertes: conceptos que el alumno ha comprendido correctamente.
|
220 |
- Puntos a reforzar: preguntas en las que la respuesta fue incompleta o incorrecta, indicando qué conceptos clave faltaron o se confundieron.
|
221 |
- Una recomendación general sobre si el alumno demuestra comprender los fundamentos o si necesita repasar el tema.
|
|
|
222 |
No incluyas en el análisis las preguntas que no fueron asignadas.
|
223 |
"""
|
224 |
yield "Generando resumen final con LLM..."
|
|
|
234 |
stream=False
|
235 |
)
|
236 |
resumen_final = summary_resp.text.strip()
|
|
|
237 |
final_result = f"{feedback_text}\n\n**Resumen del desempeño:**\n{resumen_final}"
|
238 |
yield final_result
|
|
|
239 |
except Exception as e:
|
240 |
yield f"Error al procesar: {str(e)}"
|
241 |
|
|
|
253 |
title="Revisión de Exámenes (Preguntas/Respuestas enumeradas)",
|
254 |
description=(
|
255 |
"Sube las credenciales, el PDF del docente (con las preguntas y respuestas correctas) y el PDF del alumno. "
|
256 |
+
"El sistema separa las secciones 'Preguntas' y 'RESPUESTAS', parsea las enumeraciones (soportando formatos creativos) "
|
257 |
+
"y luego compara las respuestas. Se evalúa si el alumno comprende los conceptos fundamentales: si la respuesta está incompleta se indica qué falta, "
|
258 |
"si es incorrecta se comenta por qué, y se omiten las preguntas no asignadas. Finalmente, se genera un resumen con recomendaciones."
|
259 |
)
|
260 |
)
|
261 |
|
262 |
interface.launch(debug=True)
|
|