AICoverGen / src /main.py
R-Kentaren's picture
Upload src/main.py with huggingface_hub
0fa8e4d verified
Raw
History Blame Contribute Delete
38.6 kB
import spaces
import torch
import argparse
import gc
import hashlib
import json
import os
import shlex
import subprocess
from contextlib import suppress
from urllib.parse import urlparse, parse_qs
import time
import shutil
import logging
import gradio as gr
import librosa
import numpy as np
import soundfile as sf
import sox
import yt_dlp
from pedalboard import (
Pedalboard, Reverb, Compressor, HighpassFilter, LowpassFilter,
NoiseGate, Limiter, Gain, HighShelfFilter, LowShelfFilter, PeakFilter,
Distortion, Bitcrush, Chorus, Delay, Phaser, Clipping,
)
from pedalboard.io import AudioFile
from pydub import AudioSegment
import noisereduce as nr
from uvr import run_mdx
from rvc import Config, load_hubert, get_vc, rvc_infer
# Optional: import improved modules (caching, history, presets)
try:
from modules import ModelCache, HistoryManager, PresetManager
_HAS_MODULES = True
except Exception as _e:
print(f"[main] improved modules not available: {_e}")
_HAS_MODULES = False
logging.getLogger("httpx").setLevel(logging.WARNING)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
IS_ZERO_GPU = os.getenv("SPACES_ZERO_GPU")
mdxnet_models_dir = os.path.join(BASE_DIR, 'mdxnet_models')
rvc_models_dir = os.path.join(BASE_DIR, 'rvc_models')
output_dir = os.path.join(BASE_DIR, 'song_output')
# Directories for improved features
data_dir = os.path.join(BASE_DIR, 'data')
os.makedirs(data_dir, exist_ok=True)
# Singletons (lazy-init)
_history_manager = None
_preset_manager = None
def get_history_manager():
global _history_manager
if _history_manager is None and _HAS_MODULES:
_history_manager = HistoryManager(os.path.join(data_dir, 'history.json'))
return _history_manager
def get_preset_manager():
global _preset_manager
if _preset_manager is None and _HAS_MODULES:
_preset_manager = PresetManager(os.path.join(data_dir, 'presets.json'))
return _preset_manager
# Model cache (only used when not on ZeroGPU, since ZeroGPU reloads per-call)
_rvc_cache = None
def get_rvc_cache():
global _rvc_cache
if _rvc_cache is None and _HAS_MODULES and not IS_ZERO_GPU:
_rvc_cache = ModelCache(max_entries=3, max_age_seconds=1200)
return _rvc_cache
def clean_old_folders(base_path: str, max_age_seconds: int = 10800):
if not os.path.isdir(base_path):
print(f"Error: {base_path} is not a valid directory.")
return
now = time.time()
for folder_name in os.listdir(base_path):
folder_path = os.path.join(base_path, folder_name)
if os.path.isdir(folder_path):
last_modified = os.path.getmtime(folder_path)
if now - last_modified > max_age_seconds:
shutil.rmtree(folder_path)
def get_youtube_video_id(url, ignore_playlist=True):
"""
Examples:
http://youtu.be/SA2iWivDJiE
http://www.youtube.com/watch?v=_oPAwA_Udwc&feature=feedu
http://www.youtube.com/embed/SA2iWivDJiE
http://www.youtube.com/v/SA2iWivDJiE?version=3&hl=en_US
"""
if "m.youtube.com" in url:
url = url.replace("m.youtube.com", "www.youtube.com")
query = urlparse(url)
if query.hostname == 'youtu.be':
if query.path[1:] == 'watch':
return query.query[2:]
return query.path[1:]
if query.hostname in {'www.youtube.com', 'youtube.com', 'music.youtube.com'}:
if not ignore_playlist:
# use case: get playlist id not current video in playlist
with suppress(KeyError):
return parse_qs(query.query)['list'][0]
if query.path == '/watch':
return parse_qs(query.query)['v'][0]
if query.path[:7] == '/watch/':
return query.path.split('/')[1]
if query.path[:7] == '/embed/':
return query.path.split('/')[2]
if query.path[:3] == '/v/':
return query.path.split('/')[2]
# returns None for invalid YouTube url
return None
def yt_download(link):
if not link.strip():
gr.Info("You need to provide a download link.")
return None
ydl_opts = {
'format': 'bestaudio',
'outtmpl': '%(title)s',
'nocheckcertificate': True,
'ignoreerrors': True,
'no_warnings': True,
'quiet': True,
'extractaudio': True,
'postprocessors': [{'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3'}],
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
result = ydl.extract_info(link, download=True)
download_path = ydl.prepare_filename(result, outtmpl='%(title)s.mp3')
return download_path
def raise_exception(error_msg, is_webui):
if is_webui:
raise gr.Error(error_msg)
else:
raise Exception(error_msg)
def get_rvc_model(voice_model, is_webui):
rvc_model_filename, rvc_index_filename = None, None
model_dir = os.path.join(rvc_models_dir, voice_model)
for file in os.listdir(model_dir):
if os.path.isdir(file):
for ff in os.listdir(file):
ext = os.path.splitext(ff)[1]
if ext == '.pth':
rvc_model_filename = ff
if ext == '.index':
rvc_index_filename = ff
ext = os.path.splitext(file)[1]
if ext == '.pth':
rvc_model_filename = file
if ext == '.index':
rvc_index_filename = file
if rvc_model_filename is None:
error_msg = f'No model file exists in {model_dir}.'
raise_exception(error_msg, is_webui)
return os.path.join(model_dir, rvc_model_filename), os.path.join(model_dir, rvc_index_filename) if rvc_index_filename else ''
def get_audio_paths(song_dir):
orig_song_path = None
instrumentals_path = None
main_vocals_dereverb_path = None
backup_vocals_path = None
for file in os.listdir(song_dir):
if file.endswith('_Instrumental.wav'):
instrumentals_path = os.path.join(song_dir, file)
orig_song_path = instrumentals_path.replace('_Instrumental', '')
elif file.endswith('_Vocals_Main_DeReverb.wav'):
main_vocals_dereverb_path = os.path.join(song_dir, file)
elif file.endswith('_Vocals_Backup.wav'):
backup_vocals_path = os.path.join(song_dir, file)
return orig_song_path, instrumentals_path, main_vocals_dereverb_path, backup_vocals_path
def get_audio_with_suffix(song_dir, suffix="_mysuffix.wav"):
target_path = None
for file in os.listdir(song_dir):
if file.endswith(suffix):
target_path = os.path.join(song_dir, file)
break
return target_path
def convert_to_stereo(audio_path):
# Use ffmpeg to probe + convert. This is more robust than librosa
# because it doesn't depend on audioread backends.
# First, convert to a standard stereo 44100Hz WAV regardless of input format.
# This sidesteps mono/stereo detection issues and ensures the pipeline
# always gets a clean stereo WAV.
stereo_path = f'{os.path.splitext(audio_path)[0]}_stereo.wav'
# Check if already a stereo WAV at 44100Hz — if so, skip conversion
try:
info = sf.info(audio_path)
if info.channels == 2 and info.samplerate == 44100:
return audio_path
except Exception:
pass
# Use ffmpeg to convert to stereo WAV
command = shlex.split(f'ffmpeg -y -loglevel error -i "{audio_path}" -ac 2 -ar 44100 -f wav "{stereo_path}"')
result = subprocess.run(command, capture_output=True, text=True)
if result.returncode != 0 or not os.path.exists(stereo_path):
# Fallback: try librosa
try:
wave, sr = librosa.load(audio_path, mono=False, sr=44100)
if type(wave[0]) != np.ndarray:
# mono — duplicate to stereo
wave = np.stack([wave, wave])
sf.write(stereo_path, wave.T, 44100)
return stereo_path
except Exception as lib_err:
raise Exception(
f"Cannot convert audio to stereo. ffmpeg error: {result.stderr}. "
f"librosa error: {lib_err}. "
f"File: {audio_path}"
)
return stereo_path
def pitch_shift(audio_path, pitch_change):
output_path = f'{os.path.splitext(audio_path)[0]}_p{pitch_change}.wav'
if not os.path.exists(output_path):
y, sr = sf.read(audio_path)
tfm = sox.Transformer()
tfm.pitch(pitch_change)
y_shifted = tfm.build_array(input_array=y, sample_rate_in=sr)
sf.write(output_path, y_shifted, sr)
return output_path
def get_hash(filepath):
with open(filepath, 'rb') as f:
file_hash = hashlib.blake2b()
while chunk := f.read(8192):
file_hash.update(chunk)
return file_hash.hexdigest()[:11]
def display_progress(message, percent, is_webui, progress=None):
"""Update the Gradio progress bar.
Uses a smooth 0.0-1.0 scale with descriptive messages.
Falls back to print() for CLI mode.
"""
if is_webui and progress is not None:
try:
progress(float(percent), desc=message)
except Exception:
pass
else:
print(f"[{percent*100:.0f}%] {message}")
def preprocess_song(song_input, mdx_model_params, song_id, is_webui, input_type, progress, keep_orig, orig_song_path):
song_output_dir = os.path.join(output_dir, song_id)
display_progress('Separating vocals from instrumental...', 0.10, is_webui, progress)
vocals_path, instrumentals_path = run_mdx(mdx_model_params, song_output_dir, os.path.join(mdxnet_models_dir, 'UVR-MDX-NET-Voc_FT.onnx'), orig_song_path, denoise=True, keep_orig=keep_orig)
display_progress('Separating main vocals from backup vocals...', 0.20, is_webui, progress)
backup_vocals_path, main_vocals_path = run_mdx(mdx_model_params, song_output_dir, os.path.join(mdxnet_models_dir, 'UVR_MDXNET_KARA_2.onnx'), vocals_path, suffix='Backup', invert_suffix='Main', denoise=True)
display_progress('Removing reverb from vocals...', 0.30, is_webui, progress)
_, main_vocals_dereverb_path = run_mdx(mdx_model_params, song_output_dir, os.path.join(mdxnet_models_dir, 'Reverb_HQ_By_FoxJoy.onnx'), main_vocals_path, invert_suffix='DeReverb', exclude_main=True, denoise=True)
return orig_song_path, vocals_path, instrumentals_path, main_vocals_path, backup_vocals_path, main_vocals_dereverb_path
def get_audio_file(song_input, is_webui, input_type, progress):
keep_orig = False
if input_type == 'yt':
display_progress('Downloading song from YouTube...', 0.02, is_webui, progress)
song_link = song_input.split('&')[0]
orig_song_path = yt_download(song_link)
elif input_type == 'local':
orig_song_path = song_input
keep_orig = True
else:
orig_song_path = None
return keep_orig, orig_song_path
device = "cuda:0" if torch.cuda.is_available() else "cpu"
compute_half = True if torch.cuda.is_available() else False
config = Config(device, compute_half)
hubert_model = load_hubert("cuda", config.is_half, os.path.join(rvc_models_dir, 'hubert_base.pt'))
print(device, "half>>", config.is_half)
def voice_change(voice_model, vocals_path, output_path, pitch_change, f0_method, index_rate, filter_radius, rms_mix_rate, protect, crepe_hop_length, is_webui, steps):
rvc_model_path, rvc_index_path = get_rvc_model(voice_model, is_webui)
device = "cuda:0" if torch.cuda.is_available() else "cpu"
compute_half = True if torch.cuda.is_available() else False
config = Config(device, compute_half)
# Try to use cached model (avoids re-loading the .pth on every call).
# We cache only (cpt, version, net_g, tgt_sr, vc) keyed by model path + half precision flag.
cache = get_rvc_cache()
cache_key = f"vc::{rvc_model_path}::{config.is_half}"
cached = cache.get(cache_key) if cache else None
cache_hit = False
if cached is not None:
cpt, version, net_g, tgt_sr, vc = cached
cache_hit = True
else:
cpt, version, net_g, tgt_sr, vc = get_vc(device, config.is_half, config, rvc_model_path)
if cache is not None:
cache.put(cache_key, (cpt, version, net_g, tgt_sr, vc))
# convert main vocals
global hubert_model
rvc_infer(rvc_index_path, index_rate, vocals_path, output_path, pitch_change, f0_method, cpt, version, net_g, filter_radius, tgt_sr, rms_mix_rate, protect, crepe_hop_length, vc, hubert_model, steps)
# Do NOT delete cpt here when cached - the cache owns it.
# When not cached, free it now to release memory.
if not cache_hit:
del cpt
gc.collect()
def add_audio_effects(audio_path, reverb_rm_size, reverb_wet, reverb_dry, reverb_damping,
highpass_freq=80.0, lowpass_freq=0.0,
low_shelf_gain=0.0, low_shelf_freq=200.0,
mid_peak_gain=0.0, mid_peak_freq=1000.0, mid_peak_q=1.0,
high_shelf_gain=0.0, high_shelf_freq=5000.0,
noise_gate_threshold=-55.0, noise_gate_ratio=10.0,
compressor_threshold=-15.0, compressor_ratio=4.0,
limiter_ceiling=-1.0, final_gain=0.0,
soundgoodizer=0.0,
distortion_drive=0.0,
bitcrush_bits=0.0,
chorus_depth=0.0, chorus_rate=0.0, chorus_mix=0.0,
delay_seconds=0.0, delay_feedback=0.0, delay_mix=0.0,
phaser_depth=0.0, phaser_rate=0.0, phaser_mix=0.0,
clipping_threshold=0.0):
"""Apply audio effects chain to vocals.
Extended version with full EQ, dynamics, soundgoodizer, saturation,
modulation (chorus/phaser), delay, and bitcrush controls.
All gain/freq params have safe defaults that match the original behavior.
"""
output_path = f'{os.path.splitext(audio_path)[0]}_mixed.wav'
# Build the pedalboard chain
chain = []
# High-pass filter (remove rumble) - only if freq > 0
if highpass_freq and float(highpass_freq) > 0:
chain.append(HighpassFilter(cutoff_frequency_hz=float(highpass_freq)))
# Low-pass filter (remove harshness) - only if freq > 0
if lowpass_freq and float(lowpass_freq) > 0:
chain.append(LowpassFilter(cutoff_frequency_hz=float(lowpass_freq)))
# Low shelf EQ (bass)
if low_shelf_gain and float(low_shelf_gain) != 0:
chain.append(LowShelfFilter(cutoff_frequency_hz=float(low_shelf_freq), gain_db=float(low_shelf_gain)))
# Mid peak EQ
if mid_peak_gain and float(mid_peak_gain) != 0:
chain.append(PeakFilter(cutoff_frequency_hz=float(mid_peak_freq), gain_db=float(mid_peak_gain), q=float(mid_peak_q)))
# High shelf EQ (treble)
if high_shelf_gain and float(high_shelf_gain) != 0:
chain.append(HighShelfFilter(cutoff_frequency_hz=float(high_shelf_freq), gain_db=float(high_shelf_gain)))
# Noise gate
if noise_gate_threshold and float(noise_gate_threshold) < 0:
chain.append(NoiseGate(threshold_db=float(noise_gate_threshold), ratio=float(noise_gate_ratio), attack_ms=5.0, release_ms=150.0))
# Compressor
if compressor_ratio and float(compressor_ratio) > 1:
chain.append(Compressor(threshold_db=float(compressor_threshold), ratio=float(compressor_ratio), attack_ms=5.0, release_ms=100.0))
# Soundgoodizer - a "make it sound good" one-knob effect.
# Implemented as a warm multiband-style chain: gentle compression + saturation + makeup gain.
# Amount 0 = off, 1 = max.
if soundgoodizer and float(soundgoodizer) > 0:
amt = float(soundgoodizer)
# Warm compressor (glue)
chain.append(Compressor(threshold_db=-18 + (1 - amt) * 6, ratio=2.0 + amt * 2.0, attack_ms=10.0, release_ms=120.0))
# Subtle saturation via Distortion with low drive
chain.append(Distortion(drive_db=amt * 4.0))
# Makeup gain
chain.append(Gain(gain_db=amt * 2.0))
# Distortion / saturation
if distortion_drive and float(distortion_drive) > 0:
chain.append(Distortion(drive_db=float(distortion_drive)))
# Bitcrush (lo-fi effect) - bit_depth 16 = no effect, lower = more crushed
if bitcrush_bits and float(bitcrush_bits) > 0 and float(bitcrush_bits) < 16:
chain.append(Bitcrush(bit_depth=float(bitcrush_bits)))
# Clipping (hard limit)
if clipping_threshold and float(clipping_threshold) < 0:
chain.append(Clipping(threshold_db=float(clipping_threshold)))
# Chorus (modulation)
if chorus_mix and float(chorus_mix) > 0:
chain.append(Chorus(
rate_hz=float(chorus_rate) if chorus_rate else 0.5,
depth=float(chorus_depth) if chorus_depth else 0.25,
centre_delay_ms=7.0,
feedback=0.0,
mix=float(chorus_mix),
))
# Phaser (modulation)
if phaser_mix and float(phaser_mix) > 0:
chain.append(Phaser(
rate_hz=float(phaser_rate) if phaser_rate else 0.5,
depth=float(phaser_depth) if phaser_depth else 0.5,
centre_frequency_hz=1300.0,
feedback=0.0,
mix=float(phaser_mix),
))
# Delay (echo)
if delay_mix and float(delay_mix) > 0 and delay_seconds and float(delay_seconds) > 0:
chain.append(Delay(
delay_seconds=float(delay_seconds),
feedback=float(delay_feedback) if delay_feedback else 0.0,
mix=float(delay_mix),
))
# Reverb
chain.append(Reverb(room_size=float(reverb_rm_size), dry_level=float(reverb_dry), wet_level=float(reverb_wet), damping=float(reverb_damping)))
# Limiter (peak ceiling)
if limiter_ceiling is not None and float(limiter_ceiling) < 0:
chain.append(Limiter(threshold_db=float(limiter_ceiling), release_ms=100.0))
# Final gain
if final_gain and float(final_gain) != 0:
chain.append(Gain(gain_db=float(final_gain)))
board = Pedalboard(chain)
with AudioFile(audio_path) as f:
with AudioFile(output_path, 'w', f.samplerate, f.num_channels) as o:
while f.tell() < f.frames:
chunk = f.read(int(f.samplerate))
effected = board(chunk, f.samplerate, reset=False)
o.write(effected)
return output_path
def combine_audio(audio_paths, output_path, main_gain, backup_gain, inst_gain, output_format):
main_vocal_audio = AudioSegment.from_wav(audio_paths[0]) - 4 + main_gain
backup_vocal_audio = AudioSegment.from_wav(audio_paths[1]) - 6 + backup_gain
instrumental_audio = AudioSegment.from_wav(audio_paths[2]) - 7 + inst_gain
main_vocal_audio.overlay(backup_vocal_audio).overlay(instrumental_audio).export(output_path, format=output_format)
@spaces.GPU(duration=65)
def process_song(
song_dir, song_input, mdx_model_params, song_id, is_webui, input_type, progress,
keep_files, pitch_change, pitch_change_all, voice_model, index_rate, filter_radius,
rms_mix_rate, protect, f0_method, crepe_hop_length, output_format, keep_orig, orig_song_path, steps
):
if not os.path.exists(song_dir):
os.makedirs(song_dir)
orig_song_path, vocals_path, instrumentals_path, main_vocals_path, backup_vocals_path, main_vocals_dereverb_path = preprocess_song(song_input, mdx_model_params, song_id, is_webui, input_type, progress, keep_orig, orig_song_path)
else:
vocals_path, main_vocals_path = None, None
paths = get_audio_paths(song_dir)
# if any of the audio files aren't available or keep intermediate files, rerun preprocess
if any(path is None for path in paths):
orig_song_path, vocals_path, instrumentals_path, main_vocals_path, backup_vocals_path, main_vocals_dereverb_path = preprocess_song(song_input, mdx_model_params, song_id, is_webui, input_type, progress, keep_orig, orig_song_path)
else:
orig_song_path, instrumentals_path, main_vocals_dereverb_path, backup_vocals_path = paths
ins_path = get_audio_with_suffix(song_dir, "_Voiceless.wav")
if not ins_path:
display_progress('Extracting voiceless instrumental track...', 0.40, is_webui, progress)
instrumentals_path, _ = run_mdx(
mdx_model_params,
song_dir,
os.path.join(mdxnet_models_dir, "UVR-MDX-NET-Inst_HQ_4.onnx"),
instrumentals_path,
exclude_inversion=True,
suffix="Voiceless",
denoise=False,
keep_orig=True,
base_device=("cuda" if IS_ZERO_GPU else "")
)
ins_path = get_audio_with_suffix(song_dir, "_Voiceless.wav")
else:
instrumentals_path = ins_path
pitch_change = pitch_change * 12 + pitch_change_all
ai_vocals_path = os.path.join(song_dir, f'{os.path.splitext(os.path.basename(orig_song_path))[0]}_{voice_model}_p{pitch_change}_i{index_rate}_fr{filter_radius}_rms{rms_mix_rate}_pro{protect}_{f0_method}{"" if f0_method != "mangio-crepe" else f"_{crepe_hop_length}"}_s{steps}.wav')
ai_cover_path = os.path.join(song_dir, f'{os.path.splitext(os.path.basename(orig_song_path))[0]} ({voice_model} Ver).{output_format}')
if not os.path.exists(ai_vocals_path):
display_progress('Converting voice with RVC AI model...', 0.50, is_webui, progress)
voice_change(voice_model, main_vocals_dereverb_path, ai_vocals_path, pitch_change, f0_method, index_rate, filter_radius, rms_mix_rate, protect, crepe_hop_length, is_webui, steps)
return ai_vocals_path, ai_cover_path, instrumentals_path, backup_vocals_path, vocals_path, main_vocals_path, ins_path
def apply_noisereduce(audio_list, type_output="wav"):
# https://github.com/sa-if/Audio-Denoiser
print("Noise reduce")
result = []
for audio_path in audio_list:
out_path = f"{os.path.splitext(audio_path)[0]}_nr.{type_output}"
try:
# Load audio file
audio = AudioSegment.from_file(audio_path)
# Convert to numpy with shape (channels, samples) for stereo support
samples = np.array(audio.get_array_of_samples()).astype(np.float32)
if audio.channels > 1:
samples = samples.reshape(-1, audio.channels).T # (channels, samples)
# Reduce noise (handles 1D and 2D)
reduced_noise = nr.reduce_noise(samples, sr=audio.frame_rate, prop_decrease=0.6)
# Convert back to int16 interleaved
reduced_noise = np.clip(reduced_noise, -32768, 32767).astype(np.int16)
if audio.channels > 1:
reduced_noise = reduced_noise.T.reshape(-1)
reduced_audio = AudioSegment(
reduced_noise.tobytes(),
frame_rate=audio.frame_rate,
sample_width=audio.sample_width,
channels=audio.channels,
)
reduced_audio.export(out_path, format=type_output)
result.append(out_path)
except Exception as e:
print(f"Error noisereduce: {str(e)}")
result.append(audio_path)
return result
def song_cover_pipeline(song_input, voice_model, pitch_change, keep_files,
is_webui=0, main_gain=0, backup_gain=0, inst_gain=0, index_rate=0.5, filter_radius=3,
rms_mix_rate=0.25, f0_method='rmvpe', crepe_hop_length=128, protect=0.33, pitch_change_all=0,
reverb_rm_size=0.15, reverb_wet=0.2, reverb_dry=0.8, reverb_damping=0.7, output_format='mp3',
extra_denoise=False, steps=1,
highpass_freq=80.0, lowpass_freq=0.0,
low_shelf_gain=0.0, low_shelf_freq=200.0,
mid_peak_gain=0.0, mid_peak_freq=1000.0, mid_peak_q=1.0,
high_shelf_gain=0.0, high_shelf_freq=5000.0,
noise_gate_threshold=-55.0, noise_gate_ratio=10.0,
compressor_threshold=-15.0, compressor_ratio=4.0,
limiter_ceiling=-1.0, final_gain=0.0,
soundgoodizer=0.0,
distortion_drive=0.0,
bitcrush_bits=0.0,
chorus_depth=0.0, chorus_rate=0.0, chorus_mix=0.0,
delay_seconds=0.0, delay_feedback=0.0, delay_mix=0.0,
phaser_depth=0.0, phaser_rate=0.0, phaser_mix=0.0,
clipping_threshold=0.0,
backup_vocal_infer=False, backup_vocal_pitch=0,
progress=gr.Progress()):
if not keep_files or IS_ZERO_GPU:
clean_old_folders("./song_output", 14400)
if IS_ZERO_GPU:
clean_old_folders("./rvc_models", 10800)
try:
if not song_input or not voice_model:
raise_exception('Ensure that the song input field and voice model field is filled.', is_webui)
display_progress('Starting AI cover generation...', 0.00, is_webui, progress)
with open(os.path.join(mdxnet_models_dir, 'model_data.json')) as infile:
mdx_model_params = json.load(infile)
# if youtube url
if urlparse(song_input).scheme == 'https':
input_type = 'yt'
song_id = get_youtube_video_id(song_input)
if song_id is None:
error_msg = 'Invalid YouTube url.'
raise_exception(error_msg, is_webui)
# local audio file
else:
input_type = 'local'
song_input = song_input.strip('\"')
if os.path.exists(song_input):
song_id = get_hash(song_input)
else:
error_msg = f'{song_input} does not exist.'
song_id = None
raise_exception(error_msg, is_webui)
song_dir = os.path.join(output_dir, song_id)
keep_orig, orig_song_path = get_audio_file(song_input, is_webui, input_type, progress)
orig_song_path = convert_to_stereo(orig_song_path)
start = time.time()
(
ai_vocals_path,
ai_cover_path,
instrumentals_path,
backup_vocals_path,
vocals_path,
main_vocals_path,
ins_path
) = process_song(
song_dir,
song_input,
mdx_model_params,
song_id,
is_webui,
input_type,
progress,
keep_files,
pitch_change,
pitch_change_all,
voice_model,
index_rate,
filter_radius,
rms_mix_rate,
protect,
f0_method,
crepe_hop_length,
output_format,
keep_orig,
orig_song_path,
steps,
)
end = time.time()
print(f"Execution time: {end - start:.4f} seconds")
with sf.SoundFile(ai_vocals_path) as f:
duration__ = len(f) / f.samplerate
print(f"Audio duration: {duration__:.2f} seconds")
display_progress('Applying audio effects to AI vocals...', 0.65, is_webui, progress)
nr_path = ai_vocals_path # get_audio_with_suffix(song_dir, "_nr.wav")
if extra_denoise:
ai_vocals_path = apply_noisereduce([ai_vocals_path])[0]
ai_vocals_mixed_path = add_audio_effects(
ai_vocals_path, reverb_rm_size, reverb_wet, reverb_dry, reverb_damping,
highpass_freq=highpass_freq, lowpass_freq=lowpass_freq,
low_shelf_gain=low_shelf_gain, low_shelf_freq=low_shelf_freq,
mid_peak_gain=mid_peak_gain, mid_peak_freq=mid_peak_freq, mid_peak_q=mid_peak_q,
high_shelf_gain=high_shelf_gain, high_shelf_freq=high_shelf_freq,
noise_gate_threshold=noise_gate_threshold, noise_gate_ratio=noise_gate_ratio,
compressor_threshold=compressor_threshold, compressor_ratio=compressor_ratio,
limiter_ceiling=limiter_ceiling, final_gain=final_gain,
soundgoodizer=soundgoodizer,
distortion_drive=distortion_drive,
bitcrush_bits=bitcrush_bits,
chorus_depth=chorus_depth, chorus_rate=chorus_rate, chorus_mix=chorus_mix,
delay_seconds=delay_seconds, delay_feedback=delay_feedback, delay_mix=delay_mix,
phaser_depth=phaser_depth, phaser_rate=phaser_rate, phaser_mix=phaser_mix,
clipping_threshold=clipping_threshold,
)
if pitch_change_all != 0:
display_progress('Applying overall pitch change to instrumentals...', 0.75, is_webui, progress)
instrumentals_path = pitch_shift(instrumentals_path, pitch_change_all)
backup_vocals_path = pitch_shift(backup_vocals_path, pitch_change_all)
# Backing vocal inference: run RVC on backup vocals with optional harmony pitch
ai_backup_vocals_mixed_path = None
if backup_vocal_infer and backup_vocals_path:
display_progress('Converting backing vocals with RVC...', 0.80, is_webui, progress)
backup_pitch = pitch_change + (backup_vocal_pitch * 12)
ai_backup_vocals_path = os.path.join(song_dir, f'{os.path.splitext(os.path.basename(orig_song_path))[0]}_{voice_model}_backup_p{backup_pitch}.wav')
if not os.path.exists(ai_backup_vocals_path):
voice_change(voice_model, backup_vocals_path, ai_backup_vocals_path, backup_pitch, f0_method, index_rate, filter_radius, rms_mix_rate, protect, crepe_hop_length, is_webui, steps)
# Apply same audio effects to backup vocals (slightly reduced reverb)
ai_backup_vocals_mixed_path = add_audio_effects(
ai_backup_vocals_path, reverb_rm_size * 0.7, reverb_wet * 0.5, reverb_dry, reverb_damping,
highpass_freq=highpass_freq, lowpass_freq=lowpass_freq,
low_shelf_gain=low_shelf_gain, low_shelf_freq=low_shelf_freq,
mid_peak_gain=mid_peak_gain, mid_peak_freq=mid_peak_freq, mid_peak_q=mid_peak_q,
high_shelf_gain=high_shelf_gain, high_shelf_freq=high_shelf_freq,
noise_gate_threshold=noise_gate_threshold, noise_gate_ratio=noise_gate_ratio,
compressor_threshold=compressor_threshold, compressor_ratio=compressor_ratio,
limiter_ceiling=limiter_ceiling, final_gain=final_gain,
soundgoodizer=soundgoodizer,
distortion_drive=distortion_drive,
bitcrush_bits=bitcrush_bits,
chorus_depth=chorus_depth, chorus_rate=chorus_rate, chorus_mix=chorus_mix,
delay_seconds=delay_seconds, delay_feedback=delay_feedback, delay_mix=delay_mix,
phaser_depth=phaser_depth, phaser_rate=phaser_rate, phaser_mix=phaser_mix,
clipping_threshold=clipping_threshold,
)
# Use the AI backup vocals instead of original
backup_vocals_path = ai_backup_vocals_mixed_path
display_progress('Combining AI vocals and instrumentals...', 0.90, is_webui, progress)
combine_audio([ai_vocals_mixed_path, backup_vocals_path, instrumentals_path], ai_cover_path, main_gain, backup_gain, inst_gain, output_format)
if not keep_files:
display_progress('Cleaning up intermediate files...', 0.95, is_webui, progress)
intermediate_files = [vocals_path, main_vocals_path, ai_vocals_mixed_path, nr_path]
if ai_backup_vocals_mixed_path:
intermediate_files.append(ai_backup_vocals_mixed_path)
if pitch_change_all != 0:
intermediate_files += [instrumentals_path, backup_vocals_path]
for file in intermediate_files:
if file and os.path.exists(file) and file != ins_path:
os.remove(file)
# Record history entry (if modules are available)
try:
hist = get_history_manager()
if hist is not None:
song_name = os.path.splitext(os.path.basename(orig_song_path))[0]
hist.add(
song_name=song_name,
voice_model=voice_model,
pitch=pitch_change,
pitch_all=pitch_change_all,
ai_cover_path=ai_cover_path,
duration_seconds=duration__,
output_format=output_format,
settings={
"index_rate": index_rate,
"filter_radius": filter_radius,
"rms_mix_rate": rms_mix_rate,
"protect": protect,
"f0_method": f0_method,
"crepe_hop_length": crepe_hop_length,
"steps": steps,
"main_gain": main_gain,
"backup_gain": backup_gain,
"inst_gain": inst_gain,
"reverb_rm_size": reverb_rm_size,
"reverb_wet": reverb_wet,
"reverb_dry": reverb_dry,
"reverb_damping": reverb_damping,
"extra_denoise": extra_denoise,
"processing_time_seconds": round(end - start, 2),
"backup_vocal_infer": backup_vocal_infer,
"backup_vocal_pitch": backup_vocal_pitch,
},
)
except Exception as hist_err:
print(f"[history] could not record entry: {hist_err}")
display_progress('AI cover generated successfully!', 1.0, is_webui, progress)
return ai_cover_path
except Exception as e:
raise_exception(str(e), is_webui)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Generate a AI cover song in the song_output/id directory.', add_help=True)
parser.add_argument('-i', '--song-input', type=str, required=True, help='Link to a YouTube video or the filepath to a local mp3/wav file to create an AI cover of')
parser.add_argument('-dir', '--rvc-dirname', type=str, required=True, help='Name of the folder in the rvc_models directory containing the RVC model file and optional index file to use')
parser.add_argument('-p', '--pitch-change', type=int, required=True, help='Change the pitch of AI Vocals only. Generally, use 1 for male to female and -1 for vice-versa. (Octaves)')
parser.add_argument('-k', '--keep-files', action=argparse.BooleanOptionalAction, help='Whether to keep all intermediate audio files generated in the song_output/id directory, e.g. Isolated Vocals/Instrumentals')
parser.add_argument('-ir', '--index-rate', type=float, default=0.5, help='A decimal number e.g. 0.5, used to reduce/resolve the timbre leakage problem. If set to 1, more biased towards the timbre quality of the training dataset')
parser.add_argument('-fr', '--filter-radius', type=int, default=3, help='A number between 0 and 7. If >=3: apply median filtering to the harvested pitch results. The value represents the filter radius and can reduce breathiness.')
parser.add_argument('-rms', '--rms-mix-rate', type=float, default=0.25, help="A decimal number e.g. 0.25. Control how much to use the original vocal's loudness (0) or a fixed loudness (1).")
parser.add_argument('-palgo', '--pitch-detection-algo', type=str, default='rmvpe', help='Best option is rmvpe (clarity in vocals), then mangio-crepe (smoother vocals).')
parser.add_argument('-hop', '--crepe-hop-length', type=int, default=128, help='If pitch detection algo is mangio-crepe, controls how often it checks for pitch changes in milliseconds. The higher the value, the faster the conversion and less risk of voice cracks, but there is less pitch accuracy. Recommended: 128.')
parser.add_argument('-pro', '--protect', type=float, default=0.33, help='A decimal number e.g. 0.33. Protect voiceless consonants and breath sounds to prevent artifacts such as tearing in electronic music. Set to 0.5 to disable. Decrease the value to increase protection, but it may reduce indexing accuracy.')
parser.add_argument('-mv', '--main-vol', type=int, default=0, help='Volume change for AI main vocals in decibels. Use -3 to decrease by 3 decibels and 3 to increase by 3 decibels')
parser.add_argument('-bv', '--backup-vol', type=int, default=0, help='Volume change for backup vocals in decibels')
parser.add_argument('-iv', '--inst-vol', type=int, default=0, help='Volume change for instrumentals in decibels')
parser.add_argument('-pall', '--pitch-change-all', type=int, default=0, help='Change the pitch/key of vocals and instrumentals. Changing this slightly reduces sound quality')
parser.add_argument('-rsize', '--reverb-size', type=float, default=0.15, help='Reverb room size between 0 and 1')
parser.add_argument('-rwet', '--reverb-wetness', type=float, default=0.2, help='Reverb wet level between 0 and 1')
parser.add_argument('-rdry', '--reverb-dryness', type=float, default=0.8, help='Reverb dry level between 0 and 1')
parser.add_argument('-rdamp', '--reverb-damping', type=float, default=0.7, help='Reverb damping between 0 and 1')
parser.add_argument('-oformat', '--output-format', type=str, default='mp3', help='Output format of audio file. mp3 for smaller file size, wav for best quality')
args = parser.parse_args()
rvc_dirname = args.rvc_dirname
if not os.path.exists(os.path.join(rvc_models_dir, rvc_dirname)):
raise Exception(f'The folder {os.path.join(rvc_models_dir, rvc_dirname)} does not exist.')
cover_path = song_cover_pipeline(args.song_input, rvc_dirname, args.pitch_change, args.keep_files,
main_gain=args.main_vol, backup_gain=args.backup_vol, inst_gain=args.inst_vol,
index_rate=args.index_rate, filter_radius=args.filter_radius,
rms_mix_rate=args.rms_mix_rate, f0_method=args.pitch_detection_algo,
crepe_hop_length=args.crepe_hop_length, protect=args.protect,
pitch_change_all=args.pitch_change_all,
reverb_rm_size=args.reverb_size, reverb_wet=args.reverb_wetness,
reverb_dry=args.reverb_dryness, reverb_damping=args.reverb_damping,
output_format=args.output_format)
print(f'[+] Cover generated at {cover_path}')