Spaces:
Sleeping
Sleeping
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) | |