===============================================
N I N E N I N E S I X πΌ
===============================================
/\_/\
( -.- )ββββ
> ^ < β
===============================================
This pipeline takes audio datasets from HuggingFace and converts them into tokenized neural codec representations ready for training KaniTTS-2. It is built specifically around KaniTTS-2's codec and speaker embedding architecture β do not expect compatibility with other models unless they use the same NanoCodec tokenizer and WavLM-based speaker embedder.
The pipeline does two major things in sequence:
Both stages are controlled entirely through config.yaml.
make installedmake install
Creates a venv/ and installs all dependencies. Takes ~5 minutes on first run.
make login
Paste your token from huggingface.co/settings/tokens. Use a token with Write permission if you plan to upload datasets.
nano config.yaml
See the Configuration Reference section below for a full explanation of every setting.
make run
| Command | Description |
|---|---|
make install | Create venv and install all dependencies |
make login | Authenticate with Hugging Face |
make run | Run main.py via venv |
make clean | Remove the venv directory |
make help | Show command reference |
config.yaml
β
βΌ
PipelineManager.validate()
ββ check clustering column conflicts
ββ check group_sp_emb column source feasibility
β
βΌ (for each dataset in hf_datasets)
DatasetProcessor.load_dataset()
β
βΌ (if add_speaker_emb: true)
SpeakerEmbeddingProcessor.process()
ββ Step 1-4 Audio resampling (22050 Hz main + 16 kHz copy for WavLM)
ββ Step 5 Filter samples < 0.7 s
ββ Step 6 WavLM embedding inference (single or multi-GPU)
ββ Step 7 Filter NaN embeddings (mandatory)
ββ Step 8 [optional] UMAP + HDBSCAN clustering β speaker_column
ββ Step 9 [optional] Per-speaker embedding averaging β grouped_sp_emb
β
βΌ (if add_speaker_emb: false, group_sp_emb.do_this: true)
SpeakerEmbeddingGrouper.group() β standalone, uses existing embeddings
β
βΌ
NanoCodec Tokenization (single or multi-GPU workers)
ββ Reader workers shard and stream samples into queue
ββ GPU workers encode audio β 4-layer tokens β JSONL.gz shards
β
βΌ
assemble_and_save_final_dataset()
ββ [optional] save_to_disk (local)
ββ [optional] push_to_hub (HuggingFace)
Everything is controlled by config.yaml. Here is a fully annotated example:
# βββ Core tokenization settings βββββββββββββββββββββββββββββββββββββββββββββββ
base_settings:
audio_codec: nvidia/nemo-nano-codec-22khz-0.6kbps-12.5fps
num_readers: 8
qsize: 100000
OUT_DIR: shards
gzip_level: 1
buffer_size: 16777216
lines_per_file: 50000
load_dataset_num_proc: 20
# βββ Speaker embedding settings βββββββββββββββββββββββββββββββββββββββββββββββ
speaker_embedding_settings:
add_speaker_emb: true
model:
name: "nineninesix/speaker-emb-tbr"
embedding_column: "wavlm_embedding"
audio:
target_sample_rate: 16000
max_duration_sec: 30.0
processing:
batch_size: 2
use_multiprocessing: true
clustering:
do_clusters: true
speaker_column: label
UMAP:
n_neighbors: 15
min_dist: 0.1
metric: cosine
random_state: 42
n_components: 5
HDBSCAN:
min_cluster_size: 15
min_samples: 3
metric: euclidean
cluster_selection_method: eom
group_sp_emb:
do_this: true
group_by_column_name: label
grouped_embedding_column: grouped_sp_emb
# βββ Datasets βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
hf_datasets:
- name: your-username/dataset-name
sub_name: null
split: train
text_column_name: text
audio_column_name: audio
speaker_column_name: null
add_constant:
- key: speaker_id
value: alice
# βββ Output βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
save_settings:
local: train_dataset
hf_upload: null
base_settings| Key | Description | Default |
|---|---|---|
audio_codec | NeMo NanoCodec model from HuggingFace Hub | nvidia/nemo-nano-codec-22khz-0.6kbps-12.5fps |
num_readers | Number of CPU reader processes that stream data into the tokenization queue | 8 |
qsize | Max items held in the shared queue between readers and GPU workers | 100000 |
OUT_DIR | Directory for intermediate JSONL.gz shards | shards |
gzip_level | Gzip compression level (1 = fastest, 9 = smallest) | 1 |
buffer_size | Write buffer in bytes (16 MB recommended) | 16777216 |
lines_per_file | How many samples per output shard file | 50000 |
load_dataset_num_proc | Parallel processes used when loading datasets from HF | 20 |
speaker_embedding_settingsadd_speaker_embMaster switch. Set to true to run the full speaker embedding pipeline (WavLM inference + optional clustering + optional grouping). Set to false to skip embedding entirely β unless group_sp_emb.do_this: true, in which case grouping runs standalone on embeddings that already exist in the dataset.
model| Key | Description |
|---|---|
name | HuggingFace model repo for the WavLM speaker embedder |
embedding_column | Name of the output column added to the dataset (e.g. wavlm_embedding) |
The model used is Orange/Speaker-wavLM-tbr (or nineninesix/speaker-emb-tbr). It outputs 128-dimensional L2-normalized embedding vectors.
audio| Key | Description |
|---|---|
target_sample_rate | WavLM operates at 16000 Hz β do not change unless you switch models |
max_duration_sec | Audio clips longer than this are truncated before embedding. 20β30 s is recommended. |
Samples shorter than 0.7 seconds are automatically removed before embedding β they produce unreliable speaker representations.
processing| Key | Description |
|---|---|
batch_size | Samples per batch per GPU during WavLM inference |
use_multiprocessing | Enable multi-GPU inference (one process per GPU); set false for single-GPU |
With multiple GPUs throughput scales near-linearly (e.g. 4 GPUs β 4Γ speed). Single GPU works fine too.
clusteringUnsupervised speaker discovery using UMAP + HDBSCAN. Useful when the source dataset has no speaker labels, or when you want to automatically discover speaker clusters across a mixed dataset.
| Key | Description |
|---|---|
do_clusters | Enable clustering. If false, all keys below are ignored |
speaker_column | Name of the integer cluster ID column added to the dataset. Default: speaker_id. Must not conflict with any add_constant key β the pipeline will warn you at startup if it does. |
UMAP parametersReduces the 128-dim WavLM embeddings to a lower-dimensional space before clustering. Standard UMAP parameters apply.
| Key | Meaning |
|---|---|
n_components | Target dimensionality (5 is a good default) |
n_neighbors | Balances local vs global structure |
min_dist | Minimum distance between points in low-dim space |
metric | Distance metric (cosine works well for normalized embeddings) |
random_state | Seed for reproducibility |
HDBSCAN parametersDensity-based hierarchical clustering. Samples not assigned to any cluster (noise, label = -1) are removed from the dataset.
| Key | Meaning |
|---|---|
min_cluster_size | Minimum number of samples to form a cluster |
min_samples | Controls how conservative the clustering is |
metric | Distance metric for clustering |
cluster_selection_method | eom (Excess of Mass) or leaf |
Note: If many samples become noise (-1), try reducing min_cluster_size or min_samples.
group_sp_embAverages all per-sample WavLM embeddings for each unique speaker value, and writes the average back as a new column. This gives the model a stable, generalized speaker representation rather than the noisy per-utterance vector.
| Key | Description |
|---|---|
do_this | Enable per-speaker embedding averaging |
group_by_column_name | Column used to group samples by speaker (e.g. the clustering output column, or a constant like speaker_id) |
grouped_embedding_column | Name of the new averaged embedding column added to the dataset |
The resulting column (grouped_sp_emb by default) is automatically preserved through tokenization and appears in the final dataset alongside the codec tokens.
Before running, the pipeline checks that group_by_column_name will actually exist in the dataset. It looks for the column in three places:
add_constant keys in any dataset configspeaker_column, when do_clusters: true)speaker_column_name of any dataset configIf none of these sources can provide the column, a warning is shown and you are offered the option to disable grouping for the current run. If you are sure the column already exists in the source dataset, you can proceed by answering n.
If add_speaker_emb: false but group_sp_emb.do_this: true, the grouper runs independently β it assumes the embedding column already exists in the loaded dataset (e.g. from a previous pipeline run). This is useful to re-run only the grouping step without re-computing embeddings.
hf_datasetsList of datasets to process. All datasets are processed sequentially and merged into a single final dataset.
| Key | Description |
|---|---|
name | HuggingFace dataset repo (e.g. openslr/librispeech_asr) |
sub_name | Dataset configuration / subset (e.g. clean). Set to null if none. |
split | Split to load: train, test, validation, or a slice like train[:500] |
text_column_name | Column containing the text transcription |
audio_column_name | Column containing the audio |
speaker_column_name | Column containing speaker identity (if present in the source dataset). Set to null if absent. |
add_constant | List of {key, value} pairs added as a constant column to every sample. Useful for tagging language, speaker, dataset source, etc. |
Note on add_constant and speaker_column_name: These are the two ways to inject a speaker column when the source dataset has no speaker labels. If you are running clustering, the pipeline produces its own speaker column β set speaker_column_name: null in that case.
save_settings| Key | Description |
|---|---|
local | Path to save the final assembled dataset to disk (HuggingFace Arrow format). Set to null to skip. |
hf_upload | HuggingFace repo to push the final dataset to (e.g. your-username/dataset-name). Set to null to skip. Uploaded as private by default. |
During processing, each GPU worker writes compressed shards to OUT_DIR:
shards/
dataset-name-worker00-00000.jsonl.gz
dataset-name-worker00-00001.jsonl.gz
dataset-name-worker01-00000.jsonl.gz
...
After assembly, each sample contains:
{
"text": "Hello world",
"nano_layer_1": [123, 456, 789, ...],
"nano_layer_2": [234, 567, 890, ...],
"nano_layer_3": [345, 678, 901, ...],
"nano_layer_4": [456, 789, 012, ...],
"encoded_len": 150,
"wavlm_embedding": [0.021, -0.043, ...],
"label": 7,
"grouped_sp_emb": [0.019, -0.038, ...]
}
Which columns appear depends on your config:
nano_layer_*, encoded_len β always present (core tokenization output)wavlm_embedding β present when add_speaker_emb: truelabel (or your speaker_column) β present when do_clusters: truegrouped_sp_emb (or your grouped_embedding_column) β present when group_sp_emb.do_this: trueadd_constant keys and speaker_column_name values β passed throughRuntimeError: β ERROR: No CUDA devices found!
Check drivers and CUDA toolkit with nvidia-smi.
Run make login and paste a token with Write permission from huggingface.co/settings/tokens.
num_readers (more CPU reader processes)load_dataset_num_proc (faster initial dataset loading)nvidia-smiprocessing.batch_size in speaker_embedding_settingsqsizenum_readersLower min_cluster_size and min_samples in HDBSCAN settings. Noise samples are removed from the dataset before tokenization.
At startup you may see:
β οΈ WARNING: Clustering column naming conflict detected!
This means speaker_column in clustering matches a key in add_constant. The pipeline will prompt you to enter a different column name. To avoid this, change speaker_column in config.yaml before running.
The embedder uses Orange/Speaker-wavLM-tbr, a WavLM encoder fine-tuned for speaker recognition with a stats-pooling + projection head. It outputs 128-dimensional L2-normalized vectors.
Key properties:
The pipeline maintains two audio channels to avoid quality loss:
audio_spk column is decoded at 16 kHz only for embedding
Both are produced from the same raw bytes to avoid double re-encoding.Before embedding:
max_duration_sec are truncated (not padded)NaN filtering happens before clustering and grouping β both downstream steps receive clean data.
By default, wavlm_embedding contains one vector per utterance. This is noisy β different recordings of the same speaker vary due to room acoustics, microphone, emotion, speaking rate.
group_sp_emb solves this by averaging all utterance embeddings for each unique speaker:
Speaker A: emb_1, emb_2, emb_3, ... β mean(emb_1..N) β grouped_sp_emb
Speaker B: emb_1, emb_2, emb_3, ... β mean(emb_1..N) β grouped_sp_emb
Every sample of Speaker A receives the same averaged vector. This forces the model to learn a stable speaker representation that doesn't overfit to individual recording artifacts.
Why this matters for TTS training: If the model sees a different speaker vector for every utterance of the same speaker, it may learn to associate voice timbre with specific recording conditions rather than the actual speaker. Averaged embeddings remove this confound and lead to better speaker generalization.
Use clustering when:
n_components dimensions (default: 5)-1 (HDBSCAN noise) are droppedspeaker_columnCluster IDs are integers (0, 1, 2, ...). They have no fixed meaning across runs β HDBSCAN is non-deterministic unless random_state is set in UMAP.
Too many noise samples (removed):
min_cluster_size β makes clusters easier to formmin_samples β less conservative noise detectionToo few clusters (everything merged):
min_cluster_size β forces finer clustersn_components in UMAP β preserves more local structureClusters do not correspond to real speakers:
n_neighbors in UMAP β more global structure preservedmetric: cosine in UMAP (best for normalized embeddings)The most common and recommended setup:
clustering:
do_clusters: true
speaker_column: label
group_sp_emb:
do_this: true
group_by_column_name: label # group by cluster ID
grouped_embedding_column: grouped_sp_emb
The pipeline runs clustering first, assigns integer cluster IDs, then computes the average embedding per cluster. The result: every sample in the same speaker cluster receives the same stable averaged vector.
All datasets in hf_datasets are processed sequentially and merged into one final dataset. Each dataset can have:
add_constant fields (e.g. language, dataset source tag)speaker_column_nameExample: mixing a labeled multi-speaker dataset with a single-speaker dataset:
hf_datasets:
- name: openslr/librispeech_asr
sub_name: clean
split: train
text_column_name: text
audio_column_name: audio
speaker_column_name: speaker_id # already has speaker labels
add_constant:
- key: lang
value: en
- name: your-org/single-speaker-dataset
sub_name: null
split: train
text_column_name: sentence
audio_column_name: audio
speaker_column_name: null
add_constant:
- key: speaker_id # inject a constant speaker label
value: alice
- key: lang
value: en
When add_speaker_emb: true and do_clusters: true, clustering runs independently per dataset. Each dataset's speaker IDs are independent integers β they do not share a global speaker space across datasets.
speaker_embedding_settings:
add_speaker_emb: true
clustering:
do_clusters: false
group_sp_emb:
do_this: false
hf_datasets:
- name: your-org/dataset
speaker_column_name: null
add_constant:
- key: speaker
value: alice
speaker_embedding_settings:
add_speaker_emb: true
clustering:
do_clusters: false
group_sp_emb:
do_this: true
group_by_column_name: speaker_id # use existing labels
grouped_embedding_column: grouped_sp_emb
hf_datasets:
- name: your-org/dataset
speaker_column_name: speaker_id # column already in dataset
speaker_embedding_settings:
add_speaker_emb: true
clustering:
do_clusters: true
speaker_column: label
UMAP:
n_components: 5
metric: cosine
HDBSCAN:
min_cluster_size: 15
min_samples: 3
group_sp_emb:
do_this: true
group_by_column_name: label
grouped_embedding_column: grouped_sp_emb
hf_datasets:
- name: your-org/unlabeled-dataset
speaker_column_name: null
speaker_embedding_settings:
add_speaker_emb: false
group_sp_emb:
do_this: false
speaker_embedding_settings:
add_speaker_emb: false # skip re-embedding
model:
embedding_column: wavlm_embedding
group_sp_emb:
do_this: true
group_by_column_name: speaker_id
grouped_embedding_column: grouped_sp_emb
The grouper will run standalone using the wavlm_embedding column that already exists in the loaded dataset.
After computing grouped_sp_emb (the per-speaker averaged embedding), you can measure how similar each utterance's own wavlm_embedding is to its speaker's averaged vector. This cosine similarity score is a powerful signal for audio quality:
Filtering with a threshold around 0.6 removes the worst samples while keeping natural voice variation. You will typically lose 2β10% of samples depending on dataset quality β this is normal and desirable.
It`s example for our work...
Both wavlm_embedding and grouped_sp_emb are L2-normalized 128-dim vectors. Cosine similarity between two L2-normalized vectors is simply their dot product:
import numpy as np
from datasets import load_from_disk
dataset = load_from_disk("train_dataset")
def add_cosine_sim(sample):
emb = np.array(sample["wavlm_embedding"], dtype=np.float32)
grp = np.array(sample["grouped_sp_emb"], dtype=np.float32)
# Both are L2-normalized, so dot product == cosine similarity
sim = float(np.dot(emb, grp))
return {"cosine_sim": sim}
dataset = dataset.map(add_cosine_sim, num_proc=10, desc="Computing cosine similarity")
# Inspect the distribution
sims = np.array(dataset["cosine_sim"])
print(f"Mean: {sims.mean():.3f}")
print(f"Median: {np.median(sims):.3f}")
print(f"< 0.0: {(sims < 0.0).sum()} samples ({(sims < 0.0).mean()*100:.1f}%)")
print(f"< 0.3: {(sims < 0.3).sum()} samples ({(sims < 0.3).mean()*100:.1f}%)")
print(f"< 0.6: {(sims < 0.6).sum()} samples ({(sims < 0.6).mean()*100:.1f}%)")
If you see a large fraction of samples with cosine similarity near 0 or negative β that dataset has serious quality problems and cleaning is essential.
THRESHOLD = 0.6
filtered = dataset.filter(
lambda s: s["cosine_sim"] >= THRESHOLD,
num_proc=10,
desc=f"Filtering cosine_sim < {THRESHOLD}",
)
print(f"Before: {len(dataset)} samples")
print(f"After: {len(filtered)} samples ({len(filtered)/len(dataset)*100:.1f}% retained)")
# Save the filtered dataset
filtered.save_to_disk("train_dataset_filtered")
After removing bad samples, the previously computed grouped_sp_emb vectors are stale β they were averaged over the unfiltered set. You must recompute them. Use the standalone script:
venv/bin/python regroup_embeddings.py \
--dataset train_dataset_filtered \
--embedding-col wavlm_embedding \
--group-col label \
--output-col grouped_sp_emb \
--output train_dataset_clean
To also push the result to HuggingFace:
venv/bin/python regroup_embeddings.py \
--dataset train_dataset_filtered \
--embedding-col wavlm_embedding \
--group-col label \
--output-col grouped_sp_emb \
--output train_dataset_clean \
--hub your-username/dataset-clean
All arguments:
| Argument | Default | Description |
|---|---|---|
--dataset | required | Path to filtered dataset (Arrow format from save_to_disk) |
--embedding-col | wavlm_embedding | Per-utterance embedding column |
--group-col | label | Speaker grouping column (cluster ID or speaker name) |
--output-col | grouped_sp_emb | Output column name for the recomputed averages |
--output | required | Local path to save the result |
--hub | none | HuggingFace repo to push to (optional) |
1. make run β full pipeline (embed + cluster + group)
2. compute cosine_sim per sample β inspect distribution
3. filter by threshold (β₯ 0.6) β remove bad audio
4. regroup_embeddings.py β recompute grouped_sp_emb on clean data
5. use train_dataset_clean for training
If after regrouping you still see many low-similarity samples, consider tightening the threshold (0.65β0.7) or reviewing your clustering parameters β poor clusters lead to noisy group averages.
Apache 2.0. See LICENSE for details.
WavLM model (Orange/Speaker-wavLM-tbr): CC-BY-SA-3.0 (Orange SA).
2 commits
Python
91.9%
Makefile
8.1%
===============================================
N I N E N I N E S I X πΌ
===============================================
/\_/\
( -.- )ββββ
> ^ < β
===============================================
This pipeline takes audio datasets from HuggingFace and converts them into tokenized neural codec representations ready for training KaniTTS-2. It is built specifically around KaniTTS-2's codec and speaker embedding architecture β do not expect compatibility with other models unless they use the same NanoCodec tokenizer and WavLM-based speaker embedder.
The pipeline does two major things in sequence:
Both stages are controlled entirely through config.yaml.
make installedmake install
Creates a venv/ and installs all dependencies. Takes ~5 minutes on first run.
make login
Paste your token from huggingface.co/settings/tokens. Use a token with Write permission if you plan to upload datasets.
nano config.yaml
See the Configuration Reference section below for a full explanation of every setting.
make run
| Command | Description |
|---|---|
make install | Create venv and install all dependencies |
make login | Authenticate with Hugging Face |
make run | Run main.py via venv |
make clean | Remove the venv directory |
make help | Show command reference |
config.yaml
β
βΌ
PipelineManager.validate()
ββ check clustering column conflicts
ββ check group_sp_emb column source feasibility
β
βΌ (for each dataset in hf_datasets)
DatasetProcessor.load_dataset()
β
βΌ (if add_speaker_emb: true)
SpeakerEmbeddingProcessor.process()
ββ Step 1-4 Audio resampling (22050 Hz main + 16 kHz copy for WavLM)
ββ Step 5 Filter samples < 0.7 s
ββ Step 6 WavLM embedding inference (single or multi-GPU)
ββ Step 7 Filter NaN embeddings (mandatory)
ββ Step 8 [optional] UMAP + HDBSCAN clustering β speaker_column
ββ Step 9 [optional] Per-speaker embedding averaging β grouped_sp_emb
β
βΌ (if add_speaker_emb: false, group_sp_emb.do_this: true)
SpeakerEmbeddingGrouper.group() β standalone, uses existing embeddings
β
βΌ
NanoCodec Tokenization (single or multi-GPU workers)
ββ Reader workers shard and stream samples into queue
ββ GPU workers encode audio β 4-layer tokens β JSONL.gz shards
β
βΌ
assemble_and_save_final_dataset()
ββ [optional] save_to_disk (local)
ββ [optional] push_to_hub (HuggingFace)
Everything is controlled by config.yaml. Here is a fully annotated example:
# βββ Core tokenization settings βββββββββββββββββββββββββββββββββββββββββββββββ
base_settings:
audio_codec: nvidia/nemo-nano-codec-22khz-0.6kbps-12.5fps
num_readers: 8
qsize: 100000
OUT_DIR: shards
gzip_level: 1
buffer_size: 16777216
lines_per_file: 50000
load_dataset_num_proc: 20
# βββ Speaker embedding settings βββββββββββββββββββββββββββββββββββββββββββββββ
speaker_embedding_settings:
add_speaker_emb: true
model:
name: "nineninesix/speaker-emb-tbr"
embedding_column: "wavlm_embedding"
audio:
target_sample_rate: 16000
max_duration_sec: 30.0
processing:
batch_size: 2
use_multiprocessing: true
clustering:
do_clusters: true
speaker_column: label
UMAP:
n_neighbors: 15
min_dist: 0.1
metric: cosine
random_state: 42
n_components: 5
HDBSCAN:
min_cluster_size: 15
min_samples: 3
metric: euclidean
cluster_selection_method: eom
group_sp_emb:
do_this: true
group_by_column_name: label
grouped_embedding_column: grouped_sp_emb
# βββ Datasets βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
hf_datasets:
- name: your-username/dataset-name
sub_name: null
split: train
text_column_name: text
audio_column_name: audio
speaker_column_name: null
add_constant:
- key: speaker_id
value: alice
# βββ Output βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
save_settings:
local: train_dataset
hf_upload: null
base_settings| Key | Description | Default |
|---|---|---|
audio_codec | NeMo NanoCodec model from HuggingFace Hub | nvidia/nemo-nano-codec-22khz-0.6kbps-12.5fps |
num_readers | Number of CPU reader processes that stream data into the tokenization queue | 8 |
qsize | Max items held in the shared queue between readers and GPU workers | 100000 |
OUT_DIR | Directory for intermediate JSONL.gz shards | shards |
gzip_level | Gzip compression level (1 = fastest, 9 = smallest) | 1 |
buffer_size | Write buffer in bytes (16 MB recommended) | 16777216 |
lines_per_file | How many samples per output shard file | 50000 |
load_dataset_num_proc | Parallel processes used when loading datasets from HF | 20 |
speaker_embedding_settingsadd_speaker_embMaster switch. Set to true to run the full speaker embedding pipeline (WavLM inference + optional clustering + optional grouping). Set to false to skip embedding entirely β unless group_sp_emb.do_this: true, in which case grouping runs standalone on embeddings that already exist in the dataset.
model| Key | Description |
|---|---|
name | HuggingFace model repo for the WavLM speaker embedder |
embedding_column | Name of the output column added to the dataset (e.g. wavlm_embedding) |
The model used is Orange/Speaker-wavLM-tbr (or nineninesix/speaker-emb-tbr). It outputs 128-dimensional L2-normalized embedding vectors.
audio| Key | Description |
|---|---|
target_sample_rate | WavLM operates at 16000 Hz β do not change unless you switch models |
max_duration_sec | Audio clips longer than this are truncated before embedding. 20β30 s is recommended. |
Samples shorter than 0.7 seconds are automatically removed before embedding β they produce unreliable speaker representations.
processing| Key | Description |
|---|---|
batch_size | Samples per batch per GPU during WavLM inference |
use_multiprocessing | Enable multi-GPU inference (one process per GPU); set false for single-GPU |
With multiple GPUs throughput scales near-linearly (e.g. 4 GPUs β 4Γ speed). Single GPU works fine too.
clusteringUnsupervised speaker discovery using UMAP + HDBSCAN. Useful when the source dataset has no speaker labels, or when you want to automatically discover speaker clusters across a mixed dataset.
| Key | Description |
|---|---|
do_clusters | Enable clustering. If false, all keys below are ignored |
speaker_column | Name of the integer cluster ID column added to the dataset. Default: speaker_id. Must not conflict with any add_constant key β the pipeline will warn you at startup if it does. |
UMAP parametersReduces the 128-dim WavLM embeddings to a lower-dimensional space before clustering. Standard UMAP parameters apply.
| Key | Meaning |
|---|---|
n_components | Target dimensionality (5 is a good default) |
n_neighbors | Balances local vs global structure |
min_dist | Minimum distance between points in low-dim space |
metric | Distance metric (cosine works well for normalized embeddings) |
random_state | Seed for reproducibility |
HDBSCAN parametersDensity-based hierarchical clustering. Samples not assigned to any cluster (noise, label = -1) are removed from the dataset.
| Key | Meaning |
|---|---|
min_cluster_size | Minimum number of samples to form a cluster |
min_samples | Controls how conservative the clustering is |
metric | Distance metric for clustering |
cluster_selection_method | eom (Excess of Mass) or leaf |
Note: If many samples become noise (-1), try reducing min_cluster_size or min_samples.
group_sp_embAverages all per-sample WavLM embeddings for each unique speaker value, and writes the average back as a new column. This gives the model a stable, generalized speaker representation rather than the noisy per-utterance vector.
| Key | Description |
|---|---|
do_this | Enable per-speaker embedding averaging |
group_by_column_name | Column used to group samples by speaker (e.g. the clustering output column, or a constant like speaker_id) |
grouped_embedding_column | Name of the new averaged embedding column added to the dataset |
The resulting column (grouped_sp_emb by default) is automatically preserved through tokenization and appears in the final dataset alongside the codec tokens.
Before running, the pipeline checks that group_by_column_name will actually exist in the dataset. It looks for the column in three places:
add_constant keys in any dataset configspeaker_column, when do_clusters: true)speaker_column_name of any dataset configIf none of these sources can provide the column, a warning is shown and you are offered the option to disable grouping for the current run. If you are sure the column already exists in the source dataset, you can proceed by answering n.
If add_speaker_emb: false but group_sp_emb.do_this: true, the grouper runs independently β it assumes the embedding column already exists in the loaded dataset (e.g. from a previous pipeline run). This is useful to re-run only the grouping step without re-computing embeddings.
hf_datasetsList of datasets to process. All datasets are processed sequentially and merged into a single final dataset.
| Key | Description |
|---|---|
name | HuggingFace dataset repo (e.g. openslr/librispeech_asr) |
sub_name | Dataset configuration / subset (e.g. clean). Set to null if none. |
split | Split to load: train, test, validation, or a slice like train[:500] |
text_column_name | Column containing the text transcription |
audio_column_name | Column containing the audio |
speaker_column_name | Column containing speaker identity (if present in the source dataset). Set to null if absent. |
add_constant | List of {key, value} pairs added as a constant column to every sample. Useful for tagging language, speaker, dataset source, etc. |
Note on add_constant and speaker_column_name: These are the two ways to inject a speaker column when the source dataset has no speaker labels. If you are running clustering, the pipeline produces its own speaker column β set speaker_column_name: null in that case.
save_settings| Key | Description |
|---|---|
local | Path to save the final assembled dataset to disk (HuggingFace Arrow format). Set to null to skip. |
hf_upload | HuggingFace repo to push the final dataset to (e.g. your-username/dataset-name). Set to null to skip. Uploaded as private by default. |
During processing, each GPU worker writes compressed shards to OUT_DIR:
shards/
dataset-name-worker00-00000.jsonl.gz
dataset-name-worker00-00001.jsonl.gz
dataset-name-worker01-00000.jsonl.gz
...
After assembly, each sample contains:
{
"text": "Hello world",
"nano_layer_1": [123, 456, 789, ...],
"nano_layer_2": [234, 567, 890, ...],
"nano_layer_3": [345, 678, 901, ...],
"nano_layer_4": [456, 789, 012, ...],
"encoded_len": 150,
"wavlm_embedding": [0.021, -0.043, ...],
"label": 7,
"grouped_sp_emb": [0.019, -0.038, ...]
}
Which columns appear depends on your config:
nano_layer_*, encoded_len β always present (core tokenization output)wavlm_embedding β present when add_speaker_emb: truelabel (or your speaker_column) β present when do_clusters: truegrouped_sp_emb (or your grouped_embedding_column) β present when group_sp_emb.do_this: trueadd_constant keys and speaker_column_name values β passed throughRuntimeError: β ERROR: No CUDA devices found!
Check drivers and CUDA toolkit with nvidia-smi.
Run make login and paste a token with Write permission from huggingface.co/settings/tokens.
num_readers (more CPU reader processes)load_dataset_num_proc (faster initial dataset loading)nvidia-smiprocessing.batch_size in speaker_embedding_settingsqsizenum_readersLower min_cluster_size and min_samples in HDBSCAN settings. Noise samples are removed from the dataset before tokenization.
At startup you may see:
β οΈ WARNING: Clustering column naming conflict detected!
This means speaker_column in clustering matches a key in add_constant. The pipeline will prompt you to enter a different column name. To avoid this, change speaker_column in config.yaml before running.
The embedder uses Orange/Speaker-wavLM-tbr, a WavLM encoder fine-tuned for speaker recognition with a stats-pooling + projection head. It outputs 128-dimensional L2-normalized vectors.
Key properties:
The pipeline maintains two audio channels to avoid quality loss:
audio_spk column is decoded at 16 kHz only for embedding
Both are produced from the same raw bytes to avoid double re-encoding.Before embedding:
max_duration_sec are truncated (not padded)NaN filtering happens before clustering and grouping β both downstream steps receive clean data.
By default, wavlm_embedding contains one vector per utterance. This is noisy β different recordings of the same speaker vary due to room acoustics, microphone, emotion, speaking rate.
group_sp_emb solves this by averaging all utterance embeddings for each unique speaker:
Speaker A: emb_1, emb_2, emb_3, ... β mean(emb_1..N) β grouped_sp_emb
Speaker B: emb_1, emb_2, emb_3, ... β mean(emb_1..N) β grouped_sp_emb
Every sample of Speaker A receives the same averaged vector. This forces the model to learn a stable speaker representation that doesn't overfit to individual recording artifacts.
Why this matters for TTS training: If the model sees a different speaker vector for every utterance of the same speaker, it may learn to associate voice timbre with specific recording conditions rather than the actual speaker. Averaged embeddings remove this confound and lead to better speaker generalization.
Use clustering when:
n_components dimensions (default: 5)-1 (HDBSCAN noise) are droppedspeaker_columnCluster IDs are integers (0, 1, 2, ...). They have no fixed meaning across runs β HDBSCAN is non-deterministic unless random_state is set in UMAP.
Too many noise samples (removed):
min_cluster_size β makes clusters easier to formmin_samples β less conservative noise detectionToo few clusters (everything merged):
min_cluster_size β forces finer clustersn_components in UMAP β preserves more local structureClusters do not correspond to real speakers:
n_neighbors in UMAP β more global structure preservedmetric: cosine in UMAP (best for normalized embeddings)The most common and recommended setup:
clustering:
do_clusters: true
speaker_column: label
group_sp_emb:
do_this: true
group_by_column_name: label # group by cluster ID
grouped_embedding_column: grouped_sp_emb
The pipeline runs clustering first, assigns integer cluster IDs, then computes the average embedding per cluster. The result: every sample in the same speaker cluster receives the same stable averaged vector.
All datasets in hf_datasets are processed sequentially and merged into one final dataset. Each dataset can have:
add_constant fields (e.g. language, dataset source tag)speaker_column_nameExample: mixing a labeled multi-speaker dataset with a single-speaker dataset:
hf_datasets:
- name: openslr/librispeech_asr
sub_name: clean
split: train
text_column_name: text
audio_column_name: audio
speaker_column_name: speaker_id # already has speaker labels
add_constant:
- key: lang
value: en
- name: your-org/single-speaker-dataset
sub_name: null
split: train
text_column_name: sentence
audio_column_name: audio
speaker_column_name: null
add_constant:
- key: speaker_id # inject a constant speaker label
value: alice
- key: lang
value: en
When add_speaker_emb: true and do_clusters: true, clustering runs independently per dataset. Each dataset's speaker IDs are independent integers β they do not share a global speaker space across datasets.
speaker_embedding_settings:
add_speaker_emb: true
clustering:
do_clusters: false
group_sp_emb:
do_this: false
hf_datasets:
- name: your-org/dataset
speaker_column_name: null
add_constant:
- key: speaker
value: alice
speaker_embedding_settings:
add_speaker_emb: true
clustering:
do_clusters: false
group_sp_emb:
do_this: true
group_by_column_name: speaker_id # use existing labels
grouped_embedding_column: grouped_sp_emb
hf_datasets:
- name: your-org/dataset
speaker_column_name: speaker_id # column already in dataset
speaker_embedding_settings:
add_speaker_emb: true
clustering:
do_clusters: true
speaker_column: label
UMAP:
n_components: 5
metric: cosine
HDBSCAN:
min_cluster_size: 15
min_samples: 3
group_sp_emb:
do_this: true
group_by_column_name: label
grouped_embedding_column: grouped_sp_emb
hf_datasets:
- name: your-org/unlabeled-dataset
speaker_column_name: null
speaker_embedding_settings:
add_speaker_emb: false
group_sp_emb:
do_this: false
speaker_embedding_settings:
add_speaker_emb: false # skip re-embedding
model:
embedding_column: wavlm_embedding
group_sp_emb:
do_this: true
group_by_column_name: speaker_id
grouped_embedding_column: grouped_sp_emb
The grouper will run standalone using the wavlm_embedding column that already exists in the loaded dataset.
After computing grouped_sp_emb (the per-speaker averaged embedding), you can measure how similar each utterance's own wavlm_embedding is to its speaker's averaged vector. This cosine similarity score is a powerful signal for audio quality:
Filtering with a threshold around 0.6 removes the worst samples while keeping natural voice variation. You will typically lose 2β10% of samples depending on dataset quality β this is normal and desirable.
It`s example for our work...
Both wavlm_embedding and grouped_sp_emb are L2-normalized 128-dim vectors. Cosine similarity between two L2-normalized vectors is simply their dot product:
import numpy as np
from datasets import load_from_disk
dataset = load_from_disk("train_dataset")
def add_cosine_sim(sample):
emb = np.array(sample["wavlm_embedding"], dtype=np.float32)
grp = np.array(sample["grouped_sp_emb"], dtype=np.float32)
# Both are L2-normalized, so dot product == cosine similarity
sim = float(np.dot(emb, grp))
return {"cosine_sim": sim}
dataset = dataset.map(add_cosine_sim, num_proc=10, desc="Computing cosine similarity")
# Inspect the distribution
sims = np.array(dataset["cosine_sim"])
print(f"Mean: {sims.mean():.3f}")
print(f"Median: {np.median(sims):.3f}")
print(f"< 0.0: {(sims < 0.0).sum()} samples ({(sims < 0.0).mean()*100:.1f}%)")
print(f"< 0.3: {(sims < 0.3).sum()} samples ({(sims < 0.3).mean()*100:.1f}%)")
print(f"< 0.6: {(sims < 0.6).sum()} samples ({(sims < 0.6).mean()*100:.1f}%)")
If you see a large fraction of samples with cosine similarity near 0 or negative β that dataset has serious quality problems and cleaning is essential.
THRESHOLD = 0.6
filtered = dataset.filter(
lambda s: s["cosine_sim"] >= THRESHOLD,
num_proc=10,
desc=f"Filtering cosine_sim < {THRESHOLD}",
)
print(f"Before: {len(dataset)} samples")
print(f"After: {len(filtered)} samples ({len(filtered)/len(dataset)*100:.1f}% retained)")
# Save the filtered dataset
filtered.save_to_disk("train_dataset_filtered")
After removing bad samples, the previously computed grouped_sp_emb vectors are stale β they were averaged over the unfiltered set. You must recompute them. Use the standalone script:
venv/bin/python regroup_embeddings.py \
--dataset train_dataset_filtered \
--embedding-col wavlm_embedding \
--group-col label \
--output-col grouped_sp_emb \
--output train_dataset_clean
To also push the result to HuggingFace:
venv/bin/python regroup_embeddings.py \
--dataset train_dataset_filtered \
--embedding-col wavlm_embedding \
--group-col label \
--output-col grouped_sp_emb \
--output train_dataset_clean \
--hub your-username/dataset-clean
All arguments:
| Argument | Default | Description |
|---|---|---|
--dataset | required | Path to filtered dataset (Arrow format from save_to_disk) |
--embedding-col | wavlm_embedding | Per-utterance embedding column |
--group-col | label | Speaker grouping column (cluster ID or speaker name) |
--output-col | grouped_sp_emb | Output column name for the recomputed averages |
--output | required | Local path to save the result |
--hub | none | HuggingFace repo to push to (optional) |
1. make run β full pipeline (embed + cluster + group)
2. compute cosine_sim per sample β inspect distribution
3. filter by threshold (β₯ 0.6) β remove bad audio
4. regroup_embeddings.py β recompute grouped_sp_emb on clean data
5. use train_dataset_clean for training
If after regrouping you still see many low-similarity samples, consider tightening the threshold (0.65β0.7) or reviewing your clustering parameters β poor clusters lead to noisy group averages.
Apache 2.0. See LICENSE for details.
WavLM model (Orange/Speaker-wavLM-tbr): CC-BY-SA-3.0 (Orange SA).
2 commits
Python
91.9%
Makefile
8.1%