IdentificacionIA/audio_iot.py
2026-06-25 11:06:15 -06:00

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()
"""