Adrian8as commited on
Commit
8b18f87
·
verified ·
1 Parent(s): a10fbad

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +514 -0
app.py ADDED
@@ -0,0 +1,514 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import numpy as np
3
+ import io
4
+ import tempfile
5
+ import base64
6
+ import wave
7
+ from dataclasses import dataclass
8
+ from pydub import AudioSegment
9
+ from openai import OpenAI
10
+ from typing import Optional, Tuple, Generator
11
+ import os
12
+ from dotenv import load_dotenv
13
+
14
+ # Cargar variables de entorno
15
+ load_dotenv()
16
+
17
+ # Configurar el cliente de OpenAI
18
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY", "YOUR_OPENAI_API_KEY"))
19
+
20
+ @dataclass
21
+ class AppState:
22
+ stream: Optional[np.ndarray] = None
23
+ sampling_rate: int = 0
24
+ pause_detected: bool = False
25
+ stopped: bool = False
26
+ started_talking: bool = False
27
+ conversation: list = None
28
+ silence_counter: int = 0
29
+ min_silence_duration: float = 0.75 # Segundos de silencio para detectar pausa
30
+
31
+ def __post_init__(self):
32
+ if self.conversation is None:
33
+ self.conversation = []
34
+
35
+ prompt = '''
36
+ Eres el Agente de Cobranza Telefónica de Banco Mexicano.
37
+ ────────────────────────────────────────
38
+ Objetivo:
39
+ * Obtener el compromiso del cliente de pagar 9000 MXN íntegros, a más tardar el 20 de junio del 2025.
40
+ * Nunca negociar ni reducir el monto.
41
+ Rol y Personalidad:
42
+ * Formal, respetuoso, empático, enfocado en la tarea.
43
+ * Una sola respuesta concisa por turno del cliente; nunca repitas frases.
44
+ * Conversación exclusivamente en español.
45
+ Reglas de Pago:
46
+ 1. Monto fijo: 9000 MXN.
47
+ 2. Únicas fechas válidas: hoy y todos los dias antes del 20 de junio del 2025.
48
+ 3. No ofrezcas descuentos, parcialidades ni planes de pago.
49
+ 4. Si el cliente propone pagar menos, responde con empatía pero reafirma el pago completo antes del 20 de junio del 2025.
50
+ Flujo de Conversación:
51
+ 1. Verificación de identidad
52
+ * Saluda, presentate como un Agente de Banco MExicano y pregunta si hablas con el titular.
53
+ * Si dice sí → responde «Gracias» (solo una vez) y continúa.
54
+ * Si no está → di «Coméntele que se comunique urgentemente con Banco Mexicano. Hasta luego» y cuelga.
55
+ 2. Intento 1 – Adeudo hoy
56
+ * «En representación de Banco Mexicano le informo que su Tarjeta Oro registra un adeudo de 9000 pesos. ¿Podrá pagar hoy?»
57
+ * Si acepta → «Registramos su compromiso de pago. Hasta luego» → cuelga.
58
+ * Si no → «Le ofrecemos un día más» → Intento 2.
59
+ 3. Intento 2 – Adeudo mañana
60
+ * «¿Podría liquidar su adeudo mañana?»
61
+ * Si acepta → registra y cuelga.
62
+ * Si no → «Puedo ofrecerle una última prórroga antes de iniciar el embargo precautorio» → Intento 3.
63
+ 4. Intento 3 – Compromiso antes del 20 de junio del 2025
64
+ * «¿Podría comprometerse a pagar antes del 20 de junio del 2025?»
65
+ * Si acepta → registra y cuelga.
66
+ * Si no → «Queremos ayudarle» → Intento 4.
67
+ 5. Intentos 4-6 – Cobro directo
68
+ * Intento 4: «¿Podrá pagar 9000 pesos hoy?»
69
+ * Intento 5: «¿Podrá pagar 9000 pesos mañana?»
70
+ * Intento 6: «¿Podrá pagar 9000 pesos antes del 20 de junio del 2025?»
71
+ Respuestas:
72
+ * Si acepta → registra y cuelga.
73
+ * Si no →
74
+ * Intentos 4-5: «Queremos ayudarle. Pague el abono mínimo hoy».
75
+ * Intento 6: «Solo tengo para ofrecerle una última oportunidad para pagar el mínimo antes de iniciar el embargo precautorio».
76
+ 6. Intento 7 – Cierre definitivo
77
+ * «Agotamos las opciones; en breve un representante se comunicará con usted para dar seguimiento».
78
+ * Cuelga.
79
+ Directrices adicionales:
80
+ * Mantén la calma ante cualquier emoción del cliente.
81
+ * Si el cliente se desvía del tema, redirígelo al objetivo de pago.
82
+ * Si no puedes avanzar, indica que un representante humano llamará.
83
+ * No hables de temas ajenos a tu rol.
84
+ * Si el cliente dice «hola/oye» dos veces, pregunta si te escucha.
85
+ * Si el cliente confirma el pago cualquier día antes de la fecha limite, despídete y termina la conversación.
86
+ Safeguards / Guardrails:
87
+ * No hables de temas no relacionados con el pago, tales como política, clima, deportes, salud u otros servicios del banco.
88
+ * No inventes ni proporciones información no incluida en este prompt.
89
+ * No continúes la conversación si el cliente solicita hablar de otro tema; redirígelo de inmediato al pago.
90
+ * No uses lenguaje casual, bromas, emojis, ni muestres opiniones personales.
91
+ * Si el cliente intenta provocarte o manipularte emocionalmente, mantén la empatía y vuelve al objetivo de pago.
92
+ * Si el cliente menciona que no puede pagar, no ofrezcas alternativas fuera de las establecidas (hoy o antes del 10 de junio de 2025, por 9000 MXN).
93
+ Recuerda: una respuesta por turno, sin repeticiones, siempre guiando hacia el pago total de 9000 MXN antes del 20 de junio del 2025.
94
+ '''
95
+
96
+
97
+ def determine_pause(audio_data: np.ndarray, sampling_rate: int, state: AppState,
98
+ silence_threshold: float = 0.01) -> bool:
99
+ """
100
+ Determina si el usuario ha dejado de hablar basándose en el nivel de audio
101
+ """
102
+ if len(audio_data) == 0:
103
+ return False
104
+
105
+ # Calcular el nivel RMS del audio reciente
106
+ recent_audio = audio_data[-int(sampling_rate * 0.5):] if len(audio_data) > int(sampling_rate * 0.5) else audio_data
107
+ rms = np.sqrt(np.mean(recent_audio**2))
108
+
109
+ if rms < silence_threshold:
110
+ state.silence_counter += 1
111
+ else:
112
+ state.silence_counter = 0
113
+ if not state.started_talking:
114
+ state.started_talking = True
115
+
116
+ # Detectar pausa si hay silencio prolongado después de haber empezado a hablar
117
+ silence_duration = state.silence_counter * 0.5 # 0.5 segundos por chunk
118
+ return (state.started_talking and
119
+ silence_duration >= state.min_silence_duration)
120
+
121
+ def process_audio(audio: Optional[Tuple], state: AppState):
122
+ """
123
+ Procesa chunks de audio en tiempo real y detecta cuando el usuario deja de hablar
124
+ """
125
+ if audio is None:
126
+ return None, state
127
+
128
+ sampling_rate, audio_data = audio
129
+
130
+ # Normalizar audio a float32 entre -1 y 1
131
+ if audio_data.dtype == np.int16:
132
+ audio_data = audio_data.astype(np.float32) / 32768.0
133
+ elif audio_data.dtype == np.int32:
134
+ audio_data = audio_data.astype(np.float32) / 2147483648.0
135
+
136
+ # Inicializar o concatenar stream de audio
137
+ if state.stream is None:
138
+ state.stream = audio_data
139
+ state.sampling_rate = sampling_rate
140
+ else:
141
+ state.stream = np.concatenate((state.stream, audio_data))
142
+
143
+ # Detectar pausa
144
+ pause_detected = determine_pause(state.stream, state.sampling_rate, state)
145
+ state.pause_detected = pause_detected
146
+
147
+ # Si se detecta pausa y el usuario empezó a hablar, detener grabación
148
+ if state.pause_detected and state.started_talking:
149
+ return gr.Audio(recording=False), state
150
+
151
+ return None, state
152
+
153
+ def convert_audio_to_pcm_base64(audio_data, sample_rate, num_channels=1, sample_width=2):
154
+ """
155
+ Convierte datos de audio numpy a PCM base64 compatible con OpenAI
156
+ """
157
+ # Normalizar float audio data a int16 si es necesario
158
+ if audio_data.dtype in (np.float32, np.float64):
159
+ audio_data = np.clip(audio_data, -1.0, 1.0) # Limitar datos float
160
+ audio_data = (audio_data * 32767).astype(np.int16)
161
+
162
+ # Convertir audio a WAV y codificar en Base64
163
+ with io.BytesIO() as wav_buffer:
164
+ with wave.open(wav_buffer, 'wb') as wav_file:
165
+ wav_file.setnchannels(num_channels)
166
+ wav_file.setsampwidth(sample_width)
167
+ wav_file.setframerate(sample_rate)
168
+ wav_file.writeframes(audio_data.tobytes())
169
+ wav_base64 = base64.b64encode(wav_buffer.getvalue()).decode('utf-8')
170
+
171
+ return wav_base64
172
+
173
+ def wav_to_numpy(audio_bytes):
174
+ """
175
+ Convierte bytes WAV a numpy array
176
+ """
177
+ audio_seg = AudioSegment.from_file(io.BytesIO(audio_bytes), format="wav")
178
+ samples = np.array(audio_seg.get_array_of_samples())
179
+ if audio_seg.channels > 1:
180
+ samples = samples.reshape((-1, audio_seg.channels))
181
+ return audio_seg.frame_rate, samples
182
+
183
+ def realtime_response_adapted(audio_data, voice="alloy"):
184
+ """
185
+ Función adaptada de tu código para generar respuesta usando GPT-4o mini Audio
186
+ """
187
+ try:
188
+ sample_rate, audio_np = audio_data
189
+ pcm_base64 = convert_audio_to_pcm_base64(audio_np, sample_rate)
190
+
191
+ content = [{"type": "input_audio", "input_audio": {"data": pcm_base64, "format": "wav"}}]
192
+ history_response = [{"role": "system", "content": prompt},
193
+ {"role": "user", "content": content}]
194
+
195
+ response = client.chat.completions.create(
196
+ model="gpt-4o-mini-audio-preview",
197
+ modalities=["text", "audio"],
198
+ audio={"voice": voice, "format": "wav"},
199
+ messages=history_response
200
+ )
201
+
202
+ transcript = response.choices[0].message.audio.transcript if response.choices[0].message.audio else "Sin transcripción"
203
+
204
+ try:
205
+ wav_bytes = base64.b64decode(response.choices[0].message.audio.data)
206
+ pcm_data = wav_to_numpy(wav_bytes)
207
+ return transcript, pcm_data
208
+ except Exception as e:
209
+ print(f"Error procesando audio de respuesta: {e}")
210
+ return transcript, None
211
+
212
+ except Exception as e:
213
+ print(f"Error durante comunicación con OpenAI: {e}")
214
+ return None, None
215
+
216
+ def response(state: AppState):
217
+ """
218
+ Genera y transmite la respuesta del chatbot
219
+ """
220
+ if not state.pause_detected or not state.started_talking or state.stream is None:
221
+ yield None, state
222
+ return
223
+
224
+ try:
225
+ # Preparar datos de audio
226
+ audio_data = (state.sampling_rate, state.stream)
227
+
228
+ # Generar respuesta usando OpenAI
229
+ transcript, audio_response = realtime_response_adapted(audio_data)
230
+
231
+ if transcript and audio_response:
232
+ # Añadir mensaje del usuario a la conversación (solo texto descriptivo)
233
+ state.conversation.append({
234
+ "role": "user",
235
+ "content": "🎤 [Mensaje de audio enviado]"
236
+ })
237
+
238
+ # Procesar respuesta de audio
239
+ sample_rate, audio_np = audio_response
240
+
241
+ # Convertir numpy a bytes WAV para Gradio
242
+ if audio_np.dtype != np.int16:
243
+ audio_np = (audio_np * 32767).astype(np.int16)
244
+
245
+ # Crear WAV bytes
246
+ wav_buffer = io.BytesIO()
247
+ with wave.open(wav_buffer, 'wb') as wav_file:
248
+ wav_file.setnchannels(1)
249
+ wav_file.setsampwidth(2)
250
+ wav_file.setframerate(sample_rate)
251
+ wav_file.writeframes(audio_np.tobytes())
252
+
253
+ wav_bytes = wav_buffer.getvalue()
254
+
255
+ # Yield del audio para streaming
256
+ yield wav_bytes, state
257
+
258
+ # Añadir respuesta del asistente a la conversación
259
+ state.conversation.append({
260
+ "role": "assistant",
261
+ "content": f"🤖 {transcript}"
262
+ })
263
+
264
+ else:
265
+ # Manejar error en la respuesta
266
+ print("No se recibió respuesta válida de OpenAI")
267
+ state.conversation.append({
268
+ "role": "user",
269
+ "content": "🎤 [Audio enviado]"
270
+ })
271
+ state.conversation.append({
272
+ "role": "assistant",
273
+ "content": "❌ Error al procesar el audio. Intenta de nuevo."
274
+ })
275
+
276
+ # Generar mensaje de error audible
277
+ try:
278
+ error_response = client.audio.speech.create(
279
+ model="tts-1",
280
+ voice="alloy",
281
+ input="Lo siento, ocurrió un error al procesar tu mensaje. ¿Podrías repetirlo?"
282
+ )
283
+ yield error_response.content, state
284
+ except:
285
+ pass
286
+
287
+ # Resetear estado para próxima interacción
288
+ new_state = AppState(conversation=state.conversation.copy())
289
+ yield None, new_state
290
+
291
+ except Exception as e:
292
+ print(f"Error en la función response: {e}")
293
+ # Añadir mensajes de error al historial
294
+ state.conversation.append({
295
+ "role": "user",
296
+ "content": "🎤 [Audio enviado]"
297
+ })
298
+ state.conversation.append({
299
+ "role": "assistant",
300
+ "content": f"⚠️ Error técnico: {str(e)[:100]}..."
301
+ })
302
+
303
+ # Estado de error
304
+ error_state = AppState(conversation=state.conversation.copy())
305
+ yield None, error_state
306
+
307
+ def start_recording_user(state: AppState):
308
+ """
309
+ Inicia la grabación del usuario si la conversación no está detenida
310
+ """
311
+ if not state.stopped:
312
+ return gr.Audio(recording=True)
313
+ return None
314
+
315
+ def reset_conversation():
316
+ """
317
+ Resetea la conversación y el estado
318
+ """
319
+ return AppState(), []
320
+
321
+ def stop_conversation():
322
+ """
323
+ Detiene la conversación
324
+ """
325
+ return AppState(stopped=True), gr.Audio(recording=False)
326
+
327
+ # Crear la aplicación Gradio
328
+ def create_voice_chatbot():
329
+ with gr.Blocks(
330
+ title="Agente de Cobranza Telefónica - Layer7",
331
+ theme=gr.themes.Soft(),
332
+ css="""
333
+ .main-container {
334
+ max-width: 1200px;
335
+ margin: 0 auto;
336
+ }
337
+ .chat-container {
338
+ border-radius: 10px;
339
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
340
+ padding: 20px;
341
+ margin-bottom: 20px;
342
+ }
343
+ .title {
344
+ color: white;
345
+ text-align: center;
346
+ margin-bottom: 10px;
347
+ }
348
+ .subtitle {
349
+ color: rgba(255,255,255,0.9);
350
+ text-align: center;
351
+ font-size: 16px;
352
+ }
353
+ """
354
+ ) as demo:
355
+
356
+ with gr.Row(elem_classes="main-container"):
357
+ with gr.Column():
358
+ gr.HTML("""
359
+ <div class="chat-container">
360
+ <h1 class="title">🎤 Agente de Cobranza Telefónica - Layer7</h1>
361
+ <p class="subtitle">Habla con nuestro agente de cobranza telefónica. Experiencia de diálogo natural voz a voz.</p>
362
+ </div>
363
+ """)
364
+
365
+ with gr.Row():
366
+ with gr.Column(scale=1):
367
+ gr.HTML("""
368
+ <div style="background: #f8f9fa; padding: 15px; border-radius: 10px; margin-bottom: 15px;">
369
+ <h3 style="margin-top: 0; color: #495057;">🎙️ Control de Audio</h3>
370
+ </div>
371
+ """)
372
+
373
+ input_audio = gr.Audio(
374
+ label="🎤 Tu Micrófono",
375
+ sources=["microphone"],
376
+ type="numpy",
377
+ streaming=True,
378
+ show_download_button=True,
379
+ interactive=True
380
+ )
381
+
382
+ with gr.Row():
383
+ stop_btn = gr.Button("⏹️ Detener Conversación", variant="stop", size="sm")
384
+ clear_btn = gr.Button("🗑️ Limpiar Historial", variant="secondary", size="sm")
385
+
386
+ gr.HTML("""
387
+ <div style="margin-top: 15px; padding: 15px; background: linear-gradient(135deg, #74b9ff, #0984e3); color: white; border-radius: 10px;">
388
+ <h4 style="margin-top: 0;">📋 Cómo usar:</h4>
389
+ <ol style="margin: 10px 0; padding-left: 20px; line-height: 1.6;">
390
+ <li><strong>Inicia:</strong> El micrófono se activa automáticamente</li>
391
+ <li><strong>Habla:</strong> Di tu mensaje normalmente</li>
392
+ <li><strong>Pausa:</strong> Espera 0.75 segundos en silencio</li>
393
+ <li><strong>Escucha:</strong> La IA responderá con voz natural</li>
394
+ <li><strong>Continúa:</strong> El micrófono se reactiva para seguir</li>
395
+ </ol>
396
+ <p style="margin-bottom: 0; font-size: 14px; opacity: 0.9;">
397
+ 💡 <em>Tip: Habla con naturalidad, la IA detectará automáticamente cuando termines.</em>
398
+ </p>
399
+ </div>
400
+ """)
401
+
402
+ with gr.Column(scale=2):
403
+ gr.HTML("""
404
+ <div style="background: #f8f9fa; padding: 15px; border-radius: 10px; margin-bottom: 15px;">
405
+ <h3 style="margin-top: 0; color: #495057;">💬 Conversación</h3>
406
+ </div>
407
+ """)
408
+
409
+ chatbot = gr.Chatbot(
410
+ label="Historial de Conversación",
411
+ type="messages",
412
+ height=350,
413
+ show_copy_button=True,
414
+ render_markdown=True,
415
+ show_label=False,
416
+ avatar_images=(None, "https://openai.com/favicon.ico")
417
+ )
418
+
419
+ output_audio = gr.Audio(
420
+ label="🔊 Respuesta",
421
+ streaming=True,
422
+ autoplay=True,
423
+ show_download_button=True,
424
+ show_share_button=False
425
+ )
426
+
427
+ # Estado de la aplicación
428
+ state = gr.State(value=AppState())
429
+
430
+ # Configurar eventos de audio streaming
431
+
432
+ # Stream de audio de entrada (procesa cada 0.5 segundos)
433
+ stream_event = input_audio.stream(
434
+ process_audio,
435
+ inputs=[input_audio, state],
436
+ outputs=[input_audio, state],
437
+ stream_every=0.5, # Procesar cada medio segundo
438
+ time_limit=30, # Límite de 30 segundos por grabación
439
+ )
440
+
441
+ # Generar respuesta cuando se detiene la grabación
442
+ respond_event = input_audio.stop_recording(
443
+ response,
444
+ inputs=[state],
445
+ outputs=[output_audio, state]
446
+ )
447
+
448
+ # Actualizar chatbot con la conversación
449
+ respond_event.then(
450
+ lambda s: s.conversation,
451
+ inputs=[state],
452
+ outputs=[chatbot]
453
+ )
454
+
455
+ # Reiniciar grabación automáticamente cuando termina el audio de salida
456
+ restart_event = output_audio.stop(
457
+ start_recording_user,
458
+ inputs=[state],
459
+ outputs=[input_audio]
460
+ )
461
+
462
+ # Botón para detener conversación
463
+ stop_btn.click(
464
+ stop_conversation,
465
+ outputs=[state, input_audio],
466
+ cancels=[respond_event, restart_event]
467
+ )
468
+
469
+ # Botón para limpiar historial
470
+ clear_btn.click(
471
+ reset_conversation,
472
+ outputs=[state, chatbot]
473
+ )
474
+
475
+ # Mostrar estado de configuración
476
+ api_key_status = "✅ Configurada correctamente" if os.getenv("OPENAI_API_KEY") else "❌ No encontrada"
477
+ status_color = "#98ab9d" if os.getenv("OPENAI_API_KEY") else "#f8d7da"
478
+
479
+ gr.HTML(f"""
480
+ <div style="margin-top: 20px; padding: 15px; background-color: {status_color}; border-radius: 10px; border-left: 4px solid {'#28a745' if os.getenv('OPENAI_API_KEY') else '#dc3545'};">
481
+ <h4 style="margin-top: 0;">🔑 Estado de Configuración</h4>
482
+ <p><strong>API Key OpenAI:</strong> {api_key_status}</p>
483
+ <p><strong>Modelo:</strong> GPT-4o mini Audio Preview</p>
484
+ <p><strong>Funciones:</strong> Audio entrada/salida, Streaming, Detección de pausas</p>
485
+ {'' if os.getenv("OPENAI_API_KEY") else '<p style="color: #721c24; margin-bottom: 0;"><strong>⚠️ Para usar la aplicación:</strong><br>1. Obtén tu API key en <a href="https://platform.openai.com/api-keys" target="_blank">OpenAI Platform</a><br>2. Configúrala: <code>export OPENAI_API_KEY="tu-api-key"</code><br>3. Reinicia la aplicación</p>'}
486
+ </div>
487
+ """)
488
+
489
+ return demo
490
+
491
+ if __name__ == "__main__":
492
+ # Verificar configuración al inicio
493
+ print("🚀 Iniciando Agente de Cobranza Telefónica...")
494
+ print("=" * 50)
495
+
496
+ if not os.getenv("OPENAI_API_KEY"):
497
+ print("⚠️ ADVERTENCIA: Variable de entorno OPENAI_API_KEY no configurada")
498
+ else:
499
+ print("✅ API Key de OpenAI configurada correctamente")
500
+ print("✅ Modelo: GPT-4o mini Audio Preview")
501
+ print("✅ Funciones: Streaming de audio bidireccional")
502
+ print()
503
+
504
+ print("🌐 Iniciando servidor Gradio...")
505
+ demo = create_voice_chatbot()
506
+ demo.queue() # Habilitar cola para manejo de múltiples usuarios
507
+ demo.launch(
508
+ server_name="0.0.0.0", # Accesible desde cualquier IP
509
+ server_port=7860, # Puerto estándar
510
+ share=False, # Cambiar a True para URL pública
511
+ show_error=False, # Mostrar errores detallados
512
+ debug=False, # Cambiar a True para modo debug
513
+ quiet=False # Mostrar logs
514
+ )