Spaces:
Running
Running
File size: 7,970 Bytes
10bebcd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | #!/usr/bin/env python3
"""Compute SR-FD reference statistics from an audio manifest.
This runs the *frozen* SR-FD extractors directly on reference waveforms and
stores only the first- and second-order moments (mean + covariance) of the
resulting feature vectors. The reference audio is never used again at training
time; only the stored moments are loaded.
Each of the paper's three targets is one invocation of this script:
# 1. low-step Whisper anchor (ASR-verified four-step generations)
python scripts/compute_reference_stats.py \
--manifest data/ref/asr_true4_good.jsonl \
--config configs/srfd_compact3.yaml --reps whisper_anchor8_p64 \
--out stats/ref_whisper_anchor_asr_true4_good.pt
# 2. teacher CTC target (ten-step teacher generations)
python scripts/compute_reference_stats.py \
--manifest data/ref/teacher_t10.jsonl \
--config configs/srfd_compact3.yaml --reps ctc_content_p64 \
--out stats/ref_ctc_content_teacher_t10.pt
# 3. real-speech CTC target (real LibriTTS voice-cloning speech)
python scripts/compute_reference_stats.py \
--manifest data/ref/real_voiceclone.jsonl \
--config configs/srfd_compact3.yaml --reps ctc_content_p64 \
--out stats/ref_ctc_content_real_voiceclone.pt
The manifest is JSONL with one object per line containing at least an
``audio`` field (path to a wav/flac file). Any other fields are ignored.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Dict, List, Optional
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT))
import torch
from srfd.extractors import build_srfd_extractors
from srfd.moments import accumulate_moments, finalize_accumulated_moments
from srfd.stats_io import save_stats
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Compute SR-FD reference statistics.")
p.add_argument("--manifest", required=True, help="JSONL manifest with an 'audio' field.")
p.add_argument("--config", required=True, help="YAML config; reads srfd.reps for extractors.")
p.add_argument("--out", required=True, help="Output .pt path.")
p.add_argument(
"--reps",
nargs="*",
default=None,
help="Subset of extractor names to compute (default: all enabled in the config).",
)
p.add_argument("--audio_field", default="audio", help="Manifest field with the audio path.")
p.add_argument("--input_sample_rate", type=int, default=0,
help="Override input sample rate (0 = use each file's native rate).")
p.add_argument("--batch_size", type=int, default=4)
p.add_argument("--max_samples", type=int, default=0, help="0 = process all rows.")
p.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
p.add_argument("--dry_run", action="store_true", help="Process 2 batches and print shapes only.")
return p.parse_args()
def _load_reps(config_path: str, keep: Optional[List[str]]) -> List[Dict]:
import yaml
with open(config_path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
reps = (data.get("srfd", {}) or {}).get("reps", []) if isinstance(data, dict) else []
if not reps:
raise ValueError(f"No srfd.reps found in {config_path}")
if keep:
keep_set = set(keep)
reps = [r for r in reps if r.get("name") in keep_set]
if not reps:
raise ValueError(f"None of {keep} matched srfd.reps names in {config_path}")
return reps
def _read_manifest(path: str, audio_field: str, limit: int) -> List[str]:
paths: List[str] = []
with open(path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
row = json.loads(line)
audio = row[audio_field]
if isinstance(audio, dict): # {"path": ...} style
audio = audio.get("path") or audio.get("array")
paths.append(str(audio))
if limit and len(paths) >= limit:
break
return paths
def _load_audio(path: str) -> tuple[torch.Tensor, int]:
import soundfile as sf
wav, sr = sf.read(path, dtype="float32", always_2d=True)
# Downmix to mono: [T, C] -> [T]
mono = torch.from_numpy(wav).mean(dim=1)
return mono, int(sr)
def _collate(batch_paths: List[str], override_sr: int) -> Dict[str, torch.Tensor]:
wavs: List[torch.Tensor] = []
lengths: List[int] = []
sr_seen: Optional[int] = None
for p in batch_paths:
wav, sr = _load_audio(p)
if override_sr:
sr = override_sr
if sr_seen is None:
sr_seen = sr
elif sr_seen != sr:
raise ValueError(
f"Mixed sample rates in one batch ({sr_seen} vs {sr}); "
"pre-resample the manifest or use --batch_size 1."
)
wavs.append(wav)
lengths.append(wav.numel())
max_len = max(lengths)
B = len(wavs)
waveform = torch.zeros(B, max_len, dtype=torch.float32)
mask = torch.zeros(B, max_len, dtype=torch.bool)
for i, (wav, n) in enumerate(zip(wavs, lengths)):
waveform[i, :n] = wav
mask[i, :n] = True
return {
"waveform": waveform,
"waveform_mask": mask,
"waveform_sample_rate": int(sr_seen or 16000),
}
def main() -> int:
args = parse_args()
reps_config = _load_reps(args.config, args.reps)
extractors = build_srfd_extractors(reps_config)
if not extractors:
raise ValueError("No enabled extractors after filtering.")
extractors = [e.to(args.device).eval() for e in extractors]
print(f"Extractors: {[e.name for e in extractors]}", file=sys.stderr)
paths = _read_manifest(args.manifest, args.audio_field, args.max_samples)
print(f"Manifest rows: {len(paths)}", file=sys.stderr)
accumulators: Dict[str, dict] = {ext.name: {} for ext in extractors}
n_batches = 0
with torch.no_grad():
for start in range(0, len(paths), args.batch_size):
batch_paths = paths[start : start + args.batch_size]
batch = _collate(batch_paths, args.input_sample_rate)
batch = {
k: (v.to(args.device) if isinstance(v, torch.Tensor) else v)
for k, v in batch.items()
}
for ext in extractors:
rep = ext(batch).to(torch.float32)
if rep.dim() != 2:
raise RuntimeError(f"{ext.name} returned {tuple(rep.shape)}; expected [B, C]")
accumulate_moments(accumulators[ext.name], rep)
if args.dry_run and n_batches < 2:
print(f"[dry_run] {ext.name}: rep {tuple(rep.shape)}", file=sys.stderr)
n_batches += 1
if args.dry_run and n_batches >= 2:
print("[dry_run] stopping after 2 batches.", file=sys.stderr)
return 0
reps_out = {}
for ext in extractors:
finalized = finalize_accumulated_moments(accumulators[ext.name])
reps_out[ext.name] = finalized
print(
f"[stats] {ext.name}: n={finalized['n']}, dim={finalized['feature_dim']}, "
f"||mu||={finalized['mu'].norm().item():.4f}",
file=sys.stderr,
)
metadata = {
"manifest": str(args.manifest),
"config": str(args.config),
"n_samples_processed": int(min(len(paths), args.max_samples) if args.max_samples else len(paths)),
"extractor_names": [e.name for e in extractors],
}
out_path = Path(args.out)
out_path.parent.mkdir(parents=True, exist_ok=True)
save_stats(str(out_path), {"reps": reps_out, "metadata": metadata})
print(f"Saved SR-FD reference stats to {out_path}", file=sys.stderr)
return 0
if __name__ == "__main__":
raise SystemExit(main())
|