gnosticdev commited on
Commit
0c50a20
·
verified ·
1 Parent(s): 569bb3d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +144 -89
app.py CHANGED
@@ -3,6 +3,7 @@ import numpy as np
3
  import librosa
4
  from scipy.signal import find_peaks
5
  from sklearn.cluster import KMeans
 
6
 
7
  class DigitalToneDecoder:
8
  def __init__(self, frame_ms=40, min_freq=300, max_freq=4000, peak_threshold=0.2, symbols=16):
@@ -21,129 +22,183 @@ class DigitalToneDecoder:
21
  return y, sr
22
 
23
  def calcular_stft(self):
24
- # Usar n_fft fijo para evitar mismatch de dimensiones
25
- n_fft = 2048
26
  hop_length = int(self.sr * self.frame_ms / 1000)
27
-
28
  stft = np.abs(librosa.stft(self.y, n_fft=n_fft, hop_length=hop_length, window="hann"))
29
  freqs = librosa.fft_frequencies(sr=self.sr, n_fft=n_fft)
30
  return stft, freqs
31
 
32
  def detectar_tonos(self, stft, freqs):
33
  tonos = []
34
- max_freq_idx = len(freqs) - 1
35
-
 
36
  for frame in stft.T:
37
- if np.max(frame) == 0:
 
38
  continue
39
-
40
- # Normalizar frame
41
  frame_norm = frame / np.max(frame)
42
-
43
- # Detectar picos con límites seguros
44
  peaks, _ = find_peaks(frame_norm, height=self.peak_threshold)
45
-
46
  if len(peaks) == 0:
47
  continue
48
-
49
- # FILTRO CRÍTICO: asegurar que los índices caben en freqs
50
- peaks_valid = peaks[peaks <= max_freq_idx]
51
-
52
- if len(peaks_valid) == 0:
53
  continue
54
-
55
- peak_freqs = freqs[peaks_valid]
56
-
57
- # Filtrar por rango de frecuencia útil
58
- peak_freqs = peak_freqs[
59
- (peak_freqs >= self.min_freq) &
60
- (peak_freqs <= self.max_freq)
61
- ]
62
-
63
- if len(peak_freqs) > 0:
64
- # Tomar la frecuencia más prominente
65
- tonos.append(peak_freqs[0])
66
-
67
- return np.array(tonos) if len(tonos) > 0 else np.array([])
68
 
69
  def crear_simbolos(self, tonos):
70
  if len(tonos) == 0:
71
  return []
72
-
73
- # Si hay menos tonos que símbolos, devolver los únicos
74
  if len(tonos) < self.symbols:
75
  return sorted(np.unique(tonos))
76
-
77
  tonos_2d = tonos.reshape(-1, 1)
78
- kmeans = KMeans(n_clusters=min(self.symbols, len(tonos_2d)), n_init=10, random_state=42)
 
79
  kmeans.fit(tonos_2d)
 
80
  return sorted(kmeans.cluster_centers_.flatten())
81
 
82
  def decodificar(self, tonos, centros):
83
- if len(centros) == 0:
84
- return ""
85
-
86
- letras = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 "
87
  texto = ""
88
-
 
 
 
89
  for f in tonos:
90
- # Encontrar centro más cercano con manejo de errores
91
- distancias = [abs(f - c) for c in centros]
92
- idx = distancias.index(min(distancias))
93
  texto += letras[idx % len(letras)]
94
-
95
  return texto
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  def analizar(audio_path, progress=gr.Progress()):
 
98
  if audio_path is None:
99
- return "⚠️ No hay audio"
100
-
101
- try:
102
- progress(0.2, desc="Cargando audio...")
103
- decoder = DigitalToneDecoder()
104
- decoder.cargar_audio(audio_path)
105
-
106
- progress(0.4, desc="Calculando STFT...")
107
- stft, freqs = decoder.calcular_stft()
108
-
109
- progress(0.6, desc="Detectando tonos...")
110
- tonos = decoder.detectar_tonos(stft, freqs)
111
-
112
- if len(tonos) == 0:
113
- return "💤 No se detectaron tonos en el rango útil (300-4000Hz)\n\n*Prueba con audio que contenga tonos puros o modulación.*"
114
-
115
- progress(0.8, desc="Agrupando frecuencias...")
116
- centros = decoder.crear_simbolos(tonos)
117
-
118
- progress(1.0, desc="Decodificando...")
119
- texto = decoder.decodificar(tonos, centros)
120
-
121
- reporte = f"📊 **DATOS TÉCNICOS:**\n"
122
- reporte += f"- Tonos detectados: {len(tonos)}\n"
123
- reporte += f"- Clusters únicos: {len(centros)}\n"
124
- reporte += f"- Frecuencias base: {[f'{c:.1f}Hz' for c in centros]}\n\n"
125
- reporte += f"🔤 **SECUENCIA DECODIFICADA:**\n\n`{texto}`\n\n"
126
- reporte += f"*Nota: Las letras se asignan por cluster de frecuencia. Interpretación subjetiva.*"
127
-
128
- return reporte
129
-
130
- except Exception as e:
131
- return f"❌ **ERROR:**\n\n{type(e).__name__}: {str(e)}"
 
 
 
 
 
 
 
 
 
132
 
133
  with gr.Blocks() as demo:
134
- gr.Markdown("""
135
- # 📡 Decodificador de Tonos Digitales (DSP Real)
136
- ## STFT + Detección de Picos + KMeans Clustering
137
-
138
- *Basado en modos digitales de radio (PSK31, RTTY).*
139
- *Decodifica frecuencias dominantes a símbolos. La interpretación es tuya.*
140
- """)
141
-
142
- audio = gr.Audio(label="Audio", type="filepath", sources=["upload", "microphone"])
143
- btn = gr.Button("Decodificar", variant="primary")
144
- output = gr.Textbox(label="Resultado", lines=12)
145
-
146
- btn.click(analizar, inputs=audio, outputs=output)
147
 
148
  if __name__ == "__main__":
149
  demo.launch()
 
3
  import librosa
4
  from scipy.signal import find_peaks
5
  from sklearn.cluster import KMeans
6
+ import itertools
7
 
8
  class DigitalToneDecoder:
9
  def __init__(self, frame_ms=40, min_freq=300, max_freq=4000, peak_threshold=0.2, symbols=16):
 
22
  return y, sr
23
 
24
  def calcular_stft(self):
25
+ n_fft = int(self.sr * 0.05)
26
+ n_fft = 2 ** int(np.ceil(np.log2(n_fft)))
27
  hop_length = int(self.sr * self.frame_ms / 1000)
 
28
  stft = np.abs(librosa.stft(self.y, n_fft=n_fft, hop_length=hop_length, window="hann"))
29
  freqs = librosa.fft_frequencies(sr=self.sr, n_fft=n_fft)
30
  return stft, freqs
31
 
32
  def detectar_tonos(self, stft, freqs):
33
  tonos = []
34
+ max_idx = len(freqs) - 1
35
+ media_global = np.mean(stft)
36
+
37
  for frame in stft.T:
38
+ energia = np.mean(frame)
39
+ if energia < media_global * 0.5:
40
  continue
41
+
 
42
  frame_norm = frame / np.max(frame)
 
 
43
  peaks, _ = find_peaks(frame_norm, height=self.peak_threshold)
44
+
45
  if len(peaks) == 0:
46
  continue
47
+
48
+ peaks = peaks[peaks <= max_idx]
49
+
50
+ if len(peaks) == 0:
 
51
  continue
52
+
53
+ peak = peaks[np.argmax(frame_norm[peaks])]
54
+ freq = freqs[peak]
55
+
56
+ if self.min_freq <= freq <= self.max_freq:
57
+ tonos.append(freq)
58
+
59
+ tonos = np.array(tonos)
60
+
61
+ if len(tonos) > 5:
62
+ tonos = np.convolve(tonos, np.ones(5)/5, mode="same")
63
+
64
+ return tonos
 
65
 
66
  def crear_simbolos(self, tonos):
67
  if len(tonos) == 0:
68
  return []
69
+
 
70
  if len(tonos) < self.symbols:
71
  return sorted(np.unique(tonos))
72
+
73
  tonos_2d = tonos.reshape(-1, 1)
74
+
75
+ kmeans = KMeans(n_clusters=self.symbols, n_init=10, random_state=42)
76
  kmeans.fit(tonos_2d)
77
+
78
  return sorted(kmeans.cluster_centers_.flatten())
79
 
80
  def decodificar(self, tonos, centros):
81
+ letras = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
 
 
 
82
  texto = ""
83
+
84
+ if len(centros) == 0:
85
+ return texto
86
+
87
  for f in tonos:
88
+ dist = [abs(f - c) for c in centros]
89
+ idx = dist.index(min(dist))
 
90
  texto += letras[idx % len(letras)]
91
+
92
  return texto
93
 
94
+
95
+ def limpiar_secuencia(texto):
96
+ resultado = ""
97
+ prev = None
98
+
99
+ for c in texto:
100
+ if c != prev:
101
+ resultado += c
102
+ prev = c
103
+
104
+ return resultado
105
+
106
+
107
+ diccionario = {
108
+ "amor","sol","luz","voz","mar","ser","ver","sal","mal","bar","real","mesa","casa","cosa",
109
+ "resto","temor","calor","dolor","alma","moral","solar","ramo","roma","rosa","loma","loro",
110
+ "roma","sombra","humo","eco","oro","oro","oro","aire","tierra","fuego","agua"
111
+ }
112
+
113
+
114
+ def generar_palabras(letras):
115
+
116
+ letras = letras.lower()
117
+
118
+ posibles = []
119
+
120
+ for l in range(3,7):
121
+
122
+ for p in itertools.permutations(letras, l):
123
+
124
+ palabra = "".join(p)
125
+
126
+ if palabra in diccionario:
127
+ posibles.append(palabra)
128
+
129
+ return list(set(posibles))
130
+
131
+
132
+ def generar_frases(palabras):
133
+
134
+ frases = []
135
+
136
+ for a in palabras:
137
+ for b in palabras:
138
+ if a != b:
139
+ frases.append(a + " " + b)
140
+
141
+ return frases[:30]
142
+
143
+
144
  def analizar(audio_path, progress=gr.Progress()):
145
+
146
  if audio_path is None:
147
+ return "no audio"
148
+
149
+ decoder = DigitalToneDecoder()
150
+
151
+ progress(0.2)
152
+
153
+ decoder.cargar_audio(audio_path)
154
+
155
+ progress(0.4)
156
+
157
+ stft, freqs = decoder.calcular_stft()
158
+
159
+ progress(0.6)
160
+
161
+ tonos = decoder.detectar_tonos(stft, freqs)
162
+
163
+ centros = decoder.crear_simbolos(tonos)
164
+
165
+ texto = decoder.decodificar(tonos, centros)
166
+
167
+ texto_limpio = limpiar_secuencia(texto)
168
+
169
+ palabras = generar_palabras(texto_limpio)
170
+
171
+ frases = generar_frases(palabras)
172
+
173
+ reporte = ""
174
+
175
+ reporte += "SECUENCIA\n"
176
+ reporte += texto + "\n\n"
177
+
178
+ reporte += "SECUENCIA LIMPIA\n"
179
+ reporte += texto_limpio + "\n\n"
180
+
181
+ reporte += "PALABRAS POSIBLES\n"
182
+ reporte += "\n".join(palabras) + "\n\n"
183
+
184
+ reporte += "FRASES POSIBLES\n"
185
+ reporte += "\n".join(frases)
186
+
187
+ return reporte
188
+
189
 
190
  with gr.Blocks() as demo:
191
+
192
+ gr.Markdown("# Analizador de Ruido a Letras y Palabras")
193
+
194
+ audio = gr.Audio(type="filepath", sources=["upload","microphone"])
195
+
196
+ boton = gr.Button("Analizar")
197
+
198
+ salida = gr.Textbox(lines=20)
199
+
200
+ boton.click(analizar, inputs=audio, outputs=salida)
201
+
 
 
202
 
203
  if __name__ == "__main__":
204
  demo.launch()