from suno_utils.utils.text import read_jsonl, write_jsonl from tqdm import tqdm import os import re from collections import defaultdict FOLDER = "/app2/suno/data/sara/imslp_filter/" THRESHOLD = 1.0 OUTPUT_PATH = os.path.join(FOLDER, f"metas_raw_labeled_dupes_thresh_{THRESHOLD}.jsonl") ARTIST_KEY = "composer" # or artists artist_full = read_jsonl(os.path.join(FOLDER, "metas_raw.jsonl"), progress=True) metas_v0 = read_jsonl(os.path.join(FOLDER, "metas_v0.jsonl"), progress=True) artist_full_clean_copy = artist_full.copy() PAREN_PATTERN = re.compile(r"\([^)]*\)") BRACKET_PATTERN = re.compile(r"\[[^\]]*\]") def preprocess_text(text: str) -> str: """ Preprocess text (title or artist name) for better comparison. Args: text: The original text Returns: Preprocessed text """ if not text: return "" # Convert to lowercase processed = text.lower() # Helper function to clean song titles (remove text in brackets) def clean_title(title): # Remove text in parentheses and square brackets clean = PAREN_PATTERN.sub("", title) clean = BRACKET_PATTERN.sub("", clean) return clean.strip().lower() # processed = clean_title(processed) # Remove special characters (but preserve numbers) processed = re.sub(r"[!?:;#&+=%*\-_,.]", "", processed) # Remove extra whitespace processed = re.sub(r"\s+", " ", processed) return processed.strip() def find_song_duplicates(song_entries, min_substring_length=4): potential_duplicates = [] # Preprocess all song titles - O(n) operation processed_entries = [] exact_match_dict = defaultdict(list) for id, title in song_entries: stripped_title = title.strip() cleaned_title = preprocess_text(stripped_title) # Skip titles that are too short after cleaning if len(cleaned_title) < min_substring_length: continue processed_entries.append((id, stripped_title, cleaned_title)) # Add to dictionary for exact match detection exact_match_dict[cleaned_title].append((id, stripped_title)) # First check for exact matches - O(n) operation for cleaned_title, entries in exact_match_dict.items(): if len(entries) > 1: # Found exact matches for i in range(len(entries)): for j in range(i + 1, len(entries)): potential_duplicates.append((entries[i], entries[j])) # Sort processed entries by cleaned title length to enable early termination processed_entries.sort(key=lambda x: len(x[2])) if THRESHOLD == 1.0: # exact matches only return potential_duplicates # Then check for substring matches for i in range(len(processed_entries)): id1, title1, clean1 = processed_entries[i] # Only need to check against longer strings (shorter strings can't contain this one) for j in range(i + 1, len(processed_entries)): id2, title2, clean2 = processed_entries[j] # Skip exact matches (we already handled them) if clean1 == clean2: continue # Skip if the length ratio is too small (prevents false positives) if len(clean1) / len(clean2) < THRESHOLD: continue # Check if one is a substring of the other if clean1 in clean2 or clean2 in clean1: potential_duplicates.append(((id1, title1), (id2, title2))) return potential_duplicates ids_to_title = {} id_to_data = {} for song_info in artist_full: if ARTIST_KEY not in song_info or "title" not in song_info: continue artist_info = song_info[ARTIST_KEY] if isinstance(artist_info, str): artist_ids = [artist_info] else: artist_ids = [x["id"] for x in song_info[ARTIST_KEY]] title = song_info["title"] id = song_info["id"] id_to_data[id] = song_info for a_id in artist_ids: if a_id not in ids_to_title: ids_to_title[a_id] = [] ids_to_title[a_id].append((id, title)) for id, data in id_to_data.items(): data["duplicate"] = False num_tracks = 0 num_dupes = 0 dupes = [] for a_id, titles in tqdm(ids_to_title.items()): dupes_maybe = find_song_duplicates(titles) # print(a_id, titles) if len(dupes_maybe) > 0: num_tracks += len(titles) num_dupes += len(dupes_maybe) for dupe_source, dupe in dupes_maybe: # print(dupe_source, dupe) # print(dupe_source[1], dupe[1]) dupes.append((dupe_source[1], dupe[1])) source = dupe_source[0] if "duplicate_source" in id_to_data[dupe_source[0]]: source = id_to_data[dupe_source[0]]["duplicate_source"] id_to_data[dupe[0]]["duplicate"] = True id_to_data[dupe[0]]["duplicate_source"] = source id_to_data[dupe_source[0]]["duplicate"] = True id_to_data[dupe_source[0]]["duplicate_source"] = source print(f"{num_tracks} tracks, {num_dupes} dupe pairs") cleaned_metas = [] filtered_metas = [] for meta in artist_full_clean_copy: id = meta["id"] if id in id_to_data: is_dupe = id_to_data[id]["duplicate"] meta["is_artist_duplicate"] = is_dupe if is_dupe: meta["duplicate_source"] = id_to_data[id]["duplicate_source"] if not is_dupe: filtered_metas.append(meta) else: filtered_metas.append(meta) cleaned_metas.append(meta) num_duplicates = len(metas_v0) - len(filtered_metas) percent_dupe = num_duplicates / len(metas_v0) * 100 print(f"Found {num_duplicates} duplicate titles out of {len(metas_v0)} tracks, {percent_dupe}%") print(f"Writing output to {OUTPUT_PATH}") write_jsonl(cleaned_metas, OUTPUT_PATH)