cesar commited on
Commit
a978321
·
verified ·
1 Parent(s): 63918e4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +141 -74
app.py CHANGED
@@ -1,17 +1,18 @@
1
  import gradio as gr
2
  import PyPDF2
3
  import os
4
- import json
5
  import vertexai
6
  from vertexai.generative_models import GenerativeModel, Part, SafetySetting
7
 
8
- # Configuración global
 
 
9
  generation_config = {
10
  "max_output_tokens": 8192,
11
  "temperature": 0,
12
  "top_p": 0.8,
13
  }
14
-
15
  safety_settings = [
16
  SafetySetting(
17
  category=SafetySetting.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
@@ -32,9 +33,17 @@ safety_settings = [
32
  ]
33
 
34
  def configurar_credenciales(json_path: str):
 
35
  os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = json_path
36
 
 
 
 
37
  def extraer_texto(pdf_path: str) -> str:
 
 
 
 
38
  texto_total = ""
39
  with open(pdf_path, "rb") as f:
40
  lector = PyPDF2.PdfReader(f)
@@ -42,58 +51,77 @@ def extraer_texto(pdf_path: str) -> str:
42
  texto_total += page.extract_text() or ""
43
  return texto_total
44
 
45
- def parsear_con_llm(texto_pdf: str, model: GenerativeModel) -> dict:
 
 
 
46
  """
47
- Prompt más flexible:
48
- - Reconoce enumeraciones en secciones 'Preguntas' y 'RESPUESTAS', p. ej. '1.', '2)', '3-'.
49
- - Permite que las preguntas tengan texto como "Teniendo en cuenta que..." sin la palabra "Pregunta".
50
- - Devuelve un JSON que asocia la pregunta X con la respuesta X.
51
  """
52
- prompt = f"""
53
- Eres un parser de texto que recibe el contenido de un PDF con:
54
- - Una sección de \"Preguntas\" enumeradas (1., 2., 3..., etc.).
55
- - Una sección de \"RESPUESTAS\" enumeradas de la misma forma.
56
-
57
- Para cada número (1, 2, 3, 4, 5, 6...), empareja la pregunta con la respuesta.
58
- Devuélvelo en un JSON con el siguiente formato:
59
-
60
- {{
61
- "Pregunta 1": "texto de la respuesta 1",
62
- "Pregunta 2": "texto de la respuesta 2",
63
- ...
64
- }}
65
-
66
- Reglas:
67
- 1. Si una pregunta dice \"1. Teniendo en cuenta...\", eso es \"Pregunta 1\".
68
- 2. Si en la sección RESPUESTAS dice \"1. Metabolismo...\", esa es la Respuesta 1.
69
- 3. Si no hay correspondencia entre pregunta y respuesta, deja la respuesta como cadena vacía.
70
- 4. Si no hay nada, devuelve un JSON vacío: {{}}.
71
-
72
- Texto PDF:
73
- {texto_pdf}
74
-
75
- Devuelve solo el JSON, sin explicaciones adicionales.
76
- """
77
- part_text = Part.from_text(prompt)
78
- response = model.generate_content(
79
- [part_text],
80
- generation_config=generation_config,
81
- safety_settings=safety_settings,
82
- stream=False
83
- )
84
-
85
- # Intentamos parsear el contenido como JSON
86
- try:
87
- data = json.loads(response.text.strip())
88
- if isinstance(data, dict):
89
- return data
90
- else:
91
- return {}
92
- except:
93
- return {}
94
-
 
 
 
 
 
 
 
 
 
 
 
 
95
  def comparar_preguntas_respuestas(dict_docente: dict, dict_alumno: dict) -> str:
96
- """Compara dict_docente vs dict_alumno y retorna retroalimentación."""
 
 
 
 
97
  retroalimentacion = []
98
  for pregunta, resp_correcta in dict_docente.items():
99
  resp_alumno = dict_alumno.get(pregunta, None)
@@ -107,11 +135,23 @@ def comparar_preguntas_respuestas(dict_docente: dict, dict_alumno: dict) -> str:
107
  )
108
  return "\n".join(retroalimentacion)
109
 
 
 
 
110
  def revisar_examen(json_cred, pdf_docente, pdf_alumno):
111
- """Función generadora que muestra progreso en Gradio con yield."""
 
 
 
 
 
 
 
 
112
  yield "Cargando credenciales..."
113
  try:
114
  configurar_credenciales(json_cred.name)
 
115
  yield "Inicializando Vertex AI..."
116
  vertexai.init(project="deploygpt", location="us-central1")
117
 
@@ -121,26 +161,51 @@ def revisar_examen(json_cred, pdf_docente, pdf_alumno):
121
  yield "Extrayendo texto del PDF del alumno..."
122
  texto_alumno = extraer_texto(pdf_alumno.name)
123
 
124
- yield "Parseando preguntas/respuestas del docente..."
125
- model = GenerativeModel(
126
- "gemini-1.5-pro-001",
127
- system_instruction=["Eres un parser estricto."]
128
- )
129
- dict_docente = parsear_con_llm(texto_docente, model)
130
-
131
- yield "Parseando preguntas/respuestas del alumno..."
132
- dict_alumno = parsear_con_llm(texto_alumno, model)
133
-
134
- yield "Comparando..."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  feedback = comparar_preguntas_respuestas(dict_docente, dict_alumno)
136
 
137
  if len(feedback.strip()) < 5:
138
  yield "No se encontraron preguntas o respuestas válidas."
139
  return
140
 
141
- yield "Generando resumen final..."
 
 
 
 
 
142
  summary_prompt = f"""
143
- Eres un profesor experto de bioquímica. Te muestro la comparación de preguntas y respuestas:
144
  {feedback}
145
  Por favor, genera un breve resumen del desempeño del alumno
146
  sin inventar preguntas adicionales.
@@ -152,13 +217,16 @@ def revisar_examen(json_cred, pdf_docente, pdf_alumno):
152
  safety_settings=safety_settings,
153
  stream=False
154
  )
155
-
156
  final_result = f"{feedback}\n\n**Resumen**\n{summary_resp.text.strip()}"
 
157
  yield final_result
158
 
159
  except Exception as e:
160
  yield f"Error al procesar: {str(e)}"
161
 
 
 
 
162
  import gradio as gr
163
 
164
  interface = gr.Interface(
@@ -166,16 +234,15 @@ interface = gr.Interface(
166
  inputs=[
167
  gr.File(label="Credenciales JSON"),
168
  gr.File(label="PDF del Docente"),
169
- gr.File(label="PDF Alumno")
170
  ],
171
- outputs="text",
172
- title="Revisión de Exámenes (Preguntas enumeradas + RESPUESTAS enumeradas)",
173
  description=(
174
- "Sube tus credenciales, el PDF del docente y el PDF del alumno. El LLM "
175
- "buscará enumeraciones (1., 2., 3., etc.) en PREGUNTAS y RESPUESTAS y "
176
- "mostrará el avance paso a paso."
177
  )
178
  )
179
 
180
  interface.launch(debug=True)
181
-
 
1
  import gradio as gr
2
  import PyPDF2
3
  import os
4
+ import re
5
  import vertexai
6
  from vertexai.generative_models import GenerativeModel, Part, SafetySetting
7
 
8
+ # --------------------
9
+ # CONFIGURACIÓN GLOBAL
10
+ # --------------------
11
  generation_config = {
12
  "max_output_tokens": 8192,
13
  "temperature": 0,
14
  "top_p": 0.8,
15
  }
 
16
  safety_settings = [
17
  SafetySetting(
18
  category=SafetySetting.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
 
33
  ]
34
 
35
  def configurar_credenciales(json_path: str):
36
+ """Configura credenciales de Google Cloud a partir de un archivo JSON."""
37
  os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = json_path
38
 
39
+ # -----------
40
+ # LECTURA PDF
41
+ # -----------
42
  def extraer_texto(pdf_path: str) -> str:
43
+ """
44
+ Extrae el texto de todas las páginas de un PDF con PyPDF2.
45
+ Retorna un string con todo el texto concatenado.
46
+ """
47
  texto_total = ""
48
  with open(pdf_path, "rb") as f:
49
  lector = PyPDF2.PdfReader(f)
 
51
  texto_total += page.extract_text() or ""
52
  return texto_total
53
 
54
+ # -----------
55
+ # PARSEO TEXTO
56
+ # -----------
57
+ def split_secciones(texto: str) -> (str, str):
58
  """
59
+ Separa el texto en dos partes: la sección 'Preguntas' y la sección 'RESPUESTAS'.
60
+ - Busca la palabra 'Preguntas' (o 'PREGUNTAS') y 'RESPUESTAS' (o 'RESPUESTAS').
61
+ - Devuelve (texto_preguntas, texto_respuestas).
62
+ Si no las encuentra, devuelvo (texto, "") o similar.
63
  """
64
+ # Usamos re.IGNORECASE para ignorar mayúsculas/minúsculas
65
+ # Buscamos la posición de 'Preguntas' y 'RESPUESTAS' en el string
66
+ match_preg = re.search(r'(?i)preguntas', texto)
67
+ match_resp = re.search(r'(?i)respuestas', texto)
68
+
69
+ if not match_preg or not match_resp:
70
+ # Si no encontramos ambas, devolvemos algo por defecto
71
+ return (texto, "")
72
+
73
+ start_preg = match_preg.end() # donde termina la palabra 'Preguntas'
74
+ start_resp = match_resp.start()
75
+
76
+ # Sección de 'Preguntas' = texto entre 'Preguntas' y 'RESPUESTAS'
77
+ # Sección de 'RESPUESTAS' = texto desde 'RESPUESTAS' hasta el final
78
+ texto_preguntas = texto[start_preg:start_resp].strip()
79
+ texto_respuestas = texto[match_resp.end():].strip()
80
+ return (texto_preguntas, texto_respuestas)
81
+
82
+ def parsear_enumeraciones(texto: str) -> dict:
83
+ """
84
+ Dado un texto que contiene enumeraciones del tipo '1. ...', '2. ...', etc.,
85
+ separa cada número y su contenido.
86
+ Retorna un dict: {"Pregunta 1": "contenido", "Pregunta 2": "contenido", ...}.
87
+ """
88
+ # Dividimos en "bloques" usando lookahead para no perder el delimitador.
89
+ # Ej: 1. ... \n 2. ... \n
90
+ # Regex: busca línea que inicie con dígitos y un punto (ej: 1.)
91
+ bloques = re.split(r'(?=^\d+\.\s)', texto, flags=re.MULTILINE)
92
+
93
+ resultado = {}
94
+ for bloque in bloques:
95
+ bloque_limpio = bloque.strip()
96
+ if not bloque_limpio:
97
+ continue
98
+ # Tomamos la primera línea para ver "1. " o "2. "
99
+ linea_principal = bloque_limpio.split("\n", 1)[0]
100
+ # Extraer el número
101
+ match_num = re.match(r'^(\d+)\.\s*(.*)', linea_principal)
102
+ if match_num:
103
+ numero = match_num.group(1)
104
+ # El resto del contenido es el bloque completo sin la línea principal
105
+ # o bien group(2) + la parte posterior
106
+ resto = ""
107
+ if "\n" in bloque_limpio:
108
+ resto = bloque_limpio.split("\n", 1)[1].strip()
109
+ else:
110
+ # No hay más líneas, sólo la principal
111
+ resto = match_num.group(2)
112
+
113
+ resultado[f"Pregunta {numero}"] = resto.strip()
114
+ return resultado
115
+
116
+ # ------------
117
+ # COMPARACIÓN
118
+ # ------------
119
  def comparar_preguntas_respuestas(dict_docente: dict, dict_alumno: dict) -> str:
120
+ """
121
+ Compara dict_docente vs dict_alumno y retorna retroalimentación.
122
+ - Si la 'Pregunta X' no está en dict_alumno, => 'No fue asignada'.
123
+ - Si sí está => mostramos la respuesta del alumno y la supuesta 'correcta'.
124
+ """
125
  retroalimentacion = []
126
  for pregunta, resp_correcta in dict_docente.items():
127
  resp_alumno = dict_alumno.get(pregunta, None)
 
135
  )
136
  return "\n".join(retroalimentacion)
137
 
138
+ # -----------
139
+ # FUNCIÓN LÓGICA
140
+ # -----------
141
  def revisar_examen(json_cred, pdf_docente, pdf_alumno):
142
+ """
143
+ Función generadora que muestra progreso en Gradio con yield.
144
+ 1. Configuramos credenciales
145
+ 2. Extraemos texto de PDFs
146
+ 3. Separamos secciones 'Preguntas' y 'RESPUESTAS' en docente y alumno
147
+ 4. Parseamos enumeraciones
148
+ 5. Comparamos
149
+ 6. Llamamos a LLM para un resumen final
150
+ """
151
  yield "Cargando credenciales..."
152
  try:
153
  configurar_credenciales(json_cred.name)
154
+
155
  yield "Inicializando Vertex AI..."
156
  vertexai.init(project="deploygpt", location="us-central1")
157
 
 
161
  yield "Extrayendo texto del PDF del alumno..."
162
  texto_alumno = extraer_texto(pdf_alumno.name)
163
 
164
+ yield "Dividiendo secciones (docente)..."
165
+ preguntas_doc, respuestas_doc = split_secciones(texto_docente)
166
+
167
+ yield "Dividiendo secciones (alumno)..."
168
+ preguntas_alum, respuestas_alum = split_secciones(texto_alumno)
169
+
170
+ yield "Parseando enumeraciones (docente)..."
171
+ dict_preg_doc = parsear_enumeraciones(preguntas_doc)
172
+ dict_resp_doc = parsear_enumeraciones(respuestas_doc)
173
+
174
+ # Unimos dict_preg_doc y dict_resp_doc para crear un dict final
175
+ # Ej: "Pregunta 1" en dict_preg_doc con "Pregunta 1" en dict_resp_doc
176
+ # => dict_docente["Pregunta 1"] = "Respuesta 1..."
177
+ dict_docente = {}
178
+ for key_preg, texto_preg in dict_preg_doc.items():
179
+ # Revisar si en dict_resp_doc hay el mismo 'Pregunta X'
180
+ resp_doc = dict_resp_doc.get(key_preg, "")
181
+ # Unimos la respuesta en un sólo string
182
+ dict_docente[key_preg] = resp_doc
183
+
184
+ yield "Parseando enumeraciones (alumno)..."
185
+ dict_preg_alum = parsear_enumeraciones(preguntas_alum)
186
+ dict_resp_alum = parsear_enumeraciones(respuestas_alum)
187
+
188
+ # Unir en un dict final de alumno
189
+ dict_alumno = {}
190
+ for key_preg, texto_preg in dict_preg_alum.items():
191
+ resp_alum = dict_resp_alum.get(key_preg, "")
192
+ dict_alumno[key_preg] = resp_alum
193
+
194
+ yield "Comparando preguntas/respuestas..."
195
  feedback = comparar_preguntas_respuestas(dict_docente, dict_alumno)
196
 
197
  if len(feedback.strip()) < 5:
198
  yield "No se encontraron preguntas o respuestas válidas."
199
  return
200
 
201
+ yield "Generando resumen final con LLM..."
202
+ # Llamada final al LLM:
203
+ model = GenerativeModel(
204
+ "gemini-1.5-pro-001",
205
+ system_instruction=["Eres un profesor experto de bioquímica. No inventes preguntas."]
206
+ )
207
  summary_prompt = f"""
208
+ Comparación de preguntas y respuestas:
209
  {feedback}
210
  Por favor, genera un breve resumen del desempeño del alumno
211
  sin inventar preguntas adicionales.
 
217
  safety_settings=safety_settings,
218
  stream=False
219
  )
 
220
  final_result = f"{feedback}\n\n**Resumen**\n{summary_resp.text.strip()}"
221
+
222
  yield final_result
223
 
224
  except Exception as e:
225
  yield f"Error al procesar: {str(e)}"
226
 
227
+ # -----------------
228
+ # INTERFAZ DE GRADIO
229
+ # -----------------
230
  import gradio as gr
231
 
232
  interface = gr.Interface(
 
234
  inputs=[
235
  gr.File(label="Credenciales JSON"),
236
  gr.File(label="PDF del Docente"),
237
+ gr.File(label="PDF del Alumno")
238
  ],
239
+ outputs="text", # so we can see partial yields
240
+ title="Revisión de Exámenes (Preguntas/Respuestas enumeradas)",
241
  description=(
242
+ "Sube credenciales, el PDF del docente y del alumno. "
243
+ "Se busca la palabra 'Preguntas' y 'RESPUESTAS', parseamos enumeraciones (1., 2., etc.), "
244
+ "luego comparamos y finalmente pedimos un resumen al LLM."
245
  )
246
  )
247
 
248
  interface.launch(debug=True)