import asyncio from pydub import AudioSegment import os import threading import http.server import socketserver import socket import time import subprocess import urllib.request # Para enviar HTTP GET a la ESP32 (sin librerías extra) # ================= CONFIGURACIÓN ================= IP_BOCINA = "192.168.15.135" PUERTO_ESP32 = 80 PUERTO_HTTP = 8000 CARPETA_AUDIO = "public_audio" if not os.path.exists(CARPETA_AUDIO): os.makedirs(CARPETA_AUDIO) # ================= AUTO-DETECTAR MI IP ================= def obtener_mi_ip(): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: s.connect((IP_BOCINA, 80)) IP = s.getsockname()[0] except Exception: IP = '127.0.0.1' finally: s.close() return IP MI_IP = obtener_mi_ip() print(f"[RED] IP auto-detectada para el servidor de audio: {MI_IP}") def iniciar_servidor_http(): class Handler(http.server.SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): super().__init__(*args, directory=CARPETA_AUDIO, **kwargs) def log_message(self, format, *args): print(f"[SERVIDOR HTTP] Petición de ESP32: {args[0]}") def handle(self): try: super().handle() except (BrokenPipeError, ConnectionResetError): pass def copyfile(self, source, outputfile): try: super().copyfile(source, outputfile) except (BrokenPipeError, ConnectionResetError): pass class ReusableTCPServer(socketserver.TCPServer): allow_reuse_address = True try: with ReusableTCPServer(("", PUERTO_HTTP), Handler) as httpd: print(f"Servidor de Audio activo en http://{MI_IP}:{PUERTO_HTTP}") httpd.serve_forever() except Exception as e: print(f"ERROR CRÍTICO AL LEVANTAR EL SERVIDOR: {e}") # ================= ORDEN A LA ESP32 ================= # La ESP32 usa HTTP GET en /play?url=..., NO WebSocket. # Antes usábamos WebSocket al puerto 81 → la ESP32 nunca recibió nada. def _mandar_orden_reproduccion(nombre_archivo): url_mp3 = f"http://{MI_IP}:{PUERTO_HTTP}/{nombre_archivo}" # Construimos la URL exacta que espera el handlePlay() de la ESP32 url_orden = f"http://{IP_BOCINA}:{PUERTO_ESP32}/play?url={url_mp3}" try: print(f"Ordenando a ESP32 reproducir: {url_mp3}") with urllib.request.urlopen(url_orden, timeout=5) as respuesta: estado = respuesta.read().decode() print(f"ESP32 respondió: {estado}") return True except Exception as e: print(f" Error enviando orden a la bocina: {e}") return False # ================= CONTROL DE VOLUMEN ================= def _mandar_orden_volumen(nivel=18): """Establece volumen directamente (0-21)""" url_orden = f"http://{IP_BOCINA}:{PUERTO_ESP32}/volume?level={nivel}" try: with urllib.request.urlopen(url_orden, timeout=5) as respuesta: print(f"Volumen: {respuesta.read().decode()}") return True except Exception as e: print(f"Error cambiando volumen: {e}") return False def subir_volumen(): """Sube 1 nivel de volumen""" url_orden = f"http://{IP_BOCINA}:{PUERTO_ESP32}/volume_up" try: with urllib.request.urlopen(url_orden, timeout=5) as respuesta: print(f" {respuesta.read().decode()}") return True except Exception as e: print(f"Error: {e}") return False def bajar_volumen(): """Baja 1 nivel de volumen""" url_orden = f"http://{IP_BOCINA}:{PUERTO_ESP32}/volume_down" try: with urllib.request.urlopen(url_orden, timeout=5) as respuesta: print(f"{respuesta.read().decode()}") return True except Exception as e: print(f"Error: {e}") return False # ================= LÓGICA PRINCIPAL ================= def enviar_archivos_bocina(lista_archivos): if not lista_archivos: return try: # 1. Unimos los audios audio_final = AudioSegment.empty() for archivo in lista_archivos: if os.path.exists(archivo): audio_final += AudioSegment.from_file(archivo) # 2. WAV temporal ruta_temp = os.path.join(CARPETA_AUDIO, "temp.wav") audio_final.export(ruta_temp, format="wav") # 3. Nombre único anti-caché timestamp = int(time.time()) nombre_salida = f"saludo_{timestamp}.mp3" ruta_salida = os.path.join(CARPETA_AUDIO, nombre_salida) # 4. MP3 ligero para reducir la presión sobre el buffer de Audio.h # El fix completo del OOM va en el .ino (ver abajo) comando_ffmpeg = [ "/usr/bin/ffmpeg", "-y", "-i", ruta_temp, "-codec:a", "libmp3lame", "-b:a", "48k", "-ar", "24000", "-ac", "1", # FILTRO ACÚSTICO: Corta todo lo que no sea voz humana "-af", "highpass=f=150, lowpass=f=7500", "-map_metadata", "-1", "-id3v2_version", "0", "-write_xing", "0", ruta_salida ] subprocess.run(comando_ffmpeg, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # 5. Limpieza del temporal if os.path.exists(ruta_temp): os.remove(ruta_temp) # 6. Enviamos la orden en un hilo separado # (evita conflicto con el runtime de InsightFace/ONNX) def _lanzar_orden(): time.sleep(0.5) # Aseguramos que el archivo ya está en disco _mandar_orden_reproduccion(nombre_salida) # Limpiamos MP3 viejos DESPUÉS de que la ESP32 confirmó for archivo in os.listdir(CARPETA_AUDIO): if archivo.startswith("saludo_") and archivo != nombre_salida: try: os.remove(os.path.join(CARPETA_AUDIO, archivo)) except Exception: pass hilo = threading.Thread(target=_lanzar_orden, daemon=True) hilo.start() except Exception as e: print(f"Error procesando el audio: {e}") # ============================================================================== # AUTO-ARRANQUE DEL SERVIDOR (Hilo en segundo plano) # ============================================================================== servidor_thread = threading.Thread(target=iniciar_servidor_http, daemon=True) servidor_thread.start() """ import asyncio from pydub import AudioSegment import os import threading import http.server import socketserver import socket import time import subprocess import urllib.request # Para enviar HTTP GET a la ESP32 (sin librerías extra) # ================= CONFIGURACIÓN ================= IP_BOCINA = "192.168.1.77" # La IP de la ESP32 que se definio como estatica PUERTO_ESP32 = 80 # La ESP32 escucha HTTP en el puerto 80 PUERTO_HTTP = 8000 CARPETA_AUDIO = "public_audio" if not os.path.exists(CARPETA_AUDIO): os.makedirs(CARPETA_AUDIO) # ================= SERVIDOR HTTP ================= def obtener_mi_ip(): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: s.connect(('10.255.255.255', 1)) IP = s.getsockname()[0] except Exception: IP = '127.0.0.1' finally: s.close() return IP MI_IP = obtener_mi_ip() def iniciar_servidor_http(): class Handler(http.server.SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): super().__init__(*args, directory=CARPETA_AUDIO, **kwargs) def log_message(self, format, *args): print(f"[SERVIDOR HTTP] Petición de ESP32: {args[0]}") def handle(self): try: super().handle() except (BrokenPipeError, ConnectionResetError): pass def copyfile(self, source, outputfile): try: super().copyfile(source, outputfile) except (BrokenPipeError, ConnectionResetError): pass class ReusableTCPServer(socketserver.TCPServer): allow_reuse_address = True try: with ReusableTCPServer(("", PUERTO_HTTP), Handler) as httpd: print(f"Servidor de Audio activo en http://{MI_IP}:{PUERTO_HTTP}") httpd.serve_forever() except Exception as e: print(f"ERROR CRÍTICO AL LEVANTAR EL SERVIDOR: {e}") # ================= ORDEN A LA ESP32 ================= # La ESP32 usa HTTP GET en /play?url=..., NO WebSocket. # Antes usábamos WebSocket al puerto 81 → la ESP32 nunca recibió nada. def _mandar_orden_reproduccion(nombre_archivo): url_mp3 = f"http://{MI_IP}:{PUERTO_HTTP}/{nombre_archivo}" # Construimos la URL exacta que espera el handlePlay() de la ESP32 url_orden = f"http://{IP_BOCINA}:{PUERTO_ESP32}/play?url={url_mp3}" try: print(f"Ordenando a ESP32 reproducir: {url_mp3}") with urllib.request.urlopen(url_orden, timeout=5) as respuesta: estado = respuesta.read().decode() print(f"ESP32 respondió: {estado}") return True except Exception as e: print(f" Error enviando orden a la bocina: {e}") return False # ================= LÓGICA PRINCIPAL ================= def enviar_archivos_bocina(lista_archivos): if not lista_archivos: return try: # 1. Unimos los audios audio_final = AudioSegment.empty() for archivo in lista_archivos: if os.path.exists(archivo): audio_final += AudioSegment.from_file(archivo) # 2. WAV temporal ruta_temp = os.path.join(CARPETA_AUDIO, "temp.wav") audio_final.export(ruta_temp, format="wav") # 3. Nombre único anti-caché timestamp = int(time.time()) nombre_salida = f"saludo_{timestamp}.mp3" ruta_salida = os.path.join(CARPETA_AUDIO, nombre_salida) # 4. MP3 ligero para reducir la presión sobre el buffer de Audio.h # El fix completo del OOM va en el .ino (ver abajo) comando_ffmpeg = [ "/usr/bin/ffmpeg", "-y", "-i", ruta_temp, "-codec:a", "libmp3lame", "-b:a", "48k", "-ar", "24000", "-ac", "1", # FILTRO ACÚSTICO: Corta todo lo que no sea voz humana "-af", "highpass=f=150, lowpass=f=7500", "-map_metadata", "-1", "-id3v2_version", "0", "-write_xing", "0", ruta_salida ] subprocess.run(comando_ffmpeg, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # 5. Limpieza del temporal if os.path.exists(ruta_temp): os.remove(ruta_temp) # 6. Enviamos la orden en un hilo separado # (evita conflicto con el runtime de InsightFace/ONNX) def _lanzar_orden(): time.sleep(0.5) # Aseguramos que el archivo ya está en disco _mandar_orden_reproduccion(nombre_salida) # Limpiamos MP3 viejos DESPUÉS de que la ESP32 confirmó for archivo in os.listdir(CARPETA_AUDIO): if archivo.startswith("saludo_") and archivo != nombre_salida: try: os.remove(os.path.join(CARPETA_AUDIO, archivo)) except Exception: pass hilo = threading.Thread(target=_lanzar_orden, daemon=True) hilo.start() except Exception as e: print(f"Error procesando el audio: {e}") # ============================================================================== # AUTO-ARRANQUE DEL SERVIDOR (Hilo en segundo plano) # ============================================================================== servidor_thread = threading.Thread(target=iniciar_servidor_http, daemon=True) servidor_thread.start() """