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