350 lines
12 KiB
Python
350 lines
12 KiB
Python
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()
|
|
""" |