diff --git a/vigilancia/.env.example b/vigilancia/.env.example index de88ea4..3f422ed 100644 --- a/vigilancia/.env.example +++ b/vigilancia/.env.example @@ -51,6 +51,8 @@ GOOGLE_TV_APAGAR=1 GOOGLE_TV_PITIDO_ACTIVO=1 GOOGLE_TV_ESPERA_SG=15 GOOGLE_TV_PITIDO_SG=6 +GOOGLE_TV_REPETICIONES=3 +GOOGLE_TV_TEXTO_FINAL_SG=30 VOZ_EDGE=es-ES-AlvaroNeural VOZ_PITCH=-15Hz VOZ_RATE=-10% diff --git a/vigilancia/config.py b/vigilancia/config.py index 97a9e81..9bb2abb 100644 --- a/vigilancia/config.py +++ b/vigilancia/config.py @@ -72,3 +72,5 @@ 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_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 58adad7..10d7083 100644 --- a/vigilancia/teleprompter.py +++ b/vigilancia/teleprompter.py @@ -141,3 +141,55 @@ def generar_video(texto, ruta_audio, oraciones, nombre="anuncio.mp4"): proc.stdin.close() proc.wait() return ruta_video + + +def generar_video_texto_final(texto, segundos, nombre="texto_final.mp4"): + """Video estatico con el texto completo en pantalla durante `segundos` s.""" + from PIL import Image, ImageDraw, ImageFont + + 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() + gap = 18 + + lineas = [] + linea = [] + ancho = 0 + for w in texto.split(): + a = font.getlength(w) + if ancho + a + gap > W - 80 and linea: + lineas.append(linea) + linea = [w] + ancho = a + else: + linea.append(w) + ancho += a + gap + if linea: + lineas.append(linea) + + n_frames = int(segundos * FPS) + cmd = [FFMPEG, "-y", "-f", "rawvideo", "-vcodec", "rawvideo", + "-s", f"{W}x{H}", "-pix_fmt", "rgb24", "-r", str(FPS), "-i", "-", + "-c:v", "libx264", "-preset", "veryfast", "-pix_fmt", "yuv420p", + "-an", ruta_video] + proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, + stderr=subprocess.DEVNULL) + + for i in range(n_frames): + img = Image.new("RGB", (W, H), (8, 8, 8)) + d = ImageDraw.Draw(img) + y = H // 2 - (len(lineas) // 2) * 80 + 30 + for linea_words in lineas: + x = 40 + for w in linea_words: + d.text((x, y), w, fill=(255, 255, 255), font=font) + x += font.getlength(w) + gap + y += 80 + try: + proc.stdin.write(img.tobytes()) + except BrokenPipeError: + break + + proc.stdin.close() + proc.wait() + return ruta_video diff --git a/vigilancia/voz.py b/vigilancia/voz.py index 4738314..fede6a5 100644 --- a/vigilancia/voz.py +++ b/vigilancia/voz.py @@ -99,6 +99,22 @@ def _url_video(texto): return None, None +def _url_texto_final(texto): + """Video estatico con el texto completo (al final del bucle).""" + if config.GOOGLE_TV_TEXTO_FINAL_SG <= 0: + return None + try: + from teleprompter import generar_video_texto_final + ruta = generar_video_texto_final(texto, config.GOOGLE_TV_TEXTO_FINAL_SG) + _arrancar_servidor() + if _server is None: + return None + return f"http://{_ip_lan()}:{config.GOOGLE_TV_PUERTO}/{os.path.basename(ruta)}" + except Exception as e: + print(f"[Voz] No se pudo generar el texto final: {e}") + return None + + def _url_audio(texto, nombre="anuncio.mp3"): ruta = sintetizar(texto, nombre=nombre) if ruta is None: @@ -229,6 +245,7 @@ def anuncio_en_teles(texto): return url_pitido = _url_pitido() if config.GOOGLE_TV_PITIDO_ACTIVO else None url_silencio = _url_silencio() if url_pitido else None + url_final = _url_texto_final(texto) try: import pychromecast @@ -264,10 +281,18 @@ def anuncio_en_teles(texto): c, url_pitido, "audio/wav", "pitido", f"Pitido de emergencia en {c.cast_info.friendly_name}") - _reproducir_y_esperar( - c, url_mensaje, tipo_mensaje, texto[:80], - f"Anuncio en {c.cast_info.friendly_name} " - f"(vol {config.GOOGLE_TV_VOLUMEN})") + rep = max(1, config.GOOGLE_TV_REPETICIONES) + for n in range(rep): + _reproducir_y_esperar( + c, url_mensaje, tipo_mensaje, texto[:80], + f"Anuncio {n + 1}/{rep} en {c.cast_info.friendly_name} " + f"(vol {config.GOOGLE_TV_VOLUMEN})") + + if url_final: + _reproducir_y_esperar( + c, url_final, "video/mp4", "texto final", + f"Texto final {config.GOOGLE_TV_TEXTO_FINAL_SG}s en " + f"{c.cast_info.friendly_name}") if config.GOOGLE_TV_APAGAR_VIA_ADB: from tv_adb import apagar_tv