mamba-cot-tts / code /tests /test_plan_audit_guards.py
maxpicy's picture
Add files using upload-large-folder tool
6e802a4 verified
Raw History Blame Contribute Delete
11.2 kB
# test_plan_audit_guards.py: The plan tier's silent-failure guards.
from __future__ import annotations
import sys
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
if str(REPO_ROOT / "scripts") not in sys.path:
sys.path.insert(0, str(REPO_ROOT / "scripts"))
torch = pytest.importorskip("torch")
np = pytest.importorskip("numpy")
PLAN_LO = 6
N_PLAN = 3
V_OLD = 10
V_NEW = V_OLD + N_PLAN
PAD = 4
def _pad_to(v, mult=PAD):
return v if v % mult == 0 else v + (mult - v % mult)
class _FakeTok:
def __init__(self, n):
self._n = n
def __len__(self):
return self._n
class _FakeReg:
def __init__(self, n_vocab, plan_lo, n_plan):
self.tokenizer = _FakeTok(n_vocab)
self.plan_token_id_min = plan_lo
self.plan_token_id_max = plan_lo + n_plan - 1
self.plan_token_ids = set(range(plan_lo, plan_lo + n_plan))
class _TinyModel(torch.nn.Module):
def __init__(self, n_vocab, n_speakers=5, d=4, pad=PAD):
super().__init__()
rows = _pad_to(n_vocab, pad)
self.emb = torch.nn.Embedding(rows, d)
self.lm_head = torch.nn.Linear(d, rows, bias=False)
self.speaker_table = torch.nn.Embedding(n_speakers, d)
self.token_registry = _FakeReg(n_vocab, PLAN_LO, N_PLAN)
def _write_ckpt(tmp_path, model, name="ck.pt"):
p = tmp_path / name
torch.save({"model_state_dict": model.state_dict(), "step": 7}, p)
return str(p)
class TestVocabSplice:
def test_splice_is_the_hand_computed_answer_with_padding(self):
import train
v_old_pad, v_new_pad = _pad_to(V_OLD), _pad_to(V_NEW)
assert (v_old_pad, v_new_pad) == (12, 16)
assert v_new_pad - v_old_pad != N_PLAN
old = torch.arange(v_old_pad * 2, dtype=torch.float32).reshape(v_old_pad, 2)
fresh = torch.full((v_new_pad, 2), -99.0)
out = train._splice_plan_vocab_rows(old, fresh, PLAN_LO, N_PLAN, V_OLD)
assert tuple(out.shape) == (v_new_pad, 2)
assert torch.equal(out[:PLAN_LO], old[:PLAN_LO])
assert torch.equal(out[PLAN_LO:PLAN_LO + N_PLAN],
torch.full((N_PLAN, 2), -99.0))
assert out[9].tolist() == old[6].tolist()
assert out[12].tolist() == old[9].tolist()
assert torch.equal(out[V_NEW:], torch.full((v_new_pad - V_NEW, 2), -99.0))
def test_splice_refuses_a_checkpoint_smaller_than_the_old_vocab(self):
import train
with pytest.raises(RuntimeError, match="not produced by the vocabulary"):
train._splice_plan_vocab_rows(torch.zeros(V_OLD - 2, 2),
torch.zeros(_pad_to(V_NEW), 2),
PLAN_LO, N_PLAN, V_OLD)
def test_splice_refuses_a_model_too_small_to_hold_the_result(self):
import train
with pytest.raises(RuntimeError, match="does not fit"):
train._splice_plan_vocab_rows(torch.zeros(V_OLD, 2),
torch.zeros(V_OLD, 2),
PLAN_LO, N_PLAN, V_OLD)
def test_vocab_row_keys_finds_padded_embedding_and_head_but_not_speaker(self):
import train
m = _TinyModel(V_NEW)
assert m.emb.weight.shape[0] == _pad_to(V_NEW) > V_NEW
keys = train._vocab_row_keys(m, m.state_dict())
assert keys == {"emb.weight", "lm_head.weight"}
def test_mismatched_vocab_is_a_hard_error_not_a_warning(self, tmp_path):
import train
old_model = _TinyModel(V_OLD)
path = _write_ckpt(tmp_path, old_model)
new_model = _TinyModel(V_NEW)
before = new_model.emb.weight.detach().clone()
with pytest.raises(RuntimeError, match="VOCABULARY-sized"):
train.load_checkpoint(path, new_model, device="cpu")
assert torch.equal(new_model.emb.weight.detach(), before)
def test_splice_flag_loads_the_old_rows_exactly(self, tmp_path):
import train
old_model = _TinyModel(V_OLD)
torch.nn.init.normal_(old_model.emb.weight, std=1.0)
torch.nn.init.normal_(old_model.lm_head.weight, std=1.0)
old_emb = old_model.emb.weight.detach().clone()
old_head = old_model.lm_head.weight.detach().clone()
path = _write_ckpt(tmp_path, old_model)
new_model = _TinyModel(V_NEW)
fresh_plan_rows = new_model.emb.weight.detach()[PLAN_LO:PLAN_LO + N_PLAN].clone()
step = train.load_checkpoint(path, new_model, device="cpu",
splice_plan_vocab=True)
assert step == 7
got = new_model.emb.weight.detach()
assert torch.equal(got[:PLAN_LO], old_emb[:PLAN_LO])
assert torch.equal(got[PLAN_LO:PLAN_LO + N_PLAN], fresh_plan_rows)
assert torch.equal(got[PLAN_LO + N_PLAN:V_NEW], old_emb[PLAN_LO:V_OLD])
head = new_model.lm_head.weight.detach()
assert torch.equal(head[:PLAN_LO], old_head[:PLAN_LO])
assert torch.equal(head[PLAN_LO + N_PLAN:V_NEW], old_head[PLAN_LO:V_OLD])
def test_matching_vocab_still_loads_with_no_flag(self, tmp_path):
import train
src = _TinyModel(V_NEW)
torch.nn.init.normal_(src.emb.weight, std=1.0)
path = _write_ckpt(tmp_path, src)
dst = _TinyModel(V_NEW)
train.load_checkpoint(path, dst, device="cpu")
assert torch.equal(dst.emb.weight.detach(), src.emb.weight.detach())
def test_speaker_table_resize_is_still_a_legitimate_silent_drop(self, tmp_path):
import train
src = _TinyModel(V_NEW, n_speakers=5)
torch.nn.init.normal_(src.emb.weight, std=1.0)
path = _write_ckpt(tmp_path, src)
dst = _TinyModel(V_NEW, n_speakers=9)
train.load_checkpoint(path, dst, device="cpu")
assert torch.equal(dst.emb.weight.detach(), src.emb.weight.detach())
assert dst.speaker_table.weight.shape[0] == 9
class TestQuantiserNaN:
def test_bin_index_known_answers(self):
import plan_extract as pe
edges = np.array([1.0, 2.0, 3.0])
assert pe._bin_index(0.5, edges) == 0
assert pe._bin_index(1.0, edges) == 1
assert pe._bin_index(2.5, edges) == 2
assert pe._bin_index(99.0, edges) == 3
@pytest.mark.parametrize("bad", [float("nan"), float("inf"), float("-inf")])
def test_non_finite_raises_instead_of_saturating(self, bad):
import plan_extract as pe
with pytest.raises(pe.PlanQuantiseError, match="non-finite"):
pe._bin_index(bad, np.array([1.0, 2.0, 3.0]))
def test_nan_energy_no_longer_becomes_the_loudest_bin(self):
import plan_extract as pe
dur_s = 0.1
w = [{"word": "a", "dur_s": dur_s,
"dur_frames": max(1, int(round(dur_s * pe.FRAME_RATE_HZ))),
"f0_mean_hz": 200.0, "energy_rms": float("nan")}]
with pytest.raises(pe.PlanQuantiseError):
pe.quantise_plan(w)
def test_a_finite_row_still_quantises(self):
import plan_extract as pe
dur_s = 0.4
w = [{"word": "a", "dur_s": dur_s,
"dur_frames": max(1, int(round(dur_s * pe.FRAME_RATE_HZ))),
"f0_mean_hz": 200.0, "energy_rms": 0.05}]
q = pe.quantise_plan(w)
assert 0 <= q[0]["energy_bin"] < pe.N_ENERGY_BINS
assert 0 <= q[0]["dur_bin"] < pe.N_DUR_BINS
assert 0 <= q[0]["pitch_bin"] < pe.N_PITCH_BINS
w2 = [dict(w[0], energy_rms=0.0)]
assert pe.quantise_plan(w2)[0]["energy_bin"] == 0
class TestInjectorBinRange:
def _inj(self):
from model import PlanInjector
return PlanInjector(d_model=8, n_pitch_bins=16, n_duration_bins=16,
n_energy_bins=8, d_plan=4, scaffold_dropout=0.0)
def test_pitch_sentinel_is_accepted_as_absent(self):
inj = self._inj()
sentinel = inj.word_vectors(torch.tensor([[[16, 4, 2]]]))
absent = inj.word_vectors(torch.tensor([[[-1, 4, 2]]]))
assert torch.equal(sentinel, absent)
@pytest.mark.parametrize("bins,axis", [([3, 16, 2], "duration"),
([3, 4, 8], "energy")])
def test_out_of_range_on_the_other_channels_raises(self, bins, axis):
inj = self._inj()
with pytest.raises(ValueError, match=axis):
inj.word_vectors(torch.tensor([[bins]]))
def test_valid_top_bins_still_pass(self):
inj = self._inj()
v = inj.word_vectors(torch.tensor([[[15, 15, 7]]]))
assert v.shape == (1, 1, 4)
assert bool(v.abs().sum() > 0)
class TestCellWavOrdering:
def _make(self, tmp_path, n):
d = tmp_path / "cellA"
d.mkdir()
for i in range(n):
(d / f"sample_{i:02d}.wav").write_bytes(b"")
cells = {"cellA": {100 + i: {} for i in range(n)}}
return cells, tmp_path
def test_order_is_numeric_beyond_99(self, tmp_path):
pfp = pytest.importorskip("plan_flip_probe")
cells, wavdir = self._make(tmp_path, 105)
got = pfp.cell_wavs(cells, "cellA", wavdir)
assert got[100].name == "sample_00.wav"
assert got[111].name == "sample_11.wav"
assert got[200].name == "sample_100.wav"
assert got[204].name == "sample_104.wav"
def test_a_gap_in_the_decode_run_raises(self, tmp_path):
pfp = pytest.importorskip("plan_flip_probe")
cells, wavdir = self._make(tmp_path, 5)
(wavdir / "cellA" / "sample_02.wav").unlink()
cells["cellA"].pop(102)
with pytest.raises(SystemExit, match="complete 0"):
pfp.cell_wavs(cells, "cellA", wavdir)
class TestIntegrityReadsRealFields:
def _rec(self, n_frames=40, **extra):
r = {"speech_tokens": list(range(n_frames)), "dur_window": [20, 80]}
r.update(extra)
return r
def test_status_name_matches_the_generator(self):
pfp = pytest.importorskip("plan_flip_probe")
gen = pytest.importorskip("gen_fish_cot_samples")
assert pfp.PLAN_STATUS_OK_NAME == gen.PLAN_STATUS_OK
def test_malformed_rate_is_counted_from_plan_status(self):
pfp = pytest.importorskip("plan_flip_probe")
cells = {"self": {
0: self._rec(plan_status="ok", plan_ok=True),
1: self._rec(plan_status="unterminated", plan_ok=False,
plan_window_fallback=True),
2: self._rec(plan_status="malformed", plan_ok=False,
plan_window_fallback=True),
}}
rep = pfp._integrity(cells, None, "plan_used")["self"]
assert rep["n_plans_parsed"] == 3
assert rep["n_plans_malformed"] == 2
assert rep["n_plan_window_fallback"] == 2
def test_a_plan_free_cell_reports_no_plans_and_does_not_raise(self):
pfp = pytest.importorskip("plan_flip_probe")
cells = {"base": {0: self._rec(), 1: self._rec()}}
rep = pfp._integrity(cells, None, "plan_used")["base"]
assert rep["n_plans_parsed"] == 0
assert rep["n_plans_malformed"] == 0
assert rep["n_plan_window_fallback"] == 0