- pet_telegram.py: cliente de Telegram con cola y hilo de envio propio, para que la red nunca bloquee la vision ni la escucha de audio. Reintentos con retroceso exponencial, respeto del retry_after ante limites de frecuencia y descarte de los avisos mas viejos si la cola se llena. - pet_alerts.py: punto unico de reparto de avisos. Toma el fotograma mas reciente, dibuja la caja de la mascota detectada y la marca de tiempo, guarda la captura en capturas/ y la manda a Telegram. Anti-repeticion de 10 s en consola y de ALERT_COOLDOWN (60 s) para capturas y Telegram. - scripts/telegram_check.py: valida el token, deduce el chat_id del ultimo mensaje recibido por el bot y manda mensaje y foto de prueba. - pet_speed.py: integra el reparto de avisos y anade capturas y estado de Telegram a las estadisticas. - Readme.md: seccion de Telegram y estado de la Fase B.
139 lines
5.2 KiB
Python
139 lines
5.2 KiB
Python
"""
|
|
Reparto de avisos: qué sale por consola, qué se guarda como captura y qué se
|
|
manda a Telegram.
|
|
|
|
Las capturas se toman del fotograma más reciente del stream de vídeo en el
|
|
instante del aviso (no del que se esté analizando), y se anotan con las cajas
|
|
de la última detección más una marca de tiempo.
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
|
|
import cv2
|
|
|
|
# Tipos de aviso que merecen foto (y por tanto Telegram)
|
|
TIPOS_CON_CAPTURA = ("carrera", "ladrido")
|
|
|
|
# Anti-repetición por consola: sin esto, un perro corriendo imprime 14 líneas
|
|
# por segundo y tapa las estadísticas.
|
|
CONSOLA_POR_DEFECTO = {"carrera": 10.0, "calmado": 10.0, "ladrido": 0.0}
|
|
|
|
|
|
class AlertDispatcher:
|
|
"""
|
|
Punto único por el que pasan todos los avisos del monitor.
|
|
|
|
`alert(kind, mensaje)` hace tres cosas, cada una con su propia cadencia:
|
|
- imprime por consola (anti-repetición corta),
|
|
- guarda una captura anotada en disco,
|
|
- la envía a Telegram.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
stream,
|
|
notifier=None,
|
|
snapshot_dir=None,
|
|
save_snapshots=True,
|
|
capture_cooldown=60.0,
|
|
console_cooldowns=None,
|
|
jpeg_quality=85,
|
|
):
|
|
self._stream = stream
|
|
self._notifier = notifier
|
|
self.snapshot_dir = snapshot_dir
|
|
self.save_snapshots = save_snapshots
|
|
self.capture_cooldown = capture_cooldown
|
|
self.console_cooldowns = dict(CONSOLA_POR_DEFECTO)
|
|
if console_cooldowns:
|
|
self.console_cooldowns.update(console_cooldowns)
|
|
self.jpeg_quality = jpeg_quality
|
|
|
|
self._cajas = []
|
|
self._ultimo_consola = {}
|
|
self._ultima_captura = {}
|
|
self.snapshots = 0
|
|
self.last_snapshot = None
|
|
|
|
def set_pet_boxes(self, cajas):
|
|
"""Guarda las cajas del último fotograma analizado, para dibujarlas."""
|
|
self._cajas = list(cajas)
|
|
|
|
def alert(self, kind, message):
|
|
"""
|
|
Emite un aviso. Devuelve True si hizo algo (lo imprimiera o no).
|
|
`kind` es "carrera", "ladrido" o "calmado".
|
|
"""
|
|
ahora = time.time()
|
|
consola = ahora - self._ultimo_consola.get(kind, 0.0) >= self.console_cooldowns.get(kind, 10.0)
|
|
captura = (
|
|
kind in TIPOS_CON_CAPTURA
|
|
and ahora - self._ultima_captura.get(kind, 0.0) >= self.capture_cooldown
|
|
)
|
|
|
|
if consola:
|
|
self._ultimo_consola[kind] = ahora
|
|
print(message, flush=True)
|
|
if captura:
|
|
self._ultima_captura[kind] = ahora
|
|
self._captura(message)
|
|
|
|
return consola or captura
|
|
|
|
def stats(self):
|
|
"""Resumen para la línea de estadísticas."""
|
|
partes = [f"capturas {self.snapshots}"]
|
|
if self._notifier is not None:
|
|
partes.append(
|
|
f"telegram {self._notifier.sent} enviados"
|
|
+ (f", {self._notifier.errors} fallos" if self._notifier.errors else "")
|
|
+ (f", {self._notifier.dropped} descartados" if self._notifier.dropped else "")
|
|
)
|
|
return " | ".join(partes)
|
|
|
|
# --- interno ---------------------------------------------------------
|
|
def _captura(self, message):
|
|
frame = self._stream.latest_frame()
|
|
anotado = self._anota(frame, message) if frame is not None else None
|
|
|
|
if anotado is not None and self.save_snapshots and self.snapshot_dir:
|
|
self._guarda(anotado, message)
|
|
|
|
if self._notifier is None:
|
|
return
|
|
if anotado is None:
|
|
self._notifier.send_message(message)
|
|
return
|
|
|
|
ok, buffer = cv2.imencode(".jpg", anotado, [int(cv2.IMWRITE_JPEG_QUALITY), self.jpeg_quality])
|
|
if ok:
|
|
self._notifier.send_photo(buffer.tobytes(), message)
|
|
else:
|
|
self._notifier.send_message(message)
|
|
|
|
def _anota(self, frame, message):
|
|
"""Copia el fotograma con las cajas detectadas y una marca de tiempo."""
|
|
imagen = frame.copy()
|
|
for etiqueta, confianza, (x1, y1, x2, y2) in self._cajas:
|
|
color = (0, 0, 255) if "corriendo" in message or "Ladrido" in message else (0, 200, 0)
|
|
cv2.rectangle(imagen, (int(x1), int(y1)), (int(x2), int(y2)), color, 2)
|
|
texto = f"{etiqueta} {confianza:.2f}"
|
|
(ancho, alto), _ = cv2.getTextSize(texto, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)
|
|
cv2.rectangle(imagen, (int(x1), int(y1) - alto - 8), (int(x1) + ancho + 6, int(y1)), color, -1)
|
|
cv2.putText(imagen, texto, (int(x1) + 3, int(y1) - 5),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
|
|
|
|
sello = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
cv2.putText(imagen, sello, (12, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 4)
|
|
cv2.putText(imagen, sello, (12, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
|
|
return imagen
|
|
|
|
def _guarda(self, imagen, message):
|
|
os.makedirs(self.snapshot_dir, exist_ok=True)
|
|
tipo = "ladrido" if "Ladrido" in message else "carrera" if "corriendo" in message else "aviso"
|
|
nombre = time.strftime("%Y-%m-%d_%H-%M-%S") + f"_{tipo}.jpg"
|
|
ruta = os.path.join(self.snapshot_dir, nombre)
|
|
if cv2.imwrite(ruta, imagen):
|
|
self.snapshots += 1
|
|
self.last_snapshot = ruta
|