Spaces:
Sleeping
Sleeping
File size: 17,914 Bytes
bda104d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 | from torch.utils.data import Dataset
from .loading_utils import *
import torch
import os
import random
from tqdm import tqdm
import pandas as pd
from hydra.utils import instantiate
import logging
class BasicProcessor:
def __init__(self, probability = 1.0, split = None):
self.probability = probability
self.split = split
def process(self, annot):
"""
Process the annotation if the probability is less than the probability and the split is in the split list
"""
if random.random() < self.probability:
if self.split is None:
return self.process(annot)
if self.split is not None and annot['split'] in self.split:
return self.process(annot)
else:
return annot
else:
return annot
def __call__(self, annot):
return self.process(annot)
class RandomNSentencesProcessor(BasicProcessor):
def process(self, annot):
prompt = annot['prompt']
sentences = prompt.split('.')
n_sentences = len(sentences)
keep_n_sentences = random.randint(1, n_sentences)
random_sentences = random.sample(random_sentences, keep_n_sentences)
prompt = '. '.join(random_sentences)
return prompt
class ShuffleSentencesProcessor(BasicProcessor):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def process(self, annot):
prompt = annot['prompt']
sentences = prompt.split('.')
random.shuffle(sentences)
prompt = '. '.join(sentences)
return prompt
class ShuffleTagsProcessor(BasicProcessor):
def __init__(self, replace_caption_p = 0.5, **kwargs):
super().__init__(**kwargs)
self.replace_caption_p = replace_caption_p
def process(self, annot):
## if tags is in the annotation, it will be a comma-separated string
## drop anywhere from all but 1 to 1 tag, and shuffle
if 'tags' in annot:
tags = annot['tags'].split(',')
else:
return annot
len_tags = len(tags)
drop_tags = random.randint(1, len_tags-1)
tags_to_drop = random.sample(tags, drop_tags)
tags = [tag for tag in tags if tag not in tags_to_drop]
random.shuffle(tags)
annot['tags'] = '. '.join(tags)
# if replace caption, replace else add it to the caption
if random.random() < self.replace_caption_p:
annot['prompt'] = annot['tags']
else:
annot['prompt'] = annot['prompt'] + '.' + annot['tags']
return annot
class TextAudioDataset(Dataset):
def __init__(self,
annotations = None,
get_annotations_function = None,
task_kwargs = None,
target_n_samples = 96000,
target_sr = 48000,
return_audio = True,
return_text = True,
concept = None,
return_full_audio = False,
preextracted_features = False,
truncate_preextracted = 50,
split = None,
filter_split = None,
root_dir = None,
new_dir = None,
limit_n = None,
processors = [],
**kwargs
):
# Get annotations either directly or from function
if annotations is not None:
self.annotations = annotations
elif get_annotations_function is not None:
# Support both string and callable
if isinstance(get_annotations_function, str):
task_kwargs = task_kwargs or {}
import importlib
# parse the fully qualified function name
module_name, func_name = get_annotations_function.rsplit('.', 1)
module = importlib.import_module(module_name)
get_annotations_func = getattr(module, func_name)
self.annotations = get_annotations_func(**task_kwargs)
else:
task_kwargs = task_kwargs or {}
self.annotations = get_annotations_function(**task_kwargs)
else:
raise ValueError("Must provide either annotations or get_annotations_function")
self.target_n_samples = target_n_samples
self.target_sr = target_sr
self.return_audio = return_audio
self.return_text = return_text
self.concept = concept
self.return_full_audio = return_full_audio
self.preextracted_features = preextracted_features
self.truncate_preextracted = truncate_preextracted
self.split = split
self.root_dir = root_dir
self.new_dir = new_dir
self.limit_n = limit_n
# Update split if needed
if split is not None and split != 'keep':
for annot in self.annotations:
annot['split'] = split
elif split == 'keep':
# Keep original splits from annotations, or set to 'train' if not present
for annot in self.annotations:
if 'split' not in annot or annot['split'] not in ['train', 'val', 'test']:
annot['split'] = 'train'
elif split is None and len(self.annotations) > 0 and 'split' not in self.annotations[0].keys():
for annot in self.annotations:
annot['split'] = 'train'
if filter_split is not None:
self.annotations = [annot for annot in self.annotations if annot['split'] in filter_split]
annot_df = pd.DataFrame(self.annotations)
try:
annot_df['file_index'] = pd.factorize(annot_df['file_path'])[0]
except Exception as e:
print(e)
annot_df['file_path'] = annot_df['file_path'].apply(lambda x: x.replace(root_dir, new_dir) if root_dir is not None and new_dir is not None else x)
self.annotations = annot_df.to_dict('records')
# Filter out annotations whose feature/audio file is missing (avoids IndexError when retrying in __getitem__)
if self.return_audio and self.preextracted_features:
n_before = len(self.annotations)
self.annotations = [
a for a in self.annotations
if os.path.exists(a['file_path'].replace('.mp3', '.npy').replace('.wav', '.npy'))
]
if len(self.annotations) < n_before:
logging.warning(
f"Filtered out {n_before - len(self.annotations)} annotations with missing feature files. "
f"Dataset has {len(self.annotations)} samples."
)
elif self.return_audio and not self.preextracted_features:
n_before = len(self.annotations)
def _audio_exists(a):
p = a['file_path']
return os.path.exists(p.replace('.npy', '.mp3')) or os.path.exists(p.replace('.npy', '.wav'))
self.annotations = [a for a in self.annotations if _audio_exists(a)]
if len(self.annotations) < n_before:
logging.warning(
f"Filtered out {n_before - len(self.annotations)} annotations with missing audio files. "
f"Dataset has {len(self.annotations)} samples."
)
if self.limit_n is not None and self.limit_n < len(self.annotations):
self.annotations = self.annotations[:self.limit_n]
print(f"Limiting dataset to {self.limit_n} samples")
else:
print(f"Dataset has {len(self.annotations)} samples")
assert return_audio or return_text, "At least one of return_audio or return_text must be True (duh)"
self.processors = [instantiate(processor) for processor in processors]
def purge(self):
if self.return_audio and not self.preextracted_features:
raise NotImplementedError("Purging your audio dataset is probably a bad idea")
else:
file_paths = [annot['file_path'] for annot in self.annotations]
for file_path in file_paths:
os.remove(file_path)
print(f"Removed {len(file_paths)} files")
def __len__(self):
return len(self.annotations)
def __getitem__(self, idx, return_full_audio = False, hop = None, verbose = False):
return_full_audio = self.return_full_audio if return_full_audio is None else return_full_audio
annot = self.annotations[idx]
if self.return_audio:
if not self.preextracted_features:
# file_path = annot['file_path'].replace('.npy','.mp3').replace('.wav','.mp3')
# check if the mp3 file exists
if os.path.exists(annot['file_path'].replace('.npy','.mp3')):
file_path = annot['file_path'].replace('.npy','.mp3')
elif os.path.exists(annot['file_path'].replace('.npy','.wav')):
file_path = annot['file_path'].replace('.npy','.wav')
else:
return self.__getitem__(idx+1)
annot['file_path'] = file_path
try:
audio = load_full_and_split(
annot['file_path'],
self.target_sr,
self.target_n_samples,
hop=hop,
verbose=verbose
) if return_full_audio else load_audio_chunk(
annot['file_path'],
target_sr=self.target_sr,
target_n_samples=self.target_n_samples,
verbose=verbose
)
audio = audio.mean(1)
except Exception as e:
return self.__getitem__(idx+1)
else:
file_path = annot['file_path'].replace('.mp3','.npy').replace('.wav','.npy')
try:
audio = np.load(file_path,mmap_mode='r')
# Preextracted features may be stored per-clip as a single vector [D]
# or as multiple frames [T, D]; sample a random frame when framewise.
if audio.ndim > 1:
rand_index = random.randint(0, audio.shape[0]-1)
audio = audio[rand_index]
audio = torch.tensor(np.asarray(audio))
except Exception as e:
return self.__getitem__(idx+1)
if self.return_text:
possible_captions = annot['caption']
# ramdomly choose a caption hash
random_hash = random.choice(list(possible_captions.keys()))
caption = possible_captions[random_hash]
return_dict = {}
if self.return_audio:
# Clone so storage is resizable; avoids DataLoader collate error with mmap/numpy-derived tensors
return_dict['audio'] = audio.clone() if isinstance(audio, torch.Tensor) else audio
return_dict['file_path'] = annot['file_path']
if self.return_text:
return_dict['prompt'] = caption
return_dict['file_idx'] = annot['file_index']
for processor in self.processors:
return_dict = processor(return_dict)
return return_dict
def extract_features(self, model, extract_method = 'extract_features', extract_kwargs = {}, out_key = 'embedding',hop = None, return_full_audio = True, verbose = False):
device = next(model.parameters()).device
print(f"Extracting features with {extract_method} method on {device} device") if verbose else None
for param in model.parameters():
param.requires_grad = False
try:
model.eval()
except:
pass
for i in range(len(self)):
try:
item = self.__getitem__(i, return_full_audio = return_full_audio, hop = hop, verbose = verbose)
file_path = self.annotations[i]['file_path'].replace('.mp3','.npy').replace('.wav','.npy')
audio = item['audio'].squeeze(1).to(device)
if audio.shape[0] > 200 :
chunks = torch.split(audio, 200, dim=0)
chunks = list(chunks)
audio_features = []
for chunk in chunks:
feat = getattr(model, extract_method)(chunk, **extract_kwargs)
if out_key is not None:
feat = feat[out_key]
audio_features.append(feat)
audio_features = torch.cat(audio_features, dim=0)
else:
audio_features = getattr(model, extract_method)(audio.to(device), **extract_kwargs)
if out_key is not None:
audio_features = audio_features[out_key]
print(f"Extracted features for {file_path}, shape: {audio_features.shape}") if verbose else None
yield audio_features, file_path
except Exception as e:
print(f"Error extracting features for {file_path}: {e}") if verbose else None
continue
def extract_and_save_features(self, model, save_dir = None, extract_method = 'extract_features', extract_kwargs = {}, out_key = 'embedding', hop = None, return_full_audio = True, limit_n = None, save = False, verbose = True, root_path = None, done_ids = None):
print(self.__len__())
audio_features_all = []
counter = 0
skipped_count = 0
save_dir = '' if save_dir is None else save_dir
done_ids = done_ids or set()
if 's3://' in save_dir:
import boto3
import io
client = boto3.client('s3')
else:
client = None
import io
# filter self.annotations to only include files that are not in done_ids
new_annotations = []
for annot in self.annotations:
fp = annot['file_path']
fp = fp.replace(root_path+'/','')
# remove extension
fp = fp.replace('.mp3','').replace('.wav','').replace('.npy','')
if fp not in done_ids:
new_annotations.append(annot)
self.annotations = new_annotations
for audio_features, file_path in (pbar:= tqdm(self.extract_features(model, extract_method = extract_method, extract_kwargs = extract_kwargs, out_key = out_key, hop = hop, return_full_audio = return_full_audio, verbose = verbose))):
# print(file_path, root_path, save_dir)
if root_path is not None:
file_path = file_path.replace(root_path+'/','')
save_path = os.path.join(save_dir, file_path)
if save and audio_features is not None:
#remove the root path from the file path
if 's3://' in save_dir:
bucket, key = save_dir.replace("s3://", "").split("/", 1)
key = f"{key}/{file_path}"
# local_path = os.path.join(local_temp_dir, file_path)
# os.makedirs(os.path.dirname(local_path), exist_ok=True)
# np.save(local_path, audio_features.detach().cpu().numpy())
pbar.set_description(f"Uploading features to s3://{bucket}/{key}") if verbose else None
try:
# client.upload_file(save_path, bucket, key)
buffer = io.BytesIO()
np.save(buffer, audio_features.detach().cpu().numpy())
buffer.seek(0)
client.put_object(Bucket=bucket, Key=key, Body=buffer)
except Exception as e:
print(f"Error uploading to s3: {e}") if verbose else None
# os.remove(local_path)
else:
pbar.set_description(f"Saving features in {save_path}, shape: {audio_features.shape}")
os.makedirs(os.path.dirname(save_path), exist_ok=True)
if os.path.exists(save_path):
os.remove(save_path)
np.save(save_path, audio_features.detach().cpu().numpy())
if not save and audio_features is not None:
pbar.set_description(f"{file_path}, shape: {audio_features.shape}")
pass
audio_features_all.append(audio_features.detach().cpu()) if audio_features is not None else None
counter += 1
if limit_n and counter >= limit_n:
break
if skipped_count > 0:
print(f"Skipped {skipped_count} already processed items") if verbose else None
try:
print(f"Returning {len(audio_features_all)} features") if verbose else None
all_= torch.stack(audio_features_all)
print(f"Stacked features, shape: {all_.shape}") if verbose else None
return all_
except:
return None |