One backbone, many checkpoints, distilled to reproduce the teacher representations that TTSDS2 uses to score synthetic speech.
The headline idea: instead of running 9+ independent teacher models at evaluation time (HuBERT, WavLM, wav2vec 2.0, Whisper, d-Vector, WeSpeaker, WORLD F0, MPM, syllables/sec, emotion2vec, …), we train a single XLS-R 300M student to predict each teacher's representation on 1-second chunks of multilingual Emilia. Each finetune produces one checkpoint of the same architecture; downstream code can load whichever checkpoint matches the factor it cares about (Generic, Speaker, Prosody, Intelligibility, Emotion).
Status: Phase 1 done. All nine finetunes have trained 10 k steps on 10% of multilingual Emilia and produced final.pt checkpoints under gs://sdm-ckpts/<experiment>/. Active focus is the cross-teacher probe matrix (Phase 2) — see ROADMAP.md.
ARCHITECTURE.md— pipeline, Teacher protocol, TTSDS2 factor taxonomy, XLA-bf16 safety choices.ROADMAP.md— Phase 1 done (9 finetunes); Phase 2 active (cross-teacher probes); Phase 3 (wristband Gaussian) queued.TPU_RESOURCES.md— GCP project, TPU pool inventory, GCS bucket layout..tpu/README.md— per-experiment provisioning scripts (sourced from.env, no longer gitignored).TTSDS2_paper.pdf— the paper this project is downstream of.
sdm/
data/
streaming_emilia.py # multilingual Emilia → 1s chunks → fixed shapes
teachers/ # one module per teacher kind, dispatched via build_teacher
__init__.py # registry keyed on cfg.teacher.kind
hf_ssl.py # mHuBERT-147 (and any HF SSL with output_hidden_states)
hf_ctc.py # wav2vec2 ASR encoder (intelligibility)
whisper_encoder.py # multilingual Whisper-small encoder
dvector_torchscript.py
wespeaker.py
pyworld_f0.py # 1-D scalar F0 mean per chunk
mpm.py # Masked Prosody Model L7 (English-only, 22.05 kHz)
g2p_speaking_rate.py # syllables/sec from transcript phonemization
emotion2vec.py # FunASR emotion2vec base
modeling/
distill_model.py # backbone wrapper; "fairseq_w2v2" / "tiny" / "hf" branches
wav2vec2_fairseq.py # in-tree XLS-R port (Fp32 GN/LN, baked pos weight_norm, SDPA)
losses/
wristband_gaussian.py # Gaussian regularizer (queued for Phase 3)
train/
run_distill.py # entrypoint: python -m sdm.train.run_distill --config <yaml>
xla_utils.py # XLA / FSDP / multi-process plumbing
io.py # local + gs:// checkpoint I/O
preempt.py # SIGTERM handling for spot TPUs
wandb_utils.py # lazy W&B integration
dotenv.py # loads WANDB_API_KEY / HF token from .env
configs/
finetune_xlsr.yaml # generic — mHuBERT-147 L8 (the reference run)
finetune_xlsr_fairseq.yaml # generic variant with the in-tree fairseq XLS-R port (kept for parity)
finetune_dvector.yaml # speaker — d-Vector torchscript (TTSDS bundle)
finetune_wespeaker.yaml # speaker — wespeaker-unofficial multilingual SimAMResNet34
finetune_pitch.yaml # prosody — WORLD F0 mean per chunk
finetune_mpm.yaml # prosody — Masked Prosody Model layer 7
finetune_speaking_rate.yaml # prosody — syllables/sec from G2P on transcripts
finetune_w2v2_asr.yaml # intelligibility — wav2vec2 ASR encoder
finetune_mwhisper.yaml # intelligibility — multilingual Whisper-small encoder
finetune_emotion2vec.yaml # emotion — FunASR emotion2vec base
finetune_tiny.yaml # CPU debug variant (tiny in-tree transformer)
scripts/
run_finetune.sh # canonical in-VM launcher; takes CONFIG=<path>
run_probes.sh # in-VM driver for Phase 2 (consolidate + cross-teacher probes)
_tpu_common.sh # shared bootstrap (clone, uv sync, .env, env vars)
consolidate_weights.py # pulls per-experiment final.pt into a probe staging dir
run_linear_probes.py # 9 × 9 cross-teacher R² matrix (Phase 2)
tpu/ # generic capacity-hunting helpers (bake_vm, sdm_tpu CLI, …)
.tpu/ # per-experiment provisioning (committed; secrets via .env)
_lib.sh # shared poll/create/launch helpers; sources .env
provision_<short>.sh # one per experiment; brings up VM, scp .env, tmux launch
provision_probes.sh # brings up sdm-probes and runs scripts/run_probes.sh
provision_all.sh # nohup fan-out across all 8 finetunes
teardown.sh # gcloud delete for every known VM + kill local poll loops
logs/ # runtime poll-loop logs (gitignored)
tests/ # pytest suite covering the live distill path
pyproject.toml # uv-managed; deps split into base / teachers / tpu / tracking / dev
- Python ≥ 3.10,
uvfor dependency management. - A
.envat the repo root with at leastHF_TOKEN=…,WANDB_API_KEY=…, and (for TPU provisioning)SDM_GCP_PROJECT=…. Seesdm/dotenv.pyand.tpu/_lib.shfor the variables that get read. The.envitself is gitignored; the provisioning scripts source it for secrets. - For TPU runs: a GCS bucket reachable from the slice (configs default to
gs://sdm-ckpts/<experiment>/) and a baked TPU image with matchingtorch+torch_xlawheels.
uv sync --extra dev --extra trackingThe teachers extra installs heavyweight feature extractors (ttsds, pyworld, wespeaker-unofficial, FunASR, …) and is only needed on a host that runs the teachers offline — not on TPU training hosts where the configured teacher loads at startup.
Local CPU smoke (uses the tiny in-tree backbone — fast, no GPU/TPU needed):
uv run python -m sdm.train.run_distill --config configs/finetune_tiny.yamlSingle-host GPU/CPU run of the reference config:
uv run python -m sdm.train.run_distill --config configs/finetune_xlsr.yamlTPU (spot, with retry on preemption) — single experiment:
./.tpu/provision_pitch.sh # or provision_<any>.shThis polls gcloud tpu-vm create until v6e-8 capacity opens in europe-west4-a, SCPs .env onto the VM, clones the repo, and launches scripts/run_finetune.sh CONFIG=… inside tmux -s sdm. scripts/run_finetune.sh handles SIGTERM on preemption (flush + exit 130) and loops python -m sdm.train.run_distill so resume_from_latest in the YAML picks up where we left off.
TPU — fan out all finetunes:
./.tpu/provision_all.sh
tail -f .tpu/logs/*.logTear down everything:
./.tpu/teardown.sh./.tpu/provision_probes.shBrings up a single sdm-probes v6e-8, runs scripts/run_probes.sh on the VM:
consolidate_weights.pypulls all 9final.ptfromgs://sdm-ckpts/,run_linear_probes.pyloads each backbone once, computes features + teacher targets on 100 utterances ofmythicinfinity/libritts dev.clean(override viaSDM_PROBE_DATASET/SDM_PROBE_SPLIT/SDM_PROBE_UTTERANCES), then fits Ridge probes across the 9 × 9 grid (with--layer-sweepfor the per-layer breakdown).- The resulting
matrix.jsonis uploaded togs://sdm-ckpts/probes/<run-id>/.
LibriTTS is the default because it is small, English, and well-known — enough signal to sanity-check the factor groupings before scaling the probe set up to a multilingual held-out slice.
uv run pytest -qExercises the live path: streaming Emilia → chunk collate → XLS-R + configured teacher → masked loss → checkpoint I/O.
See ARCHITECTURE.md#adding-a-teacher for the full contract. Sketch:
- Create
sdm/data/teachers/<kind>.pyexposing aTeacher-shaped class with__call__(audio, chunk_mask, **ctx) -> Tensor[B, N_chunks, D]. - Register the
<kind>string insdm/data/teachers/__init__.py. - Drop a
configs/finetune_<kind>.yamlmatching the field shape of the existing configs. - Copy a
.tpu/provision_<other>.shto.tpu/provision_<kind>.sh, changeNAMEandCONFIG, add it toprovision_all.sh+teardown.sh. ./.tpu/provision_<kind>.sh.