sofia-ai / app.py
conalep-agente's picture
Update app.py
db183c2 verified
Raw
History Blame Contribute Delete
61.5 kB
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()