| import json
|
| import numpy as np
|
| import os
|
| import argparse
|
| from pathlib import Path
|
| from sklearn.metrics.pairwise import cosine_similarity
|
| from tqdm import tqdm
|
|
|
| def parse_args():
|
| parser = argparse.ArgumentParser(description="Cluster speakers based on feature similarity (emo_vec or condition).")
|
| parser.add_argument("--input", type=Path, required=True, help="Input manifest (singles).")
|
| parser.add_argument("--output", type=Path, required=True, help="Output manifest (clustered).")
|
| parser.add_argument("--threshold", type=float, default=0.92, help="Cosine similarity threshold.")
|
| parser.add_argument("--feature", type=str, default="emo_vec", choices=["emo_vec", "condition", "combined"],
|
| help="Which feature to use for clustering finding. 'condition' is usually better for speaker identity.")
|
| return parser.parse_args()
|
|
|
| def main():
|
| args = parse_args()
|
|
|
| print(f"Loading manifest from {args.input}...")
|
| entries = []
|
| with args.input.open('r', encoding='utf-8') as f:
|
| for line in f:
|
| if line.strip():
|
| entries.append(json.loads(line))
|
|
|
| print(f"Found {len(entries)} files. Using feature: '{args.feature}' with threshold {args.threshold}...")
|
|
|
| valid_entries = []
|
| current_speaker_id = 0
|
| last_vec = None
|
|
|
| dataset_root = args.input.parent
|
|
|
| for i, entry in enumerate(tqdm(entries)):
|
|
|
| paths_to_load = []
|
| if args.feature == "emo_vec":
|
| paths_to_load.append(entry.get('emo_vec_path'))
|
| elif args.feature == "condition":
|
| paths_to_load.append(entry.get('condition_path'))
|
| elif args.feature == "combined":
|
| paths_to_load.append(entry.get('condition_path'))
|
| paths_to_load.append(entry.get('emo_vec_path'))
|
|
|
| current_vec_list = []
|
| load_failed = False
|
|
|
| for rel_path in paths_to_load:
|
| if not rel_path:
|
| load_failed = True
|
| break
|
| vec_path = dataset_root / rel_path
|
| if not vec_path.exists():
|
| load_failed = True
|
| break
|
|
|
| try:
|
|
|
| vec = np.load(vec_path)
|
|
|
|
|
| if vec.ndim > 1:
|
| vec = vec.mean(axis=0)
|
|
|
| vec = vec.reshape(-1)
|
| current_vec_list.append(vec)
|
| except Exception as e:
|
|
|
| load_failed = True
|
| break
|
|
|
| if load_failed or not current_vec_list:
|
| continue
|
|
|
|
|
| current_vec = np.concatenate(current_vec_list, axis=0)
|
| current_vec = current_vec.reshape(1, -1)
|
|
|
| if last_vec is None:
|
|
|
| entry['speaker'] = f"auto_speaker_{current_speaker_id}"
|
| last_vec = current_vec
|
| else:
|
|
|
| score = cosine_similarity(last_vec, current_vec)[0][0]
|
|
|
| if score > args.threshold:
|
|
|
| entry['speaker'] = f"auto_speaker_{current_speaker_id}"
|
|
|
| else:
|
|
|
| current_speaker_id += 1
|
| entry['speaker'] = f"auto_speaker_{current_speaker_id}"
|
| last_vec = current_vec
|
|
|
| valid_entries.append(entry)
|
|
|
| print(f"Clustering complete. Found {current_speaker_id + 1} distinct speaker groups.")
|
| print(f"Saving to {args.output}...")
|
|
|
| args.output.parent.mkdir(parents=True, exist_ok=True)
|
| with args.output.open('w', encoding='utf-8') as f:
|
| for entry in valid_entries:
|
| f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
|
| print("Done!")
|
|
|
| if __name__ == "__main__":
|
| main()
|
|
|