11 Commits
Author SHA1 Message Date
Salvador E. Tropea c87923faeb Bumped version to 1.1.0 2025-07-11 18:04:42 -03:00
Salvador E. Tropea 64070adf77 [Tool] Made all executable 2025-07-11 17:52:20 -03:00
Salvador E. Tropea b8ee1856c5 [Demix][Fixed] Handling of not generated stems 2025-07-11 17:51:45 -03:00
Salvador E. Tropea f973796f07 [Demucs][Replaced] Julius up/down sampler by torchaudio
Has less distortion.
2025-07-11 17:14:05 -03:00
Salvador E. Tropea 9a1297beaf [Demucs][Logger][Added] Signature
And made more robust the sync between sig and kwargs
2025-07-11 17:12:42 -03:00
Salvador E. Tropea 93856c88eb [Demucs][Logger][Fixed] Wiener iterations default 2025-07-11 17:12:02 -03:00
Salvador E. Tropea 75ed37ed3b [Fixed] Restored wiener
Used by the mdx.safetensors file, one HDemucs uses CaC and the
other Wiener
2025-07-11 17:09:35 -03:00
Salvador E. Tropea 08fb457a9d [Demucs][Removed] Weiner filtering support
Currently unused, will be restored only if we find a model using it
2025-07-11 10:01:33 -03:00
Salvador E. Tropea dfcce31318 [Demucs][Logger][Added] Better STFT framework log 2025-07-11 09:59:04 -03:00
Salvador E. Tropea 499aac5e80 [Demucs] Better logger
- Better code
- Better output
2025-07-11 09:24:53 -03:00
Salvador E. Tropea 6760256cd7 [Added] Debug information about Demucs
So we can know the architecture used
2025-07-10 13:08:18 -03:00
17 changed files with 406 additions and 235 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
[project]
name = "audio-separation"
description = "Audio separation (aka demixing) nodes, for Vocals, Instruments, Bass, Drums and Others. Using MDX-Net, no extra dependencies, support for batch and resample."
version = "1.0.0"
description = "Audio separation (aka demixing) nodes, for Vocals, Instruments, Bass, Drums and Others (experimental Piano and Guitar). Using MDX-Net and Demucs, no extra dependencies, support for batch and resample. Choose between High Quality and Speed. All safetensor models (No ONNX, No PyTorch)"
version = "1.1.0"
license = { file = "LICENSE" }
dependencies = []
+14 -3
View File
@@ -11,8 +11,8 @@ import typing as tp
import torch
from torch import nn
from torch.nn import functional as F
import torchaudio
from .resample_frac import resample_frac # From julius
from .demucs_code import capture_init, center_trim, unfold
from .CrossTransformerEncoder import LayerScale
@@ -373,6 +373,8 @@ class Demucs(nn.Module):
if rescale:
rescale_module(self, reference=rescale)
self.upsampler = self.downsampler = None
def valid_length(self, length):
"""
Return the nearest valid length to use with the model so that
@@ -413,7 +415,16 @@ class Demucs(nn.Module):
x = F.pad(x, (delta // 2, delta - delta // 2))
if self.resample:
x = resample_frac(x, 1, 2)
if self.upsampler is None:
# Create the resamplers as instance attributes.
# They will be automatically moved to the correct device when model.to(device) is called.
self.upsampler = torchaudio.transforms.Resample(orig_freq=self.samplerate,
new_freq=2 * self.samplerate,
lowpass_filter_width=24).to(x.device)
self.downsampler = torchaudio.transforms.Resample(orig_freq=2 * self.samplerate,
new_freq=self.samplerate,
lowpass_filter_width=24).to(x.device)
x = self.upsampler(x)
saved = []
for encode in self.encoder:
@@ -429,7 +440,7 @@ class Demucs(nn.Module):
x = decode(x + skip)
if self.resample:
x = resample_frac(x, 2, 1)
x = self.downsampler(x)
x = x * std + mean
x = center_trim(x, length)
x = x.view(x.size(0), len(self.sources), self.audio_channels, x.size(-1))
+6 -2
View File
@@ -265,19 +265,23 @@ class DemixerDemucs(DemixerGeneric):
output_dict = {
"waveform": output_tensor,
"sample_rate": model.samplerate,
"stem": stem_name.capitalize()
"stem": stem_name.capitalize(),
"generated": True,
}
final_outputs.append(output_dict)
else:
logger.debug(f"Stem '{stem_name}' not produced by model. Returning silence.")
# Create a silent tensor with the correct shape, device, and dtype
silent_waveform = torch.zeros(ref_batch_size, ref_channels, ref_samples, dtype=torch.float32)
if not input_was_batched:
silent_waveform = silent_waveform.squeeze(0)
# Wrap the silent tensor in the ComfyUI AUDIO dictionary format
silent_dict = {
"waveform": silent_waveform,
"sample_rate": model.samplerate,
"stem": stem_name.capitalize()
"stem": stem_name.capitalize(),
"generated": False,
}
final_outputs.append(silent_dict)
+322
View File
@@ -0,0 +1,322 @@
from fractions import Fraction
import logging
from ..utils.misc import NODES_NAME
rlogger = logging.getLogger(f"{NODES_NAME}.demucs_log")
class DemucsModelInfo(object):
def __init__(self, index, klass_name: str, kwargs: dict, logger, weights, extra=False, sig=None):
super().__init__()
self.index = index
self.kwargs = kwargs
self.logger = logger
self.extra = extra
self.extra_indent = ""
self.weights = weights
self.signature = sig
# Always log these fundamental parameters
sr = kwargs.get('samplerate', 44100)
if sr != 44100:
rlogger.warning("Model not configured for 44.1 kHz sample rate")
a_ch = kwargs.get('audio_channels', 2)
if a_ch != 2:
rlogger.warning("Model not configured for stereo")
if klass_name == "HTDemucs":
self.htdemucs()
elif klass_name == "HDemucs":
self.hdemucs()
elif klass_name == "Demucs":
self.demucs()
else:
logger.warning(f"No specific logger for model class: {klass_name}. "
"Displaying raw kwargs.")
for key, value in kwargs.items():
logger(f" - {key}: {value}")
def get(self, key, default=None):
return self.kwargs.get(key, default)
def log_type(self, name):
start = "" if self.index < 0 else f"{self.index+1}. "
msg = f" {start}Type: {name}"
if self.signature:
msg += f" [{self.signature}]"
self.logger(msg)
def _log_param(self, param_name, default, description="", unit="", indent=" ", can_skip=False):
"""
Logs a parameter if its value is different from the default, or if it's a key parameter.
Args:
param_name (str): The name of the parameter to check.
default: The default value for this parameter.
description (str): A user-friendly description of the parameter.
unit (str): An optional unit to display after the value (e.g., 'Hz').
indent (str): The indentation string for the log message.
"""
value = self.get(param_name, default)
# We log if the value is not the default, or if it's a fundamental parameter.
is_default = (value == default)
is_important = param_name in ['sources', 'segment']
if is_default and not is_important and not self.extra:
return None
if can_skip and is_default:
return None
if param_name == 'sources' and self.weights:
value = [s if w == 1.0 else ('' if not w else f'{w}*{s}') for s, w in zip(value, self.weights)]
if unit == '%':
value *= 100
unit_str = f" {unit}" if unit else ""
desc_str = description if description else param_name.capitalize().replace('_', ' ')
indent += self.extra_indent
value_str = f"{value.numerator}/{value.denominator}" if isinstance(value, Fraction) else str(value)
n = (39 - len(desc_str) - len(value_str) - len(unit_str))*" "
return f" {indent}- {desc_str}: {value_str}{unit_str} {n}({param_name})"
def log_param(self, param_name, default, description="", unit="", indent=" ", can_skip=False):
res = self._log_param(param_name, default, description, unit, indent, can_skip)
if res is not None:
self.logger(res)
def add(self, param_name, default, description="", unit="", indent=" ", can_skip=False):
res = self._log_param(param_name, default, description, unit, indent, can_skip)
if res is not None:
self.params.append(res)
def reset(self):
self.params = []
def sub_section(self, name):
self.logger(f" {name}:")
def section(self, name):
self.logger(" " + "-" * 40)
self.sub_section(name)
def flush(self, name, is_sub=False):
if self.params:
if is_sub:
self.sub_section(self.extra_indent + name)
else:
self.section(name)
for p in self.params:
self.logger(p)
def structure(self, ch=64, depth=6, with_lstm=False, with_ch_tm=False):
self.reset()
self.add('channels', ch, "Initial hidden channels")
self.add('depth', depth, "Number of U-Net layers")
self.add('growth', 2.0, "Channel growth factor per layer")
self.add('rewrite', True, "Use 1x1 convolutions in blocks")
if with_lstm:
self.add('lstm_layers', 0, "Number of main LSTM layers", can_skip=True)
if with_ch_tm:
self.add('channels_time', None, "Specific channels for time branch", can_skip=True)
self.flush("Structure")
def convolutions(self, advanced=False):
self.reset()
self.add('kernel_size', 8)
self.add('stride', 4)
if advanced:
self.add('time_stride', 2, "Final time layer stride")
self.add('context', 1, "Decoder context window size")
if advanced:
self.add('context_enc', 0, "Encoder context window size")
self.flush("Convolutions")
def normalization(self):
self.reset()
self.add('norm_starts', 4, "Start at layer")
self.add('norm_groups', 4, "Number of groups")
self.flush("Normalization")
def dconv(self, full=True):
if self.get('dconv_mode', 1) <= 0:
return
self.reset()
where = ['', 'In encoder', 'In decoder', 'In encoder and decoder'][self.get('dconv_mode', 1)]
self.add('dconv_mode', 1, where)
self.add('dconv_depth', 2, "Number of layers in DConv branch")
if full:
comp = 4
init = 1e-4
else:
comp = 8
init = 1e-3
self.add('dconv_comp', comp, "Channel compression factor")
self.add('dconv_init', init, "Initial scale")
if full:
self.add('dconv_attn', 4, "Layer to start attention in DConv")
self.add('dconv_lstm', 4, "Layer to start LSTM in DConv")
self.flush("DConv Residual Branch")
def stft(self):
self.reset()
self.add('nfft', 4096, "Frequency Bins")
# Decode the method
cac = self.get('cac')
niters = self.get('wiener_iters', 0)
if cac:
zout = "Complex as Channels (CaC)"
elif niters >= 0:
zout = "Wiener filtering"
else:
zout = "Naive iSTFT from masking"
self.add('___', zout, "Framework")
self.add('cac', True, "Use Complex as Channels")
if not self.get('cac', True):
self.add('wiener_iters', 0, "Wiener filter iterations")
self.flush("STFT")
def freq_branch(self):
self.reset()
def_ratio = None
if self.get('multi_freqs') == []:
def_ratio = []
self.add('multi_freqs', def_ratio, "Ratios for frequency band splitting")
if self.get('multi_freqs'):
self.add('multi_freqs_depth', 2, "Layers to apply frequency splitting")
self.add('freq_emb', 0.2, "Frequency embedding weight")
if self.get('freq_emb'):
indent = " "
self.add('emb_scale', 10, "Scale", indent=indent)
self.add('emb_smooth', True, "Smooth", indent=indent)
self.flush("Frequency Branch")
def demucs(self):
"""Logs the parameters for the original Demucs class."""
self.log_type("Classic Waveform Demucs (Demucs)")
self.log_param('sources', [], "Target source names")
self.log_param('segment', 40, "Segment size", unit="s")
# --- Structure & Channels ---
self.structure(ch=64, depth=6, with_lstm=True)
# --- Convolutions ---
self.convolutions()
self.reset()
self.add('gelu', True, "GeLU (not ReLU)")
if self.get('rewrite', True):
self.add('glu', True, "GLU in 1x1 rewrite (not ReLU)")
self.flush("Activations")
# --- Normalization ---
self.normalization()
# --- DConv Residual Branch ---
self.dconv()
# --- Pre/Post Processing ---
self.reset()
self.add('resample', True, "Use 2x resampling")
self.add('normalize', True, "Normalize audio on-the-fly")
self.flush("Processing")
def hdemucs(self):
"""Logs the parameters for the HDemucs (Hybrid Spectrogram/Waveform) class."""
self.log_type("Hybrid Demucs (Spectrogram + Waveform) (HDemucs)")
self.log_param('sources', [], "Target source names")
self.log_param('segment', 40, "Segment size", unit="s")
# --- Structure & Channels ---
self.structure(ch=48, depth=6, with_ch_tm=True)
# --- STFT & Spectrogram ---
self.stft()
# --- Frequency Branch ---
self.freq_branch()
# --- Convolutions ---
self.convolutions(advanced=True)
# --- Normalization ---
self.normalization()
# --- DConv Residual Branch (defaults are different from Demucs) ---
self.dconv()
def htdemucs(self):
"""Logs the parameters for the HTDemucs (Hybrid Transformer) class."""
self.log_type("Hybrid Transformer Demucs (HTDemucs)")
self.log_param('sources', [], "Target source names")
self.log_param('segment', 10, "Segment size", unit="s")
# --- Structure & Channels (defaults are different from HDemucs) ---
self.structure(ch=48, depth=4)
# --- STFT & Spectrogram ---
self.stft()
# --- Frequency Branch ---
self.freq_branch()
# --- Convolutions ---
self.convolutions(advanced=True)
# --- Normalization ---
self.normalization()
# --- DConv (defaults are different) ---
self.dconv(full=False)
# --- Transformer Block ---
if self.get('t_layers', 5) > 0:
self.extra_indent = " "
# --- Main Transformer ---
self.reset()
if self.get('bottom_channels', 0):
self.add('bottom_channels', 0, "Channels forced to")
self.add('t_hidden_scale', 4.0, "Hidden scale")
self.add('t_layers', 5, "Number of transformer layers")
self.add('t_heads', 8, "Number of attention heads")
self.add('t_dropout', 0.0, "Dropout")
self.flush("Transformer")
# --- Positional Embeddings ---
self.reset()
self.add('t_emb', 'sin', "Type")
self.add('t_weight_pos_embed', 1.0, "Weight", can_skip=True)
t_emb = self.get('t_emb', 'sin')
if t_emb == 'scaled':
self.add('t_max_positions', 10000, "Max positions")
elif t_emb == 'sin':
self.add('t_max_period', 10000.0, "Max period")
self.add('t_sin_random_shift', 0, "Random shift", can_skip=True)
elif t_emb == 'cape':
self.add('t_cape_mean_normalize', True, "Cape normalize")
self.add('t_cape_glob_loc_scale', [5000.0, 1.0, 1.4], "Cape params")
if self.get('t_cape_augment', True):
rlogger.warning("t_cape_augment is True in loaded model, should be False for inference.")
self.flush("Positional Embeddings", is_sub=True)
# --- Transformer Normalization ---
self.reset()
self.add('t_norm_first', True, "Before attention/FFN")
self.add('t_norm_in', True, "Before pos. embedding")
if self.get('t_norm_in', True):
self.add('t_norm_in_group', False, "On all timesteps")
self.add('t_group_norm', False, "Of encoder on all timesteps")
self.add('t_norm_out', True, "GroupNorm at end of layers")
self.flush("Normalization", is_sub=True)
# --- Transformer Misc ---
self.reset()
self.add('t_cross_first', False, "Cross-attention is the first layer")
self.add('t_layer_scale', True, "Layer scale")
self.add('t_gelu', True, "GeLU (not ReLU)")
self.flush("Various", is_sub=True)
# --- Sparsity ---
# Log sparsity details only if sparse attention is enabled
self.reset()
is_sparse = self.get('t_sparse_self_attn', False)
self.add('t_sparse_self_attn', False, "Use sparse self-attention")
if is_sparse:
self.add('t_sparse_cross_attn', False, "Sparse cross-attention")
self.add('t_auto_sparsity', False, "Automatic sparsity")
auto_sparsity = self.get('t_auto_sparsity', False)
if not auto_sparsity:
self.add('t_mask_type', 'diag', "Masking pattern")
self.add('t_mask_random_seed', 42, "Mask seed")
mask_t = self.get('t_mask_type', 'diag')
if 'diag' in mask_t:
self.add('t_sparse_attn_window', 500, "Window size")
if 'global' in mask_t:
self.add('t_global_window', 100, "Window size")
if 'random' in mask_t:
self.add('t_sparsity', 0.95, "Sparsity for random mask", unit="%")
self.flush("Sparsity", is_sub=True)
# Training only
# self.add(logger, kwargs, 't_weight_decay', 0.0, "Weight decay", extra=False)
# self.add(logger, kwargs, 't_lr', None, "Learning rate", extra=False)
# self.add(logger, kwargs, 't_cape_augment', True, "Learning rate", extra=False)
# self.add(logger, kwargs, 'rescale', 0.1, "Rescale trick", extra=False)
self.extra_indent = ""
+31 -5
View File
@@ -9,14 +9,15 @@ import json
import logging
from safetensors import safe_open
from .MDX_Net import MDX_Net
from ..utils.misc import NODES_NAME, json_object_hook
from ..utils.misc import NODES_NAME, json_object_hook, get_debug_level
# Demucs class imports
from .demucs_api import BagOfModels
from .demucs_log_helper import DemucsModelInfo
logger = logging.getLogger(f"{NODES_NAME}.get_model")
def get_metadata(file_path, d):
def get_metadata(file_path, d=None):
""" Read the metadata from a safetensors file """
logger.debug(f"Reading metadata from {file_path}")
metadata = {}
@@ -25,6 +26,8 @@ def get_metadata(file_path, d):
if not metadata:
raise ValueError(f"Could not read metadata from safetensors file: {file_path}")
if d is None:
return metadata
# Is this a child model?
parent = d.get('parent')
if parent:
@@ -57,7 +60,7 @@ def get_hyperparameter(metadata, parameter, as_type, default=None, warn_diff=Tru
def get_mdx_model(d):
""" Create an MDX_Net object with the specified parameters """
# Check the file is consistent we our data base
metadata = get_metadata(d['model_path'])
metadata = get_metadata(d['model_path'], d)
dim_f = get_hyperparameter(metadata, 'mdx_dim_f_set', "int", d['mdx_dim_f_set'])
channels = get_hyperparameter(metadata, 'channels', "int", d['channels'])
stages = get_hyperparameter(metadata, 'stages', "int", d['stages'])
@@ -102,8 +105,11 @@ def get_demucs_model(d):
logger.debug(f" - Redirecting class: {class_module} -> {local_class_module}")
module = importlib.import_module(local_class_module)
klass = getattr(module, class_name)
instance = klass(*args, **kwargs)
instance._signature = sig
instance._metadata = model_meta
sub_models.append({'sig': sig, 'model': klass(*args, **kwargs)})
sub_models.append({'sig': sig, 'model': instance})
# Create a mapping from signature to model instance
model_map = {m['sig']: m['model'] for m in sub_models}
@@ -113,17 +119,37 @@ def get_demucs_model(d):
segment = float(metadata.get('segment', '0'))
if not is_bag:
weights = None
final_model = ordered_models[0]
final_model.signatures = signatures
else:
logger.debug("Rebuilding BagOfModels container...")
weights = json.loads(metadata.get('weights', 'null'))
final_model = BagOfModels(ordered_models, weights=weights, segment=segment)
final_model.signatures = signatures
final_model.config_segment = segment
debug_level = get_debug_level(logger)
if debug_level >= 1:
# Show some information of the resulting model
logger.debug("Model information:")
logger.debug(f"- Total models {len(ordered_models)}")
if weights is not None and len(weights) != len(ordered_models):
raise ValueError(f"Invalid weights for {len(ordered_models)} models: {weights}")
for n, m in enumerate(ordered_models):
tp = m.__class__.__name__
w = weights[n] if weights else None
if n == 0:
ref_sources = m.sources
else:
if m.sources != ref_sources:
logger.error("The sub-model outputs doesn't match")
if w and len(m.sources) != len(w):
raise ValueError(f"Invalid {w} weights for {m.sources} sources")
num = -1 if len(ordered_models) == 1 else n
DemucsModelInfo(num, tp, m._metadata['kwargs'], logger.debug, w, extra=debug_level > 1, sig=m._signature)
return final_model
-220
View File
@@ -1,220 +0,0 @@
# File under the MIT license, see https://github.com/adefossez/julius/LICENSE for details.
# Author: adefossez, 2020
"""
Differentiable, Pytorch based resampling.
Implementation of Julius O. Smith algorithm for resampling.
See https://ccrma.stanford.edu/~jos/resample/ for details.
This implementation is specially optimized for when new_sr / old_sr is a fraction
with a small numerator and denominator when removing the gcd (e.g. new_sr = 700, old_sr = 500).
Very similar to [bmcfee/resampy](https://github.com/bmcfee/resampy) except this implementation
is optimized for the case mentioned before, while resampy is slower but more general.
"""
import math
from typing import Optional
import torch
from torch.nn import functional as F
def sinc(x: torch.Tensor):
"""
Implementation of sinc, i.e. sin(x) / x
__Warning__: the input is not multiplied by `pi`!
"""
return torch.where(x == 0, torch.tensor(1., device=x.device, dtype=x.dtype), torch.sin(x) / x)
class ResampleFrac(torch.nn.Module):
"""
Resampling from the sample rate `old_sr` to `new_sr`.
"""
def __init__(self, old_sr: int, new_sr: int, zeros: int = 24, rolloff: float = 0.945):
"""
Args:
old_sr (int): sample rate of the input signal x.
new_sr (int): sample rate of the output.
zeros (int): number of zero crossing to keep in the sinc filter.
rolloff (float): use a lowpass filter that is `rolloff * new_sr / 2`,
to ensure sufficient margin due to the imperfection of the FIR filter used.
Lowering this value will reduce anti-aliasing, but will reduce some of the
highest frequencies.
Shape:
- Input: `[*, T]`
- Output: `[*, T']` with `T' = int(new_sr * T / old_sr)
.. caution::
After dividing `old_sr` and `new_sr` by their GCD, both should be small
for this implementation to be fast.
>>> import torch
>>> resample = ResampleFrac(4, 5)
>>> x = torch.randn(1000)
>>> print(len(resample(x)))
1250
"""
super().__init__()
if not isinstance(old_sr, int) or not isinstance(new_sr, int):
raise ValueError("old_sr and new_sr should be integers")
gcd = math.gcd(old_sr, new_sr)
self.old_sr = old_sr // gcd
self.new_sr = new_sr // gcd
self.zeros = zeros
self.rolloff = rolloff
self._init_kernels()
def _init_kernels(self):
if self.old_sr == self.new_sr:
return
kernels = []
sr = min(self.new_sr, self.old_sr)
# rolloff will perform antialiasing filtering by removing the highest frequencies.
# At first I thought I only needed this when downsampling, but when upsampling
# you will get edge artifacts without this, the edge is equivalent to zero padding,
# which will add high freq artifacts.
sr *= self.rolloff
# The key idea of the algorithm is that x(t) can be exactly reconstructed from x[i] (tensor)
# using the sinc interpolation formula:
# x(t) = sum_i x[i] sinc(pi * old_sr * (i / old_sr - t))
# We can then sample the function x(t) with a different sample rate:
# y[j] = x(j / new_sr)
# or,
# y[j] = sum_i x[i] sinc(pi * old_sr * (i / old_sr - j / new_sr))
# We see here that y[j] is the convolution of x[i] with a specific filter, for which
# we take an FIR approximation, stopping when we see at least `zeros` zeros crossing.
# But y[j+1] is going to have a different set of weights and so on, until y[j + new_sr].
# Indeed:
# y[j + new_sr] = sum_i x[i] sinc(pi * old_sr * ((i / old_sr - (j + new_sr) / new_sr))
# = sum_i x[i] sinc(pi * old_sr * ((i - old_sr) / old_sr - j / new_sr))
# = sum_i x[i + old_sr] sinc(pi * old_sr * (i / old_sr - j / new_sr))
# so y[j+new_sr] uses the same filter as y[j], but on a shifted version of x by `old_sr`.
# This will explain the F.conv1d after, with a stride of old_sr.
self._width = math.ceil(self.zeros * self.old_sr / sr)
# If old_sr is still big after GCD reduction, most filters will be very unbalanced, i.e.,
# they will have a lot of almost zero values to the left or to the right...
# There is probably a way to evaluate those filters more efficiently, but this is kept for
# future work.
idx = torch.arange(-self._width, self._width + self.old_sr).float()
for i in range(self.new_sr):
t = (-i/self.new_sr + idx/self.old_sr) * sr
t = t.clamp_(-self.zeros, self.zeros)
t *= math.pi
window = torch.cos(t/self.zeros/2)**2
kernel = sinc(t) * window
# Renormalize kernel to ensure a constant signal is preserved.
kernel.div_(kernel.sum())
kernels.append(kernel)
self.register_buffer("kernel", torch.stack(kernels).view(self.new_sr, 1, -1))
def forward(self, x: torch.Tensor, output_length: Optional[int] = None, full: bool = False):
"""
Resample x.
Args:
x (Tensor): signal to resample, time should be the last dimension
output_length (None or int): This can be set to the desired output length
(last dimension). Allowed values are between 0 and
ceil(length * new_sr / old_sr). When None (default) is specified, the
floored output length will be used. In order to select the largest possible
size, use the `full` argument.
full (bool): return the longest possible output from the input. This can be useful
if you chain resampling operations, and want to give the `output_length` only
for the last one, while passing `full=True` to all the other ones.
"""
if self.old_sr == self.new_sr:
return x
shape = x.shape
length = x.shape[-1]
x = x.reshape(-1, length)
x = F.pad(x[:, None], (self._width, self._width + self.old_sr), mode='replicate')
ys = F.conv1d(x, self.kernel, stride=self.old_sr) # type: ignore
y = ys.transpose(1, 2).reshape(list(shape[:-1]) + [-1])
float_output_length = torch.as_tensor(self.new_sr * length / self.old_sr)
max_output_length = torch.ceil(float_output_length).long()
default_output_length = torch.floor(float_output_length).long()
if output_length is None:
applied_output_length = max_output_length if full else default_output_length
elif output_length < 0 or output_length > max_output_length:
raise ValueError(f"output_length must be between 0 and {max_output_length}")
else:
applied_output_length = torch.tensor(output_length)
if full:
raise ValueError("You cannot pass both full=True and output_length")
return y[..., :applied_output_length] # type: ignore
def resample_frac(x: torch.Tensor, old_sr: int, new_sr: int,
zeros: int = 24, rolloff: float = 0.945,
output_length: Optional[int] = None, full: bool = False):
"""
Functional version of `ResampleFrac`, refer to its documentation for more information.
..warning::
If you call repeatidly this functions with the same sample rates, then the
resampling kernel will be recomputed every time. For best performance, you should use
and cache an instance of `ResampleFrac`.
"""
return ResampleFrac(old_sr, new_sr, zeros, rolloff).to(x)(x, output_length, full)
# Easier implementations for downsampling and upsampling by a factor of 2
# Kept for testing and reference
def _kernel_upsample2_downsample2(zeros):
# Kernel for upsampling and downsampling by a factor of 2. Interestingly,
# it is the same kernel used for both.
win = torch.hann_window(4 * zeros + 1, periodic=False)
winodd = win[1::2]
t = torch.linspace(-zeros + 0.5, zeros - 0.5, 2 * zeros)
t *= math.pi
kernel = (sinc(t) * winodd).view(1, 1, -1)
return kernel
def _upsample2(x, zeros=24):
"""
Upsample x by a factor of two. The output will be exactly twice as long as the input.
Args:
x (Tensor): signal to upsample, time should be the last dimension
zeros (int): number of zero crossing to keep in the sinc filter.
This function is kept only for reference, you should use the more generic `resample_frac`
one. This function does not perform anti-aliasing filtering.
"""
*other, time = x.shape
kernel = _kernel_upsample2_downsample2(zeros).to(x)
out = F.conv1d(x.view(-1, 1, time), kernel, padding=zeros)[..., 1:].view(*other, time)
y = torch.stack([x, out], dim=-1)
return y.view(*other, -1)
def _downsample2(x, zeros=24):
"""
Downsample x by a factor of two. The output length is half of the input, ceiled.
Args:
x (Tensor): signal to downsample, time should be the last dimension
zeros (int): number of zero crossing to keep in the sinc filter.
This function is kept only for reference, you should use the more generic `resample_frac`
one. This function does not perform anti-aliasing filtering.
"""
if x.shape[-1] % 2 != 0:
x = F.pad(x, (0, 1))
xeven = x[..., ::2]
xodd = x[..., 1::2]
*other, time = xodd.shape
kernel = _kernel_upsample2_downsample2(zeros).to(x)
out = xeven + F.conv1d(xodd.view(-1, 1, time), kernel, padding=zeros)[..., :-1].view(
*other, time)
return out.view(*other, -1).mul(0.5)
+5 -1
View File
@@ -10,8 +10,12 @@ NODES_NAME = "AudioSeparation"
NODES_DEBUG_VAR = NODES_NAME.upper() + "_NODES_DEBUG"
def get_debug_level(logger):
return logging.DEBUG - logger.getEffectiveLevel() + 1
def debugl(logger, level, msg):
if logger.getEffectiveLevel() <= logging.DEBUG - (level - 1):
if get_debug_level(logger) >= level:
logger.debug(msg)
Regular → Executable
+1
View File
@@ -1,3 +1,4 @@
#!/usr/bin/env python3
# Copyright (c) 2025 Salvador E. Tropea
# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial
# License: GPLv3
Regular → Executable
+2 -1
View File
@@ -1,3 +1,4 @@
#!/usr/bin/env python3
# Copyright (c) 2025 Salvador E. Tropea
# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial
# License: GPLv3
@@ -47,7 +48,7 @@ def demix(d, args):
if args.save_complement:
for wav in wavs[1:]:
if wav is None:
if wav is None or not wav['generated']:
continue
out_path = f"{args.out_base or base}_{wav['stem']}{out_ext}"
save_audio(wav['waveform'], wav['sample_rate'], out_path, out_format)
Regular → Executable
+1
View File
@@ -1,3 +1,4 @@
#!/usr/bin/env python3
# Copyright (c) 2025 Salvador E. Tropea
# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial
# License: GPLv3
Regular → Executable
+1
View File
@@ -1,3 +1,4 @@
#!/usr/bin/env python3
# Copyright (c) 2025 Salvador E. Tropea
# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial
# License: GPLv3
Regular → Executable
+1
View File
@@ -1,3 +1,4 @@
#!/usr/bin/env python3
# Copyright (c) 2025 Salvador E. Tropea
# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial
# License: GPLv3
Regular → Executable
+1
View File
@@ -1,3 +1,4 @@
#!/usr/bin/env python3
# Copyright (c) 2025 Salvador E. Tropea
# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial
# License: GPLv3
Regular → Executable
+1
View File
@@ -1,3 +1,4 @@
#!/usr/bin/env python3
# Copyright (c) 2025 Salvador E. Tropea
# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial
# License: GPLv3
Regular → Executable
+17 -1
View File
@@ -1,3 +1,4 @@
#!/usr/bin/env python3
# Copyright (c) 2025 Salvador E. Tropea
# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial
# License: GPLv3
@@ -13,6 +14,7 @@ import bootstrap # noqa: F401
from source.utils.logger import main_logger, logger_set_standalone
from source.utils.misc import cli_add_verbose, json_object_hook
from source.inference.get_model import get_metadata
from source.inference.demucs_log_helper import DemucsModelInfo
def show_metadata(args):
@@ -23,11 +25,25 @@ def show_metadata(args):
expanded = {k: json.loads(v, object_hook=json_object_hook) if v[0] in '{[' else v for k, v in d.items()}
pprint(expanded)
sigs = expanded.get('signatures')
if sigs:
single = len(sigs) == 1
print("Demucs model")
if not single:
print(f"Composed by {len(sigs)} submodels")
weights = expanded.get('weights')
# Demucs model
for n, s in enumerate(sigs):
d = expanded[s]
num = -1 if single else n
DemucsModelInfo(num, d['class_name'], d['kwargs'], print, weights[n] if weights else None, extra=args.verbose,
sig=s)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Shows the safetensors metadata",
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('input_file', type=str, help="Path to the input ONNX model file.")
parser.add_argument('input_file', type=str, help="Path to the input safetensors model file.")
cli_add_verbose(parser)
args = parser.parse_args()
Regular → Executable
+1
View File
@@ -1,3 +1,4 @@
#!/usr/bin/env python3
# Copyright (c) 2025 Salvador E. Tropea
# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial
# License: GPLv3
Regular → Executable
View File