Hugo Flores Garcia commited on
Commit
3a788dd
·
1 Parent(s): b88d0ca

add bytecover

Browse files
app.py CHANGED
@@ -1,4 +1,4 @@
1
- import spaces
2
  from pathlib import Path
3
  import yaml
4
  import time
@@ -15,6 +15,191 @@ import gradio as gr
15
  from vampnet.interface import Interface, signal_concat
16
  from vampnet import mask as pmask
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  device = "cuda" if torch.cuda.is_available() else "cpu"
19
 
20
  interface = Interface.default()
@@ -57,7 +242,6 @@ def shift_pitch(signal, interval: int):
57
  return signal
58
 
59
 
60
- @spaces.GPU
61
  def _vamp(
62
  seed, input_audio, model_choice,
63
  pitch_shift_amt, periodic_p,
@@ -168,7 +352,7 @@ def api_vamp(data):
168
 
169
  OUT_DIR = Path("gradio-outputs")
170
  OUT_DIR.mkdir(exist_ok=True)
171
- def harp_vamp(input_audio_file, periodic_p, n_mask_codebooks):
172
  sig = at.AudioSignal(input_audio_file)
173
  sr, samples = sig.sample_rate, sig.samples[0][0].detach().cpu().numpy()
174
  # convert to int32
@@ -192,7 +376,11 @@ def harp_vamp(input_audio_file, periodic_p, n_mask_codebooks):
192
  stretch_factor=1,
193
  )
194
 
195
- sig = at.AudioSignal(samples, sr)
 
 
 
 
196
  # write to file
197
  # clear the outdir
198
  for p in OUT_DIR.glob("*"):
@@ -200,10 +388,7 @@ def harp_vamp(input_audio_file, periodic_p, n_mask_codebooks):
200
  OUT_DIR.mkdir(exist_ok=True)
201
  outpath = OUT_DIR / f"{uuid.uuid4()}.wav"
202
  sig.write(outpath)
203
- from pyharp import AudioLabel, LabelList
204
- output_labels = LabelList()
205
- output_labels.append(AudioLabel(label='~', t=0.0, amplitude=0.5, description='generated audio'))
206
- return outpath, output_labels
207
 
208
 
209
  with gr.Blocks() as demo:
@@ -425,19 +610,47 @@ with gr.Blocks() as demo:
425
 
426
  from pyharp import ModelCard, build_endpoint
427
  card = ModelCard(
428
- name="vampnet",
429
  description="vampnet! is a model for generating audio from audio",
430
  author="hugo flores garcía",
431
  tags=["music generation"],
432
  midi_in=False,
433
  midi_out=False
434
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
 
436
  # Build a HARP-compatible endpoint
437
  app = build_endpoint(model_card=card,
438
  components=[
439
  periodic_p,
440
  n_mask_codebooks,
 
441
  ],
442
  process_fn=harp_vamp)
443
 
 
1
+ # import spaces
2
  from pathlib import Path
3
  import yaml
4
  import time
 
15
  from vampnet.interface import Interface, signal_concat
16
  from vampnet import mask as pmask
17
 
18
+ from pyharp import *
19
+ from bytecover.models.train_module import TrainModule
20
+ from bytecover.utils import initialize_logging, load_config
21
+ import pinecone
22
+ import laion_clap
23
+ from tqdm import tqdm
24
+
25
+ import os
26
+
27
+ ### INIT BYTECOVER
28
+ print(f"Is CUDA available: {torch.cuda.is_available()}")
29
+ # True
30
+ print(f"CUDA device: {torch.cuda.get_device_name(torch.cuda.current_device())}")
31
+
32
+ index_clap = pinecone.Index(os.environ["PC_API_KEY"], host=os.environ["CLAP_INDEX"]) #host='https://clap-nathan-500-index-af8053a.svc.us-west1-gcp.pinecone.io')
33
+ index_bytecover = pinecone.Index(os.environ["PC_API_KEY"], host=os.environ["BC_INDEX"]) #host='https://bytecover-nathan-500-index-af8053a.svc.us-west1-gcp.pinecone.io')
34
+
35
+ print("Loading ByteCover model")
36
+
37
+ if torch.cuda.is_available():
38
+ bytecover_config = load_config(config_path="bytecover/config_gpu.yaml")
39
+ else:
40
+ bytecover_config = load_config(config_path="bytecover/config.yaml")
41
+ bytecover_module = TrainModule(bytecover_config)
42
+ bytecover_model = bytecover_module.model
43
+ if bytecover_module.best_model_path is not None:
44
+ bytecover_model.load_state_dict(torch.load(bytecover_module.best_model_path), strict=False)
45
+ print(f"Best model loaded from checkpoint: {bytecover_module.best_model_path}")
46
+ elif bytecover_module.config["test"]["model_ckpt"] is not None:
47
+ bytecover_model.load_state_dict(torch.load(bytecover_module.config["test"]["model_ckpt"], map_location='cpu'), strict=False)
48
+ print(f'Model loaded from checkpoint: {bytecover_module.config["test"]["model_ckpt"]}')
49
+ elif bytecover_module.state == "initializing":
50
+ print("Warning: Running with random weights")
51
+
52
+ bytecover_model.eval()
53
+
54
+ print("Loading CLAP model")
55
+
56
+ if torch.cuda.is_available():
57
+ clap_model = laion_clap.CLAP_Module(enable_fusion=False, device="cuda:0")
58
+ else:
59
+ clap_model = laion_clap.CLAP_Module(enable_fusion=False)
60
+ clap_model.load_ckpt() # download the default pretrained checkpoint.
61
+
62
+ print("Models loaded!")
63
+ def convert_to_npfloat64(original_array):
64
+ #return np.array(flat_df["flat_vector_embed"][0],dtype=np.float64)
65
+ return np.array(original_array,dtype=np.float64)
66
+
67
+ def convert_to_npfloat64_to_list(vector_embed_64):
68
+ # list(flat_df["flat_vector_embed_64"][0])
69
+ return list(vector_embed_64)
70
+
71
+ def flatten_vector_embed(vector_embed):
72
+ return list(vector_embed.flatten())
73
+
74
+ def format_time(num_seconds):
75
+ return f"{num_seconds // 60}:{num_seconds % 60:02d}"
76
+
77
+ def bytecover(sig, chunk_size=3.0, bytecover_match_ct=3, clap_match_ct=3):
78
+ """
79
+ This function defines the audio processing steps
80
+ Args:
81
+ input_audio_path (str): the audio filepath to be processed.
82
+ <YOUR_KWARGS>: additional keyword arguments necessary for processing.
83
+ NOTE: These should correspond to and match order of UI elements defined below.
84
+ Returns:
85
+ output_audio_path (str): the filepath of the processed audio.
86
+ output_labels (LabelList): any labels to display.
87
+ """
88
+
89
+ """
90
+ <YOUR AUDIO LOADING CODE HERE>
91
+ """
92
+
93
+
94
+ """
95
+ <YOUR AUDIO PROCESSING CODE HERE>
96
+ """
97
+ sig_mono = sig.copy().to_mono().audio_data.squeeze(1)
98
+
99
+ # Chunk audio to desired length
100
+
101
+ chunk_samples = int(chunk_size * sig.sample_rate)
102
+ print(f"Chunk samples: {chunk_samples}")
103
+ print(f"Shape of audio: {sig_mono.shape}")
104
+ chunks = torch.tensor_split(sig_mono, [i for i in range(chunk_samples, sig_mono.shape[1], chunk_samples)], dim=1)
105
+ if chunks[-1].shape[1] < chunk_samples:
106
+ print("Cutting last chunk due to length")
107
+ chunks = tuple(list(chunks)[:-1])
108
+ print(f"Number of chunks: {len(chunks)}")
109
+
110
+ print("Getting Bytecover embeddings")
111
+ bytecover_embeddings = []
112
+ for chunk in tqdm(chunks):
113
+ result = bytecover_model.forward(chunk.to(bytecover_module.config["device"]))['f_t'].detach()
114
+ bytecover_embeddings.append(result)
115
+
116
+ clean_bytecover_embeddings = [convert_to_npfloat64_to_list(convert_to_npfloat64(flatten_vector_embed(embedding.cpu()))) for embedding in bytecover_embeddings]
117
+
118
+ print("Getting CLAP embeddings")
119
+ clap_embeddings = []
120
+ for chunk in tqdm(chunks):
121
+ result = clap_model.get_audio_embedding_from_data(chunk.numpy())
122
+ clap_embeddings.append(result)
123
+
124
+ clean_clap_embeddings = [convert_to_npfloat64_to_list(convert_to_npfloat64(flatten_vector_embed(embedding))) for embedding in clap_embeddings]
125
+
126
+ clap_matches = []
127
+ bytecover_matches = []
128
+ match_metadatas = {}
129
+
130
+ output_labels = LabelList()
131
+
132
+ times = {}
133
+
134
+ for clean_embeddings, pinecone_index, match_list, embedding_num, num_matches in zip([clean_bytecover_embeddings, clean_clap_embeddings], [index_bytecover, index_clap], [bytecover_matches, clap_matches], range(2), [bytecover_match_ct, clap_match_ct]):
135
+
136
+ for i, embedding in enumerate(clean_embeddings):
137
+
138
+ print(f"Getting match {i + 1} of {len(clean_embeddings)}")
139
+ matches = pinecone_index.query(
140
+ vector=embedding,
141
+ top_k=10,
142
+ #include_values=False,
143
+ include_metadata=True
144
+ )['matches']
145
+
146
+ # Store matches as [score, time, id]
147
+
148
+ for match in matches:
149
+ id = match['id']
150
+ if id not in match_metadatas:
151
+ match_metadatas[id] = match['metadata']
152
+ match_list.append([match['score'], i * chunk_size, id])
153
+
154
+
155
+ print("Matches obtained!")
156
+
157
+ top_matches = sorted(match_list, key=lambda item: item[0], reverse=True)
158
+
159
+ for i, match in enumerate(top_matches[:int(num_matches)]):
160
+ metadata = match_metadatas[match[2]]
161
+ song_artists = metadata['artists']
162
+ if type(song_artists) is list:
163
+ artists = ' and '.join(artists)
164
+
165
+
166
+ song_title = metadata['song']
167
+
168
+ song_link = f"https://open.spotify.com/track/{metadata['spotify_id'].split(':')[2]}"
169
+
170
+ embed_name = ['ByteCover', 'CLAP'][embedding_num]
171
+
172
+ match_time = match[1]
173
+ times[match_time] = times.get(match_time, 0) + 1
174
+
175
+ label = AudioLabel(
176
+ t=match_time,
177
+ label=f'{song_title}',
178
+ duration=chunk_size,
179
+ link=song_link,
180
+ description=f'Embedding: {embed_name}\n{song_title} by {song_artists}\nClick the tag to view on Spotify!',
181
+ amplitude=1.0 - 0.5 * (times[match_time] - 1),
182
+ color=AudioLabel.rgb_color_to_int(200, 170, 3, 10) if embedding_num == 1 else 0
183
+ )
184
+
185
+ # if embedding_num == 1:
186
+ # label.rgb_color_to_int(200, 170, 3, 240)
187
+ # else:
188
+ # pass
189
+ # #label.set_color(204, 52, 235, 240)
190
+
191
+ output_labels.append(label)
192
+
193
+ """
194
+ <YOUR AUDIO SAVING CODE HERE>
195
+ # Save processed audio and obtain default path
196
+ output_audio_path = save_audio(signal, None)
197
+ """
198
+
199
+ return output_labels
200
+
201
+ ### END BYTECOVER
202
+
203
  device = "cuda" if torch.cuda.is_available() else "cpu"
204
 
205
  interface = Interface.default()
 
242
  return signal
243
 
244
 
 
245
  def _vamp(
246
  seed, input_audio, model_choice,
247
  pitch_shift_amt, periodic_p,
 
352
 
353
  OUT_DIR = Path("gradio-outputs")
354
  OUT_DIR.mkdir(exist_ok=True)
355
+ def harp_vamp(input_audio_file, periodic_p, n_mask_codebooks, chunk_size=3.0, bytecover_match_ct=3, clap_match_ct=3):
356
  sig = at.AudioSignal(input_audio_file)
357
  sr, samples = sig.sample_rate, sig.samples[0][0].detach().cpu().numpy()
358
  # convert to int32
 
376
  stretch_factor=1,
377
  )
378
 
379
+ sig = at.AudioSignal(samples, sr).cpu()
380
+
381
+ # run bytecover
382
+ labels = bytecover(sig, chunk_size, bytecover_match_ct, clap_match_ct)
383
+
384
  # write to file
385
  # clear the outdir
386
  for p in OUT_DIR.glob("*"):
 
388
  OUT_DIR.mkdir(exist_ok=True)
389
  outpath = OUT_DIR / f"{uuid.uuid4()}.wav"
390
  sig.write(outpath)
391
+ return outpath, labels
 
 
 
392
 
393
 
394
  with gr.Blocks() as demo:
 
610
 
611
  from pyharp import ModelCard, build_endpoint
612
  card = ModelCard(
613
+ name="vampnet + aitribution",
614
  description="vampnet! is a model for generating audio from audio",
615
  author="hugo flores garcía",
616
  tags=["music generation"],
617
  midi_in=False,
618
  midi_out=False
619
  )
620
+
621
+ # BYTECOVER
622
+ # Define Gradio Components
623
+ components = [
624
+ # <YOUR UI ELEMENTS HERE>
625
+ gr.Slider(
626
+ minimum=1.0,
627
+ maximum=10.0,
628
+ step=0.5,
629
+ value=3.0,
630
+ label="Sample size (s)"
631
+ ),
632
+ gr.Slider(
633
+ minimum=0,
634
+ maximum=5,
635
+ step=1,
636
+ value=3,
637
+ label="Bytecover matches to generate"
638
+ ),
639
+ gr.Slider(
640
+ minimum=0,
641
+ maximum=5,
642
+ step=1,
643
+ value=3,
644
+ label="CLAP matches to generate"
645
+ )
646
+ ]
647
 
648
  # Build a HARP-compatible endpoint
649
  app = build_endpoint(model_card=card,
650
  components=[
651
  periodic_p,
652
  n_mask_codebooks,
653
+ *components
654
  ],
655
  process_fn=harp_vamp)
656
 
bytecover/__init__.py ADDED
File without changes
bytecover/__main__.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import wandb
2
+
3
+ from bytecover.models.train_module import TrainModule
4
+ from bytecover.utils import initialize_logging, load_config
5
+
6
+ config = load_config(config_path="config/config.yaml")
7
+ initialize_logging(config_path="config/logging_config.yaml", debug=False)
8
+ if config["wandb"]:
9
+ wandb.init(
10
+ # set the wandb project where this run will be logged
11
+ project="ByteCover",
12
+ # track hyperparameters and run metadata
13
+ config=config["train"],
14
+ )
15
+ trainer = TrainModule(config)
16
+ trainer.pipeline()
17
+ trainer.test()
bytecover/config.yaml ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ data_path: bytecover_data
2
+ file_extension: mp3
3
+ dataset_path: /bytecover_data/shs100k
4
+ save_model_path: models
5
+ debug: True
6
+ progress_bar: True
7
+ device: cpu
8
+ num_channels: 1
9
+ wandb: False
10
+
11
+ train:
12
+ mixed_precision: True
13
+ target_sr: 22050
14
+ compress_ratio: 20
15
+ max_seq_len: [100, 150, 200]
16
+ num_classes: 10000
17
+ triplet_margin: 0.3
18
+ smooth_factor: 0.1
19
+ model_ckpt: null
20
+ batch_size: 8
21
+ num_workers: 0
22
+ shuffle: True
23
+ drop_last: True
24
+ epochs: 1
25
+ learning_rate: 0.0001
26
+ patience: 4
27
+ tempo_factors: [0.7, 1.3]
28
+ log_steps: 20
29
+
30
+ val:
31
+ target_sr: 22050
32
+ compress_ratio: 20
33
+ save_val_outputs: True
34
+ max_seq_len: -1
35
+ batch_size: 1
36
+ num_workers: 8
37
+ shuffle: False
38
+ drop_last: False
39
+ output_dir: outputs_val
40
+
41
+ test:
42
+ target_sr: 22050
43
+ compress_ratio: 20
44
+ save_test_outputs: True
45
+ model_ckpt: orfium-bytecover.pt
46
+ max_seq_len: -1
47
+ batch_size: 1
48
+ num_workers: 8
49
+ shuffle: False
50
+ drop_last: False
51
+ output_dir: outputs_test
bytecover/config_gpu.yaml ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ data_path: bytecover_data
2
+ file_extension: mp3
3
+ dataset_path: /bytecover_data/shs100k
4
+ save_model_path: models
5
+ debug: True
6
+ progress_bar: True
7
+ device: cuda:0
8
+ num_channels: 1
9
+ wandb: False
10
+
11
+ train:
12
+ mixed_precision: True
13
+ target_sr: 22050
14
+ compress_ratio: 20
15
+ max_seq_len: [100, 150, 200]
16
+ num_classes: 10000
17
+ triplet_margin: 0.3
18
+ smooth_factor: 0.1
19
+ model_ckpt: null
20
+ batch_size: 8
21
+ num_workers: 0
22
+ shuffle: True
23
+ drop_last: True
24
+ epochs: 1
25
+ learning_rate: 0.0001
26
+ patience: 4
27
+ tempo_factors: [0.7, 1.3]
28
+ log_steps: 20
29
+
30
+ val:
31
+ target_sr: 22050
32
+ compress_ratio: 20
33
+ save_val_outputs: True
34
+ max_seq_len: -1
35
+ batch_size: 1
36
+ num_workers: 8
37
+ shuffle: False
38
+ drop_last: False
39
+ output_dir: outputs_val
40
+
41
+ test:
42
+ target_sr: 22050
43
+ compress_ratio: 20
44
+ save_test_outputs: True
45
+ model_ckpt: orfium-bytecover.pt
46
+ max_seq_len: -1
47
+ batch_size: 1
48
+ num_workers: 8
49
+ shuffle: False
50
+ drop_last: False
51
+ output_dir: outputs_test
bytecover/models/__init__.py ADDED
File without changes
bytecover/models/data_loader.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Dict, Literal, Tuple
3
+
4
+ import ffmpeg
5
+ import numpy as np
6
+ import pandas as pd
7
+ import torch
8
+ import torch.nn.functional as F
9
+ from torch.utils.data import DataLoader, Dataset
10
+ from torchvision import transforms
11
+
12
+ from bytecover.models.data_model import BatchDict
13
+ from bytecover.utils import bcolors
14
+
15
+
16
+ class ByteCoverDataset(Dataset):
17
+ def __init__(
18
+ self,
19
+ data_path: str,
20
+ file_ext: str,
21
+ dataset_path: str,
22
+ data_split: Literal["TRAIN", "VAL", "TEST"],
23
+ debug: bool,
24
+ target_sr: int,
25
+ max_len: int,
26
+ ) -> None:
27
+ super().__init__()
28
+ self.data_path = data_path
29
+ self.file_ext = file_ext
30
+ self.dataset_path = dataset_path
31
+ self.data_split = data_split
32
+ self.debug = debug
33
+ self.target_sr = target_sr
34
+ self.max_len = max_len
35
+ self._load_data()
36
+ self.pipeline = transforms.Compose([self._read_audio, self._pad_or_trim_audio])
37
+
38
+ def __len__(self) -> int:
39
+ return len(self.track_ids)
40
+
41
+ def __getitem__(self, index: int) -> BatchDict:
42
+ track_id = self.track_ids[index]
43
+ anchor_audio = self.pipeline(track_id)
44
+
45
+ clique_id, pos_id, neg_id = self._triplet_sampling(track_id)
46
+
47
+ if self.data_split == "TRAIN":
48
+ positive_audio = self.pipeline(pos_id)
49
+ negative_audio = self.pipeline(neg_id)
50
+ else:
51
+ positive_audio = torch.empty(0)
52
+ negative_audio = torch.empty(0)
53
+ return dict(
54
+ anchor_id=track_id,
55
+ anchor=anchor_audio,
56
+ anchor_label=torch.tensor(clique_id, dtype=torch.float),
57
+ positive_id=pos_id,
58
+ positive=positive_audio,
59
+ negative_id=neg_id,
60
+ negative=negative_audio,
61
+ )
62
+
63
+ def _triplet_sampling(self, track_id: str) -> Tuple[int, str, str]:
64
+ clique_id = self.labels.loc[track_id, "clique"]
65
+ versions = self.versions.loc[clique_id, "versions"]
66
+ np.random.shuffle(versions)
67
+ pos_list = np.setdiff1d(versions, track_id)
68
+ pos_id = np.random.choice(pos_list, 1)[0]
69
+
70
+ neg_id = self.labels[~self.labels.index.isin(versions)].sample(1).index[0]
71
+
72
+ return (clique_id, pos_id, neg_id)
73
+
74
+ def _load_data(self) -> None:
75
+ self.track_ids = np.load(
76
+ os.path.join(self.data_path, "splits", f"{self.data_split.lower()}_ids.npy"), allow_pickle=True
77
+ )
78
+
79
+ self.labels = pd.read_csv(os.path.join(self.data_path, "interim", "shs100k.csv"), usecols=["clique", "id"])
80
+ self.labels = self.labels[self.labels["id"].isin(self.track_ids)]
81
+ self.labels.dropna(inplace=True)
82
+ self.labels.set_index("id", inplace=True)
83
+ cliques = self.labels["clique"].unique()
84
+ mapping = {}
85
+ for k, clique in enumerate(cliques):
86
+ mapping[clique] = k
87
+ self.labels["clique"] = self.labels["clique"].map(lambda x: mapping[x])
88
+
89
+ self.versions = pd.read_csv(
90
+ os.path.join(self.data_path, "interim", "versions.csv"), converters={"versions": eval}
91
+ )
92
+ self.versions.dropna(inplace=True)
93
+ self.versions = self.versions[self.versions["clique"].isin(cliques)]
94
+ self.versions["clique"] = self.versions["clique"].map(lambda x: mapping[x])
95
+ self.versions.set_index("clique", inplace=True)
96
+
97
+ def _read_audio(self, track_id: str) -> torch.Tensor:
98
+ if self.debug:
99
+ seq_len = np.random.randint(10, 200) if self.max_len <= 0 else self.max_len
100
+ return torch.rand(seq_len * self.target_sr)
101
+ filename = os.path.join(self.dataset_path, f"{track_id}.{self.file_ext}")
102
+
103
+ try:
104
+ # This launches a subprocess to decode audio while down-mixing and resampling as necessary.
105
+ # Requires the ffmpeg CLI and `ffmpeg-python` package to be installed.
106
+ out, _ = (
107
+ ffmpeg.input(filename, threads=0)
108
+ .output("-", format="s16le", acodec="pcm_s16le", ac=1, ar=self.target_sr)
109
+ .run(cmd=["ffmpeg", "-nostdin"], capture_stdout=True, capture_stderr=True)
110
+ )
111
+ except ffmpeg.Error as e:
112
+ raise RuntimeError(
113
+ f"{bcolors.WARNING}Failed to load audio:{bcolors.FAIL + filename + bcolors.ENDC}\n{e.stderr.decode()}"
114
+ ) from e
115
+
116
+ # int16 ranges between -2^15 and +2^15 (±32768). By convention, floating point audio data is
117
+ # normalized to the range of [-1.0, 1.0]
118
+ audio = np.frombuffer(out, np.int16).flatten().astype(np.float32) / 32768.0
119
+
120
+ return torch.from_numpy(audio)
121
+
122
+ def _pad_or_trim_audio(self, audio: torch.Tensor) -> torch.Tensor:
123
+ if self.max_len <= 0:
124
+ return audio
125
+
126
+ if (self.data_split == "TRAIN") and (audio.shape[-1] <= self.max_len * self.target_sr):
127
+ return F.pad(audio, (0, self.max_len * self.target_sr - audio.shape[-1]))
128
+
129
+ max_offset = audio.shape[-1] - self.max_len * self.target_sr
130
+ offset = np.random.randint(max_offset) if max_offset > 0 else 0
131
+
132
+ return audio[offset : (offset + self.max_len * self.target_sr)]
133
+
134
+
135
+ def bytecover_dataloader(
136
+ data_path: str,
137
+ file_ext: str,
138
+ dataset_path: str,
139
+ data_split: Literal["TRAIN", "VAL", "TEST"],
140
+ debug: bool,
141
+ max_len: int,
142
+ batch_size: int,
143
+ target_sr: int,
144
+ **config: Dict,
145
+ ) -> DataLoader:
146
+ return DataLoader(
147
+ ByteCoverDataset(data_path, file_ext, dataset_path, data_split, debug, target_sr=target_sr, max_len=max_len),
148
+ batch_size=batch_size if max_len > 0 else 1,
149
+ num_workers=config["num_workers"],
150
+ shuffle=config["shuffle"],
151
+ drop_last=config["drop_last"],
152
+ )
bytecover/models/data_model.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TypedDict
2
+
3
+ import torch
4
+
5
+
6
+ class ValDict(TypedDict):
7
+ anchor_id: str
8
+ positive_id: str
9
+ negative_id: str
10
+ f_t: torch.Tensor
11
+ f_c: torch.Tensor
12
+
13
+
14
+ class BatchDict(TypedDict):
15
+ anchor_id: str
16
+ anchor: torch.Tensor
17
+ anchor_label: torch.Tensor
18
+ positive_id: str
19
+ positive: torch.Tensor
20
+ negative_id: str
21
+ negative: torch.Tensor
22
+
23
+
24
+ class Postfix(TypedDict):
25
+ Epoch: int
26
+ train_loss: float
27
+ train_loss_step: float
28
+ train_cls_loss: float
29
+ train_cls_loss_step: float
30
+ train_triplet_loss: float
31
+ train_triplet_loss_step: float
32
+ val_loss: float
33
+ mr1: float
34
+ mAP: float
35
+
36
+
37
+ class TestResults(TypedDict):
38
+ test_mr1: float
39
+ test_mAP: float
bytecover/models/early_stopper.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+
4
+ class EarlyStopper:
5
+ def __init__(self, patience: int = 1, min_delta: int = 0):
6
+ self.patience = patience
7
+ self.min_delta = min_delta
8
+ self.counter = 0
9
+ self.min_validation_loss = np.inf
10
+
11
+ def __call__(self, validation_loss) -> bool:
12
+ if validation_loss < self.min_validation_loss:
13
+ self.min_validation_loss = validation_loss
14
+ self.counter = 0
15
+ elif validation_loss >= (self.min_validation_loss + self.min_delta):
16
+ self.counter += 1
17
+ if self.counter >= self.patience:
18
+ return True
19
+ return False
bytecover/models/modules.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Tuple
2
+
3
+ import nnAudio.features.cqt as nnAudio
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.nn.functional as F
7
+ import torchaudio.transforms as T
8
+
9
+
10
+ class GeM(nn.Module):
11
+ def __init__(self, p=3, eps=1e-6):
12
+ super(GeM, self).__init__()
13
+ self.p = nn.Parameter(torch.ones(1) * p)
14
+ self.eps = eps
15
+
16
+ def forward(self, x):
17
+ return self.gem(x, p=self.p, eps=self.eps)
18
+
19
+ def gem(self, x, p=3, eps=1e-6):
20
+ return F.avg_pool2d(x.clamp(min=eps).pow(p), (x.size(-2), x.size(-1))).pow(1.0 / p)
21
+
22
+ def __repr__(self):
23
+ return f"{self.__class__.__name__}(p={self.p.data.tolist()[0]:.4f}, eps={str(self.eps)})"
24
+
25
+
26
+ class IBN(nn.Module):
27
+ r"""Instance-Batch Normalization layer from
28
+ `"Two at Once: Enhancing Learning and Generalization Capacities via IBN-Net"
29
+ <https://arxiv.org/pdf/1807.09441.pdf>`
30
+ Args:
31
+ planes (int): Number of channels for the input tensor
32
+ ratio (float): Ratio of instance normalization in the IBN layer
33
+ """
34
+
35
+ def __init__(self, planes, ratio):
36
+ super(IBN, self).__init__()
37
+ self.half = int(planes * ratio)
38
+ self.IN = nn.InstanceNorm2d(self.half, affine=True)
39
+ self.BN = nn.BatchNorm2d(planes - self.half)
40
+
41
+ def forward(self, x):
42
+ split = torch.split(x, self.half, 1)
43
+ out1 = self.IN(split[0].contiguous())
44
+ out2 = self.BN(split[1].contiguous())
45
+ out = torch.cat((out1, out2), 1)
46
+ return out
47
+
48
+
49
+ class Bottleneck(nn.Module):
50
+
51
+ expansion: int = 4
52
+
53
+ def __init__(
54
+ self, in_channels: int, out_channels: int, last: bool = False, downsample=None, stride=1, bias: bool = True
55
+ ):
56
+ super(Bottleneck, self).__init__()
57
+
58
+ self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=bias)
59
+ if not last:
60
+ # Apply Instance normalization in first half channels (ratio=0.5)
61
+ self.ibn = IBN(out_channels, ratio=0.5)
62
+ else:
63
+ self.ibn = nn.BatchNorm2d(out_channels)
64
+
65
+ self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=bias)
66
+ self.batch_norm2 = nn.BatchNorm2d(out_channels)
67
+
68
+ self.conv3 = nn.Conv2d(
69
+ out_channels, out_channels * self.expansion, kernel_size=1, stride=1, padding=0, bias=bias
70
+ )
71
+ self.batch_norm3 = nn.BatchNorm2d(out_channels * self.expansion)
72
+
73
+ self.downsample = downsample
74
+ self.stride = stride
75
+ self.relu = nn.ReLU()
76
+
77
+ def forward(self, x: torch.Tensor):
78
+ residual = x.clone()
79
+
80
+ x = self.conv1(x)
81
+ x = self.ibn(x)
82
+ x = self.relu(x)
83
+
84
+ x = self.conv2(x)
85
+ x = self.batch_norm2(x)
86
+ x = self.relu(x)
87
+
88
+ x = self.conv3(x)
89
+ x = self.batch_norm3(x)
90
+ x = self.relu(x)
91
+
92
+ if self.downsample is not None:
93
+ residual = self.downsample(residual)
94
+
95
+ out = residual + x
96
+ out = self.relu(out)
97
+
98
+ return out
99
+
100
+
101
+ class Resnet50(nn.Module):
102
+ def __init__(
103
+ self,
104
+ ResBlock: Bottleneck,
105
+ emb_dim: int = 2048,
106
+ num_channels: int = 1,
107
+ num_classes: int = 8858,
108
+ sr: int = 22050,
109
+ hop_lenght: int = 512,
110
+ n_bins=84,
111
+ bins_per_octave=12,
112
+ window="hann",
113
+ compress_ratio: int = 20,
114
+ tempo_factors: Tuple[float, float] = None,
115
+ ) -> None:
116
+
117
+ super(Resnet50, self).__init__()
118
+ self.in_channels = 64
119
+
120
+ self.cqt = nnAudio.CQT2010v2(
121
+ sr=sr,
122
+ hop_length=hop_lenght,
123
+ n_bins=n_bins,
124
+ bins_per_octave=bins_per_octave,
125
+ window=window,
126
+ output_format="Complex",
127
+ verbose=False,
128
+ )
129
+ self.compress = nn.AvgPool2d((1, compress_ratio))
130
+ self.time_strech = T.TimeStretch(n_freq=n_bins)
131
+ self.tempo_factors = tempo_factors
132
+
133
+ self.conv1 = nn.Conv2d(
134
+ in_channels=num_channels, out_channels=64, kernel_size=7, stride=2, padding=3, bias=False
135
+ )
136
+ self.batch_norm1 = nn.BatchNorm2d(num_features=64)
137
+ self.relu = nn.ReLU()
138
+ self.max_pool1 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
139
+
140
+ self.layer1 = self._make_layer(ResBlock, blocks=3, planes=64, stride=1)
141
+ self.layer2 = self._make_layer(ResBlock, blocks=4, planes=128, stride=2)
142
+ self.layer3 = self._make_layer(ResBlock, blocks=6, planes=256, stride=2)
143
+ self.layer4 = self._make_layer(ResBlock, blocks=3, planes=512, stride=1, last=True)
144
+
145
+ self.gem_pool = GeM()
146
+
147
+ self.bn_fc = nn.BatchNorm1d(emb_dim)
148
+ self.fc = nn.Linear(emb_dim, num_classes, bias=False)
149
+ nn.init.kaiming_normal_(self.fc.weight)
150
+
151
+ def _make_layer(self, ResBlock: Bottleneck, blocks: int, planes: int, stride: int = 1, last: bool = False):
152
+ downsample = None
153
+ if stride != 1 or self.in_channels != planes * ResBlock.expansion:
154
+ downsample = nn.Sequential(
155
+ nn.Conv2d(self.in_channels, planes * ResBlock.expansion, kernel_size=1, stride=stride, bias=False),
156
+ nn.BatchNorm2d(planes * ResBlock.expansion),
157
+ )
158
+ layers = []
159
+ layers.append(
160
+ ResBlock(in_channels=self.in_channels, out_channels=planes, stride=stride, downsample=downsample, last=last)
161
+ )
162
+ self.in_channels = planes * ResBlock.expansion
163
+ for _ in range(1, blocks):
164
+ layers.append(ResBlock(in_channels=self.in_channels, out_channels=planes, last=last))
165
+
166
+ return nn.Sequential(*layers)
167
+
168
+ def forward(self, x: torch.Tensor):
169
+
170
+ x = self.cqt(x)
171
+ # Time-strech requires complex tensors, that's why cqt function returns complex
172
+ x = torch.view_as_complex(x)
173
+ if self.tempo_factors is not None:
174
+ rate = abs(self.tempo_factors[1] - self.tempo_factors[0]) * torch.rand(1).item() + min(self.tempo_factors)
175
+ strech = (
176
+ abs(1 - rate) > 7e-2
177
+ ) # if the strech ratio is too close to 1 (i.e. 0.93 < ratio < 1.07), skip time streching
178
+ if self.training and strech:
179
+ x = self.time_strech(x, rate)
180
+ # Compress the magnitude of the CQT
181
+ x = self.compress(torch.abs(x))
182
+
183
+ # Unsqueeze to simulate 1-channel image
184
+ x = self.conv1(x.unsqueeze(1))
185
+ x = self.batch_norm1(x)
186
+ x = self.relu(x)
187
+ x = self.max_pool1(x)
188
+
189
+ x = self.layer1(x)
190
+ x = self.layer2(x)
191
+ x = self.layer3(x)
192
+ x = self.layer4(x)
193
+
194
+ f_t = self.gem_pool(x)
195
+ f_t = torch.flatten(f_t, start_dim=1)
196
+
197
+ f_c = self.bn_fc(f_t)
198
+ cls = self.fc(f_c)
199
+
200
+ return dict(f_t=f_t, f_c=f_c, cls=cls)
bytecover/models/train_module.py ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ from copy import deepcopy
4
+ from typing import Dict, List
5
+
6
+ import numpy as np
7
+ import torch
8
+ import torch.nn as nn
9
+ import wandb
10
+ from tqdm import tqdm, trange
11
+
12
+ from bytecover.models.data_model import BatchDict, Postfix, TestResults, ValDict
13
+ from bytecover.models.early_stopper import EarlyStopper
14
+ from bytecover.models.modules import Bottleneck, Resnet50
15
+ from bytecover.models.utils import (
16
+ calculate_ranking_metrics,
17
+ dataloader_factory,
18
+ dir_checker,
19
+ save_best_log,
20
+ save_logs,
21
+ save_predictions,
22
+ )
23
+
24
+ logger: logging.Logger = logging.getLogger() # The logger used to log output
25
+
26
+
27
+ class TrainModule:
28
+ def __init__(self, config: Dict) -> None:
29
+ self.config = config
30
+ self.state = "initializing"
31
+ self.best_model_path: str = None
32
+ self.num_classes = self.config["train"]["num_classes"]
33
+ self.max_len = self.config["train"]["max_seq_len"][0]
34
+
35
+ self.model = Resnet50(
36
+ Bottleneck,
37
+ num_channels=self.config["num_channels"],
38
+ num_classes=self.num_classes,
39
+ compress_ratio=self.config["train"]["compress_ratio"],
40
+ tempo_factors=self.config["train"]["tempo_factors"],
41
+ )
42
+ self.model.to(self.config["device"])
43
+ if self.config["wandb"]:
44
+ wandb.watch(self.model)
45
+ self.postfix: Postfix = {}
46
+
47
+ self.triplet_loss = nn.TripletMarginLoss(margin=config["train"]["triplet_margin"])
48
+ self.cls_loss = nn.CrossEntropyLoss(label_smoothing=config["train"]["smooth_factor"])
49
+
50
+ self.early_stop = EarlyStopper(patience=self.config["train"]["patience"])
51
+ self.optimizer = self.configure_optimizers()
52
+ if self.config["device"] != "cpu":
53
+ self.scaler = torch.cuda.amp.GradScaler(enabled=self.config["train"]["mixed_precision"])
54
+
55
+ def pipeline(self) -> None:
56
+ self.config["val"]["output_dir"] = dir_checker(self.config["val"]["output_dir"])
57
+
58
+ if self.config["train"]["model_ckpt"] is not None:
59
+ self.model.load_state_dict(torch.load(self.config["train"]["model_ckpt"]), strict=False)
60
+ logger.info(f'Model loaded from checkpoint: {self.config["train"]["model_ckpt"]}')
61
+
62
+ self.t_loaders = dataloader_factory(config=self.config, data_split="TRAIN")
63
+ self.v_loader = dataloader_factory(config=self.config, data_split="VAL")[0]
64
+
65
+ self.state = "running"
66
+
67
+ self.pbar = trange(
68
+ self.config["train"]["epochs"], disable=(not self.config["progress_bar"]), position=0, leave=True
69
+ )
70
+ for epoch in self.pbar:
71
+ if self.state in ["early_stopped", "interrupted", "finished"]:
72
+ return
73
+
74
+ self.postfix["Epoch"] = epoch
75
+ self.pbar.set_postfix(self.postfix)
76
+
77
+ try:
78
+ self.train_procedure()
79
+ except KeyboardInterrupt:
80
+ logger.warning("\nKeyboard Interrupt detected. Attempting gracefull shutdown...")
81
+ self.state = "interrupted"
82
+ except Exception as err:
83
+ raise (err)
84
+
85
+ if self.state == "interrupted":
86
+ self.validation_procedure()
87
+ self.pbar.set_postfix(
88
+ {k: self.postfix[k] for k in self.postfix.keys() & {"train_loss_step", "mr1", "mAP"}}
89
+ )
90
+
91
+ self.state = "finished"
92
+
93
+ def validate(self) -> None:
94
+ self.v_loader = dataloader_factory(config=self.config, data_split="VAL")[0]
95
+ self.state = "running"
96
+ self.validation_procedure()
97
+ self.state = "finished"
98
+
99
+ def test(self) -> None:
100
+ self.test_loader = dataloader_factory(config=self.config, data_split="TEST")[0]
101
+ self.test_results: TestResults = {}
102
+
103
+ if self.best_model_path is not None:
104
+ self.model.load_state_dict(torch.load(self.best_model_path), strict=False)
105
+ print(f"Best model loaded from checkpoint: {self.best_model_path}")
106
+ elif self.config["test"]["model_ckpt"] is not None:
107
+ self.model.load_state_dict(torch.load(self.config["test"]["model_ckpt"], map_location='cpu'), strict=False)
108
+ print(f'Model loaded from checkpoint: {self.config["test"]["model_ckpt"]}')
109
+ elif self.state == "initializing":
110
+ print("Warning: Testing with random weights")
111
+
112
+ self.state = "running"
113
+ self.test_procedure()
114
+ self.state = "finished"
115
+
116
+ def train_procedure(self) -> None:
117
+ self.model.train()
118
+ pbar_loaders = tqdm(self.t_loaders, disable=(not self.config["progress_bar"]), position=1, leave=False)
119
+ for _, t_loader in enumerate(pbar_loaders):
120
+ train_loss_list = []
121
+ train_cls_loss_list = []
122
+ train_triplet_loss_list = []
123
+ self.max_len = t_loader.dataset.max_len
124
+ pbar_loaders.set_postfix_str(f"max_seq_len={self.max_len}")
125
+ for step, batch in tqdm(
126
+ enumerate(t_loader),
127
+ total=len(t_loader),
128
+ disable=(not self.config["progress_bar"]),
129
+ position=2,
130
+ leave=False,
131
+ ):
132
+ train_step = self.training_step(batch)
133
+ self.postfix["train_loss_step"] = float(f"{train_step['train_loss_step']:.3f}")
134
+ train_loss_list.append(train_step["train_loss_step"])
135
+ self.postfix["train_cls_loss_step"] = float(f"{train_step['train_cls_loss']:.3f}")
136
+ train_cls_loss_list.append(train_step["train_cls_loss"])
137
+ self.postfix["train_triplet_loss_step"] = float(f"{train_step['train_triplet_loss']:.3f}")
138
+ train_triplet_loss_list.append(train_step["train_triplet_loss"])
139
+ self.pbar.set_postfix(
140
+ {k: self.postfix[k] for k in self.postfix.keys() & {"train_loss_step", "mr1", "mAP"}}
141
+ )
142
+ if self.config["wandb"]:
143
+ wandb.log(self.postfix)
144
+ if step % self.config["train"]["log_steps"] == 0:
145
+ save_logs(
146
+ dict(
147
+ epoch=self.postfix["Epoch"],
148
+ seq_len=self.max_len,
149
+ step=step,
150
+ train_loss_step=f"{train_step['train_loss_step']:.3f}",
151
+ train_cls_loss_step=f"{train_step['train_cls_loss']:.3f}",
152
+ train_triplet_loss_step=f"{train_step['train_triplet_loss']:.3f}",
153
+ ),
154
+ output_dir=self.config["val"]["output_dir"],
155
+ name="log_steps",
156
+ )
157
+ train_loss = torch.tensor(train_loss_list)
158
+ train_cls_loss = torch.tensor(train_cls_loss_list)
159
+ train_triplet_loss = torch.tensor(train_triplet_loss_list)
160
+ self.postfix["train_loss"] = train_loss.mean().item()
161
+ self.postfix["train_cls_loss"] = train_cls_loss.mean().item()
162
+ self.postfix["train_triplet_loss"] = train_triplet_loss.mean().item()
163
+ if self.config["wandb"]:
164
+ wandb.log(self.postfix)
165
+ self.validation_procedure()
166
+ if self.config["wandb"]:
167
+ wandb.log(self.postfix)
168
+ self.overfit_check()
169
+ self.pbar.set_postfix({k: self.postfix[k] for k in self.postfix.keys() & {"train_loss_step", "mr1", "mAP"}})
170
+
171
+ def training_step(self, batch: BatchDict) -> Dict[str, float]:
172
+ with torch.autocast(
173
+ device_type=self.config["device"].split(":")[0], enabled=self.config["train"]["mixed_precision"]
174
+ ):
175
+ anchor = self.model.forward(batch["anchor"].to(self.config["device"]))
176
+ positive = self.model.forward(batch["positive"].to(self.config["device"]))
177
+ negative = self.model.forward(batch["negative"].to(self.config["device"]))
178
+ l1 = self.triplet_loss(anchor["f_t"], positive["f_t"], negative["f_t"])
179
+ labels = nn.functional.one_hot(batch["anchor_label"].long(), num_classes=self.num_classes)
180
+ l2 = self.cls_loss(anchor["cls"], labels.float().to(self.config["device"]))
181
+ loss = l1 + l2
182
+
183
+ self.optimizer.zero_grad()
184
+ if self.config["device"] != "cpu":
185
+ self.scaler.scale(loss).backward()
186
+ self.scaler.step(self.optimizer)
187
+ self.scaler.update()
188
+ else:
189
+ loss.backward()
190
+ self.optimizer.step()
191
+
192
+ return {"train_loss_step": loss.item(), "train_triplet_loss": l1.item(), "train_cls_loss": l2.item()}
193
+
194
+ def validation_procedure(self) -> None:
195
+ self.model.eval()
196
+ embeddings: Dict[str, torch.Tensor] = {}
197
+ for batch in tqdm(self.v_loader, disable=(not self.config["progress_bar"]), position=1, leave=False):
198
+ val_dict = self.validation_step(batch)
199
+ if val_dict["f_t"].ndim == 1:
200
+ val_dict["f_c"] = val_dict["f_c"].unsqueeze(0)
201
+ val_dict["f_t"] = val_dict["f_t"].unsqueeze(0)
202
+ for anchor_id, triplet_embedding, embedding in zip(val_dict["anchor_id"], val_dict["f_t"], val_dict["f_c"]):
203
+ embeddings[anchor_id] = torch.stack([triplet_embedding, embedding])
204
+
205
+ val_outputs = self.validation_epoch_end(embeddings)
206
+ logger.info(
207
+ f"\n{' Validation Results ':=^50}\n"
208
+ + "\n".join([f'"{key}": {value}' for key, value in self.postfix.items()])
209
+ + f"\n{' End of Validation ':=^50}\n"
210
+ )
211
+
212
+ if self.config["val"]["save_val_outputs"]:
213
+ val_outputs["val_embeddings"] = torch.stack(list(embeddings.values()))[:, 1].numpy()
214
+ save_predictions(val_outputs, output_dir=self.config["val"]["output_dir"])
215
+ save_logs(self.postfix, output_dir=self.config["val"]["output_dir"])
216
+ self.model.train()
217
+
218
+ def validation_epoch_end(self, outputs: Dict[str, torch.Tensor]) -> Dict[str, np.ndarray]:
219
+ val_loss = torch.zeros(len(outputs))
220
+ pos_ids = []
221
+ neg_ids = []
222
+ clique_ids = []
223
+ for k, (anchor_id, embeddings) in enumerate(outputs.items()):
224
+ clique_id, pos_id, neg_id = self.v_loader.dataset._triplet_sampling(anchor_id)
225
+ val_loss[k] = self.triplet_loss(embeddings[0], outputs[pos_id][0], outputs[neg_id][0]).item()
226
+ pos_ids.append(pos_id)
227
+ neg_ids.append(neg_id)
228
+ clique_ids.append(clique_id)
229
+ anchor_ids = np.stack(list(outputs.keys()))
230
+ preds = torch.stack(list(outputs.values()))[:, 1]
231
+ self.postfix["val_loss"] = val_loss.mean().item()
232
+ ranks, average_precisions = calculate_ranking_metrics(embeddings=preds.numpy(), cliques=clique_ids)
233
+ self.postfix["mr1"] = ranks.mean()
234
+ self.postfix["mAP"] = average_precisions.mean()
235
+ return {
236
+ "triplet_ids": np.stack(list(zip(clique_ids, anchor_ids, pos_ids, neg_ids))),
237
+ "ranks": ranks,
238
+ "average_precisions": average_precisions,
239
+ }
240
+
241
+ def validation_step(self, batch: BatchDict) -> ValDict:
242
+ anchor_id = batch["anchor_id"]
243
+ positive_id = batch["positive_id"]
244
+ negative_id = batch["negative_id"]
245
+
246
+ features = self.model.forward(batch["anchor"].to(self.config["device"]))
247
+
248
+ return {
249
+ "anchor_id": anchor_id,
250
+ "positive_id": positive_id,
251
+ "negative_id": negative_id,
252
+ "f_t": features["f_t"].squeeze(0).detach().cpu(),
253
+ "f_c": features["f_c"].squeeze(0).detach().cpu(),
254
+ }
255
+
256
+ def test_procedure(self) -> None:
257
+ self.model.eval()
258
+ clique_ids = []
259
+ embeddings: Dict[str, torch.Tensor] = {}
260
+ for batch in tqdm(self.test_loader, disable=(not self.config["progress_bar"])):
261
+ clique_ids_batch = self.test_loader.dataset.labels.loc[batch["anchor_id"], "clique"]
262
+ test_dict = self.validation_step(batch)
263
+ if test_dict["f_c"].ndim == 1:
264
+ test_dict["f_c"] = test_dict["f_c"].unsqueeze(0)
265
+ for anchor_id, clique_id, embedding in zip(test_dict["anchor_id"], clique_ids_batch, test_dict["f_c"]):
266
+ embeddings[anchor_id] = embedding
267
+ clique_ids.append(clique_id)
268
+
269
+ test_outputs = self.test_epoch_end(embeddings, clique_ids)
270
+ logger.info(
271
+ f"\n{' Test Results ':=^50}\n"
272
+ + "\n".join([f'"{key}": {value}' for key, value in self.test_results.items()])
273
+ + f"\n{' End of Testing ':=^50}\n"
274
+ )
275
+
276
+ if self.config["test"]["save_test_outputs"]:
277
+ test_outputs["test_embeddings"] = torch.stack(list(embeddings.values())).numpy()
278
+ save_predictions(test_outputs, output_dir=self.config["test"]["output_dir"])
279
+ save_logs(self.test_results, output_dir=self.config["test"]["output_dir"])
280
+
281
+ def test_epoch_end(self, outputs: Dict[str, torch.Tensor], clique_ids: List[int]) -> Dict[str, np.ndarray]:
282
+ anchor_ids = np.stack(list(outputs.keys()))
283
+ preds = torch.stack(list(outputs.values()))
284
+ ranks, average_precisions = calculate_ranking_metrics(embeddings=preds.numpy(), cliques=clique_ids)
285
+ self.test_results["test_mr1"] = ranks.mean()
286
+ self.test_results["test_mAP"] = average_precisions.mean()
287
+ return {
288
+ "anchor_ids": np.stack(list(zip(clique_ids, anchor_ids))),
289
+ "ranks": ranks,
290
+ "average_precisions": average_precisions,
291
+ }
292
+
293
+ def overfit_check(self) -> None:
294
+ if self.early_stop(self.postfix["val_loss"]):
295
+ logger.info(f"\nValidation not improved for {self.early_stop.patience} consecutive epochs. Stopping...")
296
+ self.state = "early_stopped"
297
+
298
+ if self.early_stop.counter > 0:
299
+ logger.info("\nValidation loss was not improved")
300
+ else:
301
+ logger.info(f"\nMetric improved. New best score: {self.early_stop.min_validation_loss:.3f}")
302
+ save_best_log(self.postfix, output_dir=self.config["val"]["output_dir"])
303
+
304
+ logger.info("Saving model...")
305
+ epoch = self.postfix["Epoch"]
306
+ max_secs = self.max_len
307
+ prev_model = deepcopy(self.best_model_path)
308
+ self.best_model_path = os.path.join(
309
+ self.config["val"]["output_dir"], "model", f"best-model-{epoch=}-{max_secs=}.pt"
310
+ )
311
+ os.makedirs(os.path.dirname(self.best_model_path), exist_ok=True)
312
+ torch.save(deepcopy(self.model.state_dict()), self.best_model_path)
313
+ if prev_model is not None:
314
+ os.remove(prev_model)
315
+
316
+ def configure_optimizers(self) -> torch.optim.Optimizer:
317
+ optimizer = torch.optim.Adam(self.model.parameters(), lr=self.config["train"]["learning_rate"])
318
+
319
+ return optimizer
bytecover/models/utils.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import glob
2
+ import json
3
+ import os
4
+ import re
5
+ from typing import Dict, List, Tuple
6
+
7
+ import jsonlines
8
+ import numpy as np
9
+ import pandas as pd
10
+ from sklearn.metrics import pairwise_distances
11
+ from torch.utils.data import DataLoader
12
+
13
+ from bytecover.models.data_loader import bytecover_dataloader
14
+ from bytecover.models.data_model import Postfix
15
+
16
+
17
+ def dataloader_factory(config: Dict, data_split: str) -> List[DataLoader]:
18
+ seq_len_key = "max_seq_len"
19
+
20
+ if data_split == "TRAIN":
21
+ t_loaders = []
22
+ for L in config["train"][seq_len_key]:
23
+ t_loaders.append(
24
+ bytecover_dataloader(
25
+ data_path=config["data_path"],
26
+ file_ext=config["file_extension"],
27
+ dataset_path=config["dataset_path"],
28
+ data_split=data_split,
29
+ debug=config["debug"],
30
+ max_len=L,
31
+ **config["train"],
32
+ )
33
+ )
34
+ return t_loaders
35
+ L = config[data_split.lower()][seq_len_key]
36
+ return [
37
+ bytecover_dataloader(
38
+ data_path=config["data_path"],
39
+ file_ext=config["file_extension"],
40
+ dataset_path=config["dataset_path"],
41
+ data_split=data_split,
42
+ debug=config["debug"],
43
+ max_len=L,
44
+ **config[data_split.lower()],
45
+ )
46
+ ]
47
+
48
+
49
+ def validation_triplet_sampling(anchor_id: str, val_ids: List[str], df: pd.DataFrame) -> Dict[str, int]:
50
+ np.random.shuffle(df.loc[anchor_id, "versions"])
51
+ pos_list = np.setdiff1d(df.loc[anchor_id, "versions"], anchor_id)
52
+ pos_id = np.random.choice(pos_list, 1)[0]
53
+ pos_id = val_ids.index(pos_id)
54
+
55
+ neg_id = df.loc[~df.index.isin([anchor_id] + list(pos_list))].sample(1).index[0]
56
+ neg_id = val_ids.index(neg_id)
57
+
58
+ return dict(pos_id=pos_id, neg_id=neg_id)
59
+
60
+
61
+ def calculate_ranking_metrics(embeddings: np.ndarray, cliques: List[int]) -> Tuple[np.ndarray, np.ndarray]:
62
+ distances = pairwise_distances(embeddings)
63
+ s_distances = np.argsort(distances, axis=1)
64
+ cliques = np.array(cliques)
65
+ query_cliques = cliques[s_distances[:, 0]]
66
+ search_cliques = cliques[s_distances[:, 1:]]
67
+
68
+ query_cliques = np.tile(query_cliques, (search_cliques.shape[-1], 1)).T
69
+ mask = np.equal(search_cliques, query_cliques)
70
+
71
+ ranks = mask.argmax(axis=1)
72
+
73
+ cumsum = np.cumsum(mask, axis=1)
74
+ mask2 = mask * cumsum
75
+ mask2 = mask2 / np.arange(1, mask2.shape[-1] + 1)
76
+ average_precisions = np.sum(mask2, axis=1) / np.sum(mask, axis=1)
77
+
78
+ return (ranks, average_precisions)
79
+
80
+
81
+ def dir_checker(output_dir: str) -> str:
82
+ output_dir = re.sub(r"run-[0-9]+/*", "", output_dir)
83
+ runs = glob.glob(os.path.join(output_dir, "run-*"))
84
+ if runs != []:
85
+ max_run = max(map(lambda x: int(x.split("-")[-1]), runs))
86
+ run = max_run + 1
87
+ else:
88
+ run = 0
89
+ outdir = os.path.join(output_dir, f"run-{run}")
90
+ return outdir
91
+
92
+
93
+ def save_predictions(outputs: Dict[str, np.ndarray], output_dir: str) -> None:
94
+ os.makedirs(output_dir, exist_ok=True)
95
+ for key in outputs:
96
+ if "_ids" in key:
97
+ with jsonlines.open(os.path.join(output_dir, f"{key}.jsonl"), "w") as f:
98
+ if len(outputs[key][0]) == 4:
99
+ for clique, anchor, pos, neg in outputs[key]:
100
+ f.write({"clique_id": clique, "anchor_id": anchor, "positive_id": pos, "negative_id": neg})
101
+ else:
102
+ for clique, anchor in outputs[key]:
103
+ f.write({"clique_id": clique, "anchor_id": anchor})
104
+ else:
105
+ np.save(os.path.join(output_dir, f"{key}.npy"), outputs[key])
106
+
107
+
108
+ def save_logs(outputs: dict, output_dir: str, name: str = "log") -> None:
109
+ os.makedirs(output_dir, exist_ok=True)
110
+ log_file = os.path.join(output_dir, f"{name}.jsonl")
111
+ with jsonlines.open(log_file, "a") as f:
112
+ f.write(outputs)
113
+
114
+
115
+ def save_best_log(outputs: Postfix, output_dir: str) -> None:
116
+ os.makedirs(output_dir, exist_ok=True)
117
+ log_file = os.path.join(output_dir, "best-log.json")
118
+ with open(log_file, "w") as f:
119
+ json.dump(outputs, f, indent=2)
bytecover/utils.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from logging import config as logging_config
3
+ from typing import Dict
4
+
5
+ from yaml import FullLoader, load, safe_load
6
+
7
+
8
+ @dataclass
9
+ class bcolors:
10
+ OKGREEN: str = "\033[92m"
11
+ WARNING: str = "\033[93m"
12
+ FAIL: str = "\033[91m"
13
+ ENDC: str = "\033[0m"
14
+
15
+
16
+ def load_config(config_path: str) -> Dict:
17
+ with open(config_path) as file:
18
+ config = safe_load(file)
19
+
20
+ if config["device"] == "gpu":
21
+ config["device"] = "cuda:0"
22
+
23
+ return config
24
+
25
+
26
+ def initialize_logging(config_path: str, debug: bool) -> None:
27
+ """
28
+ Setup logging according to the configuration in the given file.
29
+ :param str config_path: The path to the file containing the logging configuration
30
+ :return:
31
+ """
32
+ with open(config_path) as yaml_fh:
33
+ config_description = load(yaml_fh, Loader=FullLoader)
34
+ if debug:
35
+ config_description["root"]["level"] = "DEBUG"
36
+ logging_config.dictConfig(config_description)
orfium-bytecover.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6fb5eb4c20ab9a7aea7fadf4893b5c1c778281caffb76af143b7a4798c3225eb
3
+ size 176417247
pinecone_generate.py ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generate ByteCover and CLAP Embeddings for a dataset and put to Pinecone
2
+
3
+ import argparse
4
+ import os
5
+ from typing import Iterator
6
+ import time
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+ import torch
11
+ import laion_clap
12
+ from tqdm import tqdm
13
+ from pinecone.grpc import PineconeGRPC as Pinecone
14
+ from pinecone import PodSpec, PineconeApiException
15
+ import ffmpeg
16
+
17
+ from bytecover.models.train_module import TrainModule
18
+ from bytecover.models.data_loader import ByteCoverDataset
19
+ from bytecover.utils import load_config
20
+
21
+ class BatchGenerator:
22
+ #
23
+ def __init__(self, batch_size: int = 10) -> None:
24
+ self.batch_size = batch_size
25
+ #
26
+ # Makes chunks out of an input DataFrame
27
+ def to_batches(self, df: pd.DataFrame) -> Iterator[pd.DataFrame]:
28
+ splits = self.splits_num(df.shape[0])
29
+ if splits <= 1:
30
+ yield df
31
+ else:
32
+ for chunk in np.array_split(df, splits):
33
+ yield chunk
34
+ #
35
+ # Determines how many chunks DataFrame contains
36
+ def splits_num(self, elements: int) -> int:
37
+ return round(elements / self.batch_size)
38
+ #
39
+ __call__ = to_batches
40
+
41
+ # quantization
42
+ def int16_to_float32(x):
43
+ return (x / 32767.0).astype(np.float32)
44
+
45
+ def float32_to_int16(x):
46
+ x = np.clip(x, a_min=-1., a_max=1.)
47
+ return (x * 32767.).astype(np.int16)
48
+
49
+ def flatten_vector_embed(vector_embed):
50
+ return list(vector_embed.flatten())
51
+
52
+ def grab_song_title(vector_name):
53
+ return vector_name.split("_")[0]
54
+
55
+ def convert_to_npfloat64(original_array):
56
+ #return np.array(flat_df["flat_vector_embed"][0],dtype=np.float64)
57
+ return np.array(original_array,dtype=np.float64)
58
+
59
+ def convert_to_npfloat64_to_list(vector_embed_64):
60
+ # list(flat_df["flat_vector_embed_64"][0])
61
+ return list(vector_embed_64)
62
+
63
+ def look_up_metadata(track_id, meta_dataframe, meta_col_interest):
64
+ # track_id: form = spotify:track:id_##,mp3
65
+ # meta_datframe: df of all the metavalues
66
+ # column options = album, artist_names, popularity, release_date, genre
67
+ df_id = track_id.split("_")[0]
68
+ meta_row = meta_dataframe[meta_dataframe['uri'] == df_id].reset_index(drop=True)
69
+ try:
70
+ return meta_row[meta_col_interest][0]
71
+ except:
72
+ return "unknown"
73
+ #return meta_row[meta_col_interest][0]
74
+
75
+ def strip_year_from_date(full_date):
76
+ if type(full_date) == int:
77
+ return str(full_date)
78
+ else:
79
+ try:
80
+ return full_date[:4]
81
+ except:
82
+ return "CHECK_THIS"
83
+
84
+ def strip_vector_clip(vector_name):
85
+ return vector_name.split(".")[0].split("_")[1]
86
+
87
+ def get_triplet_num(vector_name_str):
88
+ return str(int(vector_name_str.split("_")[2].split(".")[0]) + 1)
89
+
90
+ def generate(audio_dir, metadata_dir, index_naming_conv):
91
+
92
+ # FILE AND METADATA LOADING
93
+
94
+ file_list = [f for f in os.listdir(audio_dir)]
95
+ print(f"Found {len(file_list)} files")
96
+
97
+ meta_list = [f for f in os.listdir(metadata_dir)]
98
+ meta_list = sorted(meta_list)
99
+
100
+ meta_df = pd.read_json(metadata_dir + "/" + meta_list[0])
101
+
102
+ for i in range(1, len(meta_list)-1):
103
+ new_row = pd.read_json(metadata_dir + "/" + meta_list[i])
104
+ meta_df = pd.concat([meta_df, new_row]).reset_index(drop = True)
105
+
106
+ meta_df["year"] = meta_df.apply(lambda row: strip_year_from_date(row['release_date']),axis=1)
107
+
108
+ # BYTECOVER MODEL INITIALIZATION
109
+
110
+ print("Loading ByteCover model")
111
+
112
+ bytecover_config = load_config(config_path="bytecover/config.yaml")
113
+ bytecover_module = TrainModule(bytecover_config)
114
+ bytecover_model = bytecover_module.model
115
+ if bytecover_module.best_model_path is not None:
116
+ bytecover_model.load_state_dict(torch.load(bytecover_module.best_model_path), strict=False)
117
+ print(f"Best model loaded from checkpoint: {bytecover_module.best_model_path}")
118
+ elif bytecover_module.config["test"]["model_ckpt"] is not None:
119
+ bytecover_model.load_state_dict(torch.load(bytecover_module.config["test"]["model_ckpt"], map_location='cpu'), strict=False)
120
+ print(f'Model loaded from checkpoint: {bytecover_module.config["test"]["model_ckpt"]}')
121
+ elif bytecover_module.state == "initializing":
122
+ print("Warning: Running with random weights")
123
+
124
+ bytecover_model.eval()
125
+
126
+ # BYTECOVER EMBEDDING GENERATION
127
+
128
+ audio_dict_bytecover = {}
129
+ for file in tqdm(file_list, desc="Generating Bytecover Embeddings"):
130
+ file_path = audio_dir + file
131
+ # try statement here allows you to skip to items you haven't yet embedded if you stop this step midway (if a key exists, you move on to next key)
132
+ try:
133
+ audio_dict_bytecover[file]
134
+ except:
135
+ # Load audio
136
+ try:
137
+ # This launches a subprocess to decode audio while down-mixing and resampling as necessary.
138
+ # Requires the ffmpeg CLI and `ffmpeg-python` package to be installed.
139
+ out, _ = (
140
+ ffmpeg.input(file_path, threads=0)
141
+ .output("-", format="s16le", acodec="pcm_s16le", ac=1, ar=22050)
142
+ .run(cmd=["ffmpeg", "-nostdin"], capture_stdout=True, capture_stderr=True)
143
+ )
144
+ except ffmpeg.Error as e:
145
+ raise RuntimeError(
146
+ f"Failed to load audio:{file_path}\n{e.stderr.decode()}"
147
+ ) from e
148
+ audio = np.frombuffer(out, np.int16).flatten().astype(np.float32) / 32768.0
149
+ song_tensor = torch.from_numpy(audio)
150
+ # this step grabs a ByteCover embedding
151
+ audio_embed = bytecover_model.forward(song_tensor.to(bytecover_module.config["device"]))['f_t'].detach()
152
+ audio_dict_bytecover[file] = audio_embed.squeeze()
153
+
154
+ # CLAP MODEL INITIALIZATION
155
+
156
+ print("Loading CLAP model")
157
+
158
+ clap_model = laion_clap.CLAP_Module(enable_fusion=False)
159
+ clap_model.load_ckpt() # download the default pretrained checkpoint.
160
+
161
+ # CLAP EMBEDDING GENERATION
162
+
163
+ audio_dict_CLAP = {}
164
+ for file in tqdm(file_list, desc="Generating CLAP Embeddings"):
165
+ # try statement here allows you to skip to items you haven't yet embedded if you stop this step midway (if a key exists, you move on to next key)
166
+ try:
167
+ audio_dict_CLAP[file]
168
+ except:
169
+ # Get audio embeddings from audio data
170
+ full_path = audio_dir + "/" + file
171
+ # this step grabs a CLAP embedding from laion_clap library
172
+ audio_embed = clap_model.get_audio_embedding_from_filelist(x = [full_path], use_tensor=False)
173
+ audio_dict_CLAP[file] = audio_embed
174
+
175
+ # DATAFRAME GENERATION
176
+
177
+ flat_dfs = []
178
+
179
+ for audio_dict in [audio_dict_CLAP, audio_dict_bytecover]:
180
+
181
+ flat_df = pd.DataFrame(audio_dict.items(), columns=['vector_name','vector_embed']).reset_index()
182
+ flat_df.columns=['vector_id','vector_name','vector_embed']
183
+
184
+ flat_df["song_title"] = flat_df.apply(lambda row: grab_song_title(row['vector_name']),axis=1)
185
+ flat_df["flat_vector_embed"] = flat_df.apply(lambda row: flatten_vector_embed(row['vector_embed']),axis=1)
186
+
187
+ flat_df["flat_vector_embed_64"] = flat_df.apply(lambda row: convert_to_npfloat64(row['flat_vector_embed']),axis=1)
188
+ flat_df["flat_vector_embed_64_list"] = flat_df.apply(lambda row: convert_to_npfloat64_to_list(row['flat_vector_embed_64']),axis=1)
189
+
190
+ flat_df["genre"] = flat_df.apply(lambda row: look_up_metadata(row['vector_name'], meta_df, 'genre'),axis=1)
191
+ flat_df["album"] = flat_df.apply(lambda row: look_up_metadata(row['vector_name'], meta_df, 'album'),axis=1)
192
+ flat_df["name"] = flat_df.apply(lambda row: look_up_metadata(row['vector_name'], meta_df, 'name'),axis=1)
193
+ flat_df["artist"] = flat_df.apply(lambda row: look_up_metadata(row['vector_name'], meta_df, 'artist_names'),axis=1)
194
+ flat_df["year"] = flat_df.apply(lambda row: look_up_metadata(row['vector_name'], meta_df, 'year'),axis=1)
195
+ flat_df["vector_clip_num"] = flat_df.apply(lambda row: strip_vector_clip(row['vector_name']),axis=1)
196
+ flat_df['embedding_triplet_num'] = flat_df.vector_name.apply(get_triplet_num)
197
+
198
+ flat_dfs.append(flat_df)
199
+
200
+ print("unique songs:", len(flat_df.song_title.unique()))
201
+
202
+ # PINECONE UPLOAD
203
+
204
+ api_key = os.environ['PC_API_KEY']
205
+ pc = Pinecone(api_key=api_key)
206
+
207
+ index_name_clap = f'clap-{index_naming_conv}' # free (comes with plan, can have 100k records)
208
+ index_name_bytecover = f'bytecover-{index_naming_conv}' # free (comes with plan, can have 100k records)
209
+ index_env = 'us-west1-gcp' # NOT free (take down when not in use)
210
+ pod_type = 'p1.x1' # NOT free (take down when not in use)
211
+
212
+ for index_name, flat_df, index_dim in zip([index_name_clap, index_name_bytecover], flat_dfs, [512, 2048]):
213
+
214
+ try:
215
+ pc.create_index(
216
+ name=index_name,
217
+ dimension=index_dim,
218
+ metric="cosine",
219
+ spec=PodSpec(
220
+ environment=index_env,
221
+ pod_type=pod_type,
222
+ pods=1
223
+ ),
224
+ deletion_protection="disabled"
225
+ )
226
+ except PineconeApiException:
227
+ print(f"WARNING: INDEX {index_name} ALREADY EXISTS")
228
+ time.sleep(5)
229
+
230
+ index = pc.Index(index_name)
231
+
232
+ batch_id = 0
233
+
234
+ df_batcher = BatchGenerator(64)
235
+
236
+ for batch_df in tqdm(df_batcher(flat_df), desc="Uploading batches"):
237
+ #print(batch_df)
238
+ batch_id = batch_id + 1
239
+ index.upsert(vectors=list(zip(batch_df["vector_name"],batch_df["flat_vector_embed_64_list"])))
240
+
241
+ failed_list_update_metadata = []
242
+
243
+ for vec_id in tqdm(range(0,len(flat_df)), desc="Adding metadata"):
244
+ try:
245
+ row = flat_df.iloc[vec_id]
246
+ index.update(id=str(row['vector_name']),
247
+ set_metadata={"genre": row['genre'],
248
+ "song" : row['name'],
249
+ "album": row['album'],
250
+ "artists": row['artist'],
251
+ "year" : str(row['year']),
252
+ "clip_num" : row['vector_clip_num'],
253
+ "triplet_num": str(row['embedding_triplet_num']),
254
+ "spotify_id" : row['song_title']
255
+ })
256
+ except:
257
+ print("failed on:", vec_id)
258
+ failed_list_update_metadata.append(vec_id)
259
+
260
+ pc.create_collection(index_name, index_name)
261
+
262
+
263
+
264
+
265
+
266
+ if __name__ == "__main__":
267
+ parser = argparse.ArgumentParser(description="Generate ByteCover and CLAP Embeddings for a dataset and put to Pinecone")
268
+
269
+ parser.add_argument('audio_dir')
270
+ parser.add_argument('metadata_dir')
271
+ parser.add_argument('index_name')
272
+
273
+ args = parser.parse_args()
274
+
275
+ generate(args.audio_dir, args.metadata_dir, args.index_name)
requirements.txt CHANGED
@@ -5,6 +5,24 @@ loralib
5
  wavebeat @ git+https://github.com/hugofloresgarcia/wavebeat
6
  lac @ git+https://github.com/hugofloresgarcia/lac.git
7
  descript-audiotools @ git+https://github.com/hugofloresgarcia/audiotools.git
8
- -e git+https://github.com/audacitorch/pyharp.git@develop#egg=pyharp
9
  torch_pitch_shift
10
- gradio
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  wavebeat @ git+https://github.com/hugofloresgarcia/wavebeat
6
  lac @ git+https://github.com/hugofloresgarcia/lac.git
7
  descript-audiotools @ git+https://github.com/hugofloresgarcia/audiotools.git
 
8
  torch_pitch_shift
9
+ gradio
10
+
11
+ --extra-index-url https://download.pytorch.org/whl/cu113
12
+ -e git+https://github.com/TEAMuP-dev/pyharp.git@np/overlaycolor#egg=pyharp
13
+ pinecone
14
+ # towhee
15
+ av
16
+ laion_clap
17
+ # pyshorteners
18
+ nnAudio
19
+ ffmpeg-python
20
+ torchvision
21
+ torch
22
+ jsonlines
23
+ wandb
24
+ tqdm
25
+ # For pinecone_generate only
26
+ google-api-python-client
27
+ protoc-gen-openapiv2-protoc3-19
28
+ transformers==4.30.0