Switching work to CLI support

This commit is contained in:
Dilan Boskan 2021-02-04 16:38:44 +01:00
parent 05407586c3
commit 1ce4949639
8 changed files with 980 additions and 52 deletions

View File

@ -11,10 +11,11 @@ from PySide2.QtWinExtras import (QWinTaskbarButton)
# -Root imports-
from .resources.resources_manager import ResourcePaths
from .windows import (mainwindow, settingswindow)
from .inference import converter_v4
from .inference import converter_v4_copy as converter_v4
# -Other-
import datetime as dt
from collections import defaultdict
from collections import OrderedDict
import os
import sys
# Code annotation
@ -96,6 +97,30 @@ DEFAULT_SETTINGS = {
}
def dict_to_HTMLtable(x: dict, header: str) -> str:
"""
Convert a 1D-dictionary into an HTML table
"""
# Generate table contents
values = ''
for i, (key, value) in enumerate(x.items()):
if i % 2:
color = "#000"
else:
color = "#333"
values += f'<tr style="background-color:{color};border:none;"><td>{key}</td><td>{value}</td></tr>\n'
# HTML string
htmlTable = """
<table border="1" cellspacing="0" cellpadding="0" width="100%">
<tr><th colspan="2" style="background-color:#555500">{header}</th></tr>
{values}
</table>
""".format(header=header,
values=values)
# Return HTML string
return htmlTable
class CustomApplication(QtWidgets.QApplication):
def __init__(self):
# -Init Application-
@ -196,9 +221,8 @@ class CustomApplication(QtWidgets.QApplication):
"""
Extract the saved seperation data
"""
seperation_data = converter_v4.default_data.copy()
seperation_data = OrderedDict()
seperation_data['useModel'] = 'instrumental'
# Input/Export
# self.windows['settings'].inputPaths
seperation_data['input_paths'] = ['B:/boska/Desktop/Test inference/test.mp3']
@ -214,16 +238,18 @@ class CustomApplication(QtWidgets.QApplication):
seperation_data['modelFolder'] = self.windows['settings'].ui.checkBox_modelFolder.isChecked()
seperation_data['customParameters'] = self.windows['settings'].ui.checkBox_customParameters.isChecked()
# Combobox
seperation_data['useModel'] = 'instrumental'
seperation_data['instrumentalModel'] = self.windows['settings'].ui.comboBox_instrumental.currentData()
seperation_data['vocalModel'] = ""
seperation_data['stackModel'] = self.windows['settings'].ui.comboBox_stacked.currentData()
# Lineedit (Constants)
seperation_data['sr'] = int(self.windows['settings'].ui.lineEdit_sr.text())
seperation_data['sr_stacked'] = int(self.windows['settings'].ui.lineEdit_sr_stacked.text())
seperation_data['hop_length'] = int(self.windows['settings'].ui.lineEdit_hopLength.text())
seperation_data['hop_length_stacked'] = int(self.windows['settings'].ui.lineEdit_hopLength_stacked.text())
seperation_data['window_size'] = int(self.windows['settings'].ui.comboBox_winSize.currentText())
seperation_data['window_size_stacked'] = int(self.windows['settings'].ui.comboBox_winSize_stacked.currentText())
seperation_data['n_fft'] = int(self.windows['settings'].ui.lineEdit_nfft.text())
seperation_data['sr_stacked'] = int(self.windows['settings'].ui.lineEdit_sr_stacked.text())
seperation_data['hop_length_stacked'] = int(self.windows['settings'].ui.lineEdit_hopLength_stacked.text())
seperation_data['window_size_stacked'] = int(self.windows['settings'].ui.comboBox_winSize_stacked.currentText())
seperation_data['n_fft_stacked'] = int(self.windows['settings'].ui.lineEdit_nfft_stacked.text())
# -Complex variables (Difficult to extract)-
# Stack passes
@ -237,9 +263,13 @@ class CustomApplication(QtWidgets.QApplication):
resType = resType.lower().replace(' ', '_')
seperation_data['resType'] = resType
if set(converter_v4.default_data.keys()) != set(seperation_data.keys()):
self.debug_to_command(f'Extracted Keys do not equal keys set by default converter!\nExtracted Keys: {sorted(list(seperation_data.keys()))}\nShould be Keys: {sorted(list(converter_v4.default_data.keys()))}',
priority=1)
return seperation_data
def debug_to_command(self, text: str, priority: Optional[int] = 'default'):
def debug_to_command(self, text: str, priority: Optional[int] = 'default', debug_prefix: bool = True):
"""
Shortcut function for mainwindow write to gui
@ -254,6 +284,8 @@ class CustomApplication(QtWidgets.QApplication):
'default': QtGui.QColor("#CCC")}
if self.in_debug_mode():
self.windows['main'].ui.textBrowser_command.setTextColor(debug_colours[priority])
if debug_prefix:
text = f'DEBUG: {text}'
self.windows['main'].write_to_command(text)
self.windows['main'].ui.textBrowser_command.setTextColor(debug_colours['default'])
else:
@ -462,6 +494,7 @@ class MainWindow(QtWidgets.QWidget):
"""
self.app.debug_to_command('Opening settings window...')
# Reshow window
self.app.windows['settings'].setWindowState(Qt.WindowNoState)
self.app.windows['settings'].show()
# Focus window
self.app.windows['settings'].activateWindow()
@ -473,21 +506,15 @@ class MainWindow(QtWidgets.QWidget):
"""
Seperate given files
"""
self.app.debug_to_command('Seperating Files...')
# -Disable Seperation Button
self.ui.pushButton_seperate.setEnabled(False)
# -Setup WinTaskbar-
self.winTaskbar.setOverlayAccessibleDescription('Seperating...')
self.winTaskbar.setOverlayIcon(QtGui.QIcon(ResourcePaths.images.folder))
self.winTaskbar_progress.setVisible(True)
# -Extract seperation info from GUI-
seperation_data = self.app.extract_seperation_data()
pprint.pprint(seperation_data)
self.app.debug_to_command(dict_to_HTMLtable(seperation_data, 'Seperation Data'),
debug_prefix=False)
# -Seperation-
# Create instance
vocalRemover = converter_v4.VocalRemoverWorker(seperation_data=seperation_data,)
# Bind events
vocalRemover.signals.start.connect(self.seperation_start)
vocalRemover.signals.message.connect(self.seperation_write)
vocalRemover.signals.progress.connect(self.seperation_update_progress)
vocalRemover.signals.error.connect(self.seperation_error)
@ -518,6 +545,19 @@ class MainWindow(QtWidgets.QWidget):
self.inputPaths = inputPaths
# -Seperation Methods-
def seperation_start(self):
"""
Seperation has started
"""
self.app.debug_to_command(f'The seperation has started.',
priority=2)
# Disable Seperation Button
self.ui.pushButton_seperate.setEnabled(False)
# Setup WinTaskbar
self.winTaskbar.setOverlayAccessibleDescription('Seperating...')
self.winTaskbar.setOverlayIcon(QtGui.QIcon(ResourcePaths.images.folder))
self.winTaskbar_progress.setVisible(True)
def seperation_write(self, text: str, priority: Optional[int] = 'default'):
"""
Write to GUI
@ -529,6 +569,7 @@ class MainWindow(QtWidgets.QWidget):
Update both progressbars in Taskbar and GUI
with the given progress
"""
self.app.debug_to_command(f'Updating progress: {progress}%')
self.winTaskbar_progress.setValue(progress)
self.ui.progressBar.setValue(progress)
@ -541,9 +582,21 @@ class MainWindow(QtWidgets.QWidget):
Index 0: Error Message
Index 1: Detailed Message
"""
print(message)
self.app.debug_to_command(f'An error occured!\nMessage: {message[0]}\nDetailed Message: {message[1]}',
priority=1)
msg = QtWidgets.QMessageBox()
msg.setWindowTitle('An Error Occurred')
msg.setIcon(QtWidgets.QMessageBox.Icon.Critical)
msg.setText(
message[0] + '\n\nIf the issue is not clear, please contact the creator and attach a screenshot of the detailed message with the file and settings that caused it!')
msg.setDetailedText(message[1])
msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
msg.setWindowFlag(Qt.WindowStaysOnTopHint)
msg.exec_()
def seperation_finish(self, elapsed_time: str = '1:02:03'):
self.seperation_finish(failed=True)
def seperation_finish(self, elapsed_time: str = '1:02:03', failed: bool = False):
"""
Finished seperation
"""
@ -555,8 +608,9 @@ class MainWindow(QtWidgets.QWidget):
self.winTaskbar.clearOverlayIcon()
self.winTaskbar_progress.setVisible(False)
self.seperation_update_progress(0)
self.seperation_update_progress(100)
self.app.debug_to_command(f'The seperation has finished.',
priority=2)
# -Create MessageBox-
msg = QtWidgets.QMessageBox()
msg.setWindowTitle('Seperation Complete')

View File

@ -178,7 +178,6 @@ class VocalRemover:
command_base_text = self._get_base_text()
model_device, music_file = self._get_model_device_file()
constants = self._get_constants(model_device['model_name'])
print(loop_num, constants)
# -Update loop specific variables
self.loop_data['constants'] = constants
self.loop_data['command_base_text'] = command_base_text
@ -301,7 +300,7 @@ class VocalRemover:
# -Get data-
total_loops = get_total_loops()
folder_path, file_add_on = get_folderPath_fileAddOn()
self.write_to_gui(text='\n\nLoading models...',
self.write_to_gui(text='Loading models...',
include_base_text=False)
models, devices = get_models_devices()
self.write_to_gui(text='Done!',
@ -491,11 +490,11 @@ class VocalRemover:
for progrs, i in enumerate(pbar):
# Progress management
if progrs_info == '1/2':
progres_step = 0.1 + 0.3 * (progrs / n_window)
progres_step = 0.1 + 0.35 * (progrs / n_window)
elif progrs_info == '2/2':
progres_step = 0.4 + 0.3 * (progrs / n_window)
progres_step = 0.45 + 0.35 * (progrs / n_window)
else:
progres_step = 0.1 + 0.6 * (progrs / n_window)
progres_step = 0.1 + 0.7 * (progrs / n_window)
self.write_to_gui(progress_step=progres_step)
if self.update_progress is None:
progress = self._get_progress(progres_step)
@ -591,14 +590,14 @@ class VocalRemover:
self.loop_data['X_phase'] = X_phase
self.write_to_gui(text='Done!',
progress_step=0.7)
progress_step=0.8)
def _post_process(self):
"""
Post process
"""
self.write_to_gui(text='Post processing...',
progress_step=0.7)
progress_step=0.8)
pred_inv = np.clip(self.loop_data['X_mag'] - self.loop_data['prediction'], 0, np.inf)
prediction = spec_utils.mask_silence(self.loop_data['prediction'], pred_inv)
@ -606,14 +605,14 @@ class VocalRemover:
self.loop_data['prediction'] = prediction
self.write_to_gui(text='Done!',
progress_step=0.75)
progress_step=0.85)
def _inverse_stft_of_instrumentals_and_vocals(self):
"""
Inverse stft of instrumentals and vocals
"""
self.write_to_gui(text='Inverse stft of instruments and vocals...',
progress_step=0.75)
progress_step=0.85)
y_spec = self.loop_data['prediction'] * self.loop_data['X_phase']
wav_instrument = spec_utils.spectrogram_to_wave(y_spec,
@ -629,7 +628,7 @@ class VocalRemover:
self.loop_data['v_spec'] = v_spec
self.write_to_gui(text='Done!',
progress_step=0.8)
progress_step=0.9)
def _save_files(self):
"""
@ -699,7 +698,7 @@ class VocalRemover:
return vocal_name, instrumental_name, folder_path
self.write_to_gui(text='Saving Files...',
progress_step=0.8)
progress_step=0.9)
vocal_name, instrumental_name, folder_path = get_vocal_instrumental_name()
@ -728,7 +727,7 @@ class VocalRemover:
self.loop_data['wav_vocals'].T, self.loop_data['sampling_rate'])
self.write_to_gui(text='Done!',
progress_step=0.9)
progress_step=0.95)
def _save_mask(self):
"""
@ -769,7 +768,7 @@ class VocalRemover:
If errors are found, an exception is raised
"""
# Check input paths
if len(seperation_data['input_paths']):
if not len(seperation_data['input_paths']):
# No music file specified
raise TypeError('No music file to seperate defined!')
if (not isinstance(seperation_data['input_paths'], tuple) and
@ -823,6 +822,7 @@ class WorkerSignals(QtCore.QObject):
Index 1: Detailed Message
'''
start = QtCore.Signal()
finished = QtCore.Signal(str)
message = QtCore.Signal(str)
progress = QtCore.Signal(int)
@ -852,9 +852,15 @@ class VocalRemoverWorker(VocalRemover, QtCore.QRunnable):
stime = time.perf_counter()
try:
self.signals.start.emit()
self.seperate_files()
except Exception as e:
self.signals.error.emit(e)
import traceback
traceback_text = ''.join(traceback.format_tb(e.__traceback__))
message = f'Traceback Error: "{traceback_text}"\n{type(e).__name__}: "{e}"\nIf the issue is not clear, please contact the creator and attach a screenshot of the detailed message with the file and settings that caused it!'
print(traceback_text)
print(type(e).__name__, e)
self.signals.error.emit([str(e), message])
return
elapsed_seconds = int(time.perf_counter() - stime)

View File

@ -0,0 +1,865 @@
"""
Seperate music files with the v4 engine
"""
# pylint: disable=no-name-in-module, import-error
# -Required for conversion-
import cv2
import librosa
import numpy as np
import soundfile as sf
import torch
# -Root imports-
from .lib.lib_v4 import dataset
from .lib.lib_v4 import nets
from .lib.lib_v4 import spec_utils
# -Other-
# Loading Bar
from tqdm import tqdm
# Timer
import datetime as dt
import time
import os
# Annotating
from PySide2 import QtCore # (QRunnable, QThread, QObject, Signal, Slot)
from PySide2 import QtWidgets
from typing import (Dict, Tuple, Optional, Callable)
default_data = {
# Paths
'input_paths': [], # List of paths
'export_path': '', # Export path
# Processing Options
'gpuConversion': False,
'postProcess': True,
'tta': True,
'outputImage': False,
# Models
'instrumentalModel': '', # Path to instrumental (not needed if not used)
'vocalModel': '', # Path to vocal model (not needed if not used)
'stackModel': '', # Path to stacked model (not needed if not used)
'useModel': 'instrumental', # Either 'instrumental' or 'vocal'
# Stack Options
'stackPasses': 0,
'stackOnly': False,
'saveAllStacked': False,
# Model Folder
'modelFolder': False, # Model Test Mode
# Constants
'sr': 44_100,
'hop_length': 1_024,
'window_size': 320,
'n_fft': 2_048,
# Stacked
'sr_stacked': 44_100,
'hop_length_stacked': 1_024,
'window_size_stacked': 320,
'n_fft_stacked': 2_048,
# Resolution Type
'resType': 'kaiser_fast',
# Whether to override constants embedded in the model file name
'customParameters': False,
}
class VocalRemover:
def __init__(self, seperation_data: dict, write_to_command: Optional[Callable[[str], None]] = None, update_progress: Optional[Callable[[int], None]] = None):
self.write_to_command = write_to_command
self.update_progress = update_progress
# GUI parsed data
self.seperation_data = seperation_data
# Data that is determined once
self.general_data = {
'total_files': len(self.seperation_data['input_paths']),
'total_loops': None,
'folder_path': None,
'file_add_on': None,
'models': {},
'devices': {},
}
# Updated on every conversion or loop
self.loop_data = {
# File specific
'file_base_name': None,
'file_num': 0,
# Loop specific
'command_base_text': None,
'loop_num': 0,
'progress_step': 0.0,
'music_file': None,
'model_device': {
'model': None,
'device': None,
'model_name': None,
},
'constants': {
'sr': None,
'hop_length': None,
'window_size': None,
'n_fft': None,
},
'X': None,
'X_mag': None,
'X_phase': None,
'prediction': None,
'sampling_rate': None,
'wav_vocals': None,
'wav_instrument': None,
'y_spec': None,
'v_spec': None,
# Spectogram from last seperation
'temp_spectogramm': None,
}
def seperate_files(self):
"""
Seperate all files
"""
# Track time
stime = time.perf_counter()
self._check_for_valid_inputs(self.seperation_data)
self._fill_general_data()
for file_num, file_path in enumerate(self.seperation_data['input_paths'], start=1):
self._seperate(file_path,
file_num)
# Free RAM
torch.cuda.empty_cache()
self.write_to_gui('Conversion(s) Completed and Saving all Files!',
include_base_text=False)
self.write_to_gui(f'Time Elapsed: {time.strftime("%H:%M:%S", time.gmtime(int(time.perf_counter() - stime)))}',
include_base_text=False)
def write_to_gui(self, text: Optional[str] = None, include_base_text: bool = True, progress_step: Optional[float] = None):
"""
Update progress and/or write text to the command line
A new line '\\n' will be automatically appended to the text
"""
# Progress is given
progress = self._get_progress(progress_step)
if text is not None:
# Text is given
if include_base_text:
# Include base text
text = f"{self.loop_data['command_base_text']} {text}"
if self.write_to_command is not None:
# Text widget is given
self.write_to_command(text)
else:
# No text widget so write to console
if progress_step is not None:
text = f'{int(progress)} %\t{text}'
if not 'done' in text.lower():
# Skip 'Done!' text as it clutters the terminal
print(text)
if self.update_progress is not None:
# Progress widget is given
self.update_progress(progress)
def _seperate(self, file_path: str, file_num: int):
"""
Seperate given music file,
file_num is used to determine progress
"""
# -Update file specific variables-
self.loop_data['file_num'] = file_num
self.loop_data['music_file'] = file_path
self.loop_data['file_base_name'] = self._get_file_base_name(file_path)
for loop_num in range(self.general_data['total_loops']):
self.loop_data['loop_num'] = loop_num
# -Get loop specific variables-
command_base_text = self._get_base_text()
model_device = self._get_model_device_file()
constants = self._get_constants(model_device['model_name'])
# -Update loop specific variables
self.loop_data['constants'] = constants
self.loop_data['command_base_text'] = command_base_text
self.loop_data['model_device'] = model_device
# -Seperation-
if not self.loop_data['loop_num']:
print('A')
# First loop
self._load_wave_source()
self._wave_to_spectogram()
if self.seperation_data['postProcess']:
# Postprocess
self._post_process()
self._inverse_stft_of_instrumentals_and_vocals()
self._save_files()
else:
# End of seperation
if self.seperation_data['outputImage']:
self._save_mask()
self.write_to_gui(text='Completed Seperation!\n',
progress_step=1)
def _fill_general_data(self):
"""
Fill the data implemented in general_data
"""
def get_folderPath_fileAddOn() -> Tuple[str, str]:
"""
Get export path and text, whic hwill be appended on the music files name
"""
file_add_on = ''
if self.seperation_data['modelFolder']:
# Model Test Mode selected
# -Instrumental-
if os.path.isfile(self.seperation_data['instrumentalModel']):
file_add_on += os.path.splitext(os.path.basename(self.seperation_data['instrumentalModel']))[0]
# -Vocal-
elif os.path.isfile(self.seperation_data['vocalModel']):
file_add_on += os.path.splitext(os.path.basename(self.seperation_data['vocalModel']))[0]
# -Stack-
if os.path.isfile(self.seperation_data['stackModel']):
file_add_on += '-' + os.path.splitext(os.path.basename(self.seperation_data['stackModel']))[0]
# Generate paths
folder_path = os.path.join(self.seperation_data['export_path'], file_add_on)
file_add_on = f'_{file_add_on}'
if not os.path.isdir(folder_path):
# Folder does not exist
os.mkdir(folder_path)
else:
# Not Model Test Mode selected
folder_path = self.seperation_data['export_path']
return folder_path, file_add_on
def get_models_devices() -> list:
"""
Return models and devices found
"""
models = {}
devices = {}
# -Instrumental-
if os.path.isfile(self.seperation_data['instrumentalModel']):
device = torch.device('cpu')
model = nets.CascadedASPPNet(self.seperation_data['n_fft'])
model.load_state_dict(torch.load(self.seperation_data['instrumentalModel'],
map_location=device))
if torch.cuda.is_available() and self.seperation_data['gpuConversion']:
device = torch.device('cuda:0')
model.to(device)
models['instrumental'] = model
devices['instrumental'] = device
# -Vocal-
elif os.path.isfile(self.seperation_data['vocalModel']):
device = torch.device('cpu')
model = nets.CascadedASPPNet(self.seperation_data['n_fft'])
model.load_state_dict(torch.load(self.seperation_data['vocalModel'],
map_location=device))
if torch.cuda.is_available() and self.seperation_data['gpuConversion']:
device = torch.device('cuda:0')
model.to(device)
models['vocal'] = model
devices['vocal'] = device
# -Stack-
if os.path.isfile(self.seperation_data['stackModel']):
device = torch.device('cpu')
model = nets.CascadedASPPNet(self.seperation_data['n_fft'])
model.load_state_dict(torch.load(self.seperation_data['stackModel'],
map_location=device))
if torch.cuda.is_available() and self.seperation_data['gpuConversion']:
device = torch.device('cuda:0')
model.to(device)
models['stack'] = model
devices['stack'] = device
return models, devices
def get_total_loops() -> int:
"""
Determine how many loops the program will
have to prepare for
"""
if self.seperation_data['stackOnly']:
# Stack Conversion Only
total_loops = self.seperation_data['stackPasses']
else:
# 1 for the instrumental/vocal
total_loops = 1
# Add number of stack pass loops
total_loops += self.seperation_data['stackPasses']
return total_loops
# -Get data-
total_loops = get_total_loops()
folder_path, file_add_on = get_folderPath_fileAddOn()
self.write_to_gui(text='Loading models...',
include_base_text=False)
models, devices = get_models_devices()
self.write_to_gui(text='Done!',
include_base_text=False)
# -Set data-
self.general_data['total_loops'] = total_loops
self.general_data['folder_path'] = folder_path
self.general_data['file_add_on'] = file_add_on
self.general_data['models'] = models
self.general_data['devices'] = devices
# -Data Getter Methods-
def _get_base_text(self) -> str:
"""
Determine the prefix text of the console
"""
loop_add_on = ''
if self.general_data['total_loops'] > 1:
# More than one loop for conversion
loop_add_on = f" ({self.loop_data['loop_num']+1}/{self.general_data['total_loops']})"
return 'File {file_num}/{total_files}:{loop} '.format(file_num=self.loop_data['file_num'],
total_files=self.general_data['total_files'],
loop=loop_add_on)
def _get_constants(self, model_name: str) -> dict:
"""
Get the sr, hop_length, window_size, n_fft
"""
if self.loop_data['loop_num'] == 0:
# Instrumental/Vocal Model
seperation_params = {
'sr': self.seperation_data['sr_stacked'],
'hop_length': self.seperation_data['hop_length_stacked'],
'window_size': self.seperation_data['window_size_stacked'],
'n_fft': self.seperation_data['n_fft_stacked'],
}
else:
# Stacked model
seperation_params = {
'sr': self.seperation_data['sr'],
'hop_length': self.seperation_data['hop_length'],
'window_size': self.seperation_data['window_size'],
'n_fft': self.seperation_data['n_fft'],
}
if self.seperation_data['customParameters']:
# Typed constants are fixed
return seperation_params
# -Decode Model Name-
text = model_name.replace('.pth', '')
text_parts = text.split('_')[1:]
for text_part in text_parts:
if 'sr' in text_part:
text_part = text_part.replace('sr', '')
if text_part.isdecimal():
try:
seperation_params['sr'] = int(text_part)
continue
except ValueError:
# Cannot convert string to int
pass
if 'hl' in text_part:
text_part = text_part.replace('hl', '')
if text_part.isdecimal():
try:
seperation_params['hop_length'] = int(text_part)
continue
except ValueError:
# Cannot convert string to int
pass
if 'w' in text_part:
text_part = text_part.replace('w', '')
if text_part.isdecimal():
try:
seperation_params['window_size'] = int(text_part)
continue
except ValueError:
# Cannot convert string to int
pass
if 'nf' in text_part:
text_part = text_part.replace('nf', '')
if text_part.isdecimal():
try:
seperation_params['n_fft'] = int(text_part)
continue
except ValueError:
# Cannot convert string to int
pass
return seperation_params
def _get_model_device_file(self) -> dict:
"""
Get the used models and devices for this loop
Also extract the model name and the music file
which will be used
"""
model_device = {
'model': None,
'device': None,
'model_name': None,
}
if not self.loop_data['loop_num']:
# First Iteration
if self.seperation_data['stackOnly']:
if os.path.isfile(self.seperation_data['stackModel']):
model_device['model'] = self.general_data['models']['stack']
model_device['device'] = self.general_data['devices']['stack']
model_device['model_name'] = os.path.basename(self.seperation_data['stackModel'])
else:
raise ValueError(f'Selected stack only model, however, stack model path file cannot be found\nPath: "{self.seperation_data["stackModel"]}"') # nopep8
else:
model_device['model'] = self.general_data['models'][self.seperation_data['useModel']]
model_device['device'] = self.general_data['devices'][self.seperation_data['useModel']]
model_device['model_name'] = os.path.basename(
self.seperation_data[f'{self.seperation_data["useModel"]}Model'])
else:
# Every other iteration
model_device['model'] = self.general_data['models']['stack']
model_device['device'] = self.general_data['devices']['stack']
model_device['model_name'] = os.path.basename(self.seperation_data['stackModel'])
return model_device
def _get_file_base_name(self, file_path: str) -> str:
"""
Get the path infos for the given music file
"""
return f"{self.loop_data['file_num']}_{os.path.splitext(os.path.basename(file_path))[0]}"
# -Seperation Methods-
def _load_wave_source(self):
"""
Load the wave source
"""
self.write_to_gui(text='Loading wave source...',
progress_step=0)
X, sampling_rate = librosa.load(path=self.loop_data['music_file'],
sr=self.loop_data['constants']['sr'],
mono=False, dtype=np.float32,
res_type=self.seperation_data['resType'])
if X.ndim == 1:
X = np.asarray([X, X])
self.loop_data['X'] = X
self.loop_data['sampling_rate'] = sampling_rate
self.write_to_gui(text='Done!',
progress_step=0.1)
def _wave_to_spectogram(self):
"""
Wave to spectogram
"""
def preprocess(X_spec):
X_mag = np.abs(X_spec)
X_phase = np.angle(X_spec)
return X_mag, X_phase
def execute(X_mag_pad, roi_size, n_window, device, model, progrs_info: str = ''):
model.eval()
with torch.no_grad():
preds = []
if self.update_progress is None:
bar_format = '{desc} |{bar}{r_bar}'
else:
bar_format = '{l_bar}{bar}{r_bar}'
pbar = tqdm(range(n_window), bar_format=bar_format)
for progrs, i in enumerate(pbar):
# Progress management
if progrs_info == '1/2':
progres_step = 0.1 + 0.35 * (progrs / n_window)
elif progrs_info == '2/2':
progres_step = 0.45 + 0.35 * (progrs / n_window)
else:
progres_step = 0.1 + 0.7 * (progrs / n_window)
self.write_to_gui(progress_step=progres_step)
if self.update_progress is None:
progress = self._get_progress(progres_step)
text = f'{int(progress)} %'
if progress < 10:
text += ' '
pbar.set_description_str(text)
start = i * roi_size
X_mag_window = X_mag_pad[None, :, :,
start:start + self.seperation_data['window_size']]
X_mag_window = torch.from_numpy(X_mag_window).to(device)
pred = model.predict(X_mag_window)
pred = pred.detach().cpu().numpy()
preds.append(pred[0])
pred = np.concatenate(preds, axis=2)
return pred
def inference(X_spec, device, model):
X_mag, X_phase = preprocess(X_spec)
coef = X_mag.max()
X_mag_pre = X_mag / coef
n_frame = X_mag_pre.shape[2]
pad_l, pad_r, roi_size = dataset.make_padding(n_frame,
self.seperation_data['window_size'], model.offset)
n_window = int(np.ceil(n_frame / roi_size))
X_mag_pad = np.pad(
X_mag_pre, ((0, 0), (0, 0), (pad_l, pad_r)), mode='constant')
pred = execute(X_mag_pad, roi_size, n_window,
device, model)
pred = pred[:, :, :n_frame]
return pred * coef, X_mag, np.exp(1.j * X_phase)
def inference_tta(X_spec, device, model):
X_mag, X_phase = preprocess(X_spec)
coef = X_mag.max()
X_mag_pre = X_mag / coef
n_frame = X_mag_pre.shape[2]
pad_l, pad_r, roi_size = dataset.make_padding(n_frame,
self.seperation_data['window_size'], model.offset)
n_window = int(np.ceil(n_frame / roi_size))
X_mag_pad = np.pad(
X_mag_pre, ((0, 0), (0, 0), (pad_l, pad_r)), mode='constant')
pred = execute(X_mag_pad, roi_size, n_window,
device, model, progrs_info='1/2')
pred = pred[:, :, :n_frame]
pad_l += roi_size // 2
pad_r += roi_size // 2
n_window += 1
X_mag_pad = np.pad(
X_mag_pre, ((0, 0), (0, 0), (pad_l, pad_r)), mode='constant')
pred_tta = execute(X_mag_pad, roi_size, n_window,
device, model, progrs_info='2/2')
pred_tta = pred_tta[:, :, roi_size // 2:]
pred_tta = pred_tta[:, :, :n_frame]
return (pred + pred_tta) * 0.5 * coef, X_mag, np.exp(1.j * X_phase)
self.write_to_gui(text='Stft of wave source...',
progress_step=0.1)
if not self.loop_data['loop_num']:
X = spec_utils.wave_to_spectrogram(wave=self.loop_data['X'],
hop_length=self.seperation_data['hop_length'],
n_fft=self.seperation_data['n_fft'])
else:
X = self.loop_data['temp_spectogramm']
if self.seperation_data['tta']:
prediction, X_mag, X_phase = inference_tta(X_spec=X,
device=self.loop_data['model_device']['device'],
model=self.loop_data['model_device']['model'])
else:
prediction, X_mag, X_phase = inference(X_spec=X,
device=self.loop_data['model_device']['device'],
model=self.loop_data['model_device']['model'])
self.loop_data['prediction'] = prediction
self.loop_data['X'] = X
self.loop_data['X_mag'] = X_mag
self.loop_data['X_phase'] = X_phase
self.write_to_gui(text='Done!',
progress_step=0.8)
def _post_process(self):
"""
Post process
"""
self.write_to_gui(text='Post processing...',
progress_step=0.8)
pred_inv = np.clip(self.loop_data['X_mag'] - self.loop_data['prediction'], 0, np.inf)
prediction = spec_utils.mask_silence(self.loop_data['prediction'], pred_inv)
self.loop_data['prediction'] = prediction
self.write_to_gui(text='Done!',
progress_step=0.85)
def _inverse_stft_of_instrumentals_and_vocals(self):
"""
Inverse stft of instrumentals and vocals
"""
self.write_to_gui(text='Inverse stft of instruments and vocals...',
progress_step=0.85)
y_spec = self.loop_data['prediction'] * self.loop_data['X_phase']
wav_instrument = spec_utils.spectrogram_to_wave(y_spec,
hop_length=self.seperation_data['hop_length'])
v_spec = np.clip(self.loop_data['X_mag'] - self.loop_data['prediction'], 0, np.inf) * self.loop_data['X_phase']
wav_vocals = spec_utils.spectrogram_to_wave(v_spec,
hop_length=self.seperation_data['hop_length'])
self.loop_data['wav_vocals'] = wav_vocals
self.loop_data['wav_instrument'] = wav_instrument
# Needed for mask creation
self.loop_data['y_spec'] = y_spec
self.loop_data['v_spec'] = v_spec
self.loop_data['temp_spectogramm'] = y_spec
self.write_to_gui(text='Done!',
progress_step=0.9)
def _save_files(self):
"""
Save the files
"""
def get_vocal_instrumental_name() -> Tuple[str, str, str]:
"""
Get vocal and instrumental file names and update the
folder_path temporarily if needed
"""
loop_num = self.loop_data['loop_num']
total_loops = self.general_data['total_loops']
file_base_name = self.loop_data['file_base_name']
vocal_name = None
instrumental_name = None
folder_path = self.general_data['folder_path']
# Get the Suffix Name
if (not loop_num or
loop_num == (total_loops - 1)): # First or Last Loop
if self.seperation_data['stackOnly']:
if loop_num == (total_loops - 1): # Last Loop
if not (total_loops - 1): # Only 1 Loop
vocal_name = '(Vocals)'
instrumental_name = '(Instrumental)'
else:
vocal_name = '(Vocal_Final_Stacked_Output)'
instrumental_name = '(Instrumental_Final_Stacked_Output)'
elif self.seperation_data['useModel'] == 'instrumental':
if not loop_num: # First Loop
vocal_name = '(Vocals)'
if loop_num == (total_loops - 1): # Last Loop
if not (total_loops - 1): # Only 1 Loop
instrumental_name = '(Instrumental)'
else:
instrumental_name = '(Instrumental_Final_Stacked_Output)'
elif self.seperation_data['useModel'] == 'vocal':
if not loop_num: # First Loop
instrumental_name = '(Instrumental)'
if loop_num == (total_loops - 1): # Last Loop
if not (total_loops - 1): # Only 1 Loop
vocal_name = '(Vocals)'
else:
vocal_name = '(Vocals_Final_Stacked_Output)'
if self.seperation_data['useModel'] == 'vocal':
# Reverse names
vocal_name, instrumental_name = instrumental_name, vocal_name
elif self.seperation_data['saveAllStacked']:
stacked_folder_name = file_base_name + ' Stacked Outputs' # nopep8
folder_path = os.path.join(folder_path, stacked_folder_name)
if not os.path.isdir(folder_path):
os.mkdir(folder_path)
if self.seperation_data['stackOnly']:
vocal_name = f'(Vocal_{loop_num}_Stacked_Output)'
instrumental_name = f'(Instrumental_{loop_num}_Stacked_Output)'
elif (self.seperation_data['useModel'] == 'vocal' or
self.seperation_data['useModel'] == 'instrumental'):
vocal_name = f'(Vocals_{loop_num}_Stacked_Output)'
instrumental_name = f'(Instrumental_{loop_num}_Stacked_Output)'
if self.seperation_data['useModel'] == 'vocal':
# Reverse names
vocal_name, instrumental_name = instrumental_name, vocal_name
return vocal_name, instrumental_name, folder_path
self.write_to_gui(text='Saving Files...',
progress_step=0.9)
vocal_name, instrumental_name, folder_path = get_vocal_instrumental_name()
# -Save files-
# Instrumental
if instrumental_name is not None:
instrumental_file_name = f"{self.loop_data['file_base_name']}_{instrumental_name}{self.general_data['file_add_on']}.wav"
instrumental_path = os.path.join(folder_path,
instrumental_file_name)
sf.write(instrumental_path,
self.loop_data['wav_instrument'].T, self.loop_data['sampling_rate'])
# Vocal
if vocal_name is not None:
vocal_file_name = f"{self.loop_data['file_base_name']}_{vocal_name}{self.general_data['file_add_on']}.wav"
vocal_path = os.path.join(folder_path,
vocal_file_name)
sf.write(vocal_path,
self.loop_data['wav_vocals'].T, self.loop_data['sampling_rate'])
self.write_to_gui(text='Done!',
progress_step=0.95)
def _save_mask(self):
"""
Save output image
"""
mask_path = os.path.join(self.general_data['folder_path'], self.loop_data['file_base_name'])
with open('{}_Instruments.jpg'.format(mask_path), mode='wb') as f:
image = spec_utils.spectrogram_to_image(self.loop_data['y_spec'])
_, bin_image = cv2.imencode('.jpg', image)
bin_image.tofile(f)
with open('{}_Vocals.jpg'.format(mask_path), mode='wb') as f:
image = spec_utils.spectrogram_to_image(self.loop_data['v_spec'])
_, bin_image = cv2.imencode('.jpg', image)
bin_image.tofile(f)
# -Other Methods-
def _get_progress(self, progress_step: Optional[float] = None) -> float:
"""
Get current conversion progress in percent
"""
if progress_step is not None:
self.loop_data['progress_step'] = progress_step
try:
base = (100 / self.general_data['total_files'])
progress = base * (self.loop_data['file_num'] - 1)
progress += (base / self.general_data['total_loops']) * \
(self.loop_data['loop_num'] + self.loop_data['progress_step'])
except TypeError:
# One data point not specified yet
progress = 0
return progress
def _check_for_valid_inputs(self, seperation_data: dict):
"""
Check if all inputs have been entered correctly.
If errors are found, an exception is raised
"""
# Check input paths
if not len(seperation_data['input_paths']):
# No music file specified
raise TypeError('No music file to seperate defined!')
if (not isinstance(seperation_data['input_paths'], tuple) and
not isinstance(seperation_data['input_paths'], list)):
# Music file not specified in a list or tuple
raise TypeError('Please specify your music file path/s in a list or tuple!')
for input_path in seperation_data['input_paths']:
# Go through each music file
if not os.path.isfile(input_path):
# Invalid path
raise TypeError(f'Invalid music file! Please make sure that the file still exists or that the path is valid!\nPath: "{input_path}"') # nopep8
# Output path
if (not os.path.isdir(seperation_data['export_path']) and
not seperation_data['export_path'] == ''):
# Export path either invalid or not specified
raise TypeError(f'Invalid export directory! Please make sure that the directory still exists or that the path is valid!\nPath: "{self.seperation_data["export_path"]}"') # nopep8
# Check models
if not seperation_data['useModel'] in ['vocal', 'instrumental']:
# Invalid 'useModel'
raise TypeError("Parameter 'useModel' has to be either 'vocal' or 'instrumental'")
if not os.path.isfile(seperation_data[f"{seperation_data['useModel']}Model"]):
# No or invalid instrumental/vocal model given
# but model is needed
raise TypeError(f"Not specified or invalid model path for {seperation_data['useModel']} model!")
if (not os.path.isfile(seperation_data['stackModel']) and
(seperation_data['stackOnly'] or
seperation_data['stackPasses'] > 0)):
# No or invalid stack model given
# but model is needed
raise TypeError(f"Not specified or invalid model path for stacked model!")
# --GUI ONLY--
class WorkerSignals(QtCore.QObject):
'''
Defines the signals available from a running worker thread.
Supported signals are:
finished
str: Time elapsed
message
str: Message to write to GUI
progress
int (0-100): Progress update
error
Tuple[str, str]:
Index 0: Error Message
Index 1: Detailed Message
'''
start = QtCore.Signal()
finished = QtCore.Signal(str)
message = QtCore.Signal(str)
progress = QtCore.Signal(int)
error = QtCore.Signal(tuple)
class VocalRemoverWorker(VocalRemover, QtCore.QRunnable):
'''
Threaded Vocal Remover
Only use in conjunction with GUI
'''
def __init__(self, seperation_data: dict):
super(VocalRemoverWorker, self).__init__(seperation_data)
super(VocalRemover, self).__init__(seperation_data)
super(QtCore.QRunnable, self).__init__()
self.signals = WorkerSignals()
self.seperation_data = seperation_data
@QtCore.Slot()
def run(self):
"""
Seperate files
"""
import time
stime = time.perf_counter()
try:
self.signals.start.emit()
self.seperate_files()
except Exception as e:
import traceback
traceback_text = ''.join(traceback.format_tb(e.__traceback__))
message = f'Traceback Error: "{traceback_text}"\n{type(e).__name__}: "{e}"\nIf the issue is not clear, please contact the creator and attach a screenshot of the detailed message with the file and settings that caused it!'
print(traceback_text)
print(type(e).__name__, e)
self.signals.error.emit([str(e), message])
return
elapsed_seconds = int(time.perf_counter() - stime)
time = dt.timedelta(seconds=elapsed_seconds)
self.signals.finished.emit(str(time))
def write_to_gui(self, text: Optional[str] = None, include_base_text: bool = True, progress_step: Optional[float] = None):
if text is not None:
if include_base_text:
# Include base text
text = f"{self.loop_data['command_base_text']} {text}"
self.signals.message.emit(text)
if progress_step is not None:
self.signals.progress.emit(self._get_progress(progress_step))

View File

@ -78,7 +78,7 @@ QTextBrowser {
font: 8pt &quot;Courier&quot;;
}</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1">
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,0">
<property name="spacing">
<number>0</number>
</property>
@ -509,7 +509,7 @@ border-bottom-left-radius: 0px;</string>
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Courier'; font-size:8pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:10pt;&quot;&gt;COMMAND LINE [LOG LEVEL=1]&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;NAN&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>

View File

@ -795,12 +795,12 @@ QRadioButton[menu=&quot;true&quot;]::indicator::checked {
</property>
<item>
<property name="text">
<string notr="true">Kaiser Best</string>
<string notr="true">Kaiser Fast</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">Kaiser Fast</string>
<string notr="true">Kaiser Best</string>
</property>
</item>
<item>
@ -1452,8 +1452,8 @@ QRadioButton[menu=&quot;true&quot;]::indicator::checked {
<rect>
<x>0</x>
<y>0</y>
<width>98</width>
<height>40</height>
<width>792</width>
<height>519</height>
</rect>
</property>
<property name="styleSheet">
@ -1529,8 +1529,8 @@ QRadioButton[menu=&quot;true&quot;]::indicator::checked {
<rect>
<x>0</x>
<y>0</y>
<width>98</width>
<height>40</height>
<width>792</width>
<height>519</height>
</rect>
</property>
<property name="styleSheet">
@ -1609,8 +1609,8 @@ QRadioButton[menu=&quot;true&quot;]::indicator::checked {
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>371</height>
<width>792</width>
<height>519</height>
</rect>
</property>
<property name="minimumSize">

3
src/windows/.py Normal file
View File

@ -0,0 +1,3 @@
Error: File '.ui' is not valid
while executing 'c:\users\boska\appdata\local\programs\python\python39\lib\site-packages\PySide2\uic -g python .ui'

View File

@ -396,8 +396,8 @@ class Ui_SettingsWindow(object):
self.gridLayout_6.addWidget(self.comboBox_engine, 1, 0, 1, 1)
self.comboBox_resType = QComboBox(self.frame_12)
self.comboBox_resType.addItem(u"Kaiser Best")
self.comboBox_resType.addItem(u"Kaiser Fast")
self.comboBox_resType.addItem(u"Kaiser Best")
self.comboBox_resType.addItem(u"Scipy")
self.comboBox_resType.setObjectName(u"comboBox_resType")
self.comboBox_resType.setMinimumSize(QSize(0, 25))
@ -669,7 +669,7 @@ class Ui_SettingsWindow(object):
self.scrollAreaWidgetContents = QWidget()
self.scrollAreaWidgetContents.setObjectName(
u"scrollAreaWidgetContents")
self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 98, 40))
self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 792, 519))
self.scrollAreaWidgetContents.setStyleSheet(u"QFrame#frame_engine, QFrame#frame_modelOptions {\n"
" background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0.221409, y2:0.587, stop:0.119318 rgba(85, 78, 163, 255), stop:0.683616 rgba(0, 0, 0, 0));\n"
"}")
@ -704,7 +704,7 @@ class Ui_SettingsWindow(object):
self.scrollAreaWidgetContents_4 = QWidget()
self.scrollAreaWidgetContents_4.setObjectName(
u"scrollAreaWidgetContents_4")
self.scrollAreaWidgetContents_4.setGeometry(QRect(0, 0, 98, 40))
self.scrollAreaWidgetContents_4.setGeometry(QRect(0, 0, 792, 519))
self.scrollAreaWidgetContents_4.setStyleSheet(u"QFrame#frame_engine, QFrame#frame_modelOptions {\n"
" background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0.221409, y2:0.587, stop:0.119318 rgba(85, 78, 163, 255), stop:0.683616 rgba(0, 0, 0, 0));\n"
"}")
@ -740,7 +740,7 @@ class Ui_SettingsWindow(object):
self.scrollAreaWidgetContents_5 = QWidget()
self.scrollAreaWidgetContents_5.setObjectName(
u"scrollAreaWidgetContents_5")
self.scrollAreaWidgetContents_5.setGeometry(QRect(0, 0, 600, 371))
self.scrollAreaWidgetContents_5.setGeometry(QRect(0, 0, 792, 519))
self.scrollAreaWidgetContents_5.setMinimumSize(QSize(600, 0))
self.scrollAreaWidgetContents_5.setStyleSheet(u"QFrame#frame_engine, QFrame#frame_modelOptions {\n"
" background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0.221409, y2:0.587, stop:0.119318 rgba(85, 78, 163, 255), stop:0.683616 rgba(0, 0, 0, 0));\n"

View File

@ -8,17 +8,17 @@ from src.inference.converter_v4 import (VocalRemover, default_data)
parser = argparse.ArgumentParser(description='This is the terminal version of UVR.')
# -IO-
parser.add_argument('-i', '--inputs', metavar='Path/s', type=str, nargs='+',
parser.add_argument('-i', '--inputs', metavar='InputPath/s', type=str, nargs='+',
required=True, dest='input_paths',
help='Path to music file/s')
parser.add_argument('-o', '--output', metavar='Path', type=str, nargs=1,
parser.add_argument('-o', '--output', metavar='ExportPath', type=str, nargs=1,
required=True, dest='export_path',
help='Path to output directory')
# -Models-
parser.add_argument('-inst', '--instrumentalModel', metavar='Path', type=str, nargs=1,
parser.add_argument('-inst', '--instrumentalModel', metavar='InstrumentalPath', type=str, nargs=1,
required=True, dest='instrumentalModel',
help='Path to instrumental model')
parser.add_argument('-stacked', '--stackedModel', metavar='Path', type=str, nargs=1,
parser.add_argument('-stacked', '--stackedModel', metavar='StackedPath', type=str, nargs=1,
required=False, dest='stackModel',
help='Path to stacked model')
# -Settings-