Implementación completa de audio_pipeline y app Gradio
Browse files- app.py +17 -8
- audio_pipeline.py +58 -95
app.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import gradio as gr
|
| 3 |
from audio_pipeline import (
|
|
@@ -8,22 +10,29 @@ from audio_pipeline import (
|
|
| 8 |
)
|
| 9 |
|
| 10 |
def procesar_wav(input_wav_path):
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
| 14 |
limpiar_stems(stems_dir)
|
|
|
|
|
|
|
| 15 |
combinar_stems_sin_vocales(stems_dir)
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
demo = gr.Interface(
|
| 22 |
fn=procesar_wav,
|
| 23 |
inputs=gr.Audio(label="Sube un archivo .wav", type="filepath"),
|
| 24 |
outputs=gr.Audio(label="Base instrumental limpia", type="filepath"),
|
| 25 |
title="Procesador de WAV a Base Instrumental",
|
| 26 |
-
description="Sube tu
|
| 27 |
)
|
| 28 |
|
| 29 |
if __name__ == "__main__":
|
|
|
|
| 1 |
+
# app.py
|
| 2 |
+
|
| 3 |
import os
|
| 4 |
import gradio as gr
|
| 5 |
from audio_pipeline import (
|
|
|
|
| 10 |
)
|
| 11 |
|
| 12 |
def procesar_wav(input_wav_path):
|
| 13 |
+
# 1) Separar con Demucs y obtener carpeta de stems
|
| 14 |
+
stems_dir = separar_audio_demucs(input_wav_path)
|
| 15 |
+
|
| 16 |
+
# 2) Limpiar stems (_cleaned.wav)
|
| 17 |
limpiar_stems(stems_dir)
|
| 18 |
+
|
| 19 |
+
# 3) Combinar stems sin vocales en base_instrumental.wav
|
| 20 |
combinar_stems_sin_vocales(stems_dir)
|
| 21 |
+
|
| 22 |
+
# 4) Reducir ruido sobre la base resultante
|
| 23 |
+
base_instru = os.path.join(stems_dir, "base_instrumental.wav")
|
| 24 |
+
clean_instru = os.path.join(stems_dir, "base_instrumental_clean.wav")
|
| 25 |
+
reducir_ruido(base_instru, clean_instru)
|
| 26 |
+
|
| 27 |
+
# 5) Devolver la ruta del WAV limpio
|
| 28 |
+
return clean_instru
|
| 29 |
|
| 30 |
demo = gr.Interface(
|
| 31 |
fn=procesar_wav,
|
| 32 |
inputs=gr.Audio(label="Sube un archivo .wav", type="filepath"),
|
| 33 |
outputs=gr.Audio(label="Base instrumental limpia", type="filepath"),
|
| 34 |
title="Procesador de WAV a Base Instrumental",
|
| 35 |
+
description="Sube tu WAV y obtén su base limpia sin vocales."
|
| 36 |
)
|
| 37 |
|
| 38 |
if __name__ == "__main__":
|
audio_pipeline.py
CHANGED
|
@@ -1,124 +1,87 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
| 2 |
import subprocess
|
| 3 |
import sys
|
| 4 |
-
import
|
| 5 |
import librosa
|
| 6 |
import numpy as np
|
| 7 |
import soundfile as sf
|
| 8 |
import noisereduce as nr
|
| 9 |
-
import scipy.signal as signal
|
| 10 |
|
| 11 |
-
#
|
| 12 |
-
|
| 13 |
-
# ###########################################
|
| 14 |
|
| 15 |
def highpass_filter(audio, sr, cutoff=80, order=4):
|
| 16 |
"""Aplica un filtro Butterworth de paso alto al audio."""
|
| 17 |
sos = signal.butter(order, cutoff, btype='highpass', fs=sr, output='sos')
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
}
|
| 35 |
-
with yt_dlp.YoutubeDL(opciones) as ydl:
|
| 36 |
-
ydl.download([url])
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def separar_audio_demucs(input_file, output_dir="separated"):
|
| 40 |
-
"""Separa stems usando Demucs."""
|
| 41 |
-
comando = [
|
| 42 |
-
sys.executable, '-m', 'demucs',
|
| 43 |
input_file,
|
| 44 |
-
|
|
|
|
| 45 |
]
|
| 46 |
-
|
| 47 |
-
subprocess.run(comando, check=True)
|
| 48 |
-
print("Separación con Demucs completada.")
|
| 49 |
-
except subprocess.CalledProcessError as e:
|
| 50 |
-
print(f"Error durante la separación con Demucs: {e}")
|
| 51 |
-
sys.exit(1)
|
| 52 |
|
| 53 |
-
#
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
def limpiar_stems(stems_dir):
|
| 58 |
-
"""Aplica reducción de ruido a cada stem
|
| 59 |
for archivo in os.listdir(stems_dir):
|
| 60 |
-
if archivo.endswith(
|
| 61 |
-
|
| 62 |
-
y, sr = librosa.load(
|
| 63 |
reduced = nr.reduce_noise(y=y, sr=sr)
|
| 64 |
-
sf.write(
|
| 65 |
-
|
| 66 |
-
# ###########################################
|
| 67 |
-
# Combina stems excluyendo la parte vocal
|
| 68 |
-
# ###########################################
|
| 69 |
|
| 70 |
def combinar_stems_sin_vocales(stems_dir):
|
| 71 |
-
"""Mezcla todos los stems excepto los
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
signals = []
|
| 79 |
-
for
|
| 80 |
-
|
| 81 |
-
print(f"Incluyendo: {archivo}")
|
| 82 |
-
y, sr = librosa.load(file_path, sr=None)
|
| 83 |
signals.append(y)
|
| 84 |
if not signals:
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
sf.write(os.path.join(stems_dir, 'base_instrumental.wav'), mezclado, sr)
|
| 90 |
-
|
| 91 |
-
# ###########################################
|
| 92 |
-
# Reducción de ruido en archivo de audio
|
| 93 |
-
# ###########################################
|
| 94 |
|
| 95 |
def reducir_ruido(input_file, output_file, noise_duration=0.5):
|
| 96 |
"""Aplica reducción de ruido basada en los primeros segundos de audio."""
|
| 97 |
y, sr = librosa.load(input_file, sr=None)
|
| 98 |
-
noise = y[:int(
|
| 99 |
reduced = nr.reduce_noise(y=y, sr=sr, y_noise=noise)
|
| 100 |
sf.write(output_file, reduced, sr)
|
| 101 |
-
|
| 102 |
-
# ###########################################
|
| 103 |
-
# Función principal
|
| 104 |
-
# ###########################################
|
| 105 |
-
|
| 106 |
-
def main():
|
| 107 |
-
url = input("Introduce la URL de YouTube: ")
|
| 108 |
-
output_name = input("Nombre base para archivos (sin extensión): ")
|
| 109 |
-
descargar_audio(url, output_filename=output_name)
|
| 110 |
-
audio_file = f"{output_name}.wav"
|
| 111 |
-
separar_audio_demucs(audio_file, output_dir="separated")
|
| 112 |
-
stems_dir = os.path.join("separated", output_name)
|
| 113 |
-
limpiar_stems(stems_dir)
|
| 114 |
-
|
| 115 |
-
output_base = os.path.join(stems_dir, 'base_instrumental.wav')
|
| 116 |
-
print("Combinando stems 'cleaned' para generar la base instrumental...")
|
| 117 |
-
combinar_stems_sin_vocales(stems_dir)
|
| 118 |
-
|
| 119 |
-
output_clean = os.path.join(stems_dir, 'base_instrumental_clean.wav')
|
| 120 |
-
print("Aplicando reducción de ruido...")
|
| 121 |
-
reducir_ruido(output_base, output_clean, noise_duration=0.5)
|
| 122 |
-
|
| 123 |
-
# Se omite la etapa de mastering para mantener la calidad original de la mezcla
|
| 124 |
-
print("Proceso completado. Revisa el archivo 'base_instrumental_clean.wav' para escuchar el resultado final.")
|
|
|
|
| 1 |
+
# audio_pipeline.py
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
import subprocess
|
| 5 |
import sys
|
| 6 |
+
import torch
|
| 7 |
import librosa
|
| 8 |
import numpy as np
|
| 9 |
import soundfile as sf
|
| 10 |
import noisereduce as nr
|
|
|
|
| 11 |
|
| 12 |
+
# Directorio base donde guardaremos todos los stems
|
| 13 |
+
BASE_STEMS_DIR = "data/stems"
|
|
|
|
| 14 |
|
| 15 |
def highpass_filter(audio, sr, cutoff=80, order=4):
|
| 16 |
"""Aplica un filtro Butterworth de paso alto al audio."""
|
| 17 |
sos = signal.butter(order, cutoff, btype='highpass', fs=sr, output='sos')
|
| 18 |
+
return signal.sosfilt(sos, audio)
|
| 19 |
+
|
| 20 |
+
def separar_audio_demucs(input_file, model="htdemucs"):
|
| 21 |
+
"""
|
| 22 |
+
Separa stems usando Demucs, los deja en:
|
| 23 |
+
data/stems/<modelo>/<base_name>/
|
| 24 |
+
y devuelve la ruta al folder con los stems.
|
| 25 |
+
"""
|
| 26 |
+
# 1) Crear directorio raíz para ese modelo
|
| 27 |
+
out_root = os.path.join(BASE_STEMS_DIR, model)
|
| 28 |
+
os.makedirs(out_root, exist_ok=True)
|
| 29 |
+
|
| 30 |
+
# 2) Ejecutar Demucs (en GPU si está disponible)
|
| 31 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 32 |
+
cmd = [
|
| 33 |
+
sys.executable, "-m", "demucs",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
input_file,
|
| 35 |
+
"--out", out_root,
|
| 36 |
+
"--device", device
|
| 37 |
]
|
| 38 |
+
subprocess.run(cmd, check=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
+
# 3) Detectar carpeta generada con los stems
|
| 41 |
+
base = os.path.splitext(os.path.basename(input_file))[0]
|
| 42 |
+
stems_dir = os.path.join(out_root, base)
|
| 43 |
+
if not os.path.isdir(stems_dir):
|
| 44 |
+
raise FileNotFoundError(f"No existe el folder de stems en {stems_dir}")
|
| 45 |
+
return stems_dir
|
| 46 |
|
| 47 |
def limpiar_stems(stems_dir):
|
| 48 |
+
"""Aplica reducción de ruido a cada stem dentro de stems_dir."""
|
| 49 |
for archivo in os.listdir(stems_dir):
|
| 50 |
+
if archivo.endswith(".wav"):
|
| 51 |
+
ruta = os.path.join(stems_dir, archivo)
|
| 52 |
+
y, sr = librosa.load(ruta, sr=None)
|
| 53 |
reduced = nr.reduce_noise(y=y, sr=sr)
|
| 54 |
+
sf.write(ruta.replace(".wav", "_cleaned.wav"), reduced, sr)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
def combinar_stems_sin_vocales(stems_dir):
|
| 57 |
+
"""Mezcla todos los stems (limpios) excepto los de vocales."""
|
| 58 |
+
# Primero, buscar stems *_cleaned.wav que no contengan 'vocal'
|
| 59 |
+
wavs = [
|
| 60 |
+
f for f in os.listdir(stems_dir)
|
| 61 |
+
if f.endswith(".wav") and
|
| 62 |
+
"cleaned" in f.lower() and
|
| 63 |
+
"vocal" not in f.lower()
|
| 64 |
+
]
|
| 65 |
+
# Si no hay cleaned, fallback a stems originales sin 'vocal'
|
| 66 |
+
if not wavs:
|
| 67 |
+
wavs = [
|
| 68 |
+
f for f in os.listdir(stems_dir)
|
| 69 |
+
if f.endswith(".wav") and "vocal" not in f.lower()
|
| 70 |
+
]
|
| 71 |
+
|
| 72 |
signals = []
|
| 73 |
+
for w in wavs:
|
| 74 |
+
y, sr = librosa.load(os.path.join(stems_dir, w), sr=None)
|
|
|
|
|
|
|
| 75 |
signals.append(y)
|
| 76 |
if not signals:
|
| 77 |
+
raise RuntimeError("No se encontraron stems para combinar.")
|
| 78 |
+
maxlen = max(len(s) for s in signals)
|
| 79 |
+
mix = sum(np.pad(s, (0, maxlen - len(s))) for s in signals) / len(signals)
|
| 80 |
+
sf.write(os.path.join(stems_dir, "base_instrumental.wav"), mix, sr)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
def reducir_ruido(input_file, output_file, noise_duration=0.5):
|
| 83 |
"""Aplica reducción de ruido basada en los primeros segundos de audio."""
|
| 84 |
y, sr = librosa.load(input_file, sr=None)
|
| 85 |
+
noise = y[:int(sr * noise_duration)]
|
| 86 |
reduced = nr.reduce_noise(y=y, sr=sr, y_noise=noise)
|
| 87 |
sf.write(output_file, reduced, sr)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|