import os
from pathlib import Path
from typing import List
from loguru import logger as log
import requests
import streamlit as st
from streamlit_option_menu import option_menu
from footer import footer
from header import header
from helpers import (
load_audio_segment,
load_list_of_songs,
plot_audio,
url_is_valid,
file_size_is_valid,
delete_old_files,
)
from service.demucs_runner import separator
from service.vocal_remover.runner import load_model, separate
from style import CSS_TABS
label_sources = {
"no_vocals.mp3": "🎶 Instrumental",
"vocals.mp3": "🎤 Vocals",
"drums.mp3": "🥁 Drums",
"bass.mp3": "🎸 Bass",
"guitar.mp3": "🎸 Guitar",
"piano.mp3": "🎹 Piano",
"other.mp3": "🎶 Other",
}
separation_mode_to_model = {
"Vocals & Instrumental (Low Quality, Faster)": (
"vocal_remover",
["vocals.mp3", "no_vocals.mp3"],
),
"Vocals & Instrumental (High Quality, Slower)": ("htdemucs", ["vocals.mp3", "no_vocals.mp3"]),
"Vocals, Drums, Bass & Other (Slower)": (
"htdemucs",
["vocals.mp3", "drums.mp3", "bass.mp3", "other.mp3"],
),
"Vocal, Drums, Bass, Guitar, Piano & Other (Slowest)": (
"htdemucs_6s",
["vocals.mp3", "drums.mp3", "bass.mp3", "guitar.mp3", "piano.mp3", "other.mp3"],
),
}
extensions = ["mp3", "wav", "ogg", "flac"]
out_path = Path("/tmp")
in_path = Path("/tmp")
@st.cache_data(show_spinner=False)
def get_sources(path, file_sources):
sources = {}
for file in file_sources:
fullpath = path / file
if fullpath.exists():
sources[file] = fullpath
return sources
def reset_execution():
st.session_state.executed = False
def show_results(model_name: str, dir_name_output: str, file_sources: List):
sources = get_sources(out_path / Path(model_name) / dir_name_output, file_sources)
tab_sources = st.tabs([f"**{label_sources.get(k)}**" for k in sources.keys()])
for i, (file, pathname) in enumerate(sources.items()):
with tab_sources[i]:
cols = st.columns(2)
with cols[0]:
auseg = load_audio_segment(pathname, "mp3")
st.image(
plot_audio(
auseg,
32767,
file=file,
model_name=model_name,
dir_name_output=dir_name_output,
),
use_column_width="always",
)
with cols[1]:
st.audio(str(pathname))
log.info(f"Displaying results for {dir_name_output} - {model_name}")
def body():
filename = None
name_song = None
st.markdown(
"
Extract Vocals & Instrumental from any song
",
unsafe_allow_html=True,
)
st.markdown(CSS_TABS, unsafe_allow_html=True)
cols = st.columns([1, 4, 1, 3, 1])
with cols[1]:
with st.columns([1, 8, 1])[1]:
option = option_menu(
menu_title=None,
options=["Examples", "Upload File", "From URL"],
icons=["cloud-upload-fill", "link-45deg", "music-note-list"],
orientation="horizontal",
styles={
"container": {
"width": "100%",
"height": "3.5rem",
"margin": "0px",
"padding": "0px",
},
"icon": {"font-size": "1rem"},
"nav-link": {
"display": "flex",
"height": "3rem",
"justify-content": "center",
"align-items": "center",
"text-align": "center",
"flex-direction": "column",
"font-size": "1rem",
"padding-left": "0px",
"padding-right": "0px",
},
},
key="option_separate",
)
if option == "Examples":
samples_song = load_list_of_songs(path="separate_songs.json")
if samples_song is not None:
name_song = st.selectbox(
label="Select a sample song and listen to sources separated",
options=list(samples_song.keys()) + [""],
format_func=lambda x: x.replace("_", " "),
index=len(samples_song),
key="select_example",
)
full_path = f"{in_path}/{name_song}"
if name_song != "" and Path(full_path).exists():
st.audio(full_path)
else:
name_song = None
elif option == "Upload File":
uploaded_file = st.file_uploader(
"Choose a file",
type=extensions,
key="file",
help="Supported formats: mp3, wav, ogg, flac.",
)
if uploaded_file is not None:
with st.spinner("Loading audio..."):
with open(in_path / uploaded_file.name, "wb") as f:
f.write(uploaded_file.getbuffer())
filename = uploaded_file.name
st.audio(uploaded_file)
elif option == "From URL":
url = st.text_input(
"Paste the URL of the audio file",
key="url_input",
help="Supported formats: mp3, wav, ogg, flac.",
)
if url != "" and url_is_valid(url):
with st.spinner("Downloading audio..."):
filename = url.split("/")[-1]
response = requests.get(url, stream=True)
if response.status_code == 200 and file_size_is_valid(
response.headers.get("Content-Length")
):
file_size = 0
with open(in_path / filename, "wb") as audio_file:
for chunk in response.iter_content(chunk_size=1024):
if chunk:
audio_file.write(chunk)
file_size += len(chunk)
if not file_size_is_valid(file_size):
audio_file.close()
os.remove(in_path / filename)
filename = None
return
st.audio(f"{in_path}/{filename}")
else:
st.error(
"Failed to download audio file. Try to download it manually and upload it."
)
filename = None
with cols[3]:
separation_mode = st.selectbox(
"Choose the separation mode",
[
"Vocals & Instrumental (Low Quality, Faster)",
"Vocals & Instrumental (High Quality, Slower)",
"Vocals, Drums, Bass & Other (Slower)",
"Vocal, Drums, Bass, Guitar, Piano & Other (Slowest)",
],
on_change=reset_execution(),
key="separation_mode",
)
if separation_mode == "Vocals & Instrumental (Low Quality, Faster)":
max_duration = 30
else:
max_duration = 15
model_name, file_sources = separation_mode_to_model[separation_mode]
if filename is not None:
song = load_audio_segment(in_path / filename, filename.split(".")[-1])
n_secs = round(len(song) / 1000)
if os.environ.get("ENV_LIMITATION", False):
with cols[3]:
start_time = st.number_input(
"Choose the start time",
min_value=0,
max_value=n_secs,
step=1,
value=0,
help=f"Maximum duration is {max_duration} seconds for this separation mode.\nDuplicate this space to [remove any limit](https://github.com/fabiogra/moseca#are-there-any-limitations).",
format="%d",
)
st.session_state.start_time = start_time
end_time = min(start_time + max_duration, n_secs)
song = song[start_time * 1000 : end_time * 1000]
st.info(
f"Audio source will be processed from {start_time} to {end_time} seconds.\nDuplicate this space to [remove any limit](https://github.com/fabiogra/moseca#are-there-any-limitations).",
icon="⏱",
)
else:
start_time = 0
end_time = n_secs
with st.columns([2, 1, 2])[1]:
execute = st.button(
"Separate Music Sources 🎶", type="primary", use_container_width=True
)
if execute or st.session_state.executed:
if execute:
st.session_state.executed = False
if not st.session_state.executed:
log.info(f"{option} - Separating {filename} with {separation_mode}...")
song.export(in_path / filename, format=filename.split(".")[-1])
with st.columns([1, 1, 1])[1]:
with st.spinner("Separating source audio, it will take a while..."):
if model_name == "vocal_remover":
model, device = load_model(pretrained_model="baseline.pth")
separate(
input=in_path / filename,
model=model,
device=device,
output_dir=out_path,
)
else:
stem = None
if separation_mode == "Vocals & Instrumental (High Quality, Slower)":
stem = "vocals"
separator(
tracks=[in_path / filename],
out=out_path,
model=model_name,
shifts=1,
overlap=0.5,
stem=stem,
int24=False,
float32=False,
clip_mode="rescale",
mp3=True,
mp3_bitrate=320,
verbose=True,
start_time=start_time,
end_time=end_time,
)
dir_name_output = ".".join(filename.split(".")[:-1])
filename = None
st.session_state.executed = True
show_results(model_name, dir_name_output, file_sources)
elif name_song is not None and option == "Examples":
show_results(model_name, name_song, file_sources)
if __name__ == "__main__":
header()
body()
footer()
delete_old_files("/tmp", 60 * 30)