From fbfc395f3bcfb7915a0bae9bd97ca3c39773ee48 Mon Sep 17 00:00:00 2001 From: minguezsanzjuanjose Date: Sun, 2 Aug 2026 15:11:11 +0200 Subject: [PATCH] 02-ago: [proyecto] vigilancia - Fase 1: sirena wail estilo Proteccion Civil (400-1000Hz, 8s) + Fase 2: titulo ALERTA rojo parpadeante en el teleprompter --- vigilancia/.env.example | 6 +++++- vigilancia/config.py | 6 +++++- vigilancia/teleprompter.py | 25 +++++++++++++++++++++++++ vigilancia/voz.py | 14 ++++++++++---- 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/vigilancia/.env.example b/vigilancia/.env.example index 3f422ed..70eac27 100644 --- a/vigilancia/.env.example +++ b/vigilancia/.env.example @@ -50,9 +50,13 @@ GOOGLE_TV_VOLUMEN=30 GOOGLE_TV_APAGAR=1 GOOGLE_TV_PITIDO_ACTIVO=1 GOOGLE_TV_ESPERA_SG=15 -GOOGLE_TV_PITIDO_SG=6 +GOOGLE_TV_PITIDO_SG=8 +GOOGLE_TV_PITIDO_ESTILO=wail GOOGLE_TV_REPETICIONES=3 GOOGLE_TV_TEXTO_FINAL_SG=30 +TV_TITULO=ALERTA +TV_TITULO_COLOR=red +TV_TITULO_PARPADEO=1 VOZ_EDGE=es-ES-AlvaroNeural VOZ_PITCH=-15Hz VOZ_RATE=-10% diff --git a/vigilancia/config.py b/vigilancia/config.py index 9bb2abb..7cc5c43 100644 --- a/vigilancia/config.py +++ b/vigilancia/config.py @@ -61,6 +61,9 @@ VOZ_MENSAJE_CONOCIDO = os.getenv( "VOZ_MENSAJE_CONOCIDO", "{nombres}, ¿qué estás haciendo? No entres ahí.", ) +TV_TITULO = os.getenv("TV_TITULO", "ALERTA").strip() +TV_TITULO_COLOR = os.getenv("TV_TITULO_COLOR", "red").strip().lower() +TV_TITULO_PARPADEO = os.getenv("TV_TITULO_PARPADEO", "1") == "1" ALEXA_APAGAR_TV_MONKEY = os.getenv("ALEXA_APAGAR_TV_MONKEY", "").strip() ADB_PATH = os.getenv( "ADB_PATH", @@ -71,6 +74,7 @@ ADB_TV_IP = os.getenv("ADB_TV_IP", "192.168.50.16:5555") GOOGLE_TV_APAGAR_VIA_ADB = os.getenv("GOOGLE_TV_APAGAR_VIA_ADB", "1") == "1" GOOGLE_TV_PITIDO_ACTIVO = os.getenv("GOOGLE_TV_PITIDO_ACTIVO", "1") == "1" GOOGLE_TV_ESPERA_SG = int(os.getenv("GOOGLE_TV_ESPERA_SG", "15")) -GOOGLE_TV_PITIDO_SG = float(os.getenv("GOOGLE_TV_PITIDO_SG", "6")) +GOOGLE_TV_PITIDO_SG = float(os.getenv("GOOGLE_TV_PITIDO_SG", "8")) +GOOGLE_TV_PITIDO_ESTILO = os.getenv("GOOGLE_TV_PITIDO_ESTILO", "wail") GOOGLE_TV_REPETICIONES = int(os.getenv("GOOGLE_TV_REPETICIONES", "3")) GOOGLE_TV_TEXTO_FINAL_SG = int(os.getenv("GOOGLE_TV_TEXTO_FINAL_SG", "30")) diff --git a/vigilancia/teleprompter.py b/vigilancia/teleprompter.py index 10d7083..9869e7d 100644 --- a/vigilancia/teleprompter.py +++ b/vigilancia/teleprompter.py @@ -64,6 +64,27 @@ def sintetizar_con_oraciones(texto, nombre="anuncio.mp3"): return ruta, oraciones +_TITULO_COLORES = { + "red": (255, 30, 30), + "rojo": (255, 30, 30), + "rosa": (255, 105, 180), + "pink": (255, 105, 180), +} + + +def _dibujar_titulo(d, t, font_title): + """Titulo parpadeante en mayusculas (rojo/rosa) en la parte superior.""" + titulo = config.TV_TITULO.upper() + if not titulo: + return + if config.TV_TITULO_PARPADEO and int(t * 2) % 2 == 1: + return + color = _TITULO_COLORES.get(config.TV_TITULO_COLOR, (255, 30, 30)) + ancho = d.textlength(titulo, font=font_title) + x = (W - ancho) // 2 + d.text((x, 30), titulo, fill=color, font=font_title) + + def _palabras(oraciones): """Distribuye las palabras dentro de cada frase por longitud.""" palabras = [] @@ -90,6 +111,7 @@ def generar_video(texto, ruta_audio, oraciones, nombre="anuncio.mp4"): n_frames = int(total * FPS) font = ImageFont.truetype(FUENTE, 52) if FUENTE else ImageFont.load_default() + font_title = ImageFont.truetype(FUENTE, 96) if FUENTE else ImageFont.load_default() gap = 18 # agrupar en lineas @@ -120,6 +142,7 @@ def generar_video(texto, ruta_audio, oraciones, nombre="anuncio.mp4"): t = i / FPS img = Image.new("RGB", (W, H), (8, 8, 8)) d = ImageDraw.Draw(img) + _dibujar_titulo(d, t, font_title) y = H // 2 - (len(lineas) // 2) * 80 + 30 for linea_words in lineas: x = 40 @@ -150,6 +173,7 @@ def generar_video_texto_final(texto, segundos, nombre="texto_final.mp4"): ruta_video = os.path.join(os.path.dirname(os.path.abspath(__file__)), "voz_media", nombre) font = ImageFont.truetype(FUENTE, 52) if FUENTE else ImageFont.load_default() + font_title = ImageFont.truetype(FUENTE, 96) if FUENTE else ImageFont.load_default() gap = 18 lineas = [] @@ -178,6 +202,7 @@ def generar_video_texto_final(texto, segundos, nombre="texto_final.mp4"): for i in range(n_frames): img = Image.new("RGB", (W, H), (8, 8, 8)) d = ImageDraw.Draw(img) + _dibujar_titulo(d, i / FPS, font_title) y = H // 2 - (len(lineas) // 2) * 80 + 30 for linea_words in lineas: x = 40 diff --git a/vigilancia/voz.py b/vigilancia/voz.py index fede6a5..77130b2 100644 --- a/vigilancia/voz.py +++ b/vigilancia/voz.py @@ -156,7 +156,7 @@ def _esperar_fin(cast, url, timeout=120): def _generar_pitido(): - """Genera una sirena de emergencia (WAV) si no existe.""" + """Genera una sirena estilo Proteccion Civil (wail) si no existe.""" ruta = os.path.join(MEDIA_DIR, "pitido_emergencia.wav") if os.path.exists(ruta): return ruta @@ -168,10 +168,16 @@ def _generar_pitido(): sr = 44100 dur = config.GOOGLE_TV_PITIDO_SG t = np.linspace(0, dur, int(sr * dur), endpoint=False) - freq = 500 + 400 * np.abs(np.sin(2 * np.pi * 0.5 * t)) + if config.GOOGLE_TV_PITIDO_ESTILO == "wail": + # wail defensa civil: sube 400->1000 y baja en ciclos de ~4s + periodo = 4.0 + fase = (t % periodo) / periodo + freq = 400 + 600 * (1 - abs(2 * fase - 1)) + else: + freq = 500 + 400 * np.abs(np.sin(2 * np.pi * 0.5 * t)) fase = 2 * np.pi * np.cumsum(freq) / sr - y = 0.7 * np.sin(fase) - fade = min(0.2, dur / 4) + y = 0.8 * np.sin(fase) + fade = min(0.3, dur / 4) n_fade = int(fade * sr) y[:n_fade] *= np.linspace(0, 1, n_fade) y[-n_fade:] *= np.linspace(1, 0, n_fade)