Accent Detective v2: student distilled on more varied audio (AISHELL-1, THCHS-30); evaluated on held-out speakers
Browse files- README.md +13 -9
- model.safetensors +2 -2
- twjudge/accent/augment.py +82 -0
README.md
CHANGED
|
@@ -73,22 +73,25 @@ These keep the judges from learning from content.
|
|
| 73 |
|
| 74 |
## Evaluation
|
| 75 |
|
| 76 |
-
Compared with the 411 M-parameter pipeline it replaces (MMS alignment + 12-layer wav2vec 2.0 + judge)
|
| 77 |
|
| 78 |
| | Original | Accent Detective |
|
| 79 |
|---|---:|---:|
|
| 80 |
| Parameters | 411 M | 18.6 M |
|
| 81 |
-
| Length-matched AUC,
|
| 82 |
-
| Same text and voice prompt, BreezyVoice ranked above CosyVoice-300M |
|
| 83 |
-
|
|
| 84 |
-
|
|
| 85 |
-
|
|
|
|
|
| 86 |
|
| 87 |
-
|
|
|
|
| 88 |
|
| 89 |
## Limitations
|
| 90 |
|
| 91 |
-
- **Southwestern Mandarin:**
|
|
|
|
| 92 |
- **Training domain:** volunteer read speech from Common Voice.
|
| 93 |
- **Not evaluated:** Hong Kong, Singapore and Malaysian Mandarin.
|
| 94 |
|
|
@@ -98,7 +101,8 @@ Research, and evaluating or steering speech synthesis towards Taiwan Mandarin. I
|
|
| 98 |
|
| 99 |
## Training data and licences
|
| 100 |
|
| 101 |
-
- **Training data:** Common Voice 25 zh-TW and Common Voice 21 zh-CN, both CC0, with speakers disjoint across splits.
|
|
|
|
| 102 |
- **Code and weights:** MIT (see `LICENSE`).
|
| 103 |
- **Distillation teacher:** TencentGameMate/chinese-wav2vec2-base (MIT).
|
| 104 |
- **Bundled lexicons:**
|
|
|
|
| 73 |
|
| 74 |
## Evaluation
|
| 75 |
|
| 76 |
+
Compared with the 411 M-parameter pipeline it replaces (MMS alignment + 12-layer wav2vec 2.0 + one judge, mean of three seeds). Every system scores the audio end to end.
|
| 77 |
|
| 78 |
| | Original | Accent Detective |
|
| 79 |
|---|---:|---:|
|
| 80 |
| Parameters | 411 M | 18.6 M |
|
| 81 |
+
| Length-matched AUC, held-out Common Voice speakers (526 Taiwan, 704 Mainland) | 0.949 | 0.940 [0.909, 0.959] |
|
| 82 |
+
| Same text and voice prompt, BreezyVoice ranked above CosyVoice-300M (1,000 pairs) | 72% | 75% |
|
| 83 |
+
| The same contrast in standard deviations of the score on real speech | 0.31 | 0.40 |
|
| 84 |
+
| Score variance explained by the TTS voice (119 texts × 4 voices) | 6.3% | 6.9% |
|
| 85 |
+
| Expected calibration error, no recalibration | 0.051 | 0.034 |
|
| 86 |
+
| CPU latency per utterance (Apple M5 Pro, 6 threads, median) | 184 ms | 16 ms |
|
| 87 |
|
| 88 |
+
- **Held-out set:** the held-out speakers were never used to train or choose any model. Their sentences are harder than the original test set's: every system scores lower on them.
|
| 89 |
+
- **Synthetic contrast:** the 1,000 pairs are 250 held-out Taiwan sentences read with four voice prompts.
|
| 90 |
|
| 91 |
## Limitations
|
| 92 |
|
| 93 |
+
- **Southwestern Mandarin:** standard Mandarin from speakers of Sichuan, Chongqing, Guizhou and Yunnan (KeSpeech) is judged Taiwan-accented 37% of the time. The overlap is largest on retroflex initials and velar nasal finals, which both varieties merge.
|
| 94 |
+
- **Other recording conditions:** on KeSpeech, standard Mandarin from the other Mainland regions is judged Taiwan-accented 3–16% of the time (11% on average), against about 5% for the original pipeline. The compact model is less robust than its teacher to recording conditions unlike Common Voice.
|
| 95 |
- **Training domain:** volunteer read speech from Common Voice.
|
| 96 |
- **Not evaluated:** Hong Kong, Singapore and Malaysian Mandarin.
|
| 97 |
|
|
|
|
| 101 |
|
| 102 |
## Training data and licences
|
| 103 |
|
| 104 |
+
- **Training data:** Common Voice 25 zh-TW and Common Voice 21 zh-CN, both CC0, with speakers disjoint across splits.
|
| 105 |
+
- **Distillation audio:** about 140 h, used without labels. It holds 84 h of the same public Common Voice audio, excluding every development, test and held-out speaker, and 55 h of AISHELL-1 and THCHS-30 (Apache 2.0) for more varied recording conditions.
|
| 106 |
- **Code and weights:** MIT (see `LICENSE`).
|
| 107 |
- **Distillation teacher:** TencentGameMate/chinese-wav2vec2-base (MIT).
|
| 108 |
- **Bundled lexicons:**
|
model.safetensors
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b1fca94d503c2b1c77a687fecb0684e8af982d6388925fe80da97bdb83b99178
|
| 3 |
+
size 74303340
|
twjudge/accent/augment.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Recording-condition augmentation: additive noise, room reverberation, telephone band, 8-bit mu-law, gain.
|
| 2 |
+
|
| 3 |
+
Used to make the student encoder (distillation) and the judges (training) less sensitive to the
|
| 4 |
+
recording channel, the weakness found on KeSpeech. Every class gets the same augmentation, so the
|
| 5 |
+
channel never becomes a label cue. Assets are packed once by `judge/scripts/augment_assets.py`:
|
| 6 |
+
|
| 7 |
+
noise.npy / noise_off.npy MUSAN noise and music (CC BY 4.0), int16 16 kHz, concatenated
|
| 8 |
+
rir.npy OpenSLR 28 room impulse responses (Apache 2.0), float32 (n, L), peak-aligned
|
| 9 |
+
|
| 10 |
+
The whole batch is processed at once on the device; band limits are applied as smooth masks on the
|
| 11 |
+
spectrum (no recursive filters, which are slow on a GPU).
|
| 12 |
+
|
| 13 |
+
aug = Augmenter("assets/aug", device="cuda")
|
| 14 |
+
x = aug(x) # (B, T) float waveforms on the device, each item augmented independently
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
|
| 21 |
+
import numpy as np
|
| 22 |
+
|
| 23 |
+
SR = 16000
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class Augmenter:
|
| 27 |
+
def __init__(self, assets: str | Path, device: str = "cpu", p: float = 0.8, seed: int = 0):
|
| 28 |
+
import torch
|
| 29 |
+
|
| 30 |
+
d = Path(assets)
|
| 31 |
+
self.noise = torch.from_numpy(np.load(d / "noise.npy").astype(np.float32) / 32768).to(device)
|
| 32 |
+
self.noise_off = np.load(d / "noise_off.npy")
|
| 33 |
+
self.rir = torch.from_numpy(np.load(d / "rir.npy")).to(device)
|
| 34 |
+
self.dev, self.p = device, p
|
| 35 |
+
self.rng = np.random.default_rng(seed)
|
| 36 |
+
|
| 37 |
+
def _noise(self, n: int):
|
| 38 |
+
k = self.rng.integers(len(self.noise_off) - 1)
|
| 39 |
+
a, b = int(self.noise_off[k]), int(self.noise_off[k + 1])
|
| 40 |
+
if b - a <= n:
|
| 41 |
+
return self.noise[a:b].repeat(n // (b - a) + 1)[:n]
|
| 42 |
+
o = self.rng.integers(a, b - n)
|
| 43 |
+
return self.noise[o:o + n]
|
| 44 |
+
|
| 45 |
+
def __call__(self, x):
|
| 46 |
+
"""x: (B, T) or (T,) float tensor on the device; each item is augmented with probability p."""
|
| 47 |
+
import torch
|
| 48 |
+
import torchaudio.functional as AF
|
| 49 |
+
|
| 50 |
+
one = x.dim() == 1
|
| 51 |
+
x = x[None] if one else x.clone()
|
| 52 |
+
Bn, T = x.shape
|
| 53 |
+
u = lambda: self.rng.random(Bn)
|
| 54 |
+
on = u() < self.p
|
| 55 |
+
rev, noi, band, mu = on & (u() < 0.35), on & (u() < 0.6), np.where(on, u(), 1.0), on & (u() < 0.15)
|
| 56 |
+
if rev.any(): # reverberation
|
| 57 |
+
i = torch.from_numpy(np.where(rev)[0]).to(x.device)
|
| 58 |
+
h = self.rir[torch.from_numpy(self.rng.integers(len(self.rir), size=len(i))).to(x.device)]
|
| 59 |
+
y = AF.fftconvolve(x[i], h)[:, :T]
|
| 60 |
+
x[i] = y * (x[i].abs().amax(1, keepdim=True) / (y.abs().amax(1, keepdim=True) + 1e-6))
|
| 61 |
+
if noi.any(): # noise at 5–25 dB SNR
|
| 62 |
+
i = torch.from_numpy(np.where(noi)[0]).to(x.device)
|
| 63 |
+
nz = torch.stack([self._noise(T) for _ in range(len(i))])
|
| 64 |
+
snr = torch.from_numpy(self.rng.uniform(5, 25, len(i))).float().to(x.device)
|
| 65 |
+
px, pn = x[i].pow(2).mean(1) + 1e-8, nz.pow(2).mean(1) + 1e-8
|
| 66 |
+
x[i] = x[i] + nz * torch.sqrt(px / (pn * 10 ** (snr / 10)))[:, None]
|
| 67 |
+
tel, low = band < 0.25, (band >= 0.25) & (band < 0.4)
|
| 68 |
+
if tel.any() or low.any(): # telephone band / cheap microphone
|
| 69 |
+
f = torch.fft.rfftfreq(T, 1 / SR, device=x.device)[None]
|
| 70 |
+
lo = np.where(tel, 300.0, 0.0)
|
| 71 |
+
hi = np.where(tel, 3400.0, np.where(low, self.rng.uniform(3000, 6000, Bn), SR))
|
| 72 |
+
lo_t, hi_t = (torch.from_numpy(v).float().to(x.device)[:, None] for v in (lo, hi))
|
| 73 |
+
g = torch.sigmoid((f - lo_t) / 40) * torch.sigmoid((hi_t - f) / 150)
|
| 74 |
+
i = torch.from_numpy(np.where(tel | low)[0]).to(x.device)
|
| 75 |
+
x[i] = torch.fft.irfft(torch.fft.rfft(x[i]) * g[i], n=T)
|
| 76 |
+
if mu.any(): # 8-bit mu-law
|
| 77 |
+
i = torch.from_numpy(np.where(mu)[0]).to(x.device)
|
| 78 |
+
peak = x[i].abs().amax(1, keepdim=True) + 1e-6
|
| 79 |
+
x[i] = AF.mu_law_decoding(AF.mu_law_encoding(x[i] / peak, 256), 256) * peak
|
| 80 |
+
gain = torch.from_numpy(np.where(on, 10 ** (self.rng.uniform(-10, 6, Bn) / 20), 1.0)).float().to(x.device)
|
| 81 |
+
x = torch.nan_to_num(x * gain[:, None]).clamp(-1, 1)
|
| 82 |
+
return x[0] if one else x
|