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)