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)