mamba-cot-tts / code /tests /test_p2cot_seams.py
maxpicy's picture
Add files using upload-large-folder tool
6e802a4 verified
Raw History Blame Contribute Delete
9.27 kB
# test_p2cot_seams.py: The plan tier's cross-file contracts.
import json
import os
import subprocess
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))
PROBE = r'''
import json, sys, tempfile
from pathlib import Path
REPO = Path(sys.argv[1])
sys.path.insert(0, str(REPO)); sys.path.insert(0, str(REPO / "scripts"))
out = {}
try:
import torch
from tokenizer import load_base_tokenizer, expand_tokenizer, TokenRegistry
import plan_extract as PX
import plan_adherence as PA
import delay_dataset as DD
import model as M
import train as TR
tok = load_base_tokenizer()
expand_tokenizer(tok)
reg = TokenRegistry(tok)
except Exception as e: # offline / missing cache
print(json.dumps({"unavailable": f"{type(e).__name__}: {e}"}))
raise SystemExit(0)
# A plan with a deliberately UNVOICED middle word and both extreme bins.
WORDS = [
{"word": "alpha", "pitch_bin": 3, "dur_bin": 4, "energy_bin": 2, "dur_frames": 7},
{"word": "sss", "pitch_bin": PX.PITCH_UNVOICED_BIN,
"dur_bin": 1, "energy_bin": 0, "dur_frames": 3},
{"word": "omega", "pitch_bin": 15, "dur_bin": 9, "energy_bin": 7, "dur_frames": 11},
]
T = sum(w["dur_frames"] for w in WORDS)
# ---- S1: extractor string -> tokenizer ids, one id per symbol --------------
plan_str = PX.plan_to_tokens(WORDS, wrap=True)
ids = tok.encode(plan_str, add_special_tokens=False)
n_symbols = 2 + sum(1 + (0 if w["pitch_bin"] == PX.PITCH_UNVOICED_BIN else 1) + 2
for w in WORDS)
out["extractor_string"] = plan_str
out["n_ids"] = len(ids)
out["n_symbols"] = n_symbols
out["ids_are_plan_block"] = all(reg.is_plan_token(int(i)) for i in ids)
# ---- S1/S2: dataset-emitted ids -> extractor parser ------------------------
row = {"prompt": "alpha sss omega",
"speech_tokens": [5] * T,
"residual_codes": [[7] * T for _ in range(DD.NUM_LEVELS - 1)],
"speaker_id": 1,
"plan": {"words": WORDS, "ok": True, "coverage": 1.0}}
p = Path(tempfile.mkdtemp()) / "rows.jsonl"
p.write_text(json.dumps(row) + "\n" + json.dumps(row) + "\n", encoding="utf-8")
ds = DD.DelayMimiDataset(str(p), tok, reg, max_seq_len=512, stage=2,
aligned=True, cot_mode="plan", audio_continue_weight=1.0)
item = ds[0]
seq = item["input_ids"].tolist()
plan_pos = [i for i, t in enumerate(seq) if reg.is_plan_token(t)]
dataset_str = "".join(tok.convert_ids_to_tokens([seq[i] for i in plan_pos]))
out["dataset_string"] = dataset_str
out["dataset_string_matches_extractor"] = (dataset_str == plan_str)
out["parsed_back"] = PX.tokens_to_plan(dataset_str)
# ---- S3: collator slots -> train adapter -> injector bins ------------------
batch = DD.DelayCollator(pad_token_id=0)([ds[0], ds[1]])
out["collator_plan_keys"] = sorted(k for k in batch if k.startswith("plan_"))
bins = TR.plan_bins_from_slots(batch["input_ids"], batch["plan_slots"], reg)
out["bins"] = bins.tolist()
inj = M.PlanInjector(d_model=16, n_pitch_bins=len(reg.pitch_ids),
n_duration_bins=len(reg.duration_ids),
n_energy_bins=len(reg.energy_ids), d_plan=8,
scaffold_dropout=0.0)
out["injector_accepts_batch"] = tuple(
inj(bins, batch["plan_schedule"]).shape) == (2, len(seq), 16)
# ---- S4: the two schedule builders ----------------------------------------
durs = [w["dur_frames"] for w in WORDS]
a0 = int(item["audio_start"])
ds_sched = item["plan_schedule"].tolist()
m_sched = M.build_plan_schedule([durs], a0, T, seq_len=len(seq))[0].tolist()
# plan_lookahead=1: the dataset places frame t one position EARLIER than
# model.build_plan_schedule does, because the input at p predicts the code at
# p+1. Shift by exactly that one position and the two must be identical.
shifted = [-1] * len(seq)
for t in range(T):
shifted[a0 + t - 1] = m_sched[a0 + t]
out["schedules_agree_modulo_lookahead"] = (shifted == ds_sched)
out["dataset_schedule_head"] = ds_sched[a0 - 2:a0 + 4]
out["model_schedule_head"] = m_sched[a0 - 2:a0 + 4]
# ---- S2/S5: PAS against the extractor's own output -------------------------
try:
out["pas_on_quantised_output"] = PA.pas(WORDS, WORDS)["pas"]
except Exception as e:
out["pas_on_quantised_output"] = f"RAISED {type(e).__name__}: {e}"
out["plan_extract_has_contract"] = {
n: hasattr(PX, n) for n in ("parse_plan_string", "extract_plan_words")}
try:
sg = PA.score_generation("g.wav", plan_str, "alpha sss omega",
extract_fn=lambda w, t, **k: PX.parse_plan_string(plan_str))
out["score_generation_self_pas"] = sg["pas"]["pas"]
except Exception as e:
out["score_generation_self_pas"] = f"RAISED {type(e).__name__}: {e}"
print(json.dumps(out))
'''
@pytest.fixture(scope="module")
def seams():
env = dict(os.environ)
env["MVC_ENABLE_PLAN_TOKENS"] = "1"
env["MVC_NUM_SPEECH_TOKENS"] = "1024"
env["MVC_NUM_CODEC_LEVELS"] = "10"
env["HF_HUB_OFFLINE"] = env.get("HF_HUB_OFFLINE", "1")
proc = subprocess.run([sys.executable, "-c", PROBE, str(REPO_ROOT)],
capture_output=True, text=True, env=env, timeout=900)
if proc.returncode != 0:
pytest.fail(f"seam probe crashed:\n{proc.stdout}\n{proc.stderr}")
line = proc.stdout.strip().splitlines()[-1]
data = json.loads(line)
if "unavailable" in data:
pytest.skip(f"real tokenizer unavailable: {data['unavailable']}")
return data
def test_plan_symbols_cost_exactly_one_token_id_each(seams):
assert seams["n_ids"] == seams["n_symbols"], seams["extractor_string"]
assert seams["ids_are_plan_block"], "a plan symbol tokenized outside the plan block"
def test_dataset_emits_exactly_the_string_the_extractor_writes(seams):
assert seams["dataset_string_matches_extractor"], (
f"dataset: {seams['dataset_string']!r}\n"
f"extractor: {seams['extractor_string']!r}")
def test_extractor_parser_recovers_the_dataset_program(seams):
expect = [{"pitch_bin": 3, "dur_bin": 4, "energy_bin": 2},
{"pitch_bin": 16, "dur_bin": 1, "energy_bin": 0},
{"pitch_bin": 15, "dur_bin": 9, "energy_bin": 7}]
assert seams["parsed_back"] == expect
def test_collator_emits_the_channels_the_train_loop_reads(seams):
assert seams["collator_plan_keys"] == [
"plan_durations", "plan_lengths", "plan_schedule", "plan_slots"]
def test_slots_decode_back_to_the_sidecar_bins(seams):
expect_row = [[3, 4, 2], [-1, 1, 0], [15, 9, 7]]
assert seams["bins"] == [expect_row, expect_row]
def test_injector_consumes_the_collated_batch_unchanged(seams):
assert seams["injector_accepts_batch"]
def test_pas_accepts_the_extractors_unvoiced_sentinel(seams):
assert seams["pas_on_quantised_output"] == 1.0, seams["pas_on_quantised_output"]
def test_training_and_decode_schedules_agree_modulo_the_lookahead(seams):
assert seams["schedules_agree_modulo_lookahead"], (
f"dataset {seams['dataset_schedule_head']} vs "
f"model {seams['model_schedule_head']}")
def test_plan_extract_exposes_the_entry_points_pas_resolves(seams):
assert seams["plan_extract_has_contract"] == {
"parse_plan_string": True, "extract_plan_words": True}
def test_score_generation_runs_through_the_real_parser(seams):
assert seams["score_generation_self_pas"] == 1.0, seams["score_generation_self_pas"]
def test_trailing_zero_duration_word_is_never_scheduled():
torch = pytest.importorskip("torch")
from model import build_plan_schedule
got = build_plan_schedule([[4, 0]], 0, 6)[0].tolist()
assert got == [0, 0, 0, 0, 0, 0], got
got = build_plan_schedule([[3, 2, 0, 0]], 0, 8)[0].tolist()
assert got == [0, 0, 0, 1, 1, 1, 1, 1], got
got = build_plan_schedule([[0, 0]], 0, 3)[0].tolist()
assert got == [-1, -1, -1], got
def test_documented_known_answers_still_hold():
pytest.importorskip("torch")
from model import build_plan_schedule
assert build_plan_schedule([[2, 3]], 0, 5)[0].tolist() == [0, 0, 1, 1, 1]
assert build_plan_schedule([[2, 2]], 0, 6)[0].tolist() == [0, 0, 1, 1, 1, 1]
assert build_plan_schedule([[3, 0, 2]], 0, 7)[0].tolist() == [0, 0, 0, 2, 2, 2, 2]
assert build_plan_schedule([[4]], 2, 3, 7)[0].tolist() == [-1, -1, 0, 0, 0, -1, -1]
@pytest.mark.skipif(
os.environ.get("MVC_ENABLE_PLAN_TOKENS") is not None,
reason="reads the AMBIENT gate; a process that exported it (a plan arm's "
"own CI) is not the default environment this asserts about")
def test_gate_off_leaves_no_plan_vocabulary():
import config
import importlib
importlib.reload(config)
assert config.ENABLE_PLAN_TOKENS is False
assert config.get_control_tokens() == [
config.USER_PROMPT_TOKEN, config.THINK_START_TOKEN, config.THINK_END_TOKEN,
config.AUDIO_START_TOKEN, config.AUDIO_END_TOKEN, config.SPEAKER_TOKEN,
] + config.EMOTION_TAGS + config.PROSODY_TAGS
def test_plan_extract_token_surface_matches_config_vocabulary():
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import plan_extract as PX
assert PX.verify_config_vocab() == "ok"