From c3ac2def48e87f6b26a19a0b5dd2e11fa71fb9eb Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 27 Jan 2026 13:59:39 -0500 Subject: [PATCH] Add idempotent voice registry system for permanent speaker-to-name assignments - voice_registry.json: append-only registry with 50 name pools per gender, locked speaker assignments, and multi-corpus support - Rewrite download script to be registry-driven: loads registry, assigns names deterministically (sorted by speaker ID), never changes existing assignments - Update docs/VOICES.md with registry system documentation - Support --registry and --corpora CLI flags for multi-corpus downloads --- docs/VOICES.md | 164 +++++++++++++++ scripts/download_diverse_voices.py | 309 +++++++++++++++++++---------- voice_registry.json | 155 +++++++++++++++ 3 files changed, 527 insertions(+), 101 deletions(-) create mode 100644 docs/VOICES.md create mode 100644 voice_registry.json diff --git a/docs/VOICES.md b/docs/VOICES.md new file mode 100644 index 0000000..4918cfa --- /dev/null +++ b/docs/VOICES.md @@ -0,0 +1,164 @@ +# Voice Corpus Documentation + +## Overview + +uncloseai-speech uses voice samples from **LibriSpeech test-clean** for Qwen3-TTS voice cloning. +LibriSpeech is a public domain corpus of read English speech from LibriVox audiobook recordings. + +- **Source:** [OpenSLR LibriSpeech](https://www.openslr.org/12) +- **HuggingFace:** [openslr/librispeech_asr](https://huggingface.co/datasets/openslr/librispeech_asr) +- **License:** Public domain (LibriVox recordings) +- **Audio:** 16kHz WAV, single speaker per file + +## Voice Registry System + +Voice-to-speaker assignments are managed by `voice_registry.json` -- an append-only, +idempotent registry that permanently locks each speaker to a voice name. + +### How It Works + +1. **Registry file** (`voice_registry.json`): Committed to git, contains all locked assignments +2. **Name pools**: 50 female + 50 male names, assigned in order as new speakers are added +3. **Deterministic assignment**: New speakers sorted by ID (ascending), names assigned in pool order +4. **Append-only**: Once a speaker is assigned a name, that assignment never changes +5. **Multi-corpus**: Registry tracks which corpus each speaker came from + +### Adding a New Corpus + +To add speakers from a new corpus: + +1. Add the corpus config to `voice_registry.json` under `corpora`: + ```json + "corpora": { + "librispeech-test-clean": { ... }, + "librispeech-dev-clean": { + "dataset": "openslr/librispeech_asr", + "config": "clean", + "split": "validation", + "description": "LibriSpeech dev-clean, 40 speakers" + } + } + ``` + +2. Run the download script with the new corpus: + ```bash + python scripts/download_diverse_voices.py --corpora librispeech-test-clean librispeech-dev-clean + ``` + +3. New speakers get the next available names from the pool. Existing assignments are untouched. + +4. Commit the updated `voice_registry.json` to lock the new assignments. + +## Test-Clean Corpus + +The `test-clean` split contains **40 speakers** (~8 minutes each, ~350MB total): + +### Female Speakers (20) + +| ID | LibriVox Name | Minutes | +|----|---------------|---------| +| 121 | Nikolle Doolin | 8.01 | +| 237 | rachelellen | 8.02 | +| 367 | Kathleen Dang | 6.74 | +| 1221 | Dianne | 8.07 | +| 1284 | Daniel Anaya | 8.16 | +| 1580 | TinyPines | 8.07 | +| 1995 | (unknown) | ~8 | +| 2094 | amycsj | 8.09 | +| 2961 | Leni | 8.07 | +| 3570 | sarac | 8.05 | +| 3575 | supergirl | 8.06 | +| 3729 | Heather Hogan | 8.03 | +| 4446 | Jen Maxwell | 8.00 | +| 4507 | Rachel Nelson-Smith | 8.05 | +| 4970 | airandwaters | 8.15 | +| 4992 | Joyce Martin | 8.21 | +| 5142 | Mary Ballard-Johansson | 8.07 | +| 5570 | Ulf Bjorklund | 8.28 | +| 5683 | Rachael Lapidis | 8.01 | +| 6829 | LadyBug | 8.24 | + +### Male Speakers (20) + +| ID | LibriVox Name | Minutes | +|----|---------------|---------| +| 61 | Paul-Gabriel Wiener | 8.08 | +| 260 | Brad Bush | 8.05 | +| 672 | Taylor Burton-Edward | 8.27 | +| 908 | Sam Stinson | 8.05 | +| 1089 | Peter Bobbe | 8.05 | +| 1188 | Duncan Murrell | 8.20 | +| 1320 | number6 | 8.02 | +| 2300 | Mitchell L Leopard | 8.19 | +| 2830 | Tim Perkins | 8.04 | +| 4077 | Nathan Markham | 8.14 | +| 5105 | elongman | 8.12 | +| 5639 | (unknown) | ~8 | +| 6930 | Nolan Fout | 8.00 | +| 7021 | (unknown) | ~8 | +| 7127 | (unknown) | ~8 | +| 7176 | (unknown) | ~8 | +| 7729 | (unknown) | ~8 | +| 8230 | (unknown) | ~8 | +| 8455 | (unknown) | ~8 | +| 8463 | (unknown) | ~8 | + +Note: Names marked (unknown) were not in the SPEAKERS.TXT mirror we fetched. + +## Voice Names + +These are our own names -- LibriSpeech only provides LibriVox usernames (like "supergirl", +"LadyBug", "number6"), not character-style voice names. + +Names are assigned from pools in `voice_registry.json` and locked permanently. +The first 21 voices (original set) are: + +**Female voices:** aria, clara, elena, grace, hazel, iris, luna, maya, ruby, sage, sofia + +**Male voices:** atlas, caleb, felix, hugo, jasper, kai, leo, marcus, owen, theo + +When all 40 test-clean speakers are registered, the remaining 19 get the next +names from the pools (amber, brooke, cora, ... for female; archer, blake, cole, ... for male). + +## How Voices Are Selected + +The script picks the best sample per speaker based on: +- Duration: prefers 5-10 seconds (ideal for voice cloning reference) +- Completeness: bonus for sentences ending with a period +- Length: penalty for very long text (>300 chars) + +## Gender Verification + +Speaker genders are fetched at runtime from the official LibriSpeech `SPEAKERS.TXT` +(via GitHub mirror). This ensures female names always map to female speakers and +male names always map to male speakers. No hardcoded gender assumptions. + +Source: https://raw.githubusercontent.com/oscarknagg/voicemap/master/data/LibriSpeech/SPEAKERS.TXT + +## Other LibriSpeech Splits + +Additional splits can be added as new corpora in the registry: + +- **train-clean-100**: ~250 speakers, 100 hours +- **train-clean-360**: ~920 speakers, 360 hours +- **train-other-500**: ~1160 speakers, 500 hours (noisier) +- **test-other**: 33 speakers (noisier conditions) +- **dev-clean**: 40 speakers (validation set) +- **dev-other**: 33 speakers + +Using larger splits would give hundreds or thousands of distinct voices, but +test-clean provides the highest quality recordings. + +## File Layout + +``` +voice_registry.json # Idempotent voice-to-speaker assignments (committed) +cloned-voices/ +├── aria.wav # Female voice sample (~7s, 16kHz) +├── atlas.wav # Male voice sample +├── ... # (40 voices when fully expanded) +└── voices_metadata.json # Speaker IDs, genders, transcripts, durations +``` + +The `cloned-voices/` directory is mounted into the Docker container at `/app/cloned-voices/`. +Voice config is in `voice_to_speaker.default.yaml`. diff --git a/scripts/download_diverse_voices.py b/scripts/download_diverse_voices.py index bfd2b6a..3e72b9e 100644 --- a/scripts/download_diverse_voices.py +++ b/scripts/download_diverse_voices.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 """ Download diverse voice samples for Qwen3-TTS voice cloning. -Uses HuggingFace datasets for LibriSpeech test-clean with multiple speakers. +Uses voice_registry.json for idempotent, permanent speaker-to-name assignments. Fetches gender from upstream SPEAKERS.TXT (OpenSLR) to ensure correct assignment. Requires: pip install datasets soundfile Usage: python scripts/download_diverse_voices.py - python scripts/download_diverse_voices.py -o cloned-voices -c voice_to_speaker.default.yaml + python scripts/download_diverse_voices.py --registry voice_registry.json + python scripts/download_diverse_voices.py --corpora librispeech-test-clean """ import os @@ -25,15 +26,9 @@ except ImportError: HAS_DATASETS = False print("Install required libraries: pip install datasets soundfile") -# Upstream SPEAKERS.TXT URL from OpenSLR (canonical gender source) -SPEAKERS_TXT_URL = "https://www.openslr.org/resources/12/raw-metadata.tar.gz" -# GitHub mirror (plain text, easier to parse) +# GitHub mirror of LibriSpeech SPEAKERS.TXT (plain text, easier to parse) SPEAKERS_TXT_GITHUB = "https://raw.githubusercontent.com/oscarknagg/voicemap/master/data/LibriSpeech/SPEAKERS.TXT" -# Voice names - female names for female speakers, male names for male speakers -FEMALE_NAMES = ["aria", "clara", "elena", "grace", "hazel", "iris", "luna", "maya", "ruby", "sage", "sofia"] -MALE_NAMES = ["atlas", "caleb", "felix", "hugo", "jasper", "kai", "leo", "marcus", "owen", "theo"] - def fetch_speaker_genders(): """Fetch gender info from upstream LibriSpeech SPEAKERS.TXT.""" @@ -66,6 +61,75 @@ def fetch_speaker_genders(): return genders +def load_registry(registry_path): + """Load voice registry from JSON file.""" + if not registry_path.exists(): + return None + with open(registry_path) as f: + return json.load(f) + + +def save_registry(registry, registry_path): + """Save voice registry to JSON file (append-only, never remove voices).""" + with open(registry_path, "w") as f: + json.dump(registry, f, indent=2) + print(f" Registry saved: {registry_path}") + + +def get_used_names(registry): + """Get set of names already assigned in the registry.""" + return set(registry.get("voices", {}).keys()) + + +def get_next_name(registry, gender): + """Get the next available name from the pool for the given gender.""" + used = get_used_names(registry) + pool = registry.get("name_pools", {}).get(gender, []) + for name in pool: + if name not in used: + return name + return None + + +def assign_speakers_to_names(registry, corpus_id, speakers_by_gender, speaker_genders): + """Assign names to new speakers deterministically. + + New speakers are sorted by ID (ascending) and assigned names in pool order. + Existing assignments are never changed. + """ + # Build reverse lookup: (corpus, speaker_id) -> name + assigned = {} + for name, info in registry.get("voices", {}).items(): + key = (info["corpus"], info["speaker_id"]) + assigned[key] = name + + new_assignments = [] + + for gender in ("female", "male"): + # Sort unassigned speakers by ID for determinism + speakers = sorted(speakers_by_gender.get(gender, []), key=lambda s: int(s)) + for sid in speakers: + key = (corpus_id, sid) + if key in assigned: + continue # Already has a name + + name = get_next_name(registry, gender) + if name is None: + print(f" WARNING: No more {gender} names available, skipping speaker {sid}") + continue + + registry["voices"][name] = { + "corpus": corpus_id, + "speaker_id": sid, + "gender": gender, + "locked": True, + } + new_assignments.append((name, sid, gender)) + print(f" NEW: {name:12s} <- speaker {sid} ({gender})") + + return new_assignments + + def pick_best_sample(samples, min_dur=4.0, max_dur=12.0, target=7.0): """Pick the best sample: prefer 5-10 seconds, clean, complete sentence.""" best = None @@ -76,7 +140,7 @@ def pick_best_sample(samples, min_dur=4.0, max_dur=12.0, target=7.0): dur = len(audio["array"]) / audio["sampling_rate"] text = s.get("text", "") - # Skip too short or too long + # Skip too short if dur < 3.0: continue @@ -110,6 +174,10 @@ def main(): help="Output path for voice config YAML") parser.add_argument("--config-runtime", default="config/voice_to_speaker.yaml", help="Runtime config path (also written if dir exists)") + parser.add_argument("--registry", default="voice_registry.json", + help="Path to voice registry JSON (default: voice_registry.json)") + parser.add_argument("--corpora", nargs="+", default=["librispeech-test-clean"], + help="Corpora to download (default: librispeech-test-clean)") parser.add_argument("--max-samples", type=int, default=20, help="Max samples to collect per speaker for selection") args = parser.parse_args() @@ -121,6 +189,16 @@ def main(): output_dir = Path(args.output_dir) output_dir.mkdir(parents=True, exist_ok=True) + registry_path = Path(args.registry) + + # Load or create registry + registry = load_registry(registry_path) + if registry is None: + print(f"ERROR: Registry not found at {registry_path}") + print(" Create it or copy from voice_registry.json") + return 1 + + print(f"Loaded registry: {len(registry.get('voices', {}))} existing voices") # Fetch genders from upstream speaker_genders = fetch_speaker_genders() @@ -131,124 +209,141 @@ def main(): print(f"\n=== Downloading Diverse Voice Samples for Qwen3-TTS ===\n") print(f"Output directory: {output_dir}") print(f"Config output: {args.config_output}") + print(f"Registry: {registry_path}") + print(f"Corpora: {', '.join(args.corpora)}") - # Load LibriSpeech test-clean - print("Loading LibriSpeech test-clean dataset from HuggingFace...") - print("(First run downloads ~1.5 GB, cached after that)\n") - dataset = load_dataset( - "openslr/librispeech_asr", - "clean", - split="test", - trust_remote_code=True - ) + # Process each corpus + all_voice_names = set() - # Group samples by speaker - print("Grouping samples by speaker...") - speaker_samples = {} - for sample in dataset: - sid = str(sample["speaker_id"]) - if sid not in speaker_samples: - speaker_samples[sid] = [] - if len(speaker_samples[sid]) < args.max_samples: - speaker_samples[sid].append(sample) - - print(f"Found {len(speaker_samples)} speakers in test-clean\n") - - # Split speakers by gender using upstream data - female_speakers = [] - male_speakers = [] - for sid in speaker_samples: - gender = speaker_genders.get(sid) - if gender == "female": - female_speakers.append(sid) - elif gender == "male": - male_speakers.append(sid) - else: - print(f" WARNING: No gender for speaker {sid}, skipping") - - print(f"Female speakers available: {len(female_speakers)}") - print(f"Male speakers available: {len(male_speakers)}") - - # Pick the best speakers (most samples, best quality) - # Sort by number of samples available (more = better selection) - female_speakers.sort(key=lambda s: len(speaker_samples[s]), reverse=True) - male_speakers.sort(key=lambda s: len(speaker_samples[s]), reverse=True) - - # Take top N for each gender - female_speakers = female_speakers[:len(FEMALE_NAMES)] - male_speakers = male_speakers[:len(MALE_NAMES)] - - if len(female_speakers) < len(FEMALE_NAMES): - print(f" WARNING: Only {len(female_speakers)} female speakers, need {len(FEMALE_NAMES)}") - if len(male_speakers) < len(MALE_NAMES): - print(f" WARNING: Only {len(male_speakers)} male speakers, need {len(MALE_NAMES)}") - - # Build voice map: assign names to speakers - voice_map = {} - for i, sid in enumerate(female_speakers): - if i < len(FEMALE_NAMES): - voice_map[FEMALE_NAMES[i]] = sid - for i, sid in enumerate(male_speakers): - if i < len(MALE_NAMES): - voice_map[MALE_NAMES[i]] = sid - - print(f"\nAssigned {len(voice_map)} voices ({len(female_speakers)}F + {len(male_speakers)}M)\n") - - # Download and save voice samples - voices = {} - for voice_name, speaker_id in voice_map.items(): - samples = speaker_samples[speaker_id] - best = pick_best_sample(samples) - if best is None: - print(f" WARNING: No suitable sample for '{voice_name}' (speaker {speaker_id})") + for corpus_id in args.corpora: + corpus_config = registry.get("corpora", {}).get(corpus_id) + if corpus_config is None: + print(f"\nERROR: Unknown corpus '{corpus_id}'. Available: {list(registry.get('corpora', {}).keys())}") continue - audio = best["audio"] - transcript = best["text"].strip() - duration = len(audio["array"]) / audio["sampling_rate"] + print(f"\n--- Corpus: {corpus_id} ---") + print(f" {corpus_config.get('description', '')}") - # Save WAV - out_path = output_dir / f"{voice_name}.wav" - sf.write(str(out_path), audio["array"], audio["sampling_rate"]) + # Load dataset + print(f" Loading {corpus_config['dataset']} ({corpus_config['config']}/{corpus_config['split']})...") + print(" (First run downloads ~1.5 GB, cached after that)\n") + dataset = load_dataset( + corpus_config["dataset"], + corpus_config["config"], + split=corpus_config["split"], + trust_remote_code=True + ) - gender = speaker_genders.get(speaker_id, "unknown") + # Group samples by speaker + print(" Grouping samples by speaker...") + speaker_samples = {} + for sample in dataset: + sid = str(sample["speaker_id"]) + if sid not in speaker_samples: + speaker_samples[sid] = [] + if len(speaker_samples[sid]) < args.max_samples: + speaker_samples[sid].append(sample) - voices[voice_name] = { - "ref_audio": f"cloned-voices/{voice_name}.wav", - "ref_text": transcript, - "speaker_id": speaker_id, - "gender": gender, - "duration": round(duration, 1), + print(f" Found {len(speaker_samples)} speakers\n") + + # Split speakers by gender + speakers_by_gender = {"female": [], "male": []} + for sid in speaker_samples: + gender = speaker_genders.get(sid) + if gender in ("female", "male"): + speakers_by_gender[gender].append(sid) + else: + print(f" WARNING: No gender for speaker {sid}, skipping") + + print(f" Female speakers: {len(speakers_by_gender['female'])}") + print(f" Male speakers: {len(speakers_by_gender['male'])}") + + # Assign names to any new speakers + new = assign_speakers_to_names(registry, corpus_id, speakers_by_gender, speaker_genders) + if new: + print(f"\n Assigned {len(new)} new voices") + save_registry(registry, registry_path) + else: + print(f"\n No new speakers to assign") + + # Download samples for all voices in this corpus + corpus_voices = { + name: info for name, info in registry["voices"].items() + if info["corpus"] == corpus_id } - print(f" {voice_name:8s} | {gender:6s} | speaker {speaker_id:5s} | {duration:.1f}s") + print(f"\n Downloading {len(corpus_voices)} voice samples...\n") - print(f"\nDownloaded {len(voices)}/{len(voice_map)} voices\n") + for voice_name, voice_info in sorted(corpus_voices.items()): + sid = voice_info["speaker_id"] + if sid not in speaker_samples: + print(f" WARNING: Speaker {sid} ({voice_name}) not in dataset") + continue + + samples = speaker_samples[sid] + best = pick_best_sample(samples) + if best is None: + print(f" WARNING: No suitable sample for '{voice_name}' (speaker {sid})") + continue + + audio = best["audio"] + transcript = best["text"].strip() + duration = len(audio["array"]) / audio["sampling_rate"] + + # Save WAV + out_path = output_dir / f"{voice_name}.wav" + sf.write(str(out_path), audio["array"], audio["sampling_rate"]) + + voice_info["ref_audio"] = f"cloned-voices/{voice_name}.wav" + voice_info["ref_text"] = transcript + voice_info["duration"] = round(duration, 1) + + all_voice_names.add(voice_name) + print(f" {voice_name:12s} | {voice_info['gender']:6s} | speaker {sid:5s} | {duration:.1f}s") + + print(f"\nDownloaded {len(all_voice_names)} voices total\n") + + # Build voices dict for config generation + voices = {} + for name in sorted(all_voice_names): + info = registry["voices"][name] + if "ref_audio" in info: + voices[name] = info # Generate YAML config print("Generating voice config...") - female_list = ", ".join(n for n in FEMALE_NAMES if n in voices) - male_list = ", ".join(n for n in MALE_NAMES if n in voices) + female_names = sorted(n for n, v in voices.items() if v["gender"] == "female") + male_names = sorted(n for n, v in voices.items() if v["gender"] == "male") + + # Order by name pool position for consistent output + female_pool = registry["name_pools"]["female"] + male_pool = registry["name_pools"]["male"] + female_names.sort(key=lambda n: female_pool.index(n) if n in female_pool else 999) + male_names.sort(key=lambda n: male_pool.index(n) if n in male_pool else 999) + + female_list = ", ".join(female_names) + male_list = ", ".join(male_names) lines = [ "# uncloseai-speech Voice Configuration", "# Diverse voice samples from LibriSpeech test-clean (public domain)", "# Each voice is a DISTINCT SPEAKER for Qwen3-TTS voice cloning", "# Gender verified from upstream LibriSpeech SPEAKERS.TXT", + "# Assignments locked in voice_registry.json (idempotent, append-only)", "#", f"# Female voices: {female_list}", f"# Male voices: {male_list}", "#", "# Source: LibriSpeech test-clean (public domain, LibriVox recordings)", - f"# {len(voices)} distinct speakers ({sum(1 for v in voices.values() if v['gender']=='female')} female, {sum(1 for v in voices.values() if v['gender']=='male')} male)", + f"# {len(voices)} distinct speakers ({len(female_names)} female, {len(male_names)} male)", "", "tts-1-qwen:", "", ] - # Write female voices first, then male - for voice_name in FEMALE_NAMES + MALE_NAMES: + # Write female voices first, then male (in pool order) + for voice_name in female_names + male_names: if voice_name not in voices: continue v = voices[voice_name] @@ -294,17 +389,29 @@ def main(): print(f" Written: {args.config_runtime}") # Save metadata JSON + meta = {} + for name in female_names + male_names: + if name in voices: + v = voices[name] + meta[name] = { + "ref_audio": v["ref_audio"], + "ref_text": v["ref_text"], + "speaker_id": v["speaker_id"], + "gender": v["gender"], + "duration": v["duration"], + } + meta_path = output_dir / "voices_metadata.json" with open(meta_path, "w") as f: - json.dump(voices, f, indent=2) + json.dump(meta, f, indent=2) print(f" Written: {meta_path}") print(f"\n=== Done! {len(voices)} diverse voices configured ===") print(f"\nVoice mapping:") - for name in FEMALE_NAMES + MALE_NAMES: + for name in female_names + male_names: if name in voices: v = voices[name] - print(f" {name:8s} -> speaker {v['speaker_id']} ({v['gender']})") + print(f" {name:12s} -> speaker {v['speaker_id']} ({v['gender']})") return 0 diff --git a/voice_registry.json b/voice_registry.json new file mode 100644 index 0000000..56b5f6d --- /dev/null +++ b/voice_registry.json @@ -0,0 +1,155 @@ +{ + "version": 1, + "voices": { + "aria": { + "corpus": "librispeech-test-clean", + "speaker_id": "2094", + "gender": "female", + "locked": true + }, + "clara": { + "corpus": "librispeech-test-clean", + "speaker_id": "3575", + "gender": "female", + "locked": true + }, + "elena": { + "corpus": "librispeech-test-clean", + "speaker_id": "2961", + "gender": "female", + "locked": true + }, + "grace": { + "corpus": "librispeech-test-clean", + "speaker_id": "8463", + "gender": "female", + "locked": true + }, + "hazel": { + "corpus": "librispeech-test-clean", + "speaker_id": "1995", + "gender": "female", + "locked": true + }, + "iris": { + "corpus": "librispeech-test-clean", + "speaker_id": "1284", + "gender": "female", + "locked": true + }, + "luna": { + "corpus": "librispeech-test-clean", + "speaker_id": "5142", + "gender": "female", + "locked": true + }, + "maya": { + "corpus": "librispeech-test-clean", + "speaker_id": "4446", + "gender": "female", + "locked": true + }, + "ruby": { + "corpus": "librispeech-test-clean", + "speaker_id": "1221", + "gender": "female", + "locked": true + }, + "sage": { + "corpus": "librispeech-test-clean", + "speaker_id": "4507", + "gender": "female", + "locked": true + }, + "sofia": { + "corpus": "librispeech-test-clean", + "speaker_id": "3729", + "gender": "female", + "locked": true + }, + "atlas": { + "corpus": "librispeech-test-clean", + "speaker_id": "6930", + "gender": "male", + "locked": true + }, + "caleb": { + "corpus": "librispeech-test-clean", + "speaker_id": "1320", + "gender": "male", + "locked": true + }, + "felix": { + "corpus": "librispeech-test-clean", + "speaker_id": "5639", + "gender": "male", + "locked": true + }, + "hugo": { + "corpus": "librispeech-test-clean", + "speaker_id": "260", + "gender": "male", + "locked": true + }, + "jasper": { + "corpus": "librispeech-test-clean", + "speaker_id": "7729", + "gender": "male", + "locked": true + }, + "kai": { + "corpus": "librispeech-test-clean", + "speaker_id": "7127", + "gender": "male", + "locked": true + }, + "leo": { + "corpus": "librispeech-test-clean", + "speaker_id": "8230", + "gender": "male", + "locked": true + }, + "marcus": { + "corpus": "librispeech-test-clean", + "speaker_id": "7176", + "gender": "male", + "locked": true + }, + "owen": { + "corpus": "librispeech-test-clean", + "speaker_id": "8455", + "gender": "male", + "locked": true + }, + "theo": { + "corpus": "librispeech-test-clean", + "speaker_id": "2830", + "gender": "male", + "locked": true + } + }, + "name_pools": { + "female": [ + "aria", "clara", "elena", "grace", "hazel", "iris", "luna", "maya", "ruby", "sage", "sofia", + "amber", "brooke", "cora", "diana", "eden", "faye", "gemma", "hope", "ivy", "jade", + "kira", "lena", "mila", "nadia", "olive", "pearl", "quinn", "rhea", "stella", "tessa", + "una", "vera", "wren", "xena", "yara", "zara", "adele", "blythe", "celeste", "daphne", + "elise", "flora", "greta", "hana", "isla", "june", "kaia", "lila", "maren", "nell" + ], + "male": [ + "atlas", "caleb", "felix", "hugo", "jasper", "kai", "leo", "marcus", "owen", "theo", + "archer", "blake", "cole", "dane", "ezra", "finn", "grant", "heath", "ivan", "jude", + "knox", "lance", "miles", "nash", "orion", "pierce", "reed", "seth", "trent", "wade", + "xander", "york", "zane", "anton", "brock", "cyrus", "drake", "ellis", "fox", "grey", + "holt", "ira", "joel", "keane", "lars", "milo", "noel", "otto", "pascal", "remy" + ] + }, + "corpora": { + "librispeech-test-clean": { + "dataset": "openslr/librispeech_asr", + "config": "clean", + "split": "test", + "description": "LibriSpeech test-clean, 40 speakers, public domain" + } + } +}