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)
|
|
|