GoergeMarckus commited on
Commit
c430985
·
verified ·
1 Parent(s): cba3fa0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +192 -176
app.py CHANGED
@@ -4,26 +4,28 @@ from audio_recorder_streamlit import audio_recorder
4
  import base64
5
  import io
6
  import soundfile as sf
7
- import tempfile
8
 
9
- # ======================================
10
- # CONFIGURACIÓN GENERAL DEL SPACE
11
- # ======================================
12
  st.set_page_config(
13
- page_title="AIDEN – Voz Latina en Vivo",
14
  layout="centered",
15
  page_icon="🎙️",
16
  )
17
 
18
- # ======================================
19
- # LOGOTIPO Y ENCABEZADO
20
- # ======================================
21
  def cargar_logo(ruta: str) -> str:
22
  with open(ruta, "rb") as f:
23
  data = f.read()
24
  return base64.b64encode(data).decode("utf-8")
25
 
26
 
 
 
27
  logo_b64 = cargar_logo("assets/aiden_logo.png")
28
 
29
  st.markdown(
@@ -31,49 +33,60 @@ st.markdown(
31
  <div style="text-align:center; margin-top:20px;">
32
  <img src="data:image/png;base64,{logo_b64}" width="180">
33
  <h1 style="font-family:sans-serif; color:white; margin-top:10px;">
34
- AIDEN — Voz Latina en Vivo
35
  </h1>
36
  <p style="color:#cccccc; font-size:17px;">
37
- Conversación por voz en español latino, con tono humano, cálido y profesional.
38
  </p>
39
  </div>
40
  """,
41
  unsafe_allow_html=True,
42
  )
43
 
44
- st.write("") # pequeño espacio visual
45
 
46
- # ======================================
47
- # MODELOS
48
- # ======================================
49
- TEXT_MODEL_NAME = "Qwen/Qwen2.5-1.5B-Instruct"
50
- ASR_MODEL_NAME = "openai/whisper-small" # Voz → texto (multilingüe)
51
- TTS_MODEL_NAME = "facebook/mms-tts-spa" # Texto voz (español)
 
 
52
 
53
 
54
  @st.cache_resource
55
  def cargar_modelo_texto():
56
- generator = pipeline(
57
- task="text-generation",
58
- model=TEXT_MODEL_NAME,
59
- torch_dtype="auto",
60
- device_map="auto",
61
- )
 
 
 
 
 
 
 
 
62
  return generator
63
 
64
 
65
  @st.cache_resource
66
  def cargar_modelo_asr():
 
67
  asr = pipeline(
68
  task="automatic-speech-recognition",
69
  model=ASR_MODEL_NAME,
70
- device_map="auto",
71
  )
72
  return asr
73
 
74
 
75
  @st.cache_resource
76
  def cargar_modelo_tts():
 
77
  tts = pipeline(
78
  task="text-to-speech",
79
  model=TTS_MODEL_NAME,
@@ -82,128 +95,70 @@ def cargar_modelo_tts():
82
 
83
 
84
  text_gen = cargar_modelo_texto()
85
- asr = cargar_modelo_asr()
86
- tts = cargar_modelo_tts()
 
87
 
88
- # ======================================
89
- # PERSONALIDAD / SISTEMA DE AIDEN VOZ
90
- # ======================================
91
- AIDEN_VOICE_SYSTEM = """
 
92
  Eres AIDEN, una inteligencia artificial latina creada por la agencia JMC Studio Digital
93
  en Guayaquil, Ecuador, desarrollada por George Márquez.
94
 
95
- Este espacio está enfocado en conversación POR VOZ, pero piensas en texto
96
- y respondes SIEMPRE en ESPAÑOL LATINO neutro.
97
-
98
- Estilo de AIDEN VOZ:
99
- - Tono humano, cálido, cercano y profesional.
100
- - Masculino adulto, seguro, sereno (como un consultor tecnológico latino).
101
- - Sin emojis.
102
- - Lenguaje claro, natural y directo.
103
- - Puedes ser un poco más cercano si el tema es personal, pero siempre respetuoso.
104
-
105
- Normas estrictas:
106
- 1. NO repitas la pregunta del usuario.
107
- 2. NO inventes información fuera del tema actual.
108
- 3. NO cambies de tema a menos que el usuario lo indique.
109
- 4. Responde en 1–3 párrafos.
110
- 5. Si el usuario pide una cantidad específica (ej: 3 ideas, 5 ejemplos),
111
- respóndelas con una lista numerada breve y clara.
112
- 6. Mantén siempre el foco en la conversación actual.
113
- 7. Si no estás seguro de algo, dilo con honestidad.
114
- 8. Si preguntan quién te creó, responde SIEMPRE:
115
  "Fui creado por JMC Studio Digital en Guayaquil, Ecuador, desarrollado por George Márquez."
116
- 9. No hagas chistes innecesarios ni uses emojis. humano, pero profesional.
117
-
118
- Frase final:
119
- Al terminar tu respuesta, añade SIEMPRE esta línea:
120
- "Si necesitas más detalles o deseas explorar otro tema, estoy aquí para ayudarte."
121
  """
122
 
123
- # ======================================
124
- # SESIÓN / HISTORIAL
125
- # ======================================
 
126
  if "voice_history" not in st.session_state:
127
- # Lista de dicts: {"role": "user" / "assistant", "content": str}
128
  st.session_state["voice_history"] = []
129
 
130
 
131
- # ======================================
132
- # UTILIDADES
133
- # ======================================
 
134
  def construir_prompt_voz(user_message: str) -> str:
135
- """
136
- Construye el prompt para AIDEN Voz usando:
137
- - Mensaje del sistema
138
- - Últimos turnos de conversación de voz
139
- """
140
  contexto = ""
141
- for msg in st.session_state["voice_history"][-6:]:
142
- if msg["role"] == "user":
143
- contexto += f"Usuario: {msg['content']}\n"
144
- else:
145
- contexto += f"AIDEN: {msg['content']}\n"
146
 
147
  prompt = (
148
- f"{AIDEN_VOICE_SYSTEM}\n\n"
149
- f"{contexto}"
150
  f"Usuario: {user_message}\n"
151
  f"AIDEN:"
152
  )
153
  return prompt
154
 
155
 
156
- def limpiar_respuesta(respuesta_cruda: str, prompt: str) -> str:
157
- """
158
- Limpia la respuesta:
159
- - Quita el prompt inicial
160
- - Elimina prefijos tipo 'Usuario:' o 'AIDEN:'
161
- - Corta frases de cierre automáticas no deseadas
162
- - Añade la frase final elegante
163
- """
164
- # 1. Recortar el prompt
165
- texto = respuesta_cruda[len(prompt):].strip()
166
-
167
- # 2. Remover prefijos de turno
168
- prefijos = ["Usuario:", "AIDEN:", "Assistant:", "User:"]
169
- for p in prefijos:
170
- if texto.startswith(p):
171
- texto = texto[len(p):].strip()
172
-
173
- # 3. Cortar residuos muy típicos
174
- frases_corte = [
175
- "Pregunta anterior",
176
- "Respuesta anterior",
177
- "¿Hay algo más",
178
- "Gracias por la consulta",
179
- "Si deseas saber más",
180
- "¿Quieres explorar otro tema",
181
- ]
182
- for frase in frases_corte:
183
- if frase in texto:
184
- texto = texto.split(frase)[0].strip()
185
-
186
- # 4. Quitar dobles saltos de línea extra al final
187
- texto = texto.strip()
188
-
189
- # 5. Garantizar cierre final elegante UNA sola vez
190
- cierre = "Si necesitas más detalles o deseas explorar otro tema, estoy aquí para ayudarte."
191
- if not texto.endswith(cierre):
192
- # Evitamos duplicar si ya lo dijo en medio
193
- if cierre in texto:
194
- # Nos quedamos hasta el final de la primera vez que lo dice
195
- texto = texto.split(cierre)[0].strip()
196
- texto = texto.rstrip(".") # evitar doble punto
197
- texto = texto + ".\n\n" + cierre
198
-
199
- return texto
200
-
201
-
202
  def generar_respuesta_voz(user_message: str) -> str:
203
- """
204
- Genera la respuesta de AIDEN para una entrada (texto ya transcrito).
205
- """
206
  prompt = construir_prompt_voz(user_message)
 
207
  result = text_gen(
208
  prompt,
209
  max_new_tokens=220,
@@ -211,87 +166,148 @@ def generar_respuesta_voz(user_message: str) -> str:
211
  temperature=0.7,
212
  top_p=0.9,
213
  )
214
- generado = result[0]["generated_text"]
215
- respuesta_limpia = limpiar_respuesta(generado, prompt)
216
 
217
- # Actualizar historial
218
- st.session_state["voice_history"].append(
219
- {"role": "user", "content": user_message}
220
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  st.session_state["voice_history"].append(
222
- {"role": "assistant", "content": respuesta_limpia}
223
  )
224
 
225
- return respuesta_limpia
226
 
227
 
228
- def texto_a_wav_bytes(texto: str) -> bytes:
229
- """
230
- Convierte texto a audio WAV en memoria usando MMS TTS español.
231
- """
232
- out = tts(texto)
233
- audio = out["audio"] # numpy array
234
- sr = out["sampling_rate"] # frecuencia de muestreo
 
235
 
236
  buf = io.BytesIO()
237
- # PCM_16 para máxima compatibilidad con el reproductor web
238
- sf.write(buf, audio, sr, format="WAV", subtype="PCM_16")
239
  buf.seek(0)
240
  return buf.read()
241
 
242
 
243
- def transcribir_audio(audio_bytes: bytes) -> str:
244
- """
245
- Guarda los bytes como WAV temporal y usa Whisper para transcribir.
246
- """
247
- with tempfile.NamedTemporaryFile(suffix=".wav", delete=True) as tmp:
248
- tmp.write(audio_bytes)
249
- tmp.flush()
250
- out = asr(tmp.name)
251
- texto = out.get("text", "").strip()
252
- return texto
253
 
 
254
 
255
- # ======================================
256
- # INTERFAZ DE USUARIO AIDEN VOZ
257
- # ======================================
258
- st.subheader("🎙️ Conversa con AIDEN por voz")
259
 
260
- st.markdown(
261
- "Pulsa el botón para grabar, habla con naturalidad y espera a que AIDEN "
262
- "transcriba y responda con voz latina profesional."
 
 
 
263
  )
264
 
265
- audio_bytes = audio_recorder()
 
 
266
 
267
  if audio_bytes:
268
- st.markdown("#### 🎧 Tu audio grabado")
269
- st.audio(audio_bytes, format="audio/wav")
 
 
270
 
271
- with st.spinner("Transcribiendo y generando respuesta de AIDEN…"):
272
- try:
273
- texto_usuario = transcribir_audio(audio_bytes)
274
- if not texto_usuario:
275
- st.error("No pude reconocer nada en el audio. Intenta hablar un poco más claro o más cerca del micrófono.")
276
- else:
277
- st.markdown("#### 🗣️ Tú dijiste:")
278
- st.write(texto_usuario)
279
 
280
- respuesta_aiden = generar_respuesta_voz(texto_usuario)
 
281
 
282
- st.markdown("#### 🤖 Respuesta de AIDEN (texto):")
283
- st.write(respuesta_aiden)
284
 
285
- audio_respuesta = texto_a_wav_bytes(respuesta_aiden)
 
 
 
286
 
287
- st.markdown("#### 🔊 Respuesta de AIDEN (voz):")
288
- st.audio(audio_respuesta, format="audio/wav")
 
 
 
 
 
 
 
289
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  except Exception as e:
291
- st.error(f"Error procesando la conversación por voz: {str(e)}")
292
 
293
  st.write("---")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  st.markdown(
295
- "<p style='text-align:center; color:gray;'>AIDEN — Voz Latina • Desarrollado por JMC Studio Digital</p>",
296
  unsafe_allow_html=True,
297
  )
 
4
  import base64
5
  import io
6
  import soundfile as sf
7
+ import torch
8
 
9
+ # =====================================
10
+ # CONFIGURACIÓN GENERAL DE LA APP
11
+ # =====================================
12
  st.set_page_config(
13
+ page_title="AIDEN – Voz Conversacional Latina",
14
  layout="centered",
15
  page_icon="🎙️",
16
  )
17
 
18
+ # =====================================
19
+ # LOGOTIPO AIDEN
20
+ # =====================================
21
  def cargar_logo(ruta: str) -> str:
22
  with open(ruta, "rb") as f:
23
  data = f.read()
24
  return base64.b64encode(data).decode("utf-8")
25
 
26
 
27
+ # IMPORTANTE: asegúrate de que este archivo exista:
28
+ # assets/aiden_logo.png
29
  logo_b64 = cargar_logo("assets/aiden_logo.png")
30
 
31
  st.markdown(
 
33
  <div style="text-align:center; margin-top:20px;">
34
  <img src="data:image/png;base64,{logo_b64}" width="180">
35
  <h1 style="font-family:sans-serif; color:white; margin-top:10px;">
36
+ AIDEN — Voz Conversacional Latina
37
  </h1>
38
  <p style="color:#cccccc; font-size:17px;">
39
+ Habla con AIDEN en español latino, con tono humano, cálido y profesional.
40
  </p>
41
  </div>
42
  """,
43
  unsafe_allow_html=True,
44
  )
45
 
46
+ st.write("")
47
 
48
+
49
+ # =====================================
50
+ # CARGA DE MODELOS
51
+ # =====================================
52
+
53
+ TEXT_MODEL_NAME = "Qwen/Qwen2.5-1.5B-Instruct" # mismo cerebro que AIDEN TEXTO
54
+ ASR_MODEL_NAME = "openai/whisper-base" # voz -> texto
55
+ TTS_MODEL_NAME = "hexgrad/Kokoro-82M" # texto -> voz (Kokoro)
56
 
57
 
58
  @st.cache_resource
59
  def cargar_modelo_texto():
60
+ """Carga el modelo de texto Qwen en GPU si existe, sino en CPU."""
61
+ kwargs = {"task": "text-generation", "model": TEXT_MODEL_NAME}
62
+ try:
63
+ if torch.cuda.is_available():
64
+ kwargs["device_map"] = "auto"
65
+ kwargs["torch_dtype"] = torch.float16
66
+ else:
67
+ # CPU
68
+ kwargs["device_map"] = "cpu"
69
+ except Exception:
70
+ # Si por alguna razón falla la detección de CUDA, que siga en modo default
71
+ pass
72
+
73
+ generator = pipeline(**kwargs)
74
  return generator
75
 
76
 
77
  @st.cache_resource
78
  def cargar_modelo_asr():
79
+ """Carga Whisper-base para reconocimiento de voz."""
80
  asr = pipeline(
81
  task="automatic-speech-recognition",
82
  model=ASR_MODEL_NAME,
 
83
  )
84
  return asr
85
 
86
 
87
  @st.cache_resource
88
  def cargar_modelo_tts():
89
+ """Carga Kokoro para texto a voz."""
90
  tts = pipeline(
91
  task="text-to-speech",
92
  model=TTS_MODEL_NAME,
 
95
 
96
 
97
  text_gen = cargar_modelo_texto()
98
+ asr_pipe = cargar_modelo_asr()
99
+ tts_pipe = cargar_modelo_tts()
100
+
101
 
102
+ # =====================================
103
+ # PERSONALIDAD DE AIDEN (VERSIÓN VOZ)
104
+ # =====================================
105
+
106
+ AIDEN_SYSTEM_PROMPT_VOZ = """
107
  Eres AIDEN, una inteligencia artificial latina creada por la agencia JMC Studio Digital
108
  en Guayaquil, Ecuador, desarrollada por George Márquez.
109
 
110
+ Tu misión en este espacio es conversar por VOZ con las personas, en español latino neutro.
111
+ Estilo:
112
+ - Tono humano, cálido, cercano y respetuoso.
113
+ - Profesional cuando hablas de trabajo, negocios, tecnología, ciencia o IA.
114
+ - Más relajado y amigable cuando te piden compañía, temas personales o cotidianos.
115
+ - Siempre educado, empático y claro.
116
+
117
+ Reglas:
118
+ 1. Responde SIEMPRE en español latino neutro.
119
+ 2. NO repitas la pregunta del usuario.
120
+ 3. No abras temas nuevos que el usuario no ha pedido.
121
+ 4. Responde en 1 a 3 párrafos máximo.
122
+ 5. Nada de emojis, ni chistes fuera de lugar. Sólo calidez humana y serenidad.
123
+ 6. Si el usuario te pregunta quién te creó, responde:
 
 
 
 
 
 
124
  "Fui creado por JMC Studio Digital en Guayaquil, Ecuador, desarrollado por George Márquez."
125
+ 7. Tu respuesta debe sonar natural cuando se lea en voz alta.
 
 
 
 
126
  """
127
 
128
+ # =====================================
129
+ # ESTADO: HISTORIAL DE VOZ
130
+ # =====================================
131
+
132
  if "voice_history" not in st.session_state:
133
+ # Lista de {"user": str, "assistant": str}
134
  st.session_state["voice_history"] = []
135
 
136
 
137
+ # =====================================
138
+ # FUNCIONES DE LÓGICA
139
+ # =====================================
140
+
141
  def construir_prompt_voz(user_message: str) -> str:
142
+ """Construye el prompt para Qwen usando las últimas interacciones de voz."""
 
 
 
 
143
  contexto = ""
144
+ # Tomamos las últimas 4 interacciones para contexto
145
+ for turno in st.session_state["voice_history"][-4:]:
146
+ contexto += f"Usuario: {turno['user']}\n"
147
+ contexto += f"AIDEN: {turno['assistant']}\n"
 
148
 
149
  prompt = (
150
+ f"{AIDEN_SYSTEM_PROMPT_VOZ}\n\n"
151
+ f"{contexto}\n"
152
  f"Usuario: {user_message}\n"
153
  f"AIDEN:"
154
  )
155
  return prompt
156
 
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  def generar_respuesta_voz(user_message: str) -> str:
159
+ """Genera respuesta de AIDEN para voz (texto que después se convertirá a audio)."""
 
 
160
  prompt = construir_prompt_voz(user_message)
161
+
162
  result = text_gen(
163
  prompt,
164
  max_new_tokens=220,
 
166
  temperature=0.7,
167
  top_p=0.9,
168
  )
 
 
169
 
170
+ full_text = result[0]["generated_text"]
171
+ raw = full_text[len(prompt):].strip()
172
+
173
+ # limpieza de prefijos tipo diálogo
174
+ for pref in ["Usuario:", "AIDEN:", "User:", "Assistant:"]:
175
+ if raw.startswith(pref):
176
+ raw = raw[len(pref):].strip()
177
+
178
+ # limpieza simple de frases genéricas largas
179
+ cortes = [
180
+ "Pregunta anterior",
181
+ "Respuesta anterior",
182
+ "¿Hay algo más",
183
+ "Si necesitas más detalles",
184
+ "Si deseas profundizar",
185
+ "¿Te gustaría saber algo más",
186
+ ]
187
+ for frase in cortes:
188
+ if frase in raw:
189
+ raw = raw.split(frase)[0].strip()
190
+
191
+ respuesta = raw.strip()
192
+
193
+ # Guardamos en historial
194
  st.session_state["voice_history"].append(
195
+ {"user": user_message, "assistant": respuesta}
196
  )
197
 
198
+ return respuesta
199
 
200
 
201
+ def texto_a_audio_bytes(texto: str) -> bytes:
202
+ """Convierte texto en audio WAV usando Kokoro y lo devuelve como bytes."""
203
+ if not texto.strip():
204
+ texto = "No recibí texto para convertir en voz."
205
+
206
+ out = tts_pipe(texto)
207
+ audio = out["audio"] # numpy array
208
+ sr = out["sampling_rate"] # frecuencia de muestreo
209
 
210
  buf = io.BytesIO()
211
+ sf.write(buf, audio, sr, format="wav")
 
212
  buf.seek(0)
213
  return buf.read()
214
 
215
 
216
+ # =====================================
217
+ # INTERFAZ PRINCIPAL — AIDEN VOZ
218
+ # =====================================
 
 
 
 
 
 
 
219
 
220
+ st.subheader("🎙️ Habla con AIDEN por voz")
221
 
222
+ st.caption(
223
+ "Pulsa el botón para grabar tu mensaje. "
224
+ "AIDEN lo entenderá, responderá en texto y luego hablará en voz."
225
+ )
226
 
227
+ # Widget de grabación de audio
228
+ audio_bytes = audio_recorder(
229
+ text="🎙️ Pulsa aquí para hablar con AIDEN",
230
+ recording_color="#1976D2", # azul AIDEN
231
+ neutral_color="#444444",
232
+ icon_size="3x",
233
  )
234
 
235
+ user_transcript = None
236
+ respuesta_aiden = None
237
+ respuesta_audio = None
238
 
239
  if audio_bytes:
240
+ try:
241
+ # 1) Reconocimiento de voz (Whisper)
242
+ asr_output = asr_pipe(audio_bytes)
243
+ user_transcript = asr_output.get("text", "").strip()
244
 
245
+ if not user_transcript:
246
+ st.warning("No pude entender claramente el audio. Intenta hablar un poco más fuerte o más cerca del micrófono.")
247
+ else:
248
+ # 2) Generar respuesta de AIDEN (texto)
249
+ respuesta_aiden = generar_respuesta_voz(user_transcript)
 
 
 
250
 
251
+ # 3) Generar voz de AIDEN
252
+ respuesta_audio = texto_a_audio_bytes(respuesta_aiden)
253
 
254
+ except Exception as e:
255
+ st.error(f"Error procesando el audio: {e}")
256
 
257
+ # Mostrar resultados del turno actual
258
+ if user_transcript:
259
+ st.markdown("### 📝 Texto reconocido")
260
+ st.write(user_transcript)
261
 
262
+ if respuesta_aiden:
263
+ st.markdown("### 🤖 Respuesta de AIDEN (texto)")
264
+ st.write(respuesta_aiden)
265
+
266
+ if respuesta_audio:
267
+ st.markdown("### 🔊 Respuesta de AIDEN (voz)")
268
+ st.audio(respuesta_audio, format="audio/wav")
269
+
270
+ st.write("---")
271
 
272
+ # =====================================
273
+ # SECCIÓN EXTRA — TEXTO → VOZ DIRECTA
274
+ # =====================================
275
+ st.subheader("📝 → 🔊 Escribe algo y escucha cómo lo diría AIDEN")
276
+
277
+ texto_manual = st.text_input(
278
+ "Escribe un texto para que AIDEN lo diga en voz alta:",
279
+ key="manual_tts_input",
280
+ )
281
+
282
+ if st.button("🔊 Reproducir voz de AIDEN", disabled=not bool(texto_manual.strip())):
283
+ if not texto_manual.strip():
284
+ st.warning("Escribe primero un texto para convertirlo en voz.")
285
+ else:
286
+ try:
287
+ audio_manual = texto_a_audio_bytes(texto_manual.strip())
288
+ st.audio(audio_manual, format="audio/wav")
289
  except Exception as e:
290
+ st.error(f"Error generando la voz de AIDEN: {e}")
291
 
292
  st.write("---")
293
+
294
+ # =====================================
295
+ # HISTORIAL DE CONVERSACIÓN
296
+ # =====================================
297
+ st.markdown("### 🧾 Historial de conversación por voz")
298
+
299
+ if not st.session_state["voice_history"]:
300
+ st.info("Aún no hay historial. Graba tu primer mensaje para iniciar la conversación.")
301
+ else:
302
+ for turno in reversed(st.session_state["voice_history"]):
303
+ st.markdown(f"**Tú:** {turno['user']}")
304
+ st.markdown(f"**AIDEN:** {turno['assistant']}")
305
+ st.markdown("---")
306
+
307
+ # =====================================
308
+ # FOOTER
309
+ # =====================================
310
  st.markdown(
311
+ "<p style='text-align:center; color:gray;'>AIDEN — Desarrollado por JMC Studio Digital</p>",
312
  unsafe_allow_html=True,
313
  )