bleondubos commited on
Commit
19e92d3
·
verified ·
1 Parent(s): 0916e83

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +225 -242
app.py CHANGED
@@ -10,63 +10,44 @@ import collections
10
  import pandas as pd
11
  import os
12
  import time
 
13
  import requests
14
  import logging
15
- import sys
16
 
17
- # Configuración de logging para HF Spaces (stdout visible en Logs)
18
- logging.basicConfig(
19
- level=logging.INFO,
20
- format='%(asctime)s - %(levelname)s - %(message)s',
21
- stream=sys.stdout,
22
- force=True
23
- )
24
- logger = logging.getLogger(__name__)
25
- logger.setLevel(logging.INFO)
26
 
27
- # Silenciar logs excesivos
28
- logging.getLogger("seisbench").setLevel(logging.WARNING)
29
- logging.getLogger("obspy").setLevel(logging.WARNING)
30
-
31
- # Variables de entorno (configurar en HF Spaces Settings → Secrets)
32
  GROQ_API_KEY = os.environ.get("api_groq")
33
  TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
34
- TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "@seismonetcl")
35
-
36
- # Verificar variables críticas
37
- if not TELEGRAM_BOT_TOKEN:
38
- logger.error("❌ TELEGRAM_BOT_TOKEN no configurado en HF Spaces Secrets")
39
- if not GROQ_API_KEY:
40
- logger.warning("⚠️ api_groq no configurado, se usará reporte básico")
41
 
42
  SERVIDOR_CHILE_CSN = "eew.csn.uchile.cl"
43
  SERVIDOR_ASIA_IRIS = "rtserve.iris.washington.edu"
44
 
45
  ESTACIONES_CHILE = [
46
- {"net": "C1", "sta": "GO01", "loc": "Iquique (Tarapacá - Costa)"},
47
- {"net": "CX", "sta": "PB01", "loc": "Pica (Tarapacá - Interior)"},
48
- {"net": "CX", "sta": "PB02", "loc": "Pozo Almonte (Tarapacá)"},
49
- {"net": "C1", "sta": "LMEL", "loc": "María Elena (Antofagasta - Interior)"},
50
- {"net": "CX", "sta": "PATCX","loc": "Patache (Tarapacá - Costa)"},
51
- {"net": "CX", "sta": "PB10", "loc": "Antofagasta (Norte Interior)"},
52
- {"net": "CX", "sta": "CLCM", "loc": "Calama (Antofagasta - Interior)"},
53
- {"net": "C1", "sta": "GO02", "loc": "Copiapó (Atacama)"},
54
- {"net": "C1", "sta": "GO03", "loc": "Vallenar / Huasco (Atacama)"},
55
- {"net": "IU", "sta": "LCO", "loc": "Las Campanas (Observatorio - Coquimbo)"},
56
- {"net": "C1", "sta": "GO06", "loc": "La Serena / Coquimbo (Costa)"},
57
- {"net": "C1", "sta": "MT01", "loc": "Santiago / Farellones (Metropolitana)"},
58
- {"net": "C1", "sta": "MT02", "loc": "Santiago / San José de Maipo (RM)"},
59
- {"net": "C1", "sta": "ROC1", "loc": "Rancagua / El Teniente (O'Higgins)"},
60
- {"net": "C1", "sta": "VNQ01", "loc": "Quintero (Valparaíso - Costa)"},
61
- {"net": "C1", "sta": "VLP01", "loc": "Valparaíso (Valparaíso - Urbano)"},
62
- {"net": "C1", "sta": "CO01", "loc": "Concepción / Talcahuano (Biobío)"},
63
- {"net": "C1", "sta": "CO03", "loc": "Chillán / Ñuble (Interior)"},
64
- {"net": "C1", "sta": "TA01", "loc": "Temuco / Araucanía"},
65
- {"net": "C1", "sta": "TA02", "loc": "Valdivia / Los Ríos"},
66
- {"net": "C1", "sta": "TA03", "loc": "Puerto Montt / Los Lagos"},
67
- {"net": "C1", "sta": "OS01", "loc": "Osorno (Los Lagos)"},
68
- {"net": "G", "sta": "COYC", "loc": "Coyhaique (Aysén)"},
69
- {"net": "G", "sta": "PAC1", "loc": "Punta Arenas (Magallanes)"}
70
  ]
71
 
72
  REDES_ASIA = [
@@ -80,281 +61,283 @@ REDES_ASIA = [
80
 
81
  MUESTRAS_VENTANA = 3000
82
  buffers_globales = {}
83
- contador_paquetes = {"total": 0, "ultimo_reset": time.time()}
84
-
85
- logger.info("[SISTEMA] Cargando PhaseNet...")
86
- try:
87
- model = PhaseNet.from_pretrained("original")
88
- model.eval()
89
- logger.info("[SISTEMA] PhaseNet cargado exitosamente")
90
- except Exception as e:
91
- logger.error(f"[SISTEMA] Error cargando PhaseNet: {e}")
92
- model = None
93
 
94
- def verificar_calidad_instrumental(data_array, componente, estacion):
95
- try:
96
- std = np.std(data_array)
97
- if std < 1e-8:
98
- return False, "Sensor muerto"
99
-
100
- max_val = np.max(np.abs(data_array))
101
- if max_val > 1e9:
102
- return False, "Saturación"
103
-
104
- return True, "OK"
105
- except Exception as e:
106
- logger.debug(f"[{estacion}] Error en verificación: {e}")
107
- return True, "OK"
108
 
109
  def analizar_paquete_global(trace):
110
- try:
111
- contador_paquetes["total"] += 1
112
- if time.time() - contador_paquetes["ultimo_reset"] > 60:
113
- logger.info(f"[MONITOR] Paquetes recibidos último minuto: {contador_paquetes['total']}")
114
- contador_paquetes["total"] = 0
115
- contador_paquetes["ultimo_reset"] = time.time()
116
-
117
- net = trace.stats.network
118
- estacion = trace.stats.station
119
- channel = trace.stats.channel
120
- componente = channel[-1] if channel else None
121
 
122
- if componente not in ['Z', 'N', 'E']:
123
- return
124
 
125
- if estacion not in buffers_globales:
126
- match_cl = next((e for e in ESTACIONES_CHILE if e["sta"] == estacion), None)
127
- if match_cl:
128
- pais = "Chile"
129
- loc = match_cl["loc"]
130
- else:
131
- match_as = next((e for e in REDES_ASIA if e["sta"] == estacion), None)
132
- pais = match_as["pais"] if match_as else "Internacional"
133
- loc = match_as["loc"] if match_as else "Ubicación Remota"
134
 
135
- buffers_globales[estacion] = {
136
- 'Z': collections.deque(maxlen=MUESTRAS_VENTANA),
137
- 'N': collections.deque(maxlen=MUESTRAS_VENTANA),
138
- 'E': collections.deque(maxlen=MUESTRAS_VENTANA),
139
- 'net': net,
140
- 'channel': channel[:-1],
141
- 'sampling_rate': trace.stats.sampling_rate,
142
- 'pais': pais,
143
- 'loc': loc,
144
- 'ultima_alerta': 0
145
- }
146
 
147
- buffers_globales[estacion][componente].extend(trace.data)
148
 
149
- if (len(buffers_globales[estacion]['Z']) == MUESTRAS_VENTANA and
150
- len(buffers_globales[estacion]['N']) == MUESTRAS_VENTANA and
151
- len(buffers_globales[estacion]['E']) == MUESTRAS_VENTANA):
152
-
153
- evaluar_ruptura(estacion)
154
-
155
- except Exception as e:
156
- logger.error(f"Error en analizar_paquete_global: {e}", exc_info=True)
157
 
158
  def evaluar_ruptura(estacion):
159
- try:
160
- if model is None:
161
- return
 
 
 
 
 
162
 
163
- meta = buffers_globales[estacion]
164
- sr = meta['sampling_rate']
165
- tiempo_actual = time.time()
166
- if tiempo_actual - meta['ultima_alerta'] < 60:
167
  return
168
-
169
- st = Stream()
170
- amplitudes_maximas = []
171
-
172
- for comp in ['Z', 'N', 'E']:
173
- data_array = np.array(meta[comp], dtype=np.float32)
174
- valido, razon = verificar_calidad_instrumental(data_array, comp, estacion)
175
- if not valido:
176
- return
177
- if np.std(data_array) < 1e-6:
178
- return
179
 
180
- data_array = data_array - np.mean(data_array)
181
- amplitudes_maximas.append(np.max(np.abs(data_array)))
182
-
183
- header = {
184
- 'network': meta['net'],
185
- 'station': estacion,
186
- 'channel': f"{meta['channel']}{comp}",
187
- 'sampling_rate': sr,
188
- 'starttime': obspy.UTCDateTime()
189
- }
190
- st.append(Trace(data=data_array, header=header))
191
 
 
 
 
192
  st.detrend("linear")
193
  st.taper(max_percentage=0.05, type="cosine")
194
- st.filter("bandpass", freqmin=0.5, freqmax=20.0, corners=4, zerophase=True)
195
 
 
196
  for trace in st:
197
  std = np.std(trace.data)
198
  if std > 0:
199
  trace.data /= std
200
 
 
201
  annotations = model.annotate(st)
202
  prob_P = np.max(annotations[0].data)
203
  prob_S = np.max(annotations[1].data)
204
 
205
- if prob_P > 0.65 and prob_S > 0.65:
 
 
206
  idx_P = np.argmax(annotations[0].data)
207
  idx_S = np.argmax(annotations[1].data)
208
 
209
  if idx_S > idx_P:
210
  delta_t = (idx_S - idx_P) / float(sr)
211
- if delta_t < 0.3 or delta_t > 180.0:
212
- return
213
 
 
 
 
 
 
214
  pais = meta['pais']
215
  ubicacion = meta['loc']
 
216
  distancia_km = delta_t * 8.2
217
  amplitud_maxima_total = np.max(amplitudes_maximas)
218
 
 
219
  if amplitud_maxima_total > 0 and distancia_km > 0:
220
  magnitud_estimada = np.log10(amplitud_maxima_total) + 1.6 * np.log10(distancia_km) - 0.15
221
  magnitud_estimada = round(max(1.0, min(9.5, magnitud_estimada)), 1)
222
  else:
223
  magnitud_estimada = 0.0
224
 
225
- logger.info(f"🚨 [{estacion}] SISMO DETECTADO! M{magnitud_estimada}, P-S={delta_t:.1f}s")
226
- meta['ultima_alerta'] = tiempo_actual
227
  disparar_alerta_ia_global(estacion, ubicacion, pais, delta_t, prob_P, prob_S, magnitud_estimada)
228
 
229
- for c in ['Z', 'N', 'E']:
230
- meta[c].clear()
231
-
232
- except Exception as e:
233
- logger.error(f"[{estacion}] Error en evaluar_ruptura: {e}", exc_info=True)
234
 
235
- def enviar_telegram_sincrono(url, payload):
 
 
 
 
 
 
 
 
236
  try:
237
- response = requests.post(url, json=payload, timeout=30)
238
- if response.status_code == 200:
239
- logger.info("✅ [TELEGRAM] Alerta enviada exitosamente")
240
- return True
241
- elif response.status_code == 429:
242
- time.sleep(5)
243
- response = requests.post(url, json=payload, timeout=30)
244
- return response.status_code == 200
245
- else:
246
- return False
247
  except Exception as e:
248
- logger.error(f"❌ [TELEGRAM] Error de conexión: {e}")
249
- return False
250
 
251
- def disparar_alerta_ia_global(estacion, ubicacion, pais, delta_t, p_prob, s_prob, magnitud):
252
- try:
253
- distancia_epicentro_km = round(delta_t * 8.2, 1)
254
- str_magnitud = f"{magnitud} Ml" if magnitud > 0 else "En cálculo"
255
-
256
- reporte_final = f"⚠️ <b>[ALERTA SÍSMICA AUTOMÁTICA]</b>\n\n• <b>País:</b> {pais}\n• <b>Estación:</b> {estacion} ({ubicacion})\n• <b>Tiempo P-S:</b> {delta_t:.1f}s\n• <b>Magnitud:</b> {str_magnitud}\n• <b>Distancia:</b> ~{distancia_epicentro_km} km\n\n<i>Reporte automático</i>"
257
 
258
- if GROQ_API_KEY:
 
259
  try:
260
- client_groq = Groq(api_key=GROQ_API_KEY)
261
- prompt = f"""[DETECCIÓN SÍSMICA AUTOMÁTICA]
262
- - País: {pais}
263
- - Ubicación: {ubicacion} (Sensor: {estacion})
264
- - Tiempo P-S: {delta_t:.1f} segundos
265
- - Distancia al epicentro: {distancia_epicentro_km} km
266
- - Confianza: P={p_prob*100:.1f}%, S={s_prob*100:.1f}%
267
- - Magnitud estimada: {str_magnitud}
268
 
269
- Genera un reporte breve en español para Telegram usando HTML (<b>negrita</b>, <i>cursiva</i>). Incluye advertencia de que es automático y no reemplaza reporte oficial del CSN."""
270
-
271
- completion = client_groq.chat.completions.create(
272
- model="llama-3.1-8b-instant",
273
- messages=[{"role": "user", "content": prompt}],
274
- max_tokens=300
275
- )
276
- reporte_final = completion.choices[0].message.content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  except Exception as e:
278
- logger.error(f"Error en Groq API: {e}")
279
-
280
- if TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID:
281
- url_gateway = f"https://seisnet.bleondubos.workers.dev/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
282
- payload = {
283
- "chat_id": str(TELEGRAM_CHAT_ID).strip(),
284
- "text": reporte_final,
285
- "parse_mode": "HTML"
286
- }
287
- threading.Thread(target=enviar_telegram_sincrono, args=(url_gateway, payload), daemon=True).start()
288
 
289
- except Exception as e:
290
- logger.error(f"Error en disparar_alerta: {e}", exc_info=True)
 
 
 
291
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  def conectar_chile_csn():
293
  while True:
294
  try:
295
- logger.info(f"[CHILE] Conectando a {SERVIDOR_CHILE_CSN}...")
296
  client = create_client(SERVIDOR_CHILE_CSN, on_data=analizar_paquete_global)
 
297
  for est in ESTACIONES_CHILE:
298
  try:
299
  client.select_stream(est["net"], est["sta"], "?H?")
300
- except Exception:
301
- pass
302
  client.run()
303
  except Exception as e:
304
- logger.error(f"[CHILE] Error: {e}. Reintentando...")
305
  time.sleep(15)
306
 
307
  def conectar_asia_iris():
308
  while True:
309
  try:
310
- logger.info(f"[ASIA] Conectando a {SERVIDOR_ASIA_IRIS}...")
311
  client = create_client(SERVIDOR_ASIA_IRIS, on_data=analizar_paquete_global)
 
312
  for est in REDES_ASIA:
313
  try:
314
  client.select_stream(est["net"], est["sta"], "BH?")
315
- except Exception:
316
- pass
317
  client.run()
318
  except Exception as e:
319
- logger.error(f"[ASIA] Error: {e}. Reintentando...")
320
  time.sleep(15)
321
 
322
- logger.info("[SISTEMA] Iniciando conexiones SeedLink...")
323
  threading.Thread(target=conectar_chile_csn, daemon=True).start()
324
  threading.Thread(target=conectar_asia_iris, daemon=True).start()
325
 
326
- # --- Interfaz Gradio ---
327
- with gr.Blocks(title="SeisNet Chile - Monitor Sísmico") as demo:
328
- gr.Markdown("# 🌍 SeisNet Chile - Monitor Sísmico en Tiempo Real")
329
- gr.Markdown("Sistema de detección automática de sismos con IA (PhaseNet)")
330
-
331
- gr.Markdown("### 📡 Estado del Sistema")
332
-
333
- def obtener_estado():
334
- estado = {
335
- "Estaciones Chile": len(ESTACIONES_CHILE),
336
- "Estaciones Asia": len(REDES_ASIA),
337
- "Buffers activos": len(buffers_globales),
338
- "PhaseNet": "✅ Cargado" if model else "❌ Error",
339
- "Telegram": "✅ Configurado" if TELEGRAM_BOT_TOKEN else "❌ Falta token",
340
- "Groq AI": "✅ Configurado" if GROQ_API_KEY else "⚠️ No configurado"
341
- }
342
- return pd.DataFrame([estado]).T
343
-
344
- estado_df = gr.DataFrame(
345
- value=obtener_estado(),
346
- label="Estado del Sistema",
347
- headers=["Métrica", "Valor"]
348
- )
349
-
350
- gr.Markdown("### 📍 Estaciones en Monitoreo")
351
- gr.DataFrame(pd.DataFrame(ESTACIONES_CHILE), label="Red Sísmica Chilena")
352
-
353
- gr.Markdown("### 🌏 Red Internacional")
354
- gr.DataFrame(pd.DataFrame(REDES_ASIA), label="Red Asia-Pacífico")
355
 
356
- gr.Markdown("---")
357
- gr.Markdown("**Nota:** Este es un sistema experimental de detección automática. No reemplaza los reportes oficiales del Centro Sismológico Nacional.")
358
 
359
- # Lanzar en HF Spaces
360
- demo.launch(server_name="0.0.0.0", server_port=7860, share=False)
 
10
  import pandas as pd
11
  import os
12
  import time
13
+ from datetime import datetime
14
  import requests
15
  import logging
 
16
 
17
+ logging.getLogger("seisbench").setLevel(logging.ERROR)
 
 
 
 
 
 
 
 
18
 
 
 
 
 
 
19
  GROQ_API_KEY = os.environ.get("api_groq")
20
  TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
21
+ TELEGRAM_CHAT_ID = "-5515143628"
 
 
 
 
 
 
22
 
23
  SERVIDOR_CHILE_CSN = "eew.csn.uchile.cl"
24
  SERVIDOR_ASIA_IRIS = "rtserve.iris.washington.edu"
25
 
26
  ESTACIONES_CHILE = [
27
+ # --- NORTE GRANDE (Arica, Tarapacá, Antofagasta) ---
28
+ {"net": "C1", "sta": "GO01", "loc": "Iquique (Tarapacá - Costa) *Latencia ~6h"}, #
29
+ {"net": "CX", "sta": "PB01", "loc": "Pica (Tarapacá - Interior)"}, #
30
+ {"net": "CX", "sta": "PB02", "loc": "Pozo Almonte (Tarapacá)"}, #
31
+ {"net": "C1", "sta": "LMEL", "loc": "María Elena (Antofagasta - Interior)"}, #
32
+ {"net": "CX", "sta": "PATCX","loc": "Pampa Alta (Antofagasta) *Latencia ~3h"}, #
33
+ {"net": "CX", "sta": "PB10", "loc": "Antofagasta (Norte Interior)"}, #
34
+
35
+ # --- NORTE CHICO & CENTRAL (Atacama, Coquimbo, Valparaíso, RM) ---
36
+ {"net": "C1", "sta": "GO02", "loc": "Copiapó (Atacama)"}, #
37
+ {"net": "C1", "sta": "GO03", "loc": "Vallenar / Huasco (Atacama)"}, #
38
+ {"net": "IU", "sta": "LCO", "loc": "Las Campanas (Observatorio - Coquimbo)"}, #
39
+ {"net": "C1", "sta": "GO06", "loc": "La Serena / Coquimbo (Costa)"}, #
40
+ {"net": "C1", "sta": "MT01", "loc": "Santiago / Farellones (Metropolitana)"}, #
41
+ {"net": "C1", "sta": "MT02", "loc": "Santiago / San José de Maipo (RM)"}, #
42
+ {"net": "C1", "sta": "ROC1", "loc": "Rancagua / El Teniente (O'Higgins)"}, #
43
+
44
+ # --- ZONA SUR & AUSTRAL (Biobío, La Araucanía, Los Lagos, Magallanes) ---
45
+ {"net": "C1", "sta": "CO01", "loc": "Concepción / Talcahuano (Biobío)"}, #
46
+ {"net": "C1", "sta": "CO03", "loc": "Chillán / Ñuble (Interior)"}, #
47
+ {"net": "C1", "sta": "TA01", "loc": "Temuco / Araucanía"}, #
48
+ {"net": "C1", "sta": "TA02", "loc": "Valdivia / Los Ríos"}, #
49
+ {"net": "C1", "sta": "TA03", "loc": "Puerto Montt / Los Lagos"}, #
50
+ {"net": "G", "sta": "COYC", "loc": "Coyhaique (Aysén)"} #
51
  ]
52
 
53
  REDES_ASIA = [
 
61
 
62
  MUESTRAS_VENTANA = 3000
63
  buffers_globales = {}
 
 
 
 
 
 
 
 
 
 
64
 
65
+ print("[SISTEMA] Cargando e inicializando PhaseNet...")
66
+ model = PhaseNet.from_pretrained("original")
67
+ model.eval()
68
+ print("[SISTEMA] PhaseNet configurado y listo para producción.")
 
 
 
 
 
 
 
 
 
 
69
 
70
  def analizar_paquete_global(trace):
71
+ net = trace.stats.network
72
+ estacion = trace.stats.station
73
+ channel = trace.stats.channel
74
+ componente = channel[-1] if channel else None
 
 
 
 
 
 
 
75
 
76
+ if componente not in ['Z', 'N', 'E']:
77
+ return
78
 
79
+ if estacion not in buffers_globales:
80
+ match_cl = next((e for e in ESTACIONES_CHILE if e["sta"] == estacion), None)
81
+ if match_cl:
82
+ pais = "Chile"
83
+ loc = match_cl["loc"]
84
+ else:
85
+ match_as = next((e for e in REDES_ASIA if e["sta"] == estacion), None)
86
+ pais = match_as["pais"] if match_as else "Internacional"
87
+ loc = match_as["loc"] if match_as else "Ubicación Remota"
88
 
89
+ buffers_globales[estacion] = {
90
+ 'Z': collections.deque(maxlen=MUESTRAS_VENTANA),
91
+ 'N': collections.deque(maxlen=MUESTRAS_VENTANA),
92
+ 'E': collections.deque(maxlen=MUESTRAS_VENTANA),
93
+ 'net': net,
94
+ 'channel': channel[:-1],
95
+ 'sampling_rate': trace.stats.sampling_rate,
96
+ 'pais': pais,
97
+ 'loc': loc
98
+ }
 
99
 
100
+ buffers_globales[estacion][componente].extend(trace.data)
101
 
102
+ if (len(buffers_globales[estacion]['Z']) == MUESTRAS_VENTANA and
103
+ len(buffers_globales[estacion]['N']) == MUESTRAS_VENTANA and
104
+ len(buffers_globales[estacion]['E']) == MUESTRAS_VENTANA):
105
+
106
+ evaluar_ruptura(estacion)
 
 
 
107
 
108
  def evaluar_ruptura(estacion):
109
+ meta = buffers_globales[estacion]
110
+ sr = meta['sampling_rate']
111
+
112
+ st = Stream()
113
+ amplitudes_maximas = []
114
+
115
+ for comp in ['Z', 'N', 'E']:
116
+ data_array = np.array(meta[comp], dtype=np.float32)
117
 
118
+ # --- FILTRO 1: ANTI-GLITCH ELECTRÓNICO ---
119
+ # Si hay un pico de energía plano absurdo (típico error de sensor muerto), descartamos.
120
+ amp_pico = np.max(np.abs(data_array))
121
+ if amp_pico > 1e7: # Umbral de tolerancia eléctrica estándar para cuentas digitales masivas
122
  return
 
 
 
 
 
 
 
 
 
 
 
123
 
124
+ data_array = data_array - np.mean(data_array)
125
+ amplitudes_maximas.append(amp_pico)
126
+
127
+ header = {
128
+ 'network': meta['net'],
129
+ 'station': estacion,
130
+ 'channel': f"{meta['channel']}{comp}",
131
+ 'sampling_rate': sr,
132
+ 'starttime': obspy.UTCDateTime()
133
+ }
134
+ st.append(Trace(data=data_array, header=header))
135
 
136
+ try:
137
+ # --- FILTRO 2: BANDPASS FILTER (1.0 Hz - 15.0 Hz) ---
138
+ # Remueve ruidos ambientales mecánicos y frecuencias parásitas antes de PhaseNet
139
  st.detrend("linear")
140
  st.taper(max_percentage=0.05, type="cosine")
141
+ st.filter("bandpass", fmin=1.0, fmax=15.0, corners=4, zerophase=True)
142
 
143
+ # Normalización estricta por desviación estándar post-filtrado
144
  for trace in st:
145
  std = np.std(trace.data)
146
  if std > 0:
147
  trace.data /= std
148
 
149
+ # Pasamos la traza limpia por el modelo de IA sismológica
150
  annotations = model.annotate(st)
151
  prob_P = np.max(annotations[0].data)
152
  prob_S = np.max(annotations[1].data)
153
 
154
+ # --- MODIFICACIÓN: Ajuste de estrictez ---
155
+ # Bajamos P a 0.70 y S a 0.60. Mantiene precisión exigiendo ambas, pero es menos estricto con la dispersión real de la señal.
156
+ if prob_P > 0.70 and prob_S > 0.60:
157
  idx_P = np.argmax(annotations[0].data)
158
  idx_S = np.argmax(annotations[1].data)
159
 
160
  if idx_S > idx_P:
161
  delta_t = (idx_S - idx_P) / float(sr)
 
 
162
 
163
+ # --- VALIDACIÓN FÍSICA SÍSMICA ---
164
+ # Un tiempo P-S menor a 0.5s o mayor a 120s en redes regionales suele ser ruido o anomalía.
165
+ if delta_t < 0.5 or delta_t > 120.0:
166
+ return
167
+
168
  pais = meta['pais']
169
  ubicacion = meta['loc']
170
+
171
  distancia_km = delta_t * 8.2
172
  amplitud_maxima_total = np.max(amplitudes_maximas)
173
 
174
+ # Cálculo calibrado de magnitud local
175
  if amplitud_maxima_total > 0 and distancia_km > 0:
176
  magnitud_estimada = np.log10(amplitud_maxima_total) + 1.6 * np.log10(distancia_km) - 0.15
177
  magnitud_estimada = round(max(1.0, min(9.5, magnitud_estimada)), 1)
178
  else:
179
  magnitud_estimada = 0.0
180
 
 
 
181
  disparar_alerta_ia_global(estacion, ubicacion, pais, delta_t, prob_P, prob_S, magnitud_estimada)
182
 
183
+ for c in ['Z', 'N', 'E']: meta[c].clear()
184
+ except:
185
+ pass
 
 
186
 
187
+ import asyncio
188
+ import aiohttp
189
+ import time
190
+
191
+ def enviar_a_telegram_background(url, payload):
192
+ """
193
+ Punto de entrada compatible con hilos que inicializa y ejecuta
194
+ el bucle asíncrono dedicado para saltarse el estrangulamiento de CPU.
195
+ """
196
  try:
197
+ loop = asyncio.new_event_loop()
198
+ asyncio.set_event_loop(loop)
199
+ loop.run_until_complete(despachar_hacia_gateway_async(url, payload))
200
+ loop.close()
 
 
 
 
 
 
201
  except Exception as e:
202
+ print(f"⚠️ [SISTEMA] Error crítico al inicializar bucle asíncrono: {e}")
 
203
 
204
+ async def despachar_hacia_gateway_async(url, payload):
205
+ """Despacha la alerta usando sockets asíncronos no bloqueantes"""
206
+ intentos_maximos = 3
207
+ timeout_estricto = aiohttp.ClientTimeout(total=25)
208
+
209
+ await asyncio.sleep(0.5)
210
 
211
+ async with aiohttp.ClientSession(trust_env=False, timeout=timeout_estricto) as session:
212
+ for intento in range(1, intentos_maximos + 1):
213
  try:
214
+ print(f"🔄 [ASYNC GATEWAY] Intentando envío (Intento {intento}/{intentos_maximos})...", flush=True)
 
 
 
 
 
 
 
215
 
216
+ headers = {
217
+ "Connection": "close",
218
+ "Content-Type": "application/json"
219
+ }
220
+
221
+ async with session.post(url, json=payload, headers=headers) as response:
222
+ status = response.status
223
+ text_response = await response.text()
224
+
225
+ print(f"📥 [ASYNC GATEWAY] Status Code recibido: {status}")
226
+
227
+ if status == 200:
228
+ print(f"✅ [TELEGRAM] ¡Alerta sísmica enviada con éxito mediante Cloudflare Workers!")
229
+ return True
230
+ elif status == 429:
231
+ print("⏳ [ASYNC GATEWAY] Rate-limit activo en la API. Esperando reintento...")
232
+ await asyncio.sleep(4)
233
+ else:
234
+ print(f"❌ [ASYNC GATEWAY] Error devuelto por endpoint: {text_response}")
235
+ return False
236
+
237
+ except asyncio.TimeoutError:
238
+ print(f"⚠️ [ASYNC GATEWAY] Timeout en intento {intento}. CPU saturada por SeisBench, reintentando...")
239
+ await asyncio.sleep(2)
240
  except Exception as e:
241
+ print(f"⚠️ [ASYNC GATEWAY] Error de conexión en socket asíncrono: {e}")
242
+ await asyncio.sleep(1)
243
+
244
+ print("❌ [TELEGRAM] Envío cancelado. No se pudo liberar el socket tras 3 intentos.")
245
+ return False
 
 
 
 
 
246
 
247
+ def disparar_alerta_ia_global(estacion, ubicacion, pais, delta_t, p_prob, s_prob, magnitud):
248
+ distancia_epicentro_km = round(delta_t * 8.2, 1)
249
+
250
+ str_magnitud = f"{magnitud} Ml" if magnitud > 0 else "En cálculo preliminar"
251
+ reporte_final = f"⚠️ <b>[ALERTA SÍSMICA AUTOMÁTICA]</b><br><br>• <b>País:</b> {pais}<br>• <b>Estación:</b> {estacion} ({ubicacion})<br>• <b>Tiempo P-S:</b> {delta_t}s<br>• <b>Magnitud Estimada:</b> {str_magnitud}<br>• <b>Epicentro estimado:</b> a ~{distancia_epicentro_km} km del sensor."
252
 
253
+ if GROQ_API_KEY:
254
+ client_groq = Groq(api_key=GROQ_API_KEY)
255
+
256
+ prompt = f"""[DETECCIÓN PRELIMINAR AUTOMÁTICA INSTRUMENTAL - MONITOREO SEISNET]
257
+ El sistema automático ha filtrado y verificado una posible señal sísmica en la corteza:
258
+ - Región / País: {pais}
259
+ - Ubicación técnica: {ubicacion} (Sensor: {estacion})
260
+ - Tiempo transcurrido entre Onda P y Onda S: {delta_t} segundos.
261
+ - Radio estimado al epicentro: {distancia_epicentro_km} km.
262
+ - Certeza de Fase: Onda P ({round(p_prob*100,1)}%), Onda S ({round(s_prob*100,1)}%).
263
+ - Magnitud Local Estimada: {str_magnitud}.
264
+
265
+ Instrucciones de formato de salida OBLIGATORIAS:
266
+ 1. Encabeza OBLIGATORIAMENTE el mensaje con el título: "⚠️ DETECCIÓN PRELIMINAR INSTRUMENTAL AUTOMÁTICA (EN PROCESO DE VALIDACIÓN)" en mayúsculas destacadas con emojis formales.
267
+ 2. Redacta el informe en ESPAÑOL de manera clara, objetiva y breve para lectura móvil.
268
+ 3. NO uses asteriscos para negrita. Si quieres usar negrita utiliza la etiqueta HTML <b>texto</b> y para cursiva <i>texto</i>.
269
+ 4. Explica explícitamente que es un cálculo de software automático en tiempo real procesado por el equipo de SeisNet y que NO reemplaza bajo ningún motivo el reporte oficial ni la opinión experta del Centro Sismológico Nacional de Chile (CSN).
270
+ 5. Llama a mantener la calma e infórmale a las zonas cercanas el tiempo estimado de viaje que tienen las ondas secundarias basado en el radio calculado.
271
+ 6. Muestra la Magnitud Local Estimada de forma clara en su sección."""
272
+
273
+ try:
274
+ completion = client_groq.chat.completions.create(
275
+ model="llama-3.1-8b-instant",
276
+ messages=[{"role": "user", "content": prompt}]
277
+ )
278
+ reporte_final = completion.choices[0].message.content
279
+ reporte_final = reporte_final.replace("\n", "<br>")
280
+ except Exception as e:
281
+ print(f"Error en Groq API: {e}. Se enviará reporte técnico base.")
282
+
283
+ print(f"\n🚨 [ALERTA GENERADA] - Despachando a cola de Telegram... 🚨\n")
284
+
285
+ if TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID:
286
+ url_gateway = f"https://seisnet.bleondubos.workers.dev/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
287
+
288
+ texto_telegram = reporte_final.replace("<br>", "\n").replace("<br/>", "\n")
289
+ texto_telegram = texto_telegram.replace("<tbd>", "TBD").replace("<TBD>", "TBD")
290
+ texto_telegram = texto_telegram.replace("</tbd>", "")
291
+
292
+ payload = {
293
+ "chat_id": str(TELEGRAM_CHAT_ID).strip(),
294
+ "text": texto_telegram,
295
+ }
296
+
297
+ threading.Thread(target=enviar_a_telegram_background, args=(url_gateway, payload), daemon=True).start()
298
+ else:
299
+ print(f"⚠️ [TELEGRAM] Envío omitido: Faltan credenciales (Token: {'OK' if TELEGRAM_BOT_TOKEN else 'FALTA'}, ChatID: {'OK' if TELEGRAM_CHAT_ID else 'FALTA'})")
300
+
301
  def conectar_chile_csn():
302
  while True:
303
  try:
304
+ print(f"[CHILE CSN] Abriendo canal explícito con {SERVIDOR_CHILE_CSN}...")
305
  client = create_client(SERVIDOR_CHILE_CSN, on_data=analizar_paquete_global)
306
+
307
  for est in ESTACIONES_CHILE:
308
  try:
309
  client.select_stream(est["net"], est["sta"], "?H?")
310
+ except:
311
+ continue
312
  client.run()
313
  except Exception as e:
314
+ print(f"[RECONEXIÓN CHILE] Reintentando en 15 segundos... Info: {e}")
315
  time.sleep(15)
316
 
317
  def conectar_asia_iris():
318
  while True:
319
  try:
320
+ print(f"[ASIA IRIS] Abriendo canal con {SERVIDOR_ASIA_IRIS}...")
321
  client = create_client(SERVIDOR_ASIA_IRIS, on_data=analizar_paquete_global)
322
+
323
  for est in REDES_ASIA:
324
  try:
325
  client.select_stream(est["net"], est["sta"], "BH?")
326
+ except:
327
+ continue
328
  client.run()
329
  except Exception as e:
330
+ print(f"[RECONEXIÓN ASIA] Reintentando en 15 segundos... Info: {e}")
331
  time.sleep(15)
332
 
 
333
  threading.Thread(target=conectar_chile_csn, daemon=True).start()
334
  threading.Thread(target=conectar_asia_iris, daemon=True).start()
335
 
336
+ with gr.Blocks() as demo:
337
+ gr.Markdown("# 🌍 Consola de Vigilancia Sísmica Total - Cobertura País Chile")
338
+ gr.Markdown("Escucha activa multipunto en el servidor del CSN sin bloqueos de comodines.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
 
340
+ with gr.Row():
341
+ gr.DataFrame(pd.DataFrame(ESTACIONES_CHILE), label="Malla de Sensores Chilenos en Monitoreo Directo")
342
 
343
+ demo.launch()