Spaces:
Sleeping
Sleeping
File size: 9,107 Bytes
00832a8 b54a6bc a978321 7cba34a b7193be 19c3ca0 a978321 7cba34a f025ed5 d329e85 19c3ca0 7cba34a b54a6bc a978321 e341366 4d995a1 a978321 b54a6bc a978321 b54a6bc a978321 19c3ca0 a978321 b7193be 85b6c95 a978321 b7193be a978321 b7193be a978321 b7193be 1b22788 a978321 b7193be a978321 19c3ca0 b7193be 19c3ca0 b7193be 19c3ca0 b7193be 19c3ca0 b54a6bc a978321 b54a6bc a978321 b7193be a978321 f025ed5 7cba34a b54a6bc a978321 f025ed5 e341366 d35d511 f025ed5 19c3ca0 f025ed5 19c3ca0 a978321 b7193be a978321 b7193be a978321 b7193be a978321 b7193be a978321 b7193be 19c3ca0 f025ed5 63918e4 85b6c95 a978321 19c3ca0 b7193be 1b22788 19c3ca0 1e129b0 19c3ca0 1e129b0 63918e4 a978321 f025ed5 7cba34a f025ed5 19c3ca0 a978321 7cba34a b54a6bc b527142 a978321 7cba34a b7193be a978321 b527142 b7193be 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 |
import gradio as gr
import PyPDF2
import os
import re
import vertexai
from vertexai.generative_models import GenerativeModel, Part, SafetySetting
from difflib import SequenceMatcher # Para comparar similitud
# --------------------
# 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' y 'RESPUESTAS' (ignorando mayúsculas/minúsculas).
"""
match_preg = re.search(r'(?i)preguntas', texto)
match_resp = re.search(r'(?i)respuestas', texto)
if not match_preg or not match_resp:
return (texto, "")
start_preg = match_preg.end() # fin de la palabra 'Preguntas'
start_resp = match_resp.start()
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 con enumeraciones del tipo '1. ...', '2. ...', etc.,
separa cada número y su contenido.
Retorna un dict: {"Pregunta 1": "contenido", "Pregunta 2": "contenido", ...}.
"""
bloques = re.split(r'(?=^\d+\.\s)', texto, flags=re.MULTILINE)
resultado = {}
for bloque in bloques:
bloque_limpio = bloque.strip()
if not bloque_limpio:
continue
linea_principal = bloque_limpio.split("\n", 1)[0]
match_num = re.match(r'^(\d+)\.\s*(.*)', linea_principal)
if match_num:
numero = match_num.group(1)
if "\n" in bloque_limpio:
resto = bloque_limpio.split("\n", 1)[1].strip()
else:
resto = match_num.group(2)
resultado[f"Pregunta {numero}"] = resto.strip()
return resultado
# ------------
# COMPARACIÓN
# ------------
def similar_textos(texto1: str, texto2: str) -> float:
"""Calcula la similitud entre dos textos (valor entre 0 y 1)."""
return SequenceMatcher(None, texto1, texto2).ratio()
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, se recomienda revisar el tema.
- Si está, se compara la respuesta del alumno con la correcta.
Se eliminan los saltos de línea en la respuesta del alumno.
"""
retroalimentacion = []
for pregunta, resp_correcta in dict_docente.items():
resp_alumno = dict_alumno.get(pregunta, None)
if resp_alumno is None or resp_alumno.strip() == "":
retroalimentacion.append(
f"**{pregunta}**\n"
f"Respuesta del alumno: No fue asignada.\n"
f"Respuesta correcta: {' '.join(resp_correcta.split())}\n"
f"Recomendación: Revisar el tema correspondiente.\n"
)
else:
# Eliminar saltos de línea y espacios extra
resp_alumno_clean = " ".join(resp_alumno.split())
resp_correcta_clean = " ".join(resp_correcta.split())
ratio = similar_textos(resp_alumno_clean.lower(), resp_correcta_clean.lower())
if ratio >= 0.8:
feedback_text = "La respuesta es correcta."
else:
feedback_text = "La respuesta no coincide completamente. Se recomienda revisar la explicación y reforzar el concepto."
retroalimentacion.append(
f"**{pregunta}**\n"
f"Respuesta del alumno: {resp_alumno_clean}\n"
f"Respuesta correcta: {resp_correcta_clean}\n"
f"{feedback_text}\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.
Realiza los siguientes pasos:
1. Configura credenciales.
2. Extrae texto de los PDFs.
3. Separa secciones 'Preguntas' y 'RESPUESTAS'.
4. Parsea las enumeraciones.
5. Compara las respuestas y genera retroalimentación con recomendaciones.
6. Llama a un LLM para generar 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)
# Unir preguntas y respuestas del docente
dict_docente = {}
for key_preg in dict_preg_doc:
resp_doc = dict_resp_doc.get(key_preg, "")
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 preguntas y respuestas del alumno
dict_alumno = {}
for key_preg in dict_preg_alum:
resp_alum = dict_resp_alum.get(key_preg, "")
dict_alumno[key_preg] = resp_alum
yield "Comparando preguntas y 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, indicando si entiende los conceptos y recomendando reforzar los puntos necesarios.
"""
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
# -----------------
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",
title="Revisión de Exámenes (Preguntas/Respuestas enumeradas)",
description=(
"Sube las credenciales, el PDF del docente (con las preguntas y respuestas correctas) y el PDF del alumno. "
"El sistema separa las secciones 'Preguntas' y 'RESPUESTAS', parsea las enumeraciones y luego compara las respuestas. "
"Finalmente, se genera un resumen con recomendaciones para reforzar los conceptos según el desempeño del alumno."
)
)
interface.launch(debug=True)
|