work on pip package

This commit is contained in:
mrfakename
2025-05-06 18:59:32 -07:00
parent 54da683d36
commit a5746eaab6
36 changed files with 2928 additions and 1251 deletions
+232 -92
View File
@@ -17,32 +17,46 @@ warnings.simplefilter("ignore", category=FutureWarning)
DEFAULT_TRAIN_PATH = "./data/example_dataset"
def is_silent_audio(audio_tensor, silence_threshold=0.95):
"""
Determine if an audio is silent and should be discarded
Args:
audio_tensor: torch.Tensor from torchaudio, shape (num_channels, num_samples)
silence_threshold: Silence threshold ratio, default 0.95 means 95%
Returns:
bool: True if audio should be discarded, False if it should be kept
"""
# Check if each sample point is zero across all channels
silent_samples = torch.all(audio_tensor == 0, dim=0)
# Calculate silence ratio
silent_ratio = torch.mean(silent_samples.float()).item()
return silent_ratio > silence_threshold
# Supported languages for tokenization
SUPPORT_LANGUAGES = {
"en": 259, "de": 260, "fr": 262, "es": 284, "it": 285,
"pt": 286, "pl": 294, "tr": 295, "ru": 267, "cs": 293,
"nl": 297, "ar": 5022, "zh": 5023, "ja": 5412, "hu": 5753,
"ko": 6152, "hi": 6680
"en": 259,
"de": 260,
"fr": 262,
"es": 284,
"it": 285,
"pt": 286,
"pl": 294,
"tr": 295,
"ru": 267,
"cs": 293,
"nl": 297,
"ar": 5022,
"zh": 5023,
"ja": 5412,
"hu": 5753,
"ko": 6152,
"hi": 6680,
}
# Regex pattern for structure markers like [Verse], [Chorus], etc.
@@ -53,13 +67,19 @@ class Text2MusicDataset(Dataset):
"""
Dataset for text-to-music generation that processes lyrics and audio files
"""
def __init__(self, train=True, train_dataset_path=DEFAULT_TRAIN_PATH,
max_duration=240.0, sample_size=None, shuffle=True,
minibatch_size=1):
def __init__(
self,
train=True,
train_dataset_path=DEFAULT_TRAIN_PATH,
max_duration=240.0,
sample_size=None,
shuffle=True,
minibatch_size=1,
):
"""
Initialize the Text2Music dataset
Args:
train: Whether this is a training dataset
train_dataset_path: Path to the dataset
@@ -72,21 +92,114 @@ class Text2MusicDataset(Dataset):
self.max_duration = max_duration
self.minibatch_size = minibatch_size
self.train = train
# Initialize language segmentation
self.lang_segment = LangSegment()
self.lang_segment.setfilters([
'af', 'am', 'an', 'ar', 'as', 'az', 'be', 'bg', 'bn', 'br', 'bs', 'ca', 'cs', 'cy', 'da', 'de', 'dz', 'el',
'en', 'eo', 'es', 'et', 'eu', 'fa', 'fi', 'fo', 'fr', 'ga', 'gl', 'gu', 'he', 'hi', 'hr', 'ht', 'hu', 'hy',
'id', 'is', 'it', 'ja', 'jv', 'ka', 'kk', 'km', 'kn', 'ko', 'ku', 'ky', 'la', 'lb', 'lo', 'lt', 'lv', 'mg',
'mk', 'ml', 'mn', 'mr', 'ms', 'mt', 'nb', 'ne', 'nl', 'nn', 'no', 'oc', 'or', 'pa', 'pl', 'ps', 'pt', 'qu',
'ro', 'ru', 'rw', 'se', 'si', 'sk', 'sl', 'sq', 'sr', 'sv', 'sw', 'ta', 'te', 'th', 'tl', 'tr', 'ug', 'uk',
'ur', 'vi', 'vo', 'wa', 'xh', 'zh', 'zu'
])
self.lang_segment.setfilters(
[
"af",
"am",
"an",
"ar",
"as",
"az",
"be",
"bg",
"bn",
"br",
"bs",
"ca",
"cs",
"cy",
"da",
"de",
"dz",
"el",
"en",
"eo",
"es",
"et",
"eu",
"fa",
"fi",
"fo",
"fr",
"ga",
"gl",
"gu",
"he",
"hi",
"hr",
"ht",
"hu",
"hy",
"id",
"is",
"it",
"ja",
"jv",
"ka",
"kk",
"km",
"kn",
"ko",
"ku",
"ky",
"la",
"lb",
"lo",
"lt",
"lv",
"mg",
"mk",
"ml",
"mn",
"mr",
"ms",
"mt",
"nb",
"ne",
"nl",
"nn",
"no",
"oc",
"or",
"pa",
"pl",
"ps",
"pt",
"qu",
"ro",
"ru",
"rw",
"se",
"si",
"sk",
"sl",
"sq",
"sr",
"sv",
"sw",
"ta",
"te",
"th",
"tl",
"tr",
"ug",
"uk",
"ur",
"vi",
"vo",
"wa",
"xh",
"zh",
"zu",
]
)
# Initialize lyric tokenizer
self.lyric_tokenizer = VoiceBpeTokenizer()
# Load dataset
self.setup_full(train, shuffle, sample_size)
logger.info(f"Dataset size: {len(self)} total {self.total_samples} samples")
@@ -94,7 +207,7 @@ class Text2MusicDataset(Dataset):
def setup_full(self, train=True, shuffle=True, sample_size=None):
"""
Load and prepare the dataset
Args:
train: Whether this is a training dataset
shuffle: Whether to shuffle the dataset
@@ -104,7 +217,7 @@ class Text2MusicDataset(Dataset):
if sample_size is not None:
pretrain_ds = pretrain_ds.select(range(sample_size))
self.pretrain_ds = pretrain_ds
self.total_samples = len(self.pretrain_ds)
@@ -118,10 +231,10 @@ class Text2MusicDataset(Dataset):
def get_lang(self, text):
"""
Detect the language of a text
Args:
text: Input text
Returns:
tuple: (primary_language, language_segments, language_counts)
"""
@@ -141,12 +254,12 @@ class Text2MusicDataset(Dataset):
def tokenize_lyrics(self, lyrics, debug=False, key=None):
"""
Tokenize lyrics into token indices
Args:
lyrics: Lyrics text
debug: Whether to print debug information
key: Optional key identifier
Returns:
list: Token indices
"""
@@ -162,7 +275,7 @@ class Text2MusicDataset(Dataset):
most_common_lang = lang_counter[0][0]
if most_common_lang == "":
most_common_lang = "en"
if most_common_lang not in SUPPORT_LANGUAGES:
raise ValueError(f"Unsupported language: {most_common_lang}")
@@ -185,7 +298,7 @@ class Text2MusicDataset(Dataset):
if not line.strip():
lyric_token_idx += [2] # Line break token
continue
try:
# Handle structure markers like [Verse], [Chorus]
if structure_pattern.match(line):
@@ -193,41 +306,49 @@ class Text2MusicDataset(Dataset):
else:
# Try tokenizing with most common language first
token_idx = self.lyric_tokenizer.encode(line, most_common_lang)
# If debug mode, show tokenization results
if debug:
toks = self.lyric_tokenizer.batch_decode([[tok_id] for tok_id in token_idx])
logger.info(f"debug using most_common_lang {line} --> {most_common_lang} --> {toks}")
toks = self.lyric_tokenizer.batch_decode(
[[tok_id] for tok_id in token_idx]
)
logger.info(
f"debug using most_common_lang {line} --> {most_common_lang} --> {toks}"
)
# If tokenization contains unknown token (1), try with segment language
if 1 in token_idx:
token_idx = self.lyric_tokenizer.encode(line, lang)
if debug:
toks = self.lyric_tokenizer.batch_decode([[tok_id] for tok_id in token_idx])
toks = self.lyric_tokenizer.batch_decode(
[[tok_id] for tok_id in token_idx]
)
logger.info(f"debug {line} --> {lang} --> {toks}")
# Add tokens and line break
lyric_token_idx = lyric_token_idx + token_idx + [2]
except Exception as e:
logger.error(f"Tokenize error: {e} for line: {line}, major_language: {lang}")
logger.error(
f"Tokenize error: {e} for line: {line}, major_language: {lang}"
)
return lyric_token_idx
def tokenize_lyrics_map(self, item, debug=False):
"""
Process and tokenize lyrics in a dataset item
Args:
item: Dataset item containing lyrics
debug: Whether to print debug information
Returns:
dict: Updated item with tokenized lyrics
"""
norm_lyrics = item["norm_lyrics"]
# Filter out prompts that match pattern "write a .* song that genre is"
pattern = r"write a .* song that genre is"
if re.search(pattern, norm_lyrics):
@@ -235,14 +356,14 @@ class Text2MusicDataset(Dataset):
item["lyric_token_idx"] = [0]
item["norm_lyrics"] = norm_lyrics
return item
key = item["keys"]
# Handle empty lyrics
if not item["norm_lyrics"].strip():
item["lyric_token_idx"] = [0]
return item
# Tokenize lyrics
item["lyric_token_idx"] = self.tokenize_lyrics(norm_lyrics, debug, key)
return item
@@ -250,10 +371,10 @@ class Text2MusicDataset(Dataset):
def get_speaker_emb_file(self, speaker_emb_path):
"""
Load speaker embedding file
Args:
speaker_emb_path: Path to speaker embedding file
Returns:
torch.Tensor or None: Speaker embedding
"""
@@ -267,10 +388,10 @@ class Text2MusicDataset(Dataset):
def get_audio(self, item):
"""
Load and preprocess audio file
Args:
item: Dataset item containing filename
Returns:
torch.Tensor or None: Processed audio tensor
"""
@@ -289,35 +410,37 @@ class Text2MusicDataset(Dataset):
# Convert mono to stereo if needed
if audio.shape[0] == 1:
audio = torch.cat([audio, audio], dim=0)
# Take first two channels if more than stereo
audio = audio[:2]
# Resample if needed
if sr != 48000:
audio = torchaudio.transforms.Resample(sr, 48000)(audio)
# Clip values to [-1.0, 1.0]
audio = torch.clamp(audio, -1.0, 1.0)
# Pad to minimum 3 seconds if needed
if audio.shape[-1] < 48000 * 3:
audio = torch.nn.functional.pad(audio, (0, 48000 * 3 - audio.shape[-1]), 'constant', 0)
audio = torch.nn.functional.pad(
audio, (0, 48000 * 3 - audio.shape[-1]), "constant", 0
)
# Check if audio is silent
if is_silent_audio(audio):
logger.error(f"Silent audio {item}")
return None
return audio
def process(self, item):
"""
Process a dataset item into model-ready features
Args:
item: Dataset item
Returns:
list: List of processed examples
"""
@@ -325,7 +448,7 @@ class Text2MusicDataset(Dataset):
audio = self.get_audio(item)
if audio is None:
return []
music_wavs = audio
# Get speaker embedding
@@ -344,7 +467,7 @@ class Text2MusicDataset(Dataset):
# Shuffle tags and join with commas
random.shuffle(prompt)
prompt = ", ".join(prompt)
# Handle recaption data if available
recaption = item.get("recaption", {})
valid_recaption = []
@@ -362,22 +485,24 @@ class Text2MusicDataset(Dataset):
lyric_token_idx = torch.tensor(lyric_token_idx).long()
lyric_token_idx = lyric_token_idx[:4096] # Limit lyric context length
lyric_mask = torch.ones(len(lyric_token_idx))
# Create lyric chunks for display
candidate_lyric_chunk = []
lyrics = item["norm_lyrics"]
lyrics_lines = lyrics.split("\n")
for lyric_line in lyrics_lines:
candidate_lyric_chunk.append({
"lyric": lyric_line,
})
candidate_lyric_chunk.append(
{
"lyric": lyric_line,
}
)
# Limit audio length
longest_length = 24 * 10 * 48000 # 240 seconds
music_wavs = music_wavs[:, :longest_length]
vocal_wavs = torch.zeros_like(music_wavs)
wav_len = music_wavs.shape[-1]
# Create example dictionary
example = {
"key": key,
@@ -396,10 +521,10 @@ class Text2MusicDataset(Dataset):
def get_full_features(self, idx):
"""
Get full features for a dataset index
Args:
idx: Dataset index
Returns:
dict: Dictionary of features
"""
@@ -420,13 +545,13 @@ class Text2MusicDataset(Dataset):
item["idx"] = idx
item = self.tokenize_lyrics_map(item)
features = self.process(item)
if features:
for feature in features:
for k, v in feature.items():
# Handle key mapping more explicitly
target_key = k + "s" # Default plural form
# Special case handling for keys that don't follow simple plural pattern
if k == "key":
target_key = "keys"
@@ -434,19 +559,19 @@ class Text2MusicDataset(Dataset):
target_key = "wav_lengths"
elif k == "candidate_lyric_chunk":
target_key = "candidate_lyric_chunks"
if v is not None and target_key in examples:
examples[target_key].append(v)
return examples
def pack_batch(self, batch):
"""
Pack a batch of examples
Args:
batch: List of examples
Returns:
dict: Packed batch
"""
@@ -462,16 +587,16 @@ class Text2MusicDataset(Dataset):
def collate_fn(self, batch):
"""
Collate function for DataLoader
Args:
batch: List of examples
Returns:
dict: Collated batch with padded tensors
"""
batch = self.pack_batch(batch)
output = {}
for k, v in batch.items():
if k in ["keys", "structured_tags", "prompts", "candidate_lyric_chunks"]:
# Pass through lists without modification
@@ -482,40 +607,55 @@ class Text2MusicDataset(Dataset):
elif k in ["src_wavs", "target_wavs", "vocal_wavs"]:
# Pad audio to max length
max_length = max(seq.shape[1] for seq in v)
padded_input_list = torch.stack([
torch.nn.functional.pad(seq, (0, max_length - seq.shape[1]), 'constant', 0)
for seq in v
])
padded_input_list = torch.stack(
[
torch.nn.functional.pad(
seq, (0, max_length - seq.shape[1]), "constant", 0
)
for seq in v
]
)
elif k in ["clap_conditions"]:
# Pad time dimension of embeddings
max_length = max(seq.shape[0] for seq in v)
v = [
torch.nn.functional.pad(seq, (0, 0, 0, max_length - seq.shape[0]), 'constant', 0)
torch.nn.functional.pad(
seq, (0, 0, 0, max_length - seq.shape[0]), "constant", 0
)
for seq in v
]
padded_input_list = torch.stack(v)
elif k == "speaker_embs":
# Stack speaker embeddings
padded_input_list = torch.stack(v)
elif k in ["chunk_masks", "clap_attention_masks", "lyric_token_ids", "lyric_masks"]:
elif k in [
"chunk_masks",
"clap_attention_masks",
"lyric_token_ids",
"lyric_masks",
]:
# Pad sequence tensors
max_length = max(len(seq) for seq in v)
padded_input_list = torch.stack([
torch.nn.functional.pad(seq, (0, max_length - len(seq)), 'constant', 0)
for seq in v
])
padded_input_list = torch.stack(
[
torch.nn.functional.pad(
seq, (0, max_length - len(seq)), "constant", 0
)
for seq in v
]
)
output[k] = padded_input_list
return output
def __getitem__(self, idx):
"""
Get item at index with error handling
Args:
idx: Dataset index
Returns:
dict: Example features
"""