cesar commited on
Commit
3778096
·
verified ·
1 Parent(s): 0dfae1c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +32 -41
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 la palabra 'Preguntas' y 'RESPUESTAS' (ignorando mayúsculas/minúsculas).
62
  """
63
- match_preg = re.search(r'(?i)preguntas', texto)
64
- match_resp = re.search(r'(?i)respuestas', texto)
65
 
66
  if not match_preg or not match_resp:
67
  return (texto, "")
68
 
69
- start_preg = match_preg.end() # fin de la palabra 'Preguntas'
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 con enumeraciones del tipo '1. ...', '2. ...', etc.,
79
  separa cada número y su contenido.
80
  Retorna un dict: {"Pregunta 1": "contenido", "Pregunta 2": "contenido", ...}.
 
81
  """
82
- bloques = re.split(r'(?=^\d+\.\s)', texto, flags=re.MULTILINE)
 
 
83
  resultado = {}
84
  for bloque in bloques:
85
- bloque_limpio = bloque.strip()
86
- if not bloque_limpio:
87
  continue
88
- linea_principal = bloque_limpio.split("\n", 1)[0]
89
- match_num = re.match(r'^(\d+)\.\s*(.*)', linea_principal)
90
- if match_num:
91
- numero = match_num.group(1)
92
- if "\n" in bloque_limpio:
93
- resto = bloque_limpio.split("\n", 1)[1].strip()
94
- else:
95
- resto = match_num.group(2)
96
- resultado[f"Pregunta {numero}"] = resto.strip()
 
 
 
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 la conclusión).
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. Compara las respuestas del alumno con las correctas.
165
- 4. Genera una retroalimentación detallada por pregunta.
166
- 5. Llama a un LLM para obtener un resumen final que incluya:
167
- - Puntos fuertes (conceptos bien entendidos).
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 y luego compara las respuestas. "
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)