| import torch |
| |
| 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 |
|
|
| |
| self.encoder = laion_clap.CLAP_Module(enable_fusion=False, amodel= 'HTSAT-tiny', tmodel='roberta') |
| if self.pretrained: |
| self.encoder.load_ckpt() |
|
|
| 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 |
|
|
| |
| self.encoder = laion_clap.CLAP_Module(enable_fusion=False, amodel= 'HTSAT-tiny', tmodel='roberta') |
| if self.pretrained: |
| self.encoder.load_ckpt() |
|
|
| 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 |
| |
| |
| 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" |
| |
| 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, |
| ) |
|
|
| |
| 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() |
|
|
| |
| x = x.view(batch_size * chs, seq_len) |
| |
| |
| x = self.spectrogram_extractor(x) |
| x = self.logmel_extractor(x) |
|
|
| if self.input_norm == "batchnorm": |
| |
| 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) |
| x = (x + 80) / 120 |
| x = (x * 2) - 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, |
| window_size = 2048, |
| hop_size = 512, |
| mel_bins = 64, |
| fmin = 50, |
| fmax = 18000, |
| ) |
| 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__() |
| |
| |
| 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): |
| |
| x = torch.cat([mixture_emb, query_emb], dim=-1) |
| stem_emb = self.fusion(x) |
| |
| return stem_emb |
|
|
| @dataclass |
| class AudioCfg: |
| model_type: str = "PANN" |
| model_name: str = "Cnn14" |
| sample_rate: int = 44100 |
| |
| 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 |
|
|
| |
| 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: |
| |
| |
| 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: |
| |
| |
| 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: |
| |
| 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): |
| |
| 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) |
| |
| |
| 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): |
| |
| 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) |
|
|
| |
| 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: |
| |
| return self.logit_scale_m.exp(), self.logit_scale_t.exp() |
| |
| |
| 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) |
| |
| |
| 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, |
| mixture_b_features, |
| stem_a_features, |
| extracted_stem_a_features, |
| 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() |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| MODEL_REGISTRY = { |
| "default": { |
| "repo_id": "yytung/fxencoder-plusplus", |
| "filename": "fxenc_plusplus_default.pt", |
| "description": "Default model", |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| } |
|
|
| 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"] |
| |
| |
| 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']}") |
| |
| |
| 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() |
| """ |
| |
| if device == "cuda" and not torch.cuda.is_available(): |
| print("CUDA not available, using CPU") |
| device = "cpu" |
| |
| |
| |
| 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") |
| |
| |
| model = FxEncoderPlusPlus( |
| embed_dim=2048, |
| audio_clap_module=True, |
| text_clap_module=True, |
| extractor_module=True, |
| device=device |
| ) |
| |
| |
| checkpoint = torch.load(model_path, map_location=device, weights_only=False) |
| |
| if "epoch" in checkpoint: |
| |
| 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: |
| |
| model.load_state_dict(checkpoint) |
| print("Loaded model checkpoint") |
| |
| model.to(device) |
| model.eval() |
| |
| |
| for param in model.parameters(): |
| param.requires_grad = False |
| |
| print(f"Model loaded successfully on {device}") |
| return model |
|
|
| |
| def load_default_model(device="cuda", **kwargs): |
| """Load the default FxEncoder++ model.""" |
| return load_model(model_name="default", device=device, **kwargs) |
|
|
| |
| |
| |
|
|
| |
| |
| |
|
|
|
|