Añadir pipeline de audio y interfaz Gradio
Browse files- app.py +34 -0
- audio_pipeline.py +124 -0
app.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from audio_pipeline import (
|
| 3 |
+
descargar_audio,
|
| 4 |
+
separar_audio_demucs,
|
| 5 |
+
limpiar_stems,
|
| 6 |
+
combinar_stems_sin_vocales,
|
| 7 |
+
reducir_ruido
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
def generar_base(url: str):
|
| 11 |
+
# 1) Descarga el audio
|
| 12 |
+
descargar_audio(url, output_filename="audio")
|
| 13 |
+
# 2) Separa stems
|
| 14 |
+
separar_audio_demucs("audio.wav", output_dir="separated")
|
| 15 |
+
stems_dir = "separated/audio"
|
| 16 |
+
# 3) Limpia stems
|
| 17 |
+
limpiar_stems(stems_dir)
|
| 18 |
+
# 4) Genera base instrumental
|
| 19 |
+
combinar_stems_sin_vocales(stems_dir)
|
| 20 |
+
# 5) Reduce ruido
|
| 21 |
+
resultado = f"{stems_dir}/base_instrumental_clean.wav"
|
| 22 |
+
reducir_ruido(f"{stems_dir}/base_instrumental.wav", resultado)
|
| 23 |
+
return resultado
|
| 24 |
+
|
| 25 |
+
demo = gr.Interface(
|
| 26 |
+
fn=generar_base,
|
| 27 |
+
inputs=gr.Text(label="URL de YouTube"),
|
| 28 |
+
outputs=gr.Audio(label="Base instrumental limpia"),
|
| 29 |
+
title="Generador de Base Instrumental",
|
| 30 |
+
description="Pega la URL de un vídeo de YouTube y espera la creación de la instrumental."
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
if __name__ == "__main__":
|
| 34 |
+
demo.launch()
|
audio_pipeline.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import yt_dlp
|
| 2 |
+
import subprocess
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 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 |
+
# Funciones de Filtros y Efectos
|
| 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 |
+
filtered = signal.sosfilt(sos, audio)
|
| 19 |
+
return filtered
|
| 20 |
+
|
| 21 |
+
# ###########################################
|
| 22 |
+
# Etapa 1: Descarga y Separación con Demucs
|
| 23 |
+
# ###########################################
|
| 24 |
+
|
| 25 |
+
def descargar_audio(url, output_filename='audio'):
|
| 26 |
+
"""Descarga el audio de YouTube y lo convierte a WAV."""
|
| 27 |
+
opciones = {
|
| 28 |
+
'format': 'bestaudio/best',
|
| 29 |
+
'outtmpl': f'{output_filename}.%(ext)s',
|
| 30 |
+
'postprocessors': [{
|
| 31 |
+
'key': 'FFmpegExtractAudio',
|
| 32 |
+
'preferredcodec': 'wav',
|
| 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 |
+
'--out', output_dir
|
| 45 |
+
]
|
| 46 |
+
try:
|
| 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 |
+
# Función para limpiar cada stem (reducción de ruido, etc.)
|
| 55 |
+
# ###########################################
|
| 56 |
+
|
| 57 |
+
def limpiar_stems(stems_dir):
|
| 58 |
+
"""Aplica reducción de ruido a cada stem en stems_dir."""
|
| 59 |
+
for archivo in os.listdir(stems_dir):
|
| 60 |
+
if archivo.endswith('.wav'):
|
| 61 |
+
file_path = os.path.join(stems_dir, archivo)
|
| 62 |
+
y, sr = librosa.load(file_path, sr=None)
|
| 63 |
+
reduced = nr.reduce_noise(y=y, sr=sr)
|
| 64 |
+
sf.write(file_path.replace('.wav', '_cleaned.wav'), reduced, sr)
|
| 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 que contienen vocales."""
|
| 72 |
+
archivos = [f for f in os.listdir(stems_dir) if f.endswith('.wav')]
|
| 73 |
+
archivos_clean = [f for f in archivos if 'cleaned' in f.lower() and 'vocal' not in f.lower()]
|
| 74 |
+
if archivos_clean:
|
| 75 |
+
lista_archivos = archivos_clean
|
| 76 |
+
else:
|
| 77 |
+
lista_archivos = [f for f in archivos if 'vocal' not in f.lower()]
|
| 78 |
+
signals = []
|
| 79 |
+
for archivo in lista_archivos:
|
| 80 |
+
file_path = os.path.join(stems_dir, archivo)
|
| 81 |
+
print(f"Incluyendo: {archivo}")
|
| 82 |
+
y, sr = librosa.load(file_path, sr=None)
|
| 83 |
+
signals.append(y)
|
| 84 |
+
if not signals:
|
| 85 |
+
print("No se encontraron stems para combinar (excluyendo vocales).")
|
| 86 |
+
return
|
| 87 |
+
max_len = max(len(s) for s in signals)
|
| 88 |
+
mezclado = sum(np.pad(s, (0, max_len - len(s))) for s in signals) / len(signals)
|
| 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(noise_duration * sr)]
|
| 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.")
|