gnosticdev commited on
Commit
4f66327
·
verified ·
1 Parent(s): 26698a8

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +188 -0
app.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import numpy as np
3
+ import pyaudio
4
+ import wave
5
+ from scipy import signal
6
+ import matplotlib.pyplot as plt
7
+ from io import BytesIO
8
+ from PIL import Image
9
+ import time
10
+ import threading
11
+
12
+ # Bloqueamos la advertencia de matplotlib sobre el backend interactivo
13
+ plt.switch_backend('Agg')
14
+
15
+ class AguaVozReal:
16
+ def __init__(self):
17
+ self.CHUNK = 1024 * 4
18
+ self.FORMAT = pyaudio.paInt16
19
+ self.CHANNELS = 1
20
+ self.RATE = 44100
21
+ self.buffer_size = self.RATE * 2
22
+ self.audio_buffer = np.zeros(self.buffer_size)
23
+ self.buffer_index = 0
24
+ self.p = pyaudio.PyAudio()
25
+ self.stream = None
26
+ self.audio_thread = None
27
+ self.running = False
28
+
29
+ def start_stream(self):
30
+ """Inicia el stream de audio en un hilo separado"""
31
+ if self.stream is None:
32
+ self.stream = self.p.open(
33
+ format=self.FORMAT,
34
+ channels=self.CHANNELS,
35
+ rate=self.RATE,
36
+ input=True,
37
+ frames_per_buffer=self.CHUNK,
38
+ stream_callback=self.audio_callback
39
+ )
40
+ self.stream.start_stream()
41
+ self.running = True
42
+
43
+ def audio_callback(self, in_data, frame_count, time_info, status):
44
+ """Rellena el buffer circular con el audio entrante"""
45
+ audio_data = np.frombuffer(in_data, dtype=np.int16)
46
+ for i, sample in enumerate(audio_data):
47
+ self.audio_buffer[self.buffer_index] = sample
48
+ self.buffer_index = (self.buffer_index + 1) % self.buffer_size
49
+ return (in_data, pyaudio.paContinue)
50
+
51
+ def get_current_pattern(self):
52
+ """Genera la imagen del patrón de agua basado en el audio actual"""
53
+ # Crear la malla de coordenadas
54
+ x = np.linspace(-2, 2, 200)
55
+ y = np.linspace(-2, 2, 200)
56
+ X, Y = np.meshgrid(x, y)
57
+ r = np.sqrt(X**2 + Y**2)
58
+ theta = np.arctan2(Y, X)
59
+
60
+ # 1. Obtener y analizar el audio del buffer
61
+ if self.buffer_index >= self.RATE:
62
+ audio_slice = self.audio_buffer[self.buffer_index-self.RATE:self.buffer_index]
63
+ else:
64
+ part1 = self.audio_buffer[self.buffer_index-self.RATE:]
65
+ part2 = self.audio_buffer[:self.buffer_index]
66
+ audio_slice = np.concatenate([part1, part2])
67
+
68
+ # 2. Calcular espectrograma para obtener energía por frecuencia
69
+ freqs, times, Sxx = signal.spectrogram(
70
+ audio_slice,
71
+ fs=self.RATE,
72
+ nperseg=512,
73
+ noverlap=256
74
+ )
75
+
76
+ # 3. Calcular energía en diferentes bandas de frecuencia
77
+ low_energy = np.sum(Sxx[freqs < 200]) if np.any(freqs < 200) else 0
78
+ mid_energy = np.sum(Sxx[(freqs >= 200) & (freqs < 1000)]) if np.any((freqs >= 200) & (freqs < 1000)) else 0
79
+ high_energy = np.sum(Sxx[freqs >= 1000]) if np.any(freqs >= 1000) else 0
80
+
81
+ # 4. Determinar el patrón de agua según la energía dominante
82
+ if low_energy > mid_energy and low_energy > high_energy:
83
+ # Modo grave: ondas circulares concéntricas
84
+ Z = 0.5 * np.sin(3 * r - 2 * time.time()) * np.exp(-0.5 * r)
85
+ titulo = "🌊 Agua: Modo Grave (Ondas Circulares)"
86
+ elif mid_energy > low_energy and mid_energy > high_energy:
87
+ # Modo medio: patrón de cuadrícula
88
+ Z = 0.5 * (np.sin(4 * X - 3 * time.time()) * np.cos(4 * Y - 2 * time.time()))
89
+ titulo = "🌀 Agua: Modo Medio (Ondas Estacionarias)"
90
+ else:
91
+ # Modo agudo: patrones hexagonales/complejos
92
+ Z = 0.3 * (np.sin(5 * r + 6 * theta) + np.sin(5 * r - 6 * theta)) * np.exp(-0.3 * r)
93
+ titulo = "❄️ Agua: Modo Agudo (Patrones Hexagonales)"
94
+
95
+ # Normalizar la altura para la visualización
96
+ Z = (Z - Z.min()) / (Z.max() - Z.min() + 1e-10)
97
+ Z = Z * 2 - 1
98
+
99
+ # 5. Generar la imagen con matplotlib
100
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
101
+
102
+ # Subplot 3D
103
+ ax3d = fig.add_subplot(121, projection='3d')
104
+ surf = ax3d.plot_surface(X, Y, Z, cmap='ocean', linewidth=0, antialiased=True, alpha=0.8)
105
+ ax3d.set_zlim(-1.5, 1.5)
106
+ ax3d.set_title("🌊 Superficie 3D")
107
+ ax3d.axis('off')
108
+
109
+ # Subplot 2D (Contorno)
110
+ contour = ax2.contourf(X, Y, Z, levels=20, cmap='Blues')
111
+ ax2.contour(X, Y, Z, levels=10, colors='darkblue', linewidths=0.5)
112
+ ax2.set_title("🗺️ Patrón de Ondas")
113
+ ax2.set_aspect('equal')
114
+ ax2.axis('off')
115
+
116
+ plt.suptitle(titulo, fontsize=16)
117
+ plt.tight_layout()
118
+
119
+ # Convertir el plot a imagen PNG para devolverla por Gradio
120
+ buf = BytesIO()
121
+ plt.savefig(buf, format='png', dpi=100)
122
+ buf.seek(0)
123
+ img = Image.open(buf)
124
+ plt.close(fig) # Importante cerrar la figura para liberar memoria
125
+ return img
126
+
127
+ # --- Instancia global de la clase y preparación del audio ---
128
+ agua_app = AguaVozReal()
129
+ # Iniciamos el stream de audio tan pronto se carga la app
130
+ try:
131
+ agua_app.start_stream()
132
+ print("Stream de audio iniciado.")
133
+ except Exception as e:
134
+ print(f"Error al iniciar stream: {e}. ¿Micrófono conectado?")
135
+
136
+ # --- Función principal para Gradio ---
137
+ def generar_patron():
138
+ """Función que llama Gradio para obtener la imagen actualizada."""
139
+ if agua_app.running:
140
+ try:
141
+ img = agua_app.get_current_pattern()
142
+ return img
143
+ except Exception as e:
144
+ # Crear imagen de error
145
+ fig, ax = plt.subplots()
146
+ ax.text(0.5, 0.5, f'Error: {str(e)}', ha='center', va='center')
147
+ ax.axis('off')
148
+ buf = BytesIO()
149
+ plt.savefig(buf, format='png')
150
+ buf.seek(0)
151
+ img = Image.open(buf)
152
+ plt.close(fig)
153
+ return img
154
+ else:
155
+ # Crear imagen de "sin audio"
156
+ fig, ax = plt.subplots()
157
+ ax.text(0.5, 0.5, 'Micrófono no disponible', ha='center', va='center')
158
+ ax.axis('off')
159
+ buf = BytesIO()
160
+ plt.savefig(buf, format='png')
161
+ buf.seek(0)
162
+ img = Image.open(buf)
163
+ plt.close(fig)
164
+ return img
165
+
166
+ # --- Interfaz Gradio ---
167
+ with gr.Blocks(title="Simulador de Agua por Voz", theme=gr.themes.Soft()) as demo:
168
+ gr.Markdown("# 🎤🌊 Simulador de Agua por Voz")
169
+ gr.Markdown("Habla al micrófono. El agua generará ondas según la *energía* de tu voz (Grave ↔ Agudo).")
170
+
171
+ with gr.Row():
172
+ with gr.Column():
173
+ audio_input = gr.Audio(source="microphone", type="numpy", label="Habla aquí")
174
+ btn_actualizar = gr.Button("Refrescar Patrón")
175
+
176
+ with gr.Column():
177
+ imagen_output = gr.Image(label="Reacción del Agua", value=generar_patron)
178
+
179
+ # Actualizar automáticamente cuando se detecta audio
180
+ audio_input.change(fn=generar_patron, outputs=imagen_input)
181
+ btn_actualizar.click(fn=generar_patron, outputs=imagen_output)
182
+
183
+ # Actualización periódica cada 2 segundos para movimiento continuo
184
+ demo.load(generar_patron, every=2, outputs=imagen_output)
185
+
186
+ # Para ejecutar localmente, pero HF usará la interfaz 'demo'
187
+ if __name__ == "__main__":
188
+ demo.launch()