CVision-assitant commited on
Commit
49bfaa5
·
verified ·
1 Parent(s): 2c401d2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +360 -17
app.py CHANGED
@@ -1,26 +1,369 @@
1
  # =====================================================================
2
- # ── ARCHIVO PRINCIPAL DE ARRANQUE ENLAZADO: app.py ──
3
  # =====================================================================
 
 
 
 
 
4
  import gradio as gr
5
- from fastapi import FastAPI
6
 
7
- # 1. Inicializamos un objeto FastAPI global para tomar el control total de las rutas
8
- app_global = FastAPI()
9
 
10
- # 2. Importamos el inyector del túnel WebSocket de Deepgram
11
- from modules.langchain_assistant.voice_module import mount_voice_websocket
12
 
13
- # 3. Montamos el proxy local de voz de forma nativa en la app global antes de compilar la UI
14
- mount_voice_websocket(app_global)
15
- print("🚀 [CVISION CORE] Túnel WebSocket Proxy de Deepgram inyectado globalmente.")
16
 
17
- # 4. Importamos la interfaz de Gradio
18
- from modules.interface import assistant_app
 
19
 
20
- # 5. Acoplamos de forma segura la interfaz de Gradio dentro de nuestra app global controlada
21
- app = gr.mount_gradio_app(app_global, assistant_app, path="/")
 
 
 
 
 
 
 
 
22
 
23
- if __name__ == "__main__":
24
- import uvicorn
25
- print("🌌 [CVISION CORE] Inicializando entorno visual Cybernova en puerto oficial 7860...")
26
- uvicorn.run(app, host="0.0.0.0", port=7860, log_level="info")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # =====================================================================
2
+ # ── MÓDULO DE VOZ CV VISION (ENTORNO ROBUSTO POR HTTP VAD) ──
3
  # =====================================================================
4
+ import os
5
+ import io
6
+ import base64
7
+ import requests
8
+ import warnings
9
  import gradio as gr
 
10
 
11
+ # Silenciamos advertencias de sintaxis de los modelos generativos
12
+ warnings.filterwarnings("ignore", category=UserWarning, module="langchain_google_genai")
13
 
14
+ # Clave de acceso directa para Deepgram REST API
15
+ DEEPGRAM_API_KEY = os.getenv("DEEPGRAM_API_KEY", "ba998efd03dae16398f44fc599dd0291367fe7ac")
16
 
17
+ # Variable Singleton global para mantener el cerebro vivo sin reinstanciar en cada frase
18
+ _GLOBAL_CHAT_ASSISTANT = None
 
19
 
20
+ def mount_voice_websocket(app):
21
+ """Mantiene compatibilidad estructural con app.py sin interferir en los puertos"""
22
+ pass
23
 
24
+ def get_or_create_voice_brain():
25
+ """Garantiza una única instancia en memoria para eliminar la latencia de carga."""
26
+ global _GLOBAL_CHAT_ASSISTANT
27
+ if _GLOBAL_CHAT_ASSISTANT is None:
28
+ try:
29
+ from modules.langchain_assistant.langchain_chat_assistant import LangChainChatAssistant
30
+ _GLOBAL_CHAT_ASSISTANT = LangChainChatAssistant()
31
+ except Exception as e:
32
+ print(f"❌ [CVISION VOICE ERR] No se pudo instanciar el cerebro principal: {str(e)}")
33
+ return _GLOBAL_CHAT_ASSISTANT
34
 
35
+ def process_audio_base64(base64_audio_str, history_state):
36
+ """
37
+ Procesa el audio recibido por chunks de VAD, realiza STT, consulta al cerebro único
38
+ de LangChain y genera la síntesis de voz en memoria RAM de forma ultra-rápida.
39
+ """
40
+ if not base64_audio_str or len(base64_audio_str.strip()) < 50:
41
+ return None, history_state, "DONE"
42
+
43
+ if history_state is None:
44
+ history_state = []
45
+
46
+ try:
47
+ # 1. Decodificación de audio enviado desde la RAM del navegador
48
+ header, encoded = base64_audio_str.split(",", 1) if "," in base64_audio_str else ("", base64_audio_str)
49
+ audio_data = base64.b64decode(encoded)
50
+
51
+ # 2. STT Nativo vía HTTP (Deepgram Nova-2)
52
+ url_stt = "https://api.deepgram.com/v1/listen?model=nova-2&language=es&smart_format=true"
53
+ headers_stt = {
54
+ "Authorization": f"Token {DEEPGRAM_API_KEY}",
55
+ "Content-Type": "audio/webm"
56
+ }
57
+
58
+ response_stt = requests.post(url_stt, headers=headers_stt, data=audio_data, timeout=8)
59
+ if response_stt.status_code != 200:
60
+ return None, history_state, "DONE"
61
+
62
+ stt_json = response_stt.json()
63
+ user_text = stt_json['results']['channels'][0]['alternatives'][0]['transcript']
64
+
65
+ if not user_text.strip() or len(user_text.strip()) < 2:
66
+ return None, history_state, "DONE"
67
+
68
+ print(f"🎙️ [CVISION VOICE] Cristian: {user_text}")
69
+
70
+ # 3. Consulta Cognitiva al Singleton de LangChain
71
+ cerebro = get_or_create_voice_brain()
72
+ if cerebro:
73
+ prompt_conversacional = (
74
+ f"{user_text} (REGLA CRÍTICA: Responde de forma puramente hablada, corta, "
75
+ f"en un solo párrafo fluido, sin usar código, viñetas, asteriscos ni símbolos matemáticos como $ o *. "
76
+ f"Escribe los números con letras, por ejemplo: cuatro en vez de 4)"
77
+ )
78
+ ai_response = cerebro.chat(input=prompt_conversacional)
79
+ else:
80
+ ai_response = "Enlace de inteligencia de LangChain fuera de línea."
81
+
82
+ print(f"🤖 [CVISION VOICE] Javier: {ai_response}")
83
+
84
+ # 4. TTS Nativo vía HTTP procesado directamente sobre memoria virtual (RAM)
85
+ url_tts = "https://api.deepgram.com/v1/speak?model=aura-2-javier-es&encoding=mp3"
86
+ headers_tts = {
87
+ "Authorization": f"Token {DEEPGRAM_API_KEY}",
88
+ "Content-Type": "application/json"
89
+ }
90
+ payload_tts = {"text": ai_response}
91
+
92
+ response_tts = requests.post(url_tts, headers=headers_tts, json=payload_tts, timeout=8)
93
+
94
+ if response_tts.status_code == 200:
95
+ # Codificación directa en buffer binario sin tocar el disco duro
96
+ audio_buffer = io.BytesIO(response_tts.content)
97
+ encoded_output = base64.b64encode(audio_buffer.read()).decode("utf-8")
98
+ audio_uri = f"data:audio/mp3;base64,{encoded_output}"
99
+ else:
100
+ return None, history_state, "DONE"
101
+
102
+ # Actualizamos el historial del componente visual
103
+ history_state.append((user_text, ai_response))
104
+ return audio_uri, history_state, "DONE"
105
+
106
+ except Exception as e:
107
+ print(f"❌ [CVISION VOICE PIPELINE CRASH]: {str(e)}")
108
+ return None, history_state, "DONE"
109
+
110
+ # =====================================================================
111
+ # ── SECCIÓN UI: INTERFAZ PLAYGROUND ADAPTADA ──
112
+ # =====================================================================
113
+ def create_voice_tab():
114
+ with gr.TabItem("🎙️ MODO VOZ", elem_id="voice_tab"):
115
+
116
+ audio_bridge_in = gr.Textbox(visible=False, elem_id="audio_bridge_in")
117
+ audio_bridge_out = gr.Textbox(visible=False, elem_id="audio_bridge_out")
118
+ control_bridge = gr.Textbox(visible=False, elem_id="control_bridge")
119
+
120
+ gr.HTML(
121
+ """
122
+ <div style='text-align: center; padding: 15px;'>
123
+ <div class='hud-neon-glow' style='font-size: 24px; font-weight: bold; text-shadow: 0 0 10px #00f0ff; color: #fff; margin-bottom: 5px;'>
124
+ CVISION INTELLIGENCE — COGNITIVE PLAYGROUND
125
+ </div>
126
+ <p style='color: #00f0ff; font-family: monospace; letter-spacing: 1px; font-size: 11px; margin: 0;'>
127
+ HTTP PIPELINE CON DETECCIÓN DE SILENCIO ACTIVA (VAD)
128
+ </p>
129
+ </div>
130
+ """
131
+ )
132
+
133
+ with gr.Row():
134
+ with gr.Column(scale=1, variant="panel", elem_id="voice_panel_container"):
135
+ gr.HTML(
136
+ """
137
+ <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; margin: 20px 0;">
138
+ <div id="cyber_sphere" style="width: 130px; height: 130px; border-radius: 50%; border: 4px solid #3a3a4a; box-shadow: 0 0 15px #1a1a2e; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease;">
139
+ <div id="cyber_core" style="width: 90px; height: 90px; border-radius: 50%; background: radial-gradient(circle, #111 40%, #222 100%); transition: all 0.3s ease;"></div>
140
+ </div>
141
+ <div id="cyber_status_text" style="color: #64dfdf; font-family: monospace; font-size: 13px; margin-top: 20px; letter-spacing: 2px; font-weight: bold; text-align: center;">
142
+ [ CANAL DE VOZ APAGADO ]
143
+ </div>
144
+ </div>
145
+ <audio id="cyber_native_speaker"></audio>
146
+ """
147
+ )
148
+ with gr.Row():
149
+ start_voice_btn = gr.Button("▶️ ENCENDER AGENTE", variant="primary", elem_id="btn_start_voice_core")
150
+ stop_voice_btn = gr.Button("🛑 APAGAR AGENTE", variant="stop", elem_id="btn_stop_voice_core")
151
+
152
+ with gr.Column(scale=2):
153
+ chatbot_display = gr.Chatbot(
154
+ label="Transcripción de la Conversación",
155
+ elem_id="voice_history_chatbot",
156
+ bubble_full_width=False,
157
+ height=300
158
+ )
159
+
160
+ # =====================================================================
161
+ # ── INTERCEPTOR JAVASCRIPT: FILTRO WEB AUDIO CON VAD NATIVO ──
162
+ # =====================================================================
163
+ gr.HTML(
164
+ """
165
+ <script>
166
+ if (window.cyber_voice_initialized === undefined) {
167
+ window.cyber_voice_initialized = true;
168
+
169
+ let mediaRecorder = null;
170
+ let audioChunks = [];
171
+ let isVoiceActive = false;
172
+ let isProcessingBackend = false;
173
+ let globalStream = null;
174
+ let audioContext = null;
175
+ let analyser = null;
176
+
177
+ // Configuración del Umbral de Silencio (VAD)
178
+ const VOICE_MIN_DECIBELS = -55;
179
+ const SILENCE_DURATION_MS = 1100; // Envía tras 1.1 segundos de silencio continuos
180
+ let lastVoiceDetectedTime = Date.now();
181
+ let vadInterval = null;
182
+
183
+ function setHUD(text, color, pulse) {
184
+ const txt = document.getElementById("cyber_status_text");
185
+ const sphere = document.getElementById("cyber_sphere");
186
+ const core = document.getElementById("cyber_core");
187
+ if (txt) txt.innerText = text;
188
+ if (sphere) { sphere.style.borderColor = color; sphere.style.boxShadow = `0 0 25px ${color}`; }
189
+ if (core) core.style.background = pulse ? `radial-gradient(circle, ${color} 20%, #111 80%)` : 'radial-gradient(circle, #111 40%, #222 100%)';
190
+ }
191
+
192
+ async function startVoicePipeline() {
193
+ if (isVoiceActive) return;
194
+ setHUD("[ INICIALIZANDO MICRÓFONO... ]", "#ffa502", false);
195
+
196
+ try {
197
+ globalStream = await navigator.mediaDevices.getUserMedia({ audio: true });
198
+ isVoiceActive = true;
199
+ isProcessingBackend = false;
200
+ setHUD("[ AGENTE EN VIVO — TE ESCUCHO ]", "#00f0ff", true);
201
+
202
+ // Configuración del analizador Web Audio para VAD
203
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
204
+ const source = audioContext.createMediaStreamSource(globalStream);
205
+ analyser = audioContext.createAnalyser();
206
+ analyser.fftSize = 512;
207
+ analyser.minDecibels = -90;
208
+ analyser.maxDecibels = -10;
209
+ analyser.smoothingTimeConstant = 0.85;
210
+ source.connect(analyser);
211
+
212
+ triggerAudioCapture();
213
+ startVADMonitor();
214
+ } catch (err) {
215
+ console.error(err);
216
+ setHUD("[ ERROR MULTIMEDIA DE ACCESO ]", "#ff4757", false);
217
+ }
218
+ }
219
+
220
+ function triggerAudioCapture() {
221
+ if (!isVoiceActive || isProcessingBackend) return;
222
+
223
+ audioChunks = [];
224
+ mediaRecorder = new MediaRecorder(globalStream, { mimeType: 'audio/webm' });
225
+
226
+ mediaRecorder.ondataavailable = e => {
227
+ if (e.data.size > 0) audioChunks.push(e.data);
228
+ };
229
+
230
+ mediaRecorder.onstop = async () => {
231
+ if (audioChunks.length > 0 && isVoiceActive && !isProcessingBackend) {
232
+ isProcessingBackend = true;
233
+ setHUD("[ JAVIER PROCESANDO... ]", "#ffa502", true);
234
+
235
+ const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
236
+ const reader = new FileReader();
237
+ reader.readAsDataURL(audioBlob);
238
+ reader.onloadend = () => {
239
+ const inputBridge = document.querySelector("#audio_bridge_in textarea");
240
+ if (inputBridge) {
241
+ inputBridge.value = reader.result;
242
+ inputBridge.dispatchEvent(new Event('input', { bubbles: true }));
243
+ }
244
+ };
245
+ }
246
+ };
247
+
248
+ mediaRecorder.start();
249
+ lastVoiceDetectedTime = Date.now();
250
+ }
251
+
252
+ function startVADMonitor() {
253
+ const bufferLength = analyser.frequencyBinCount;
254
+ const dataArray = new Float32Array(bufferLength);
255
+
256
+ vadInterval = setInterval(() => {
257
+ if (!isVoiceActive || isProcessingBackend || !mediaRecorder) return;
258
+
259
+ analyser.getFloatFrequencyData(dataArray);
260
+
261
+ // Buscamos si hay alguna frecuencia que supere el umbral mínimo de voz humana
262
+ let hasVoice = false;
263
+ for (let i = 0; i < bufferLength; i++) {
264
+ if (dataArray[i] > VOICE_MIN_DECIBELS) {
265
+ hasVoice = true;
266
+ break;
267
+ }
268
+ }
269
+
270
+ const now = Date.now();
271
+ if (hasVoice) {
272
+ lastVoiceDetectedTime = now;
273
+ } else if (now - lastVoiceDetectedTime > SILENCE_DURATION_MS) {
274
+ // Si se superó el tiempo de silencio y está grabando, cortamos para enviar inmediatamente
275
+ if (mediaRecorder.state === "recording") {
276
+ mediaRecorder.stop();
277
+ }
278
+ }
279
+ }, 100);
280
+ }
281
+
282
+ function stopVoicePipeline() {
283
+ isVoiceActive = false;
284
+ isProcessingBackend = false;
285
+ setHUD("[ CANAL DE VOZ APAGADO ]", "#3a3a4a", false);
286
+
287
+ if (vadInterval) { clearInterval(vadInterval); vadInterval = null; }
288
+
289
+ const nativePlayer = document.getElementById("cyber_native_speaker");
290
+ if (nativePlayer) nativePlayer.pause();
291
+
292
+ if (mediaRecorder && mediaRecorder.state !== "inactive") {
293
+ mediaRecorder.stop();
294
+ }
295
+ if (globalStream) {
296
+ globalStream.getTracks().forEach(track => track.stop());
297
+ }
298
+ if (audioContext && audioContext.state !== 'closed') {
299
+ audioContext.close();
300
+ }
301
+ }
302
+
303
+ // Monitor reactivo de respuesta de audio e interrupciones dinámicas
304
+ setInterval(() => {
305
+ const outputBridge = document.querySelector("#audio_bridge_out textarea");
306
+ const nativePlayer = document.getElementById("cyber_native_speaker");
307
+ const ctrlBridge = document.querySelector("#control_bridge textarea");
308
+
309
+ if (outputBridge && outputBridge.value && outputBridge.value.trim() !== "") {
310
+ setHUD("[ JAVIER HABLANDO ]", "#00ff88", true);
311
+ if (nativePlayer) {
312
+ nativePlayer.src = outputBridge.value;
313
+ nativePlayer.play()
314
+ .then(() => {
315
+ nativePlayer.onended = () => {
316
+ setHUD("[ AGENTE EN VIVO — TE ESCUCHO ]", "#00f0ff", true);
317
+ isProcessingBackend = false;
318
+ if (ctrlBridge) {
319
+ ctrlBridge.value = "";
320
+ ctrlBridge.dispatchEvent(new Event('input', { bubbles: true }));
321
+ }
322
+ // Reenganche automático e instantáneo del ciclo infinito
323
+ setTimeout(() => { triggerAudioCapture(); }, 50);
324
+ };
325
+ })
326
+ .catch(e => {
327
+ isProcessingBackend = false;
328
+ setHUD("[ AGENTE EN VIVO — TE ESCUCHO ]", "#00f0ff", true);
329
+ setTimeout(() => { triggerAudioCapture(); }, 50);
330
+ });
331
+ }
332
+ outputBridge.value = "";
333
+ outputBridge.dispatchEvent(new Event('input', { bubbles: true }));
334
+ }
335
+
336
+ if (ctrlBridge && ctrlBridge.value === "DONE" && isProcessingBackend) {
337
+ ctrlBridge.value = "";
338
+ ctrlBridge.dispatchEvent(new Event('input', { bubbles: true }));
339
+ setTimeout(() => {
340
+ const nativePlayer = document.getElementById("cyber_native_speaker");
341
+ if (!nativePlayer || nativePlayer.paused) {
342
+ isProcessingBackend = false;
343
+ setHUD("[ AGENTE EN VIVO — TE ESCUCHO ]", "#00f0ff", true);
344
+ triggerAudioCapture();
345
+ }
346
+ }, 2000);
347
+ }
348
+ }, 100);
349
+
350
+ document.addEventListener("DOMContentLoaded", () => {
351
+ setTimeout(() => {
352
+ const btnStart = document.getElementById("btn_start_voice_core");
353
+ const btnStop = document.getElementById("btn_stop_voice_core");
354
+ if (btnStart) btnStart.onclick = startVoicePipeline;
355
+ if (btnStop) btnStop.onclick = stopVoicePipeline;
356
+ }, 1200);
357
+ });
358
+ }
359
+ </script>
360
+ """
361
+ )
362
+
363
+ # Conexión síncrona reactiva en Gradio
364
+ audio_bridge_in.change(
365
+ fn=process_audio_base64,
366
+ inputs=[audio_bridge_in, chatbot_display],
367
+ outputs=[audio_bridge_out, chatbot_display, control_bridge],
368
+ show_progress="none"
369
+ )