Spaces:
Sleeping
Sleeping
| import re | |
| import os | |
| import json | |
| import random | |
| import uuid | |
| import pandas as pd | |
| from datetime import datetime | |
| from pathlib import Path | |
| from gtts import gTTS | |
| import gradio as gr | |
| from transformers import pipeline | |
| import gspread | |
| from oauth2client.service_account import ServiceAccountCredentials | |
| from gemini import responder_con_gemini | |
| # ===================================================== | |
| # CONFIGURACIÓN GENERAL | |
| # ===================================================== | |
| SPREADSHEET_ID = "1YizPDOaelHY-vTTrBSWy6Iv8P0SvNOTPNrAEgEiWIIY" | |
| NOMBRE_HOJA = "Hoja 1" # Cambia esto si tu pestaña tiene otro nombre exacto | |
| VERSION_CONSENTIMIENTO = "1.0" | |
| NOMBRE_HOJA_CONSENTIMIENTOS = "Consentimientos" | |
| ENCABEZADOS_CONSENTIMIENTO = [ | |
| "fecha_hora", | |
| "matricula", | |
| "nombre", | |
| "grupo", | |
| "consentimiento", | |
| "version_consentimiento" | |
| ] | |
| ENCABEZADOS = [ | |
| "fecha_hora", | |
| "folio_atencion", | |
| "matricula", | |
| "nombre", | |
| "grupo", | |
| "idioma", | |
| "mensaje", | |
| "clasificacion", | |
| "intencion", | |
| "tema", | |
| "label", | |
| "score", | |
| "nivel_riesgo", | |
| "semaforo", | |
| "coincidencias_riesgo", | |
| "probabilidades", | |
| "tipo_riesgo", | |
| "ideacion_suicida", | |
| "desesperanza", | |
| "aislamiento", | |
| "alerta_urgente", | |
| "respuesta_agente" | |
| ] | |
| # ===================================================== | |
| # CARGAR MODELO DE SENTIMIENTO | |
| # ===================================================== | |
| print("Cargando modelo de sentimiento...") | |
| modelo_sentimiento = pipeline( | |
| "text-classification", | |
| model="pysentimiento/robertuito-sentiment-analysis", | |
| top_k=None | |
| ) | |
| print("Modelo cargado correctamente") | |
| # ===================================================== | |
| # CONEXIÓN A GOOGLE SHEETS | |
| # ===================================================== | |
| def conectar_sheet(): | |
| """ | |
| Conecta con Google Sheets usando el Secret GOOGLE_CREDENTIALS. | |
| En Hugging Face Spaces, guarda el JSON completo de la cuenta | |
| de servicio en Settings > Secrets con el nombre exacto: | |
| GOOGLE_CREDENTIALS | |
| """ | |
| print("=" * 60) | |
| print("INICIANDO CONEXIÓN A GOOGLE SHEETS") | |
| scope = [ | |
| "https://spreadsheets.google.com/feeds", | |
| "https://www.googleapis.com/auth/drive" | |
| ] | |
| creds_json = os.getenv("GOOGLE_CREDENTIALS") | |
| print("Verificando GOOGLE_CREDENTIALS...") | |
| if creds_json: | |
| print("✅ GOOGLE_CREDENTIALS encontrado") | |
| print(f"📏 Longitud del Secret: {len(creds_json)} caracteres") | |
| else: | |
| print("❌ GOOGLE_CREDENTIALS NO encontrado") | |
| if not creds_json: | |
| raise ValueError( | |
| "No se encontró el Secret GOOGLE_CREDENTIALS. " | |
| "Verifica en Hugging Face: Settings > Secrets." | |
| ) | |
| try: | |
| creds_dict = json.loads(creds_json) | |
| print("✅ JSON leído correctamente") | |
| except Exception as e: | |
| raise ValueError( | |
| f"El Secret GOOGLE_CREDENTIALS no tiene formato JSON válido: {repr(e)}" | |
| ) from e | |
| try: | |
| creds = ServiceAccountCredentials.from_json_keyfile_dict( | |
| creds_dict, | |
| scope | |
| ) | |
| print("✅ Credenciales creadas") | |
| except Exception as e: | |
| raise ValueError( | |
| f"No se pudieron crear las credenciales de Google: {repr(e)}" | |
| ) from e | |
| try: | |
| client = gspread.authorize(creds) | |
| print("✅ Cliente autorizado") | |
| except Exception as e: | |
| raise ValueError( | |
| f"No se pudo autorizar el cliente de Google Sheets: {repr(e)}" | |
| ) from e | |
| try: | |
| archivo = client.open_by_key(SPREADSHEET_ID) | |
| print("✅ Spreadsheet abierto") | |
| except Exception as e: | |
| raise ValueError( | |
| "No se pudo abrir el Spreadsheet. " | |
| f"Revisa SPREADSHEET_ID y permisos: {repr(e)}" | |
| ) from e | |
| try: | |
| sheet = archivo.worksheet(NOMBRE_HOJA) | |
| print(f"✅ Hoja encontrada: {NOMBRE_HOJA}") | |
| except Exception as e: | |
| print( | |
| f"⚠️ No se encontró la hoja '{NOMBRE_HOJA}'. " | |
| f"Se usará la primera hoja: {repr(e)}" | |
| ) | |
| sheet = archivo.sheet1 | |
| print("✅ Primera hoja abierta correctamente") | |
| print("CONEXIÓN A GOOGLE SHEETS FINALIZADA") | |
| print("=" * 60) | |
| return sheet | |
| def asegurar_encabezados(sheet): | |
| """ | |
| Coloca los encabezados cuando la primera fila de la hoja está vacía. | |
| """ | |
| try: | |
| primera_fila = sheet.row_values(1) | |
| if not primera_fila: | |
| sheet.append_row( | |
| ENCABEZADOS, | |
| value_input_option="USER_ENTERED" | |
| ) | |
| print("✅ Encabezados agregados correctamente") | |
| else: | |
| print("✅ La hoja ya contiene encabezados") | |
| except Exception as e: | |
| print( | |
| "❌ No se pudieron verificar o agregar los encabezados: " | |
| f"{repr(e)}" | |
| ) | |
| # ===================================================== | |
| # CONSENTIMIENTO INFORMADO Y REGISTRO | |
| # ===================================================== | |
| def guardar_consentimiento(acepta, matricula, nombre, grupo): | |
| """ | |
| Verifica la aceptación, intenta guardar el consentimiento | |
| y permite avanzar a la interfaz de SOFIA. | |
| """ | |
| if not acepta: | |
| return ( | |
| gr.update(visible=True), # Mantiene el aviso visible | |
| gr.update(visible=False), # Mantiene SOFIA oculta | |
| matricula or "", | |
| nombre or "", | |
| grupo or "", | |
| "⚠️ Debes marcar la casilla de aceptación para continuar." | |
| ) | |
| matricula_final = str(matricula or "").strip() or "ANONIMO" | |
| nombre_final = str(nombre or "").strip() or "ANONIMO" | |
| grupo_final = str(grupo or "").strip() | |
| mensaje_estado = "✅ Consentimiento aceptado correctamente." | |
| try: | |
| sheet = conectar_sheet_consentimientos() | |
| sheet.append_row( | |
| [ | |
| datetime.now().strftime("%Y-%m-%d %H:%M:%S"), | |
| matricula_final, | |
| nombre_final, | |
| grupo_final, | |
| "SI", | |
| VERSION_CONSENTIMIENTO | |
| ], | |
| value_input_option="USER_ENTERED", | |
| insert_data_option="INSERT_ROWS" | |
| ) | |
| print("✅ Consentimiento guardado en Google Sheets") | |
| except Exception as e: | |
| import traceback | |
| print("=" * 80) | |
| print("ERROR AL GUARDAR EL CONSENTIMIENTO") | |
| print(traceback.format_exc()) | |
| print("=" * 80) | |
| mensaje_estado = ( | |
| "⚠️ Consentimiento aceptado. " | |
| "No fue posible guardar temporalmente el registro en Google Sheets." | |
| ) | |
| # Siempre permite avanzar cuando la casilla fue aceptada | |
| return ( | |
| gr.update(visible=False), # Oculta el aviso | |
| gr.update(visible=True), # Muestra la interfaz de SOFIA | |
| "" if matricula_final == "ANONIMO" else matricula_final, | |
| "" if nombre_final == "ANONIMO" else nombre_final, | |
| grupo_final, | |
| mensaje_estado | |
| ) | |
| # ===================================================== | |
| # FUNCIONES DE RESPALDO FOSO II | |
| # ===================================================== | |
| def detectar_pregunta_foso2(mensaje): | |
| return False | |
| def responder_foso2(mensaje): | |
| return ( | |
| "Gracias por tu pregunta. En este momento no se detectó una consulta pedagógica " | |
| "específica de FOSO II, pero puedes escribir con más detalle tu duda." | |
| ) | |
| # ===================================================== | |
| # GENERAR FOLIO DE ATENCIÓN | |
| # ===================================================== | |
| def generar_folio_atencion(): | |
| return "ATN-" + str(uuid.uuid4())[:8].upper() | |
| # ===================================================== | |
| # PATRONES DE RIESGO | |
| # ===================================================== | |
| PATRONES_RIESGO_ALTO = [ | |
| r"\bme quiero morir\b", | |
| r"\bquiero morir\b", | |
| r"\bno quiero vivir\b", | |
| r"\bquiero terminar con todo\b", | |
| r"\bquiero acabar con todo\b", | |
| r"\bquiero matarme\b", | |
| r"\bme voy a suicidar\b", | |
| r"\bsuicidarme\b", | |
| r"\bquitarme la vida\b", | |
| r"\bya no quiero vivir\b", | |
| r"\bsería mejor morir\b", | |
| r"\bseria mejor morir\b", | |
| r"\bestoy pensando en matarme\b", | |
| ] | |
| PATRONES_RIESGO_MODERADO = [ | |
| r"\bnada tiene sentido\b", | |
| r"\bno veo salida\b", | |
| r"\bya no puedo\b", | |
| r"\bquiero desaparecer\b", | |
| r"\bme siento mal\b", | |
| r"\bestoy muy triste\b", | |
| r"\bestoy deprimid[oa]\b", | |
| r"\bme siento solo\b", | |
| r"\bme siento sola\b", | |
| r"\bnadie me entiende\b", | |
| r"\btengo miedo\b", | |
| r"\bestoy muy estresad[oa]\b", | |
| r"\bme siento estresad[oa]\b", | |
| r"\btengo muchas tareas\b", | |
| r"\bestoy cansad[oa]\b", | |
| r"\bme siento abrumad[oa]\b", | |
| r"\bno puedo más\b", | |
| r"\bno puedo mas\b", | |
| r"\bya no aguanto\b", | |
| r"\btodo sería mejor sin mí\b", | |
| r"\btodo seria mejor sin mi\b", | |
| ] | |
| PATRONES_DESESPERANZA = [ | |
| r"\bnada tiene sentido\b", | |
| r"\bno veo salida\b", | |
| r"\bya no puedo\b", | |
| r"\bno puedo más\b", | |
| r"\bno puedo mas\b", | |
| r"\bya no aguanto\b", | |
| r"\bquisiera desaparecer\b", | |
| r"\btodo sería mejor sin mí\b", | |
| r"\btodo seria mejor sin mi\b", | |
| r"\bmi vida no vale\b", | |
| r"\bsoy una carga\b", | |
| ] | |
| PATRONES_AISLAMIENTO = [ | |
| r"\bme siento solo\b", | |
| r"\bme siento sola\b", | |
| r"\bnadie me entiende\b", | |
| r"\bestoy solo\b", | |
| r"\bestoy sola\b", | |
| r"\bno tengo a nadie\b", | |
| r"\bquiero aislarme\b", | |
| r"\bno quiero hablar con nadie\b", | |
| ] | |
| PATRONES_IDEACION_SUICIDA = [ | |
| r"\bme quiero morir\b", | |
| r"\bquiero morir\b", | |
| r"\bno quiero vivir\b", | |
| r"\bquiero matarme\b", | |
| r"\bme voy a suicidar\b", | |
| r"\bsuicidarme\b", | |
| r"\bquitarme la vida\b", | |
| r"\bestoy pensando en matarme\b", | |
| r"\bquisiera dormirme y no despertar\b", | |
| ] | |
| # ===================================================== | |
| # FUNCIÓN PARA CALCULAR NIVEL DE RIESGO | |
| # ===================================================== | |
| def calcular_nivel_riesgo(texto, label, score): | |
| t = str(texto or "").lower().strip() | |
| coincidencias_altas = [p for p in PATRONES_RIESGO_ALTO if re.search(p, t)] | |
| coincidencias_moderadas = [p for p in PATRONES_RIESGO_MODERADO if re.search(p, t)] | |
| if coincidencias_altas: | |
| return "alto", "🔴", coincidencias_altas + coincidencias_moderadas | |
| if coincidencias_moderadas: | |
| return "moderado", "🟠", coincidencias_moderadas | |
| if label == "NEG" and score >= 0.90: | |
| return "moderado", "🟠", ["modelo_NEG_alto"] | |
| return "bajo", "🟢", [] | |
| # ===================================================== | |
| # DETECTAR TEMA PRINCIPAL DEL MENSAJE | |
| # ===================================================== | |
| def detectar_tema(texto): | |
| t = str(texto or "").lower().strip() | |
| if any(p in t for p in ["no puedo dormir", "insomnio", "duermo mal", "no duermo"]): | |
| return "sueno" | |
| if any(p in t for p in ["triste", "tristeza", "deprimido", "deprimida", "llorar", "vacío", "vacio"]): | |
| return "tristeza" | |
| if any(p in t for p in ["ansiedad", "ansioso", "ansiosa", "estres", "estrés", "abrumado", "abrumada", "presión", "presion"]): | |
| return "ansiedad" | |
| if any(p in t for p in ["tarea", "tareas", "examen", "materia", "profe", "profesor", "calificación", "calificacion", "estudiar", "actividad"]): | |
| return "academico" | |
| if any(p in t for p in ["familia", "mamá", "mama", "papá", "papa", "casa", "hermano", "hermana", "padres"]): | |
| return "familia" | |
| if any(p in t for p in ["novio", "novia", "pareja", "terminé", "termine", "terminamos", "relación", "relacion"]): | |
| return "pareja" | |
| if any(p in t for p in ["solo", "sola", "nadie me entiende", "sin amigos", "me siento aislado", "me siento aislada"]): | |
| return "soledad" | |
| if any(p in t for p in ["horario", "documento", "trámite", "tramite", "grupo", "boleta", "inscripción", "inscripcion"]): | |
| return "administrativo" | |
| return "general" | |
| # ===================================================== | |
| # DETECCIÓN DE INDICADORES DE RIESGO SUICIDA | |
| # ===================================================== | |
| def detectar_indicadores_suicidio(texto): | |
| t = str(texto or "").lower().strip() | |
| ideacion = any(re.search(p, t) for p in PATRONES_IDEACION_SUICIDA) | |
| desesperanza = any(re.search(p, t) for p in PATRONES_DESESPERANZA) | |
| aislamiento = any(re.search(p, t) for p in PATRONES_AISLAMIENTO) | |
| if ideacion: | |
| tipo_riesgo = "riesgo_suicida_alto" | |
| alerta_urgente = "si" | |
| elif desesperanza or aislamiento: | |
| tipo_riesgo = "riesgo_socioemocional_moderado" | |
| alerta_urgente = "no" | |
| else: | |
| tipo_riesgo = "malestar_general_o_sin_indicadores" | |
| alerta_urgente = "no" | |
| return { | |
| "ideacion_suicida": "si" if ideacion else "no", | |
| "desesperanza": "si" if desesperanza else "no", | |
| "aislamiento": "si" if aislamiento else "no", | |
| "tipo_riesgo": tipo_riesgo, | |
| "alerta_urgente": alerta_urgente | |
| } | |
| # ===================================================== | |
| # ANALIZAR MENSAJE | |
| # ===================================================== | |
| def analizar_frase(texto): | |
| texto = str(texto or "").strip() | |
| if not texto: | |
| return None | |
| resultado_modelo = modelo_sentimiento(texto)[0] | |
| mejor = max(resultado_modelo, key=lambda x: x["score"]) | |
| label = mejor["label"] | |
| score = float(mejor["score"]) | |
| probabilidades = { | |
| x["label"]: round(float(x["score"]), 3) | |
| for x in resultado_modelo | |
| } | |
| texto_lower = texto.lower() | |
| nivel_riesgo, semaforo, coincidencias_riesgo = calcular_nivel_riesgo( | |
| texto_lower, label, score | |
| ) | |
| if nivel_riesgo == "alto": | |
| clasificacion = "alto_riesgo" | |
| elif nivel_riesgo == "moderado": | |
| clasificacion = "riesgo_moderado" | |
| else: | |
| clasificacion = "normal" | |
| if any(p in texto_lower for p in [ | |
| "tarea", "tareas", "profe", "profesor", "materia", "ejercicio", | |
| "actividad", "tema", "examen", "calificación", "calificacion", | |
| "muchas tareas", "no entiendo", "estudiar" | |
| ]): | |
| intencion = "academico_emocional" | |
| elif any(p in texto_lower for p in [ | |
| "horario", "documento", "trámite", "tramite", "grupo", "boleta", | |
| "inscripción", "inscripcion" | |
| ]): | |
| intencion = "administrativo" | |
| else: | |
| intencion = "emocional" | |
| tema = detectar_tema(texto) | |
| indicadores = detectar_indicadores_suicidio(texto) | |
| return { | |
| "clasificacion": clasificacion, | |
| "intencion": intencion, | |
| "label": label, | |
| "score": score, | |
| "probabilidades": probabilidades, | |
| "nivel_riesgo": nivel_riesgo, | |
| "semaforo": semaforo, | |
| "tema": tema, | |
| "coincidencias_riesgo": coincidencias_riesgo, | |
| "tipo_riesgo": indicadores["tipo_riesgo"], | |
| "ideacion_suicida": indicadores["ideacion_suicida"], | |
| "desesperanza": indicadores["desesperanza"], | |
| "aislamiento": indicadores["aislamiento"], | |
| "alerta_urgente": indicadores["alerta_urgente"] | |
| } | |
| # ===================================================== | |
| # BLOQUES DE TEXTO VARIABLES | |
| # ===================================================== | |
| def elegir(lista): | |
| return random.choice(lista) | |
| def construir_sugerencias_por_tema(tema, intencion): | |
| sugerencias_generales = [ | |
| "tratar de poner en palabras lo que estás sintiendo para entenderlo mejor", | |
| "buscar a una persona de confianza con quien puedas hablar", | |
| "hacer una pausa breve, respirar despacio y bajar un poco la tensión del momento", | |
| "evitar cargar con todo tú solo o tú sola", | |
| ] | |
| mapa = { | |
| "sueno": [ | |
| "evitar usar el celular unos minutos antes de dormir", | |
| "hacer respiraciones lentas y profundas antes de acostarte", | |
| "anotar las preocupaciones que te están quitando el sueño", | |
| "pedir apoyo si el insomnio se está volviendo frecuente" | |
| ], | |
| "tristeza": [ | |
| "expresar lo que sientes con alguien de confianza", | |
| "no aislarte por completo en este momento", | |
| "realizar una actividad sencilla que te ayude a salir un poco de la carga emocional", | |
| "buscar acompañamiento del área de orientación o psicología" | |
| ], | |
| "ansiedad": [ | |
| "hacer pausas cortas durante el día para disminuir la saturación mental", | |
| "dividir el problema en partes pequeñas en lugar de querer resolver todo al mismo tiempo", | |
| "practicar respiración profunda por unos minutos", | |
| "identificar qué situación te está generando más presión" | |
| ], | |
| "academico": [ | |
| "organizar tus tareas por prioridad y fecha de entrega", | |
| "empezar por una sola actividad en vez de pensar en todo al mismo tiempo", | |
| "pedir que te expliquen nuevamente el tema que no entiendes", | |
| "buscar apoyo con tu docente, tutor o un compañero de confianza" | |
| ], | |
| "familia": [ | |
| "hablar con calma con una persona de tu familia que te genere confianza", | |
| "buscar apoyo adulto si en casa te sientes sobrepasado o sobrepasada", | |
| "tratar de identificar qué situación familiar te está afectando más", | |
| "pedir acompañamiento institucional si el conflicto continúa" | |
| ], | |
| "pareja": [ | |
| "tomarte un momento para entender qué fue lo que más te lastimó", | |
| "evitar tomar decisiones impulsivas cuando la emoción está muy intensa", | |
| "hablar con alguien de confianza para no quedarte solo o sola con esto", | |
| "recordar que una situación afectiva difícil también necesita acompañamiento" | |
| ], | |
| "soledad": [ | |
| "acercarte a una persona con quien te sientas mínimamente en confianza", | |
| "evitar encerrarte completamente con lo que sientes", | |
| "buscar apoyo en orientación o tutoría", | |
| "dar un primer paso pequeño para pedir ayuda" | |
| ], | |
| "administrativo": [ | |
| "explicar con más detalle cuál es el trámite o la duda que necesitas resolver", | |
| "revisar si tu duda corresponde a horario, grupo, boleta o documento", | |
| "acercarte al área del plantel que lleve ese proceso", | |
| "tener a la mano tus datos escolares para recibir una orientación más clara" | |
| ], | |
| "general": sugerencias_generales | |
| } | |
| sugerencias = mapa.get(tema, sugerencias_generales) | |
| if intencion == "administrativo": | |
| sugerencias = mapa["administrativo"] | |
| elif intencion == "academico_emocional" and tema == "general": | |
| sugerencias = mapa["academico"] | |
| random.shuffle(sugerencias) | |
| return sugerencias[:3] | |
| # ===================================================== | |
| # RESPUESTA DE CRISIS | |
| # ===================================================== | |
| def construir_respuesta_crisis(nombre="Estudiante"): | |
| return ( | |
| f"Hola {nombre}. Gracias por decir lo que estás sintiendo.\n\n" | |
| "Lo que acabas de expresar requiere atención inmediata. No estás solo o sola.\n\n" | |
| "Por favor, haz esto ahora mismo:\n" | |
| "1. Busca a un adulto, familiar, docente, tutor, orientador u otra persona de confianza y dile que necesitas acompañamiento inmediato.\n" | |
| "2. No te quedes solo o sola en este momento.\n" | |
| "3. Acércate de inmediato al área de orientación, tutoría o psicología de tu plantel.\n" | |
| "4. Si sientes que podrías lastimarte o existe peligro inmediato, llama al 911 o comunícate con la Línea de la Vida: 800 911 2000.\n\n" | |
| "Tu bienestar importa y pedir ayuda ahora es muy importante.\n\n" | |
| "Respóndeme con una sola palabra:\n" | |
| "- 'solo' si estás sin compañía\n" | |
| "- 'acompañado' si hay alguien contigo" | |
| ) | |
| # ===================================================== | |
| # RESPUESTA AMPLIA DEL AGENTE | |
| # ===================================================== | |
| def construir_respuesta_amplia(resultado, nombre="Estudiante"): | |
| if not resultado: | |
| return ( | |
| f"Hola {nombre}. Gracias por escribir.\n\n" | |
| "Quiero leerte con atención. Cuéntame un poco más sobre lo que estás viviendo " | |
| "para poder orientarte mejor." | |
| ) | |
| clasificacion = resultado.get("clasificacion", "normal") | |
| intencion = resultado.get("intencion", "emocional") | |
| nivel_riesgo = resultado.get("nivel_riesgo", "bajo") | |
| tema = resultado.get("tema", "general") | |
| aperturas = [ | |
| f"Hola {nombre}. Gracias por confiar en este espacio para expresar lo que estás viviendo.", | |
| f"Hola {nombre}. Gracias por compartir lo que sientes; hacerlo ya es un paso importante.", | |
| f"Hola {nombre}. Valoro que hayas escrito sobre esto, porque lo que te pasa sí importa." | |
| ] | |
| empatia_moderada = [ | |
| "Lo que estás sintiendo merece atención, acompañamiento y un espacio para hablarlo con calma.", | |
| "A veces la presión emocional, personal o escolar puede sentirse muy pesada, y no tienes por qué cargar con todo sin apoyo.", | |
| "Cuando varias cosas se juntan al mismo tiempo, es normal sentirse cansado, abrumado o sin mucha claridad." | |
| ] | |
| preguntas_finales = [ | |
| "Si quieres, puedo ayudarte a identificar qué te está afectando más en este momento.", | |
| "Si te parece, podemos ir paso a paso para ubicar qué situación te está pesando más.", | |
| "Cuéntame un poco más y te ayudo a ordenar lo que estás sintiendo." | |
| ] | |
| if clasificacion == "alto_riesgo" or nivel_riesgo == "alto": | |
| return construir_respuesta_crisis(nombre) | |
| if clasificacion == "riesgo_moderado" or nivel_riesgo == "moderado": | |
| sugerencias = construir_sugerencias_por_tema(tema, intencion) | |
| texto_tema = { | |
| "sueno": "El problema para dormir muchas veces aparece cuando hay preocupaciones, tensión acumulada o malestar emocional.", | |
| "tristeza": "La tristeza sostenida puede ser una señal de que algo importante necesita atención y apoyo.", | |
| "ansiedad": "La ansiedad o el estrés pueden afectar tanto tus pensamientos como tu cuerpo, tu concentración y tu descanso.", | |
| "academico": "La carga escolar puede generar mucha presión cuando se acumulan tareas, exámenes o temas difíciles.", | |
| "familia": "Los problemas familiares también pueden afectar mucho el ánimo, la concentración y la sensación de estabilidad.", | |
| "pareja": "Las situaciones afectivas pueden doler profundamente y alterar el equilibrio emocional.", | |
| "soledad": "Sentirse solo o sola puede volver más pesada cualquier situación difícil.", | |
| "administrativo": "Cuando una duda escolar o administrativa no se resuelve, también puede generar estrés y frustración.", | |
| "general": "A veces cuesta poner en orden lo que sentimos, sobre todo cuando hay varias preocupaciones al mismo tiempo." | |
| } | |
| return ( | |
| f"{elegir(aperturas)}\n\n" | |
| f"{elegir(empatia_moderada)} {texto_tema.get(tema, texto_tema['general'])}\n\n" | |
| "Además de reconocer lo que te está pasando, también es importante pensar en pequeños pasos que sí puedes dar desde ahora.\n\n" | |
| "Estas opciones podrían ayudarte:\n" | |
| f"- {sugerencias[0]}\n" | |
| f"- {sugerencias[1]}\n" | |
| f"- {sugerencias[2]}\n\n" | |
| "También es recomendable que busques acompañamiento en tu plantel para no enfrentar esto en soledad. " | |
| "Puedes acercarte a tutoría, orientación educativa o psicología.\n\n" | |
| f"{elegir(preguntas_finales)} " | |
| "Puedes decirme si esto se relaciona más con la escuela, con tus emociones, con tu familia o con algún problema personal." | |
| ) | |
| if intencion == "academico_emocional": | |
| sugerencias = construir_sugerencias_por_tema("academico", intencion) | |
| return ( | |
| f"{elegir(aperturas)}\n\n" | |
| "Entiendo que la carga académica puede llegar a sentirse muy pesada, especialmente cuando se juntan tareas, exámenes, temas difíciles o presión por las calificaciones.\n\n" | |
| "Eso no significa que no puedas avanzar; muchas veces significa que necesitas apoyo, orden y empezar por una sola parte del problema.\n\n" | |
| "Podrías intentar lo siguiente:\n" | |
| f"- {sugerencias[0]}\n" | |
| f"- {sugerencias[1]}\n" | |
| f"- {sugerencias[2]}\n\n" | |
| "También puedo ayudarte a revisar contigo qué te está costando más: una materia, una tarea, una instrucción, un tema específico o simplemente la acumulación de trabajo.\n\n" | |
| "Cuéntame qué materia o actividad te está preocupando más y avanzamos paso a paso." | |
| ) | |
| if intencion == "administrativo": | |
| sugerencias = construir_sugerencias_por_tema("administrativo", intencion) | |
| return ( | |
| f"{elegir(aperturas)}\n\n" | |
| "Puedo orientarte con dudas escolares o administrativas relacionadas con horario, grupo, documentos, boletas, trámites o indicaciones generales del plantel.\n\n" | |
| "Para darte una respuesta más útil, necesito que me escribas con un poco más de detalle qué es exactamente lo que necesitas resolver.\n\n" | |
| "Por ejemplo, puedes especificar si tu duda se relaciona con:\n" | |
| f"- {sugerencias[0]}\n" | |
| f"- {sugerencias[1]}\n" | |
| f"- {sugerencias[2]}\n\n" | |
| "Entre más claro me expliques tu situación, más precisa podrá ser mi orientación." | |
| ) | |
| sugerencias = construir_sugerencias_por_tema(tema, intencion) | |
| return ( | |
| f"{elegir(aperturas)}\n\n" | |
| "A veces poner en palabras lo que nos pasa ayuda a entender mejor la situación y a encontrar un primer paso para afrontarla. " | |
| "No necesitas resolver todo de una sola vez.\n\n" | |
| "Algunas opciones que podrían ayudarte en este momento son:\n" | |
| f"- {sugerencias[0]}\n" | |
| f"- {sugerencias[1]}\n" | |
| f"- {sugerencias[2]}\n\n" | |
| "Si quieres, puedo seguir acompañándote para identificar qué es lo que más te preocupa y cómo podrías empezar a manejarlo." | |
| ) | |
| # ===================================================== | |
| # ANÁLISIS TEXTUAL | |
| # ===================================================== | |
| def construir_analisis_textual(frase, resultado): | |
| if not resultado: | |
| return "Sin análisis." | |
| probs = resultado.get("probabilidades", {}) | |
| texto = [] | |
| texto.append("ANÁLISIS EMOCIONAL") | |
| texto.append(f"Frase: {frase}") | |
| texto.append(f"Clasificación: {resultado.get('clasificacion', '')}") | |
| texto.append(f"Intención detectada: {resultado.get('intencion', '')}") | |
| texto.append(f"Tema detectado: {resultado.get('tema', '')}") | |
| texto.append(f"Etiqueta principal: {resultado.get('label', '')}") | |
| texto.append(f"Confianza: {float(resultado.get('score', 0.0)):.3f}") | |
| texto.append(f"Nivel de riesgo: {resultado.get('nivel_riesgo', '')}") | |
| texto.append(f"Semáforo: {resultado.get('semaforo', '')}") | |
| texto.append(f"Coincidencias de riesgo: {resultado.get('coincidencias_riesgo', '')}") | |
| texto.append(f"Tipo de riesgo: {resultado.get('tipo_riesgo', '')}") | |
| texto.append(f"Ideación suicida: {resultado.get('ideacion_suicida', '')}") | |
| texto.append(f"Desesperanza: {resultado.get('desesperanza', '')}") | |
| texto.append(f"Aislamiento: {resultado.get('aislamiento', '')}") | |
| texto.append(f"Alerta urgente: {resultado.get('alerta_urgente', '')}") | |
| texto.append("") | |
| texto.append("Probabilidades:") | |
| texto.append(f"- NEG: {float(probs.get('NEG', 0.0)):.3f}") | |
| texto.append(f"- NEU: {float(probs.get('NEU', 0.0)):.3f}") | |
| texto.append(f"- POS: {float(probs.get('POS', 0.0)):.3f}") | |
| return "\n".join(texto) | |
| # ===================================================== | |
| # GUARDAR EN GOOGLE SHEETS | |
| # ===================================================== | |
| def guardar_interaccion(folio_atencion, matricula, nombre, grupo, idioma, mensaje, resultado, respuesta): | |
| try: | |
| print("ENTRANDO A guardar_interaccion...") | |
| sheet = conectar_sheet() | |
| print("Conexión con Google Sheets correcta") | |
| if resultado is None: | |
| resultado = {} | |
| fila = [ | |
| datetime.now().strftime("%Y-%m-%d %H:%M:%S"), | |
| str(folio_atencion or ""), | |
| str(matricula or ""), | |
| str(nombre or ""), | |
| str(grupo or ""), | |
| str(idioma or "Español"), | |
| str(mensaje or ""), | |
| str(resultado.get("clasificacion", "")), | |
| str(resultado.get("intencion", "")), | |
| str(resultado.get("tema", "")), | |
| str(resultado.get("label", "")), | |
| str(resultado.get("score", "")), | |
| str(resultado.get("nivel_riesgo", "")), | |
| str(resultado.get("semaforo", "")), | |
| str(resultado.get("coincidencias_riesgo", "")), | |
| str(resultado.get("probabilidades", "")), | |
| str(resultado.get("tipo_riesgo", "")), | |
| str(resultado.get("ideacion_suicida", "")), | |
| str(resultado.get("desesperanza", "")), | |
| str(resultado.get("aislamiento", "")), | |
| str(resultado.get("alerta_urgente", "")), | |
| str(respuesta or "") | |
| ] | |
| # Guarda SIEMPRE al final, sin borrar registros anteriores | |
| sheet.append_row( | |
| fila, | |
| value_input_option="USER_ENTERED", | |
| insert_data_option="INSERT_ROWS" | |
| ) | |
| print("✅ Datos guardados correctamente en Google Sheets") | |
| return True, "Guardado correcto" | |
| except Exception as e: | |
| error = f"❌ Error al guardar en Google Sheets: {repr(e)}" | |
| print(error) | |
| return False, error | |
| # ===================================================== | |
| # RESPONDER CHAT | |
| # ===================================================== | |
| def responder_chat(matricula, nombre, grupo, idioma, mensaje, historial): | |
| mensaje = str(mensaje or "").strip() | |
| historial = str(historial or "").strip() | |
| if not mensaje: | |
| return historial, "", "Escribe algo.", "Sin análisis" | |
| try: | |
| nombre_mostrar = nombre.strip() if str(nombre or "").strip() else "Estudiante" | |
| matricula_final = matricula.strip() if str(matricula or "").strip() else "ANONIMO" | |
| grupo_final = grupo.strip() if str(grupo or "").strip() else "" | |
| idioma_final = str(idioma or "Español").strip() | |
| folio_atencion = generar_folio_atencion() | |
| # Consulta FOSO II. También se guarda registro. | |
| if detectar_pregunta_foso2(mensaje): | |
| respuesta_base = responder_foso2(mensaje) | |
| respuesta = f"📌 Folio de atención: {folio_atencion}\n\n{respuesta_base}" | |
| analisis = "Consulta pedagógica detectada: FOSO II" | |
| resultado_foso = { | |
| "clasificacion": "consulta_pedagogica", | |
| "intencion": "foso_ii", | |
| "tema": "foso_ii", | |
| "label": "", | |
| "score": "", | |
| "nivel_riesgo": "bajo", | |
| "semaforo": "🟢", | |
| "coincidencias_riesgo": "", | |
| "probabilidades": "", | |
| "tipo_riesgo": "consulta_pedagogica", | |
| "ideacion_suicida": "no", | |
| "desesperanza": "no", | |
| "aislamiento": "no", | |
| "alerta_urgente": "no" | |
| } | |
| guardado, mensaje_guardado = guardar_interaccion( | |
| folio_atencion, | |
| matricula_final, | |
| nombre_mostrar, | |
| grupo_final, | |
| idioma_final, | |
| mensaje, | |
| resultado_foso, | |
| respuesta | |
| ) | |
| print(mensaje_guardado) | |
| bloque_usuario = f"Tú: {mensaje}" | |
| bloque_agente = f"Agente: {respuesta}" | |
| nuevo_historial = historial + "\n\n" + bloque_usuario + "\n" + bloque_agente if historial else bloque_usuario + "\n" + bloque_agente | |
| return nuevo_historial, "", respuesta, analisis | |
| resultado = analizar_frase(mensaje) | |
| # Gemini redacta la respuesta cuando el riesgo no es alto. | |
| # Si Gemini falla, el agente usa la respuesta original como respaldo. | |
| if resultado and resultado.get("nivel_riesgo") == "alto": | |
| respuesta_base = construir_respuesta_crisis(nombre_mostrar) | |
| else: | |
| try: | |
| respuesta_base = responder_con_gemini( | |
| mensaje=mensaje, | |
| nombre=nombre_mostrar, | |
| resultado=resultado, | |
| idioma=idioma_final | |
| ) | |
| if not respuesta_base or str(respuesta_base).startswith("Error de Gemini"): | |
| respuesta_base = construir_respuesta_amplia(resultado, nombre_mostrar) | |
| except Exception as e: | |
| print(f"Error al responder con Gemini: {repr(e)}") | |
| respuesta_base = construir_respuesta_amplia(resultado, nombre_mostrar) | |
| respuesta = f"📌 Folio de atención: {folio_atencion}\n\n{respuesta_base}" | |
| analisis = construir_analisis_textual(mensaje, resultado) | |
| if resultado is not None: | |
| guardado, mensaje_guardado = guardar_interaccion( | |
| folio_atencion, | |
| matricula_final, | |
| nombre_mostrar, | |
| grupo_final, | |
| idioma_final, | |
| mensaje, | |
| resultado, | |
| respuesta | |
| ) | |
| print(mensaje_guardado) | |
| if not guardado: | |
| analisis += f"\n\nERROR DE GUARDADO:\n{mensaje_guardado}" | |
| bloque_usuario = f"Tú: {mensaje}" | |
| bloque_agente = f"Agente: {respuesta}" | |
| nuevo_historial = historial + "\n\n" + bloque_usuario + "\n" + bloque_agente if historial else bloque_usuario + "\n" + bloque_agente | |
| return nuevo_historial, "", respuesta, analisis | |
| except Exception as e: | |
| print(f"Error interno: {repr(e)}") | |
| return historial, "", f"Error interno: {repr(e)}", "Sin análisis" | |
| # ===================================================== | |
| # MICRÓFONO, TRANSCRIPCIÓN Y VOZ DE SOFIA | |
| # ===================================================== | |
| modelo_transcripcion = None | |
| def obtener_modelo_transcripcion(): | |
| """Carga Whisper solamente cuando se utiliza el micrófono por primera vez.""" | |
| global modelo_transcripcion | |
| if modelo_transcripcion is None: | |
| print("Cargando modelo de transcripción Whisper...") | |
| modelo_transcripcion = pipeline( | |
| "automatic-speech-recognition", | |
| model="openai/whisper-tiny" | |
| ) | |
| print("Modelo de transcripción cargado correctamente") | |
| return modelo_transcripcion | |
| def transcribir_audio(ruta_audio): | |
| """Convierte el audio grabado por el estudiante en texto.""" | |
| if not ruta_audio: | |
| return "" | |
| try: | |
| transcriptor = obtener_modelo_transcripcion() | |
| resultado = transcriptor( | |
| str(ruta_audio), | |
| generate_kwargs={ | |
| "language": "spanish", | |
| "task": "transcribe" | |
| } | |
| ) | |
| if isinstance(resultado, dict): | |
| texto = resultado.get("text", "") | |
| else: | |
| texto = str(resultado or "") | |
| return texto.strip() | |
| except Exception as e: | |
| print(f"Error al transcribir audio: {repr(e)}") | |
| return "" | |
| def limpiar_texto_para_voz(texto): | |
| """ | |
| Elimina Markdown, emojis, folios y símbolos visuales | |
| antes de convertir el texto en voz. | |
| """ | |
| texto = str(texto or "") | |
| if not texto: | |
| return "" | |
| # Eliminar folio de atención | |
| texto = re.sub( | |
| r"📌\s*Folio de atención:\s*[A-Z0-9-]+", | |
| "", | |
| texto, | |
| flags=re.IGNORECASE | |
| ) | |
| # Eliminar enlaces Markdown conservando solo el texto | |
| texto = re.sub( | |
| r"\[([^\]]+)\]\([^)]+\)", | |
| r"\1", | |
| texto | |
| ) | |
| # Eliminar negritas, cursivas y código Markdown | |
| texto = re.sub(r"\*\*(.*?)\*\*", r"\1", texto) | |
| texto = re.sub(r"__(.*?)__", r"\1", texto) | |
| texto = re.sub(r"\*(.*?)\*", r"\1", texto) | |
| texto = re.sub(r"_(.*?)_", r"\1", texto) | |
| texto = re.sub(r"`(.*?)`", r"\1", texto) | |
| # Eliminar encabezados Markdown | |
| texto = re.sub( | |
| r"^\s*#{1,6}\s*", | |
| "", | |
| texto, | |
| flags=re.MULTILINE | |
| ) | |
| # Eliminar cualquier asterisco restante | |
| texto = texto.replace("*", "") | |
| # Convertir listas en pausas naturales | |
| texto = re.sub( | |
| r"^\s*[-•▪►]\s*", | |
| "", | |
| texto, | |
| flags=re.MULTILINE | |
| ) | |
| texto = re.sub( | |
| r"^\s*(\d+)[.)]\s*", | |
| r"\1. ", | |
| texto, | |
| flags=re.MULTILINE | |
| ) | |
| # Eliminar emojis y símbolos visuales | |
| simbolos = [ | |
| "🔴", "🟠", "🟢", | |
| "🚨", "📞", "☎️", "📌", | |
| "✅", "⚠️", "❗", | |
| "👉", "➡️", "✔️", | |
| "•", "▪", "►" | |
| ] | |
| for simbolo in simbolos: | |
| texto = texto.replace(simbolo, "") | |
| # Mejorar pronunciación | |
| texto = texto.replace( | |
| "800 911 2000", | |
| "ochocientos, nueve once, dos mil" | |
| ) | |
| texto = texto.replace( | |
| "911", | |
| "nueve once" | |
| ) | |
| # Eliminar comillas y símbolos innecesarios | |
| texto = texto.replace('"', "") | |
| texto = texto.replace("'", "") | |
| texto = texto.replace("~", "") | |
| texto = texto.replace("_", " ") | |
| # Crear pausas naturales | |
| texto = re.sub(r"\n+", ". ", texto) | |
| texto = re.sub(r"\s+", " ", texto) | |
| texto = re.sub(r"\.{2,}", ".", texto) | |
| return texto.strip() | |
| def generar_voz_sofia(texto): | |
| """Convierte la respuesta de SOFIA en un archivo MP3 en español.""" | |
| texto_limpio = limpiar_texto_para_voz(texto) | |
| if not texto_limpio: | |
| return None | |
| try: | |
| carpeta_audio = Path("/tmp/sofia_audio") | |
| carpeta_audio.mkdir( | |
| parents=True, | |
| exist_ok=True | |
| ) | |
| ruta_audio = ( | |
| carpeta_audio / | |
| f"sofia_{uuid.uuid4().hex}.mp3" | |
| ) | |
| gTTS( | |
| text=texto_limpio, | |
| lang="es", | |
| slow=False | |
| ).save(str(ruta_audio)) | |
| return str(ruta_audio) | |
| except Exception as e: | |
| print( | |
| f"Error al generar la voz de SOFIA: {repr(e)}" | |
| ) | |
| return None | |
| def responder_chat_con_voz( | |
| matricula, | |
| nombre, | |
| grupo, | |
| idioma, | |
| mensaje, | |
| historial | |
| ): | |
| """Conserva responder_chat y añade la salida de voz.""" | |
| nuevo_historial, mensaje_limpio, respuesta, analisis = responder_chat( | |
| matricula, | |
| nombre, | |
| grupo, | |
| idioma, | |
| mensaje, | |
| historial | |
| ) | |
| audio_respuesta = generar_voz_sofia(respuesta) | |
| return ( | |
| nuevo_historial, | |
| mensaje_limpio, | |
| respuesta, | |
| analisis, | |
| audio_respuesta | |
| ) | |
| def responder_desde_microfono( | |
| matricula, | |
| nombre, | |
| grupo, | |
| idioma, | |
| audio_entrada, | |
| historial | |
| ): | |
| """ | |
| Transcribe el micrófono y envía el texto | |
| a la función original responder_chat. | |
| """ | |
| texto_transcrito = transcribir_audio(audio_entrada) | |
| if not texto_transcrito: | |
| return ( | |
| historial, | |
| "", | |
| ( | |
| "No pude reconocer el audio. " | |
| "Intenta hablar nuevamente cerca del micrófono." | |
| ), | |
| "Sin análisis", | |
| None, | |
| None | |
| ) | |
| nuevo_historial, _, respuesta, analisis = responder_chat( | |
| matricula, | |
| nombre, | |
| grupo, | |
| idioma, | |
| texto_transcrito, | |
| historial | |
| ) | |
| audio_respuesta = generar_voz_sofia(respuesta) | |
| return ( | |
| nuevo_historial, | |
| texto_transcrito, | |
| respuesta, | |
| analisis, | |
| audio_respuesta, | |
| None | |
| ) | |
| def activar_ayuda_urgente_con_voz(nombre): | |
| """Conserva la función de crisis y añade la lectura de la respuesta.""" | |
| mensaje_limpio, respuesta, analisis = activar_ayuda_urgente(nombre) | |
| audio_respuesta = generar_voz_sofia(respuesta) | |
| return ( | |
| mensaje_limpio, | |
| respuesta, | |
| analisis, | |
| audio_respuesta | |
| ) | |
| def limpiar_chat_con_voz(): | |
| """Limpia conversación, mensaje, análisis y componentes de audio.""" | |
| return "", "", "", "", None, None | |
| # ===================================================== | |
| # LIMPIAR CHAT | |
| # ===================================================== | |
| def limpiar_chat(): | |
| return "", "", "", "" | |
| # ===================================================== | |
| # RESPUESTA RÁPIDA DE CRISIS DESDE BOTÓN | |
| # ===================================================== | |
| def activar_ayuda_urgente(nombre): | |
| nombre_mostrar = str(nombre or "").strip() or "Estudiante" | |
| folio_atencion = generar_folio_atencion() | |
| respuesta_base = construir_respuesta_crisis(nombre_mostrar) | |
| respuesta = f"📌 Folio de atención: {folio_atencion}\n\n{respuesta_base}" | |
| resultado = { | |
| "clasificacion": "alto_riesgo", | |
| "intencion": "emocional", | |
| "tema": "crisis", | |
| "label": "NEG", | |
| "score": 1.0, | |
| "nivel_riesgo": "alto", | |
| "semaforo": "🔴", | |
| "coincidencias_riesgo": "boton_ayuda_urgente", | |
| "probabilidades": "", | |
| "tipo_riesgo": "riesgo_suicida_alto", | |
| "ideacion_suicida": "si", | |
| "desesperanza": "no", | |
| "aislamiento": "no", | |
| "alerta_urgente": "si" | |
| } | |
| guardado, mensaje_guardado = guardar_interaccion( | |
| folio_atencion, | |
| "BOTON_URGENCIA", | |
| nombre_mostrar, | |
| "", | |
| "Español", | |
| "El estudiante presionó el botón de ayuda urgente", | |
| resultado, | |
| respuesta | |
| ) | |
| print(mensaje_guardado) | |
| analisis = "ALERTA MANUAL: solicitud de ayuda urgente" | |
| if not guardado: | |
| analisis += f"\n\nERROR DE GUARDADO:\n{mensaje_guardado}" | |
| return "", respuesta, analisis | |
| # ===================================================== | |
| # INTERFAZ VISUAL MEJORADA Y RESPONSIVE | |
| # ===================================================== | |
| css = """ | |
| body { | |
| background: linear-gradient(135deg, #eef7f4 0%, #e8f0ff 50%, #f4edff 100%); | |
| background-attachment: fixed; | |
| font-family: Arial, sans-serif; | |
| color: #111827; | |
| } | |
| .gradio-container { | |
| max-width: 1450px !important; | |
| padding-left: 12px !important; | |
| padding-right: 12px !important; | |
| } | |
| p, li, span, div, label { | |
| color: #111827 !important; | |
| } | |
| h1, h2, h3, h4 { | |
| color: #0f172a !important; | |
| font-weight: 800 !important; | |
| } | |
| .bloque { | |
| background: #ffffff; | |
| border-radius: 18px; | |
| padding: 18px; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.10); | |
| margin-bottom: 12px; | |
| } | |
| .header-box { | |
| background: linear-gradient(90deg, #0b6b43, #0f8a57); | |
| color: white !important; | |
| border-radius: 22px; | |
| padding: 24px; | |
| margin-bottom: 15px; | |
| box-shadow: 0 4px 14px rgba(0,0,0,0.12); | |
| } | |
| .header-box, | |
| .header-box div, | |
| .header-box span, | |
| .header-box b, | |
| .header-box .titulo, | |
| .header-box .subtitulo { | |
| color: white !important; | |
| } | |
| .titulo { | |
| font-size: 34px; | |
| font-weight: 900; | |
| margin-bottom: 6px; | |
| line-height: 1.15; | |
| } | |
| .subtitulo { | |
| font-size: 17px; | |
| font-weight: 500; | |
| opacity: 1; | |
| line-height: 1.4; | |
| } | |
| .seguro { | |
| background: #f8fafc; | |
| border-radius: 16px; | |
| padding: 18px; | |
| font-size: 17px; | |
| line-height: 1.65; | |
| margin-bottom: 12px; | |
| color: #111827 !important; | |
| border: 1px solid #e5e7eb; | |
| } | |
| .estado { | |
| background: #ecfdf3; | |
| border-radius: 12px; | |
| padding: 12px; | |
| font-weight: 800; | |
| color: #0b6b43 !important; | |
| text-align: center; | |
| margin-top: 10px; | |
| border: 1px solid #bbf7d0; | |
| } | |
| .footer-note { | |
| font-size: 13px; | |
| color: #374151 !important; | |
| margin-top: 8px; | |
| } | |
| textarea, input { | |
| font-size: 16px !important; | |
| color: #111827 !important; | |
| font-weight: 500 !important; | |
| } | |
| ::placeholder { | |
| color: #6b7280 !important; | |
| opacity: 1 !important; | |
| } | |
| label { | |
| font-size: 14px !important; | |
| font-weight: 700 !important; | |
| color: #111827 !important; | |
| } | |
| button { | |
| min-height: 44px !important; | |
| font-size: 15px !important; | |
| font-weight: 700 !important; | |
| border-radius: 12px !important; | |
| } | |
| textarea[aria-label="Conversación"] { | |
| min-height: 260px !important; | |
| color: #111827 !important; | |
| } | |
| .header-box img { | |
| max-width: 70px; | |
| max-height: 70px; | |
| } | |
| @media (max-width: 768px) { | |
| .gradio-container { | |
| padding-left: 8px !important; | |
| padding-right: 8px !important; | |
| } | |
| .header-box { | |
| border-radius: 18px; | |
| padding: 18px; | |
| margin-bottom: 12px; | |
| flex-direction: row; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .header-box img { | |
| width: 50px !important; | |
| height: 50px !important; | |
| } | |
| .titulo { | |
| font-size: 23px !important; | |
| line-height: 1.2; | |
| font-weight: 900 !important; | |
| } | |
| .subtitulo { | |
| font-size: 15px !important; | |
| line-height: 1.35; | |
| font-weight: 500 !important; | |
| } | |
| .bloque { | |
| padding: 14px; | |
| border-radius: 14px; | |
| margin-bottom: 10px; | |
| } | |
| .bloque h2, | |
| .bloque h3 { | |
| font-size: 22px !important; | |
| font-weight: 900 !important; | |
| color: #111827 !important; | |
| } | |
| .bloque p, | |
| .bloque li, | |
| .seguro { | |
| font-size: 17px !important; | |
| line-height: 1.5 !important; | |
| color: #111827 !important; | |
| font-weight: 500 !important; | |
| } | |
| .seguro { | |
| padding: 14px; | |
| border-radius: 14px; | |
| } | |
| .estado { | |
| padding: 10px; | |
| font-size: 15px !important; | |
| } | |
| label { | |
| font-size: 15px !important; | |
| font-weight: 800 !important; | |
| } | |
| button { | |
| width: 100% !important; | |
| margin-bottom: 8px !important; | |
| font-size: 15px !important; | |
| } | |
| textarea[aria-label="Conversación"] { | |
| min-height: 180px !important; | |
| } | |
| textarea[aria-label="Análisis emocional"] { | |
| min-height: 160px !important; | |
| } | |
| } | |
| textarea, | |
| input { | |
| background: #ffffff !important; | |
| color: #111827 !important; | |
| -webkit-text-fill-color: #111827 !important; | |
| } | |
| textarea[aria-label="Conversación"] { | |
| background: #ffffff !important; | |
| color: #111827 !important; | |
| } | |
| textarea[aria-label="Respuesta del agente"] { | |
| background: #ffffff !important; | |
| color: #111827 !important; | |
| } | |
| textarea[aria-label="Análisis emocional"] { | |
| background: #ffffff !important; | |
| color: #111827 !important; | |
| } | |
| html { | |
| color-scheme: light !important; | |
| } | |
| ::placeholder { | |
| color: #6b7280 !important; | |
| opacity: 1 !important; | |
| } | |
| """ | |
| tema_visual = gr.themes.Soft(primary_hue="green") | |
| with gr.Blocks(theme=tema_visual, css=css, title="Sistema de Orientación para el Fortalecimiento Integral del Alumno UNEVT") as demo: | |
| gr.HTML(""" | |
| <div class="header-box" style="display:flex; align-items:center; gap:14px;"> | |
| <img src="/gradio-api/file=logo_conalep.png" | |
| alt="Logo CONALEP" | |
| style="width:70px; height:70px; object-fit:contain; background:white; border-radius:12px; padding:6px;"> | |
| <div class="titulo">🤖 Sistema de Orientación para el Fortalecimiento Integral del Alumno UNEVT</div> | |
| <div class="subtitulo">Apoyo socioemocional, Pedagogico y detección de riesgos</div> | |
| <div style="font-size:12px; margin-top:4px;"> | |
| <span style="color:white !important;"> | |
| </span><br> | |
| <div style="font-size:12px; margin-top:4px; line-height:1.5;"> | |
| <span style="color:white !important;"> | |
| <b>Desarrollado por:</b><br> | |
| Doctorante. Jorge Noé Gámez Mora<br> | |
| Mtro. Mauricio Pueblas Cornejo | |
| Evelyn González Anzaldo · Herandy Pamela Díaz Catarino . Misael Fernando Vallejo Romero | |
| </span> | |
| </div> | |
| """) | |
| with gr.Group(visible=True) as pantalla_consentimiento: | |
| gr.HTML(""" | |
| <div class="bloque" style="max-width:1000px; margin:0 auto 18px auto;"> | |
| <h2 style="text-align:center;">🔒 Consentimiento Informado, Aviso de Privacidad y Confidencialidad</h2> | |
| <h3>Bienvenido(a) a SOFIA</h3> | |
| <p> | |
| SOFIA es un agente conversacional basado en Inteligencia Artificial, | |
| diseñado para brindar orientación inicial, apoyo socioemocional y | |
| acompañamiento pedagógico a estudiantes. | |
| </p> | |
| <h3>🤖 Uso de Inteligencia Artificial</h3> | |
| <p> | |
| Las respuestas de SOFIA tienen fines informativos, preventivos y de | |
| orientación inicial. Pueden ser generadas mediante modelos de | |
| Inteligencia Artificial. | |
| </p> | |
| <h3>🔐 Aviso de Privacidad</h3> | |
| <p> | |
| Los datos que proporciones voluntariamente, como nombre, matrícula, | |
| grupo y mensajes, serán utilizados para brindar orientación inicial, | |
| detectar indicadores de riesgo socioemocional, mejorar el sistema, | |
| generar información estadística y realizar seguimiento institucional | |
| cuando sea necesario. | |
| </p> | |
| <p> | |
| Los datos serán tratados conforme al Aviso de Privacidad institucional | |
| de la Universidad Estatal del Valle de Toluca y a la normativa aplicable | |
| en materia de protección de datos personales. | |
| </p> | |
| <h3>🔒 Confidencialidad</h3> | |
| <p> | |
| La información compartida será tratada con confidencialidad. Cuando se | |
| detecten indicadores de riesgo grave para la integridad del estudiante | |
| o de terceras personas, el sistema podrá recomendar la canalización | |
| inmediata con personal institucional, servicios de salud o emergencias. | |
| </p> | |
| <h3>⚠️ Limitaciones</h3> | |
| <p> | |
| SOFIA no sustituye la atención psicológica, médica o psiquiátrica; | |
| no realiza diagnósticos clínicos, no prescribe medicamentos y no | |
| reemplaza la valoración de un profesional. | |
| </p> | |
| <h3>🚨 Emergencias</h3> | |
| <p> | |
| Si existe peligro inmediato, llama al <b>911</b>, comunícate con la | |
| <b>Línea de la Vida: 800 911 2000</b> y busca el acompañamiento de una | |
| persona adulta, familiar, tutor u orientador. | |
| </p> | |
| <p> | |
| El uso de SOFIA es voluntario. Puedes abandonar la conversación en | |
| cualquier momento. | |
| </p> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| consentimiento_matricula = gr.Textbox( | |
| label="Matrícula (opcional)", | |
| placeholder="Ej. 22012345" | |
| ) | |
| consentimiento_nombre = gr.Textbox( | |
| label="Nombre (opcional)", | |
| placeholder="Escribe tu nombre" | |
| ) | |
| consentimiento_grupo = gr.Textbox( | |
| label="Grupo (opcional)", | |
| placeholder="Ej. 210" | |
| ) | |
| acepta_consentimiento = gr.Checkbox( | |
| label=( | |
| "He leído y acepto el Aviso de Privacidad, el Consentimiento " | |
| "Informado y las condiciones de uso de SOFIA." | |
| ), | |
| value=False, | |
| interactive=True | |
| ) | |
| aceptar_consentimiento_btn = gr.Button( | |
| "✅ Aceptar e iniciar conversación", | |
| variant="primary" | |
| ) | |
| estado_consentimiento = gr.Markdown("") | |
| with gr.Group(visible=False) as pantalla_agente: | |
| # ===================================================== | |
| # SELECTOR DE IDIOMA / LENGUA | |
| # ===================================================== | |
| with gr.Row(): | |
| idioma = gr.Dropdown( | |
| choices=[ | |
| "Español", | |
| "Mazahua (Jñatrjo)" | |
| ], | |
| value="Español", | |
| label="🌎 Idioma / Lengua", | |
| info=( | |
| "Selecciona el idioma de interacción. " | |
| "La modalidad Mazahua (Jñatrjo) se encuentra " | |
| "en fase demostrativa y de validación lingüística." | |
| ), | |
| interactive=True | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.HTML(""" | |
| <div class="bloque"> | |
| <h2>¿Qué puedo hacer por ti?</h2> | |
| <p>✅ Brindarte acompañamiento inicial</p> | |
| <p>✅ Detectar señales de riesgo socioemocional</p> | |
| <p>✅ Orientarte en temas emocionales y escolares</p> | |
| <p>✅ Canalizarte si necesitas ayuda urgente</p> | |
| </div> | |
| """) | |
| gr.HTML(""" | |
| <div class="bloque"> | |
| <h3>🔒 Espacio confidencial</h3> | |
| <p>Este agente no sustituye atención psicológica, médica o de emergencia.</p> | |
| <p> | |
| Si existe peligro inmediato, llama al | |
| <span style="font-weight:900; font-size:18px; color:#d32f2f;">911</span> | |
| o a la | |
| <span style="font-weight:900; font-size:18px; color:#b71c1c;">Línea de la Vida: 800 911 2000</span>. | |
| </p> | |
| </div> | |
| """) | |
| matricula = gr.Textbox( | |
| label="Matrícula (opcional)", | |
| placeholder="Ej. 22012345 o deja vacío si prefieres anonimato" | |
| ) | |
| nombre = gr.Textbox(label="Nombre", placeholder="Escribe tu nombre") | |
| grupo = gr.Textbox(label="Grupo", placeholder="Ej. 210") | |
| with gr.Column(scale=2): | |
| gr.HTML(""" | |
| <div class="bloque"> | |
| <p style="font-size:19px; font-weight:900; color:#0b6b43; margin-bottom:10px;"> | |
| 🔒 Hola, este es un espacio seguro para ti. | |
| </p> | |
| <p style="font-size:16px; line-height:1.6; color:#333;"> | |
| Puedes hablar conmigo con confianza.<br> | |
| Todo lo que compartas será tratado con seriedad, respeto y | |
| <b>confidencialidad</b>, conforme al aviso de privacidad institucional.<br><br> | |
| Estoy aquí para apoyarte. Si te sientes en riesgo o necesitas ayuda urgente, | |
| busca a tu tutor, orientador del bienestar o a un adulto de confianza.<br><br> | |
| <span style="font-weight:900; font-size:17px;">No estás solo.</span> | |
| </p> | |
| </div> | |
| """) | |
| historial = gr.Textbox( | |
| label="Conversación", | |
| lines=16, | |
| interactive=False, | |
| placeholder="Aquí aparecerá la conversación..." | |
| ) | |
| mensaje = gr.Textbox( | |
| label="Mensaje del estudiante", | |
| placeholder="Escribe aquí cómo te sientes o qué necesitas..." | |
| ) | |
| audio_entrada = gr.Audio( | |
| sources=["microphone"], | |
| type="filepath", | |
| label="🎤 Habla con SOFIA" | |
| ) | |
| audio_salida = gr.Audio( | |
| label="🔊 Voz de SOFIA", | |
| autoplay=True, | |
| interactive=False | |
| ) | |
| with gr.Row(): | |
| enviar = gr.Button("Enviar", variant="primary") | |
| limpiar = gr.Button("Limpiar") | |
| respuesta_box = gr.Textbox(label="Respuesta del agente", lines=10, visible=False) | |
| with gr.Column(scale=1): | |
| gr.HTML(""" | |
| <div class="bloque"> | |
| <h2>Acciones rápidas</h2> | |
| <p>📋 Evaluación rápida</p> | |
| <p>📚 Recursos de apoyo</p> | |
| <p>📞 Contactar tutor u orientador</p> | |
| <p>🧠 Acompañamiento emocional inicial</p> | |
| </div> | |
| """) | |
| gr.HTML(""" | |
| <div class="bloque estado"> | |
| Estado del agente: En línea 🟢 | |
| </div> | |
| """) | |
| crisis_btn = gr.Button("🚨 ¡Necesito ayuda urgente!", variant="stop") | |
| gr.HTML(""" | |
| <div class="bloque" style="padding:14px; margin-top:10px;"> | |
| <h3 style="margin-top:0;">📞 Contactos de apoyo</h3> | |
| <a href="https://wa.me/527295254521?text=Hola,%20soy%20estudiante%20y%20necesito%20apoyo%20socioemocional." | |
| target="_blank" rel="noopener noreferrer" style="text-decoration:none;"> | |
| <button style=" | |
| width:100%; | |
| background:#25D366; | |
| color:white; | |
| padding:12px; | |
| border:none; | |
| border-radius:10px; | |
| font-size:16px; | |
| font-weight:bold; | |
| cursor:pointer; | |
| margin-top:8px;"> | |
| 💬 Orientador 1 | |
| </button> | |
| </a> | |
| <!-- Sustituye 52XXXXXXXXXX por el número real del segundo orientador --> | |
| <a href="https://wa.me/527226922008?text=Hola,%20soy%20estudiante%20y%20necesito%20apoyo%20socioemocional." | |
| target="_blank" rel="noopener noreferrer" style="text-decoration:none;"> | |
| <button style=" | |
| width:100%; | |
| background:#128C7E; | |
| color:white; | |
| padding:12px; | |
| border:none; | |
| border-radius:10px; | |
| font-size:16px; | |
| font-weight:bold; | |
| cursor:pointer; | |
| margin-top:8px;"> | |
| 💬 Orientador 2 | |
| </button> | |
| </a> | |
| <a href="tel:8009112000" style="text-decoration:none;"> | |
| <button style=" | |
| width:100%; | |
| background:#7E22CE; | |
| color:white; | |
| padding:12px; | |
| border:none; | |
| border-radius:10px; | |
| font-size:16px; | |
| font-weight:bold; | |
| cursor:pointer; | |
| margin-top:8px;"> | |
| ☎️ Línea de la Vida: 800 911 2000 | |
| </button> | |
| </a> | |
| <a href="tel:911" style="text-decoration:none;"> | |
| <button style=" | |
| width:100%; | |
| background:#D32F2F; | |
| color:white; | |
| padding:12px; | |
| border:none; | |
| border-radius:10px; | |
| font-size:16px; | |
| font-weight:bold; | |
| cursor:pointer; | |
| margin-top:8px;"> | |
| 🚨 Llamar al 911 | |
| </button> | |
| </a> | |
| </div> | |
| """) | |
| analisis_box = gr.Textbox( | |
| label="Análisis emocional", | |
| lines=18, | |
| visible=False | |
| ) | |
| aceptar_consentimiento_btn.click( | |
| guardar_consentimiento, | |
| inputs=[ | |
| acepta_consentimiento, | |
| consentimiento_matricula, | |
| consentimiento_nombre, | |
| consentimiento_grupo | |
| ], | |
| outputs=[ | |
| pantalla_consentimiento, | |
| pantalla_agente, | |
| matricula, | |
| nombre, | |
| grupo, | |
| estado_consentimiento | |
| ] | |
| ) | |
| enviar.click( | |
| responder_chat_con_voz, | |
| inputs=[matricula, nombre, grupo, idioma, mensaje, historial], | |
| outputs=[historial, mensaje, respuesta_box, analisis_box, audio_salida] | |
| ) | |
| mensaje.submit( | |
| responder_chat_con_voz, | |
| inputs=[matricula, nombre, grupo, idioma, mensaje, historial], | |
| outputs=[historial, mensaje, respuesta_box, analisis_box, audio_salida] | |
| ) | |
| limpiar.click( | |
| limpiar_chat_con_voz, | |
| inputs=[], | |
| outputs=[historial, mensaje, respuesta_box, analisis_box, audio_entrada, audio_salida] | |
| ) | |
| crisis_btn.click( | |
| activar_ayuda_urgente_con_voz, | |
| inputs=[nombre], | |
| outputs=[mensaje, respuesta_box, analisis_box, audio_salida] | |
| ) | |
| audio_entrada.change( | |
| responder_desde_microfono, | |
| inputs=[matricula, nombre, grupo, idioma, audio_entrada, historial], | |
| outputs=[historial, mensaje, respuesta_box, analisis_box, audio_salida, audio_entrada] | |
| ) | |
| if __name__ == "__main__": | |
| print("Iniciando app tesis...") | |
| demo.launch() |