Vansh Chugh
initial deploy
1a3ce68
Raw
History Blame Contribute Delete
23.3 kB
import torch
#import laion_clap
import logging
import sys
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchlibrosa.stft import Spectrogram, LogmelFilterBank
from collections import OrderedDict
from dataclasses import dataclass
import numpy as np
import os
from pathlib import Path
from huggingface_hub import hf_hub_download
def init_layer(layer):
"""Initialize a Linear or Convolutional layer. """
nn.init.xavier_uniform_(layer.weight)
if hasattr(layer, 'bias'):
if layer.bias is not None:
layer.bias.data.fill_(0.)
def init_bn(bn):
"""Initialize a Batchnorm layer. """
bn.bias.data.fill_(0.)
bn.weight.data.fill_(1.)
class ConvBlock(nn.Module):
def __init__(self, in_channels, out_channels):
super(ConvBlock, self).__init__()
self.conv1 = nn.Conv2d(in_channels=in_channels,
out_channels=out_channels,
kernel_size=(3, 3), stride=(1, 1),
padding=(1, 1), bias=False)
self.conv2 = nn.Conv2d(in_channels=out_channels,
out_channels=out_channels,
kernel_size=(3, 3), stride=(1, 1),
padding=(1, 1), bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.bn2 = nn.BatchNorm2d(out_channels)
self.init_weight()
def init_weight(self):
init_layer(self.conv1)
init_layer(self.conv2)
init_bn(self.bn1)
init_bn(self.bn2)
def forward(self, input, pool_size=(2, 2), pool_type='avg'):
x = input
x = F.relu_(self.bn1(self.conv1(x)))
x = F.relu_(self.bn2(self.conv2(x)))
if pool_type == 'max':
x = F.max_pool2d(x, kernel_size=pool_size)
elif pool_type == 'avg':
x = F.avg_pool2d(x, kernel_size=pool_size)
elif pool_type == 'avg+max':
x1 = F.avg_pool2d(x, kernel_size=pool_size)
x2 = F.max_pool2d(x, kernel_size=pool_size)
x = x1 + x2
else:
raise Exception('Incorrect argument!')
return x
class CLAP_AUDIO_ENCODER(torch.nn.Module):
def __init__(self, pretrained: bool = True, frozen: bool = False) -> None:
super().__init__()
self.pretrained = pretrained
self.frozen = frozen
# load the model
self.encoder = laion_clap.CLAP_Module(enable_fusion=False, amodel= 'HTSAT-tiny', tmodel='roberta')
if self.pretrained:
self.encoder.load_ckpt() # download the default pretrained checkpoint.
self.embed_dim = 512
def forward(self, x: torch.Tensor):
if self.frozen:
with torch.no_grad():
embed = self.encoder.get_audio_embedding_from_data(
x=x, use_tensor=True
)
else:
embed = self.encoder.get_audio_embedding_from_data(
x=x, use_tensor=True
)
return embed
class CLAP_TEXT_ENCODER(torch.nn.Module):
def __init__(self, pretrained: bool = True, frozen: bool = False) -> None:
super().__init__()
self.pretrained = pretrained
self.frozen = frozen
# load the model
self.encoder = laion_clap.CLAP_Module(enable_fusion=False, amodel= 'HTSAT-tiny', tmodel='roberta')
if self.pretrained:
self.encoder.load_ckpt() # download the default pretrained checkpoint.
self.embed_dim = 512
def forward(self, x):
if self.frozen:
with torch.no_grad():
embed = self.encoder.get_text_embedding(
x=x, use_tensor=True
)
else:
embed = self.encoder.get_text_embedding(
x=x, use_tensor=True
)
return embed
# > ================ Proposed =================== <
class MixtureFxEncoder(nn.Module):
def __init__(self, sample_rate, window_size, hop_size, mel_bins, fmin, fmax, enable_fusion=False, fusion_type='None'):
super().__init__()
self.enable_fusion = enable_fusion
self.fusion_type = fusion_type
window = "hann"
center = True
pad_mode = "reflect"
ref = 1.0
amin = 1e-10
top_db = None
self.input_norm = "minmax"
# Spectrogram extractor
self.spectrogram_extractor = Spectrogram(
n_fft=window_size,
hop_length=hop_size,
win_length=window_size,
window=window,
center=center,
pad_mode=pad_mode,
freeze_parameters=True,
)
# Logmel feature extractor
self.logmel_extractor = LogmelFilterBank(
sr=sample_rate,
n_fft=window_size,
n_mels=mel_bins,
fmin=fmin,
fmax=fmax,
ref=ref,
amin=amin,
top_db=top_db,
freeze_parameters=True,
)
self.bn0 = nn.BatchNorm2d(64)
self.conv_block1 = ConvBlock(in_channels=2, out_channels=64)
self.conv_block2 = ConvBlock(in_channels=64, out_channels=128)
self.conv_block3 = ConvBlock(in_channels=128, out_channels=256)
self.conv_block4 = ConvBlock(in_channels=256, out_channels=512)
self.conv_block5 = ConvBlock(in_channels=512, out_channels=1024)
self.conv_block6 = ConvBlock(in_channels=1024, out_channels=2048)
self.fc_1 = nn.Linear(2048, 2048, bias=True)
self.init_weight()
def init_weight(self):
init_bn(self.bn0)
init_layer(self.fc_1)
def forward(self, x):
"""
Input: (batch_size, 2, data_length)
"""
batch_size, chs, seq_len = x.size()
# move to batch dim
x = x.view(batch_size * chs, seq_len)
# extract logmel features
x = self.spectrogram_extractor(x) # (batch_size, 1, time_steps, freq_bins)
x = self.logmel_extractor(x) # (batch_size, 1, time_steps, mel_bins)
if self.input_norm == "batchnorm":
# this normalizes over mel bins which is problematic for equalization
x = x.transpose(1, 3)
x = self.bn0(x)
x = x.transpose(1, 3)
elif self.input_norm == "minmax":
x = x.clamp(-80, 40.0) # clamp the logmels between -80 and 40
x = (x + 80) / 120 # normalize the logmels between 0 and 1
x = (x * 2) - 1 # normalize the logmels between -1 and 1
elif self.input_norm == "none":
pass
else:
raise ValueError(f"Invalid input_norm: {self.input_norm}")
x = x.view(batch_size, chs, x.size(-2), x.size(-1))
x = self.conv_block1(x, pool_size=(2, 2), pool_type='avg')
x = F.dropout(x, p=0.2, training=self.training)
x = self.conv_block2(x, pool_size=(2, 2), pool_type='avg')
x = F.dropout(x, p=0.2, training=self.training)
x = self.conv_block3(x, pool_size=(2, 2), pool_type='avg')
x = F.dropout(x, p=0.2, training=self.training)
x = self.conv_block4(x, pool_size=(2, 2), pool_type='avg')
x = F.dropout(x, p=0.2, training=self.training)
x = self.conv_block5(x, pool_size=(2, 2), pool_type='avg')
x = F.dropout(x, p=0.2, training=self.training)
x = self.conv_block6(x, pool_size=(1, 1), pool_type='avg')
x = F.dropout(x, p=0.2, training=self.training)
x = torch.mean(x, dim=3)
(x1, _) = torch.max(x, dim=2)
x2 = torch.mean(x, dim=2)
x = x1 + x2
x = F.relu_(self.fc_1(x))
embedding = x
output_dict = {
'embedding': embedding,
}
return output_dict
def create_MixtureFxEncoder():
model = MixtureFxEncoder(
sample_rate = 44100, #audio_cfg.sample_rate,
window_size = 2048, #audio_cfg.window_size,
hop_size = 512, #audio_cfg.hop_size,
mel_bins = 64, #audio_cfg.mel_bins,
fmin = 50, #audio_cfg.fmin,
fmax = 18000, #audio_cfg.fmax,
)
return model
class MLPLayers(nn.Module):
def __init__(self, units=[512, 512, 512], nonlin=nn.ReLU(), dropout=0.1):
super(MLPLayers, self).__init__()
self.nonlin = nonlin
self.dropout = dropout
sequence = []
for u0, u1 in zip(units[:-1], units[1:]):
sequence.append(nn.Linear(u0, u1))
sequence.append(self.nonlin)
sequence.append(nn.Dropout(self.dropout))
sequence = sequence[:-2]
self.sequential = nn.Sequential(*sequence)
def forward(self, X):
X = self.sequential(X)
return X
class BernoulliDynamicDropout(nn.Module):
def __init__(self):
super().__init__()
self.p_min = 0.75
self.p_max = 0.95
def get_random_dropout_rate(self):
return torch.empty(1).uniform_(self.p_min, self.p_max).item()
def forward(self, x):
if self.training:
p = self.get_random_dropout_rate()
mask = torch.bernoulli(torch.full_like(x, 1-p))
return x * mask / (1 - p)
return x
class AudioExtracter(nn.Module):
def __init__(self, fx_embedding_dim=128, clap_embedding_dim=512):
super().__init__()
# Simple fusion network
self.fusion = nn.Sequential(
nn.Linear(fx_embedding_dim+clap_embedding_dim, 128),
nn.LeakyReLU(0.1),
nn.Linear(128, 128),
nn.LeakyReLU(0.1),
nn.Linear(128, 128),
)
def forward(self, mixture_emb, query_emb):
# Concatenate and project
x = torch.cat([mixture_emb, query_emb], dim=-1) # [B, 2D]
stem_emb = self.fusion(x) # [B, D]
return stem_emb
@dataclass
class AudioCfg:
model_type: str = "PANN"
model_name: str = "Cnn14"
sample_rate: int = 44100
# Param
audio_length: int = 1024
window_size: int = 1024
hop_size: int = 1024
fmin: int = 50
fmax: int = 14000
mel_bins: int = 64
clip_samples: int = 441000
class_num: int = 527
condition_dim: int = 512
class FxEncoderPlusPlus(nn.Module):
def __init__(
self,
embed_dim: int = 2048,
mixture_cfg: AudioCfg = None,
enable_fusion: bool = False,
fusion_type: str = 'None',
joint_embed_shape: int = 128,
mlp_act: str = 'relu',
audio_clap_module: bool = True,
text_clap_module: bool = False,
extractor_module: bool = True,
device: str = "cpu",
):
super().__init__()
self.mixture_cfg = mixture_cfg
self.enable_fusion = enable_fusion
self.fusion_type = fusion_type
self.joint_embed_shape = joint_embed_shape
self.mlp_act = mlp_act
self.device = device
if mlp_act == 'relu':
mlp_act_layer = nn.ReLU()
elif mlp_act == 'gelu':
mlp_act_layer = nn.GELU()
else:
raise NotImplementedError
# > ========================= FX Encoder ========================= <
self.fx_encoder = create_MixtureFxEncoder()
self.fx_encoder_transform = MLPLayers(units=[self.joint_embed_shape, self.joint_embed_shape, self.joint_embed_shape], dropout=0.1)
self.fx_encoder_projection = nn.Sequential(
nn.Linear(embed_dim, self.joint_embed_shape),
mlp_act_layer,
nn.Linear(self.joint_embed_shape, self.joint_embed_shape)
)
self.logit_scale_m = nn.Parameter(torch.ones([]) * np.log(1 / 0.07))
self.logit_scale_t = nn.Parameter(torch.ones([]) * np.log(1 / 0.07))
if audio_clap_module:
# Freeze all layers
# print("Loading CLAP Audio Model")
self.audio_clap_model = CLAP_AUDIO_ENCODER(pretrained=True, frozen=True)
self.audio_clap_model.to(device)
for param in self.audio_clap_model.parameters():
param.requires_grad = False
self.clap_dropout = BernoulliDynamicDropout()
if text_clap_module:
# Freeze all layers
# print("Loading CLAP Text Model")
self.text_clap_model = CLAP_TEXT_ENCODER(pretrained=True, frozen=True)
self.text_clap_model.to(device)
for param in self.text_clap_model.parameters():
param.requires_grad = False
if extractor_module:
# extractor
self.extractor = AudioExtracter()
self.use_audio_clap_module = audio_clap_module
self.use_text_clap_module = text_clap_module
self.use_extractor_module = extractor_module
def get_fx_embedding(self, x):
fx_emb = self.fx_encoder(x)
fx_emb = self.fx_encoder_projection(fx_emb["embedding"])
fx_emb = F.normalize(fx_emb, dim=-1)
return fx_emb
def get_fx_embedding_by_audio_query(self, x, audio_query):
# mixture fx embedding
fx_mixture_emb = self.fx_encoder(x)
fx_mixture_emb = self.fx_encoder_projection(fx_mixture_emb["embedding"])
fx_mixture_emb = F.normalize(fx_mixture_emb, dim=-1)
# stem fx embedding
query_content_embeded = self.audio_clap_model(torch.mean(audio_query, dim=1))
fx_stem_emb = self.extractor(fx_mixture_emb, query_content_embeded)
fx_stem_emb = F.normalize(fx_stem_emb, dim=-1)
return fx_mixture_emb, fx_stem_emb
def get_fx_embedding_by_text_query(self, x, text_query):
# mixture fx embedding
fx_mixture_emb = self.fx_encoder(x)
fx_mixture_emb = self.fx_encoder_projection(fx_mixture_emb["embedding"])
fx_mixture_emb = F.normalize(fx_mixture_emb, dim=-1)
# stem fx embedding
query_embeded = self.text_clap_model(text_query)
fx_stem_emb = self.extractor(fx_mixture_emb, query_embeded)
fx_stem_emb = F.normalize(fx_stem_emb, dim=-1)
return fx_mixture_emb, fx_stem_emb
def forward(
self,
mixture_a,
mixture_b,
stem_a,
query_stem,
device = None
):
if device is None:
if mixture_a is not None:
device = mixture_a.device
elif mixture_b is not None:
device = mixture_b.device
if mixture_a is None and mixture_b is None:
# a hack to get the logit scale
return self.logit_scale_m.exp(), self.logit_scale_t.exp()
# ======== Global ========
mixture_a_features = self.fx_encoder_projection(
self.fx_encoder(mixture_a)["embedding"]
)
mixture_a_features = F.normalize(mixture_a_features, dim=-1)
mixture_b_features = self.fx_encoder_projection(
self.fx_encoder(mixture_b)["embedding"]
)
mixture_b_features = F.normalize(mixture_b_features, dim=-1)
mixture_a_features_mlp = self.fx_encoder_transform(mixture_a_features)
mixture_b_features_mlp = self.fx_encoder_transform(mixture_b_features)
# ======= Local ========
stem_a_features = self.fx_encoder_projection(
self.fx_encoder(stem_a)["embedding"]
)
stem_a_features = F.normalize(stem_a_features, dim=-1)
if self.use_audio_clap_module and self.use_extractor_module:
query_stem_content_embeded = self.clap_dropout(
self.audio_clap_model(
torch.mean(query_stem, dim=1)
)
)
extracted_stem_a_features = self.extractor(mixture_a_features, query_stem_content_embeded)
extracted_stem_a_features = F.normalize(extracted_stem_a_features, dim=-1)
elif self.use_text_clap_module and self.use_extractor_module:
query_stem_content_embeded = self.text_clap_model(query_stem)
extracted_stem_a_features = self.extractor(mixture_a_features, query_stem_content_embeded)
extracted_stem_a_features = F.normalize(extracted_stem_a_features, dim=-1)
return (
mixture_a_features, # global
mixture_b_features, # global
stem_a_features, # local
extracted_stem_a_features, # local
mixture_a_features_mlp,
mixture_b_features_mlp,
self.logit_scale_m.exp(),
self.logit_scale_t.exp(),
)
def get_logit_scale(self):
return self.logit_scale_m.exp(), self.logit_scale_t.exp()
# def load_model(model_path, device):
# model = FxEncoderPlusPlus(
# embed_dim = 2048,
# audio_clap_module = True,
# extractor_module = True
# )
# # load model
# checkpoint = torch.load(model_path, map_location=device, weights_only=False)
# if "epoch" in checkpoint:
# # resuming a train checkpoint w/ epoch and optimizer state
# start_epoch = checkpoint["epoch"]
# sd = checkpoint["state_dict"]
# if next(iter(sd.items()))[0].startswith(
# "module"
# ):
# sd = {k[len("module."):]: v for k, v in sd.items()}
# model.load_state_dict(sd)
# logging.info(
# f"=> resuming checkpoint '{model_path}' (epoch {start_epoch})"
# )
# else:
# # loading a bare (model only) checkpoint for fine-tune or evaluation
# model.load_state_dict(checkpoint)
# start_epoch = 0
# model.to(device)
# model.eval()
# for param in model.parameters():
# param.requires_grad = False
# return model
# Define available models
MODEL_REGISTRY = {
"default": {
"repo_id": "yytung/fxencoder-plusplus",
"filename": "fxenc_plusplus_default.pt",
"description": "Default model",
},
# "musdb": {
# "repo_id": "yytung/fxencoder-plusplus",
# "filename": "fxenc_plusplus_musdb.pt",
# "description": "Fx-Encoder++ trained on musdb",
# },
# "medleydb": {
# "repo_id": "yytung/fxencoder-plusplus",
# "filename": "fxenc_plusplus_medleydb.pt",
# "description": "Fx-Encoder++ trained on medleydb",
# },
}
def get_model_path(model_name="default", cache_dir=None, force_download=False):
"""
Download or retrieve the path to a pretrained model.
Args:
model_name: Name of the model variant ('default', 'musdb', 'medleydb')
cache_dir: Custom cache directory. If None, uses ~/.cache/fxencoder_plusplus
force_download: Force re-download even if file exists
Returns:
Path to the model file
"""
if model_name not in MODEL_REGISTRY:
available = ", ".join(MODEL_REGISTRY.keys())
raise ValueError(f"Unknown model: {model_name}. Available models: {available}")
if cache_dir is None:
cache_dir = Path.home() / ".cache" / "fxencoder_plusplus"
else:
cache_dir = Path(cache_dir)
cache_dir.mkdir(parents=True, exist_ok=True)
model_info = MODEL_REGISTRY[model_name]
model_path = cache_dir / model_info["filename"]
# Check if already downloaded
if model_path.exists() and not force_download:
print(f"Using cached model: {model_path}")
return str(model_path)
print(f"Description: {model_info['description']}")
# Download from Hugging Face
downloaded_path = hf_hub_download(
repo_id=model_info["repo_id"],
filename=model_info["filename"],
cache_dir=str(cache_dir),
force_download=force_download
)
print(f"Model downloaded successfully to: {downloaded_path}")
return downloaded_path
def list_available_models():
"""List all available pretrained models."""
print("Available FxEncoder++ models:")
print("-" * 50)
for name, info in MODEL_REGISTRY.items():
print(f" {name}:")
print(f" - Description: {info['description']}")
print("-" * 50)
def load_model(model_name="default", model_path=None, device="cuda", auto_download=True, cache_dir=None):
"""
Load FxEncoderPlusPlus model.
Args:
model_name: Name of pretrained model ('default', 'musdb', 'medleydb')
model_path: Custom checkpoint path. If provided, ignores model_name
device: Device to load model on ('cuda' or 'cpu')
auto_download: Automatically download if model not found
cache_dir: Custom cache directory for downloaded models
Returns:
Loaded FxEncoderPlusPlus model
Examples:
# Load default base model
model = load_model()
# Load musdb model
model = load_model(model_name="musdb")
# Load medleydb model
model = load_model(model_name="medleydb")
# Load custom checkpoint
model = load_model(model_path="/path/to/custom.pt")
# List available models
list_available_models()
"""
# Handle device
if device == "cuda" and not torch.cuda.is_available():
print("CUDA not available, using CPU")
device = "cpu"
# Determine model path
if model_path is None:
if auto_download:
model_path = get_model_path(model_name, cache_dir=cache_dir)
else:
raise ValueError("model_path is None and auto_download is False")
# Create model instance with specified device
model = FxEncoderPlusPlus(
embed_dim=2048,
audio_clap_module=True,
text_clap_module=True,
extractor_module=True,
device=device
)
# Load checkpoint
checkpoint = torch.load(model_path, map_location=device, weights_only=False)
if "epoch" in checkpoint:
# resuming a train checkpoint w/ epoch and optimizer state
start_epoch = checkpoint["epoch"]
sd = checkpoint["state_dict"]
if next(iter(sd.items()))[0].startswith("module"):
sd = {k[len("module."):]: v for k, v in sd.items()}
model.load_state_dict(sd)
print(f"Loaded checkpoint from epoch {start_epoch}")
else:
# loading a bare (model only) checkpoint for fine-tune or evaluation
model.load_state_dict(checkpoint)
print("Loaded model checkpoint")
model.to(device)
model.eval()
# Freeze parameters for inference
for param in model.parameters():
param.requires_grad = False
print(f"Model loaded successfully on {device}")
return model
# Convenience functions for specific models
def load_default_model(device="cuda", **kwargs):
"""Load the default FxEncoder++ model."""
return load_model(model_name="default", device=device, **kwargs)
# def load_musdb_model(device="cuda", **kwargs):
# """Load the musdb FxEncoder++ model."""
# return load_model(model_name="musdb", device=device, **kwargs)
# def load_medleydb_model(device="cuda", **kwargs):
# """Load the medleydb FxEncoder++ model."""
# return load_model(model_name="medleydb", device=device, **kwargs)