"""CPU contract for an oracle-free learned flow-latent encoder. The test decoder is intentionally tiny. It maps required continuous Music3 flow shape [B,128,86] to [B,2,44032], never RVQ/native tokens. """ from __future__ import annotations from dataclasses import asdict from pathlib import Path import pytest import torch from torch import nn from torch.nn import functional as F from music3lab.codec import learned_flow_encoder as flow SAMPLES = 44_032 SEEDS = (101, 103, 107, 109) class TinyFrozenFlowDecoder(nn.Module): def __init__(self) -> None: super().__init__() self.gain = nn.Parameter(torch.tensor([1.25, 0.75]), requires_grad=False) def forward(self, z: torch.Tensor) -> torch.Tensor: assert z.shape[1:] == (128, 86) audio = F.interpolate(z[:, :2], size=SAMPLES, mode="linear", align_corners=False) return audio * self.gain.view(1, 2, 1) def _latents(count: int = 12) -> torch.Tensor: time = torch.linspace(-1.0, 1.0, 86) values = [] for i in range(count): z = torch.zeros(128, 86) z[0] = (i + 1) / count * time z[1] = .25 * torch.cos((i + 1) * torch.pi * time) values.append(z) return torch.stack(values) def _examples(count: int = 12) -> tuple[flow.AudioExample, ...]: with torch.no_grad(): audio = TinyFrozenFlowDecoder()(_latents(count)) return tuple(flow.AudioExample(flow.deterministic_audio_id(a), a.clone()) for a in audio) def _config() -> flow.LearnedFlowEncoderConfig: return flow.LearnedFlowEncoderConfig( latent_channels=128, latent_frames=86, audio_channels=2, audio_samples=SAMPLES, hidden_channels=8, learning_rate=.03, batch_size=3, seeds=SEEDS, ) def test_exact_geometry_frozen_decoder_and_encoder_gradients() -> None: encoder, decoder = flow.LearnedFlowEncoder(_config()), TinyFrozenFlowDecoder() audio = torch.randn(2, 2, SAMPLES) z = encoder(audio) assert z.shape == (2, 128, 86) flow.audio_reconstruction_loss(decoder(z), audio).backward() assert all(p.grad is None for p in decoder.parameters()) assert any(p.grad is not None and p.grad.abs().sum() > 0 for p in encoder.parameters()) def test_deterministic_ids_and_four_seed_splits_are_complete_disjoint_and_leak_free() -> None: examples = _examples() changed = examples[0].audio.clone() changed[0, 0] += 1e-4 assert flow.deterministic_audio_id(changed) != examples[0].sample_id first = flow.build_four_seed_splits(examples, seeds=SEEDS, validation_fraction=.25) assert first == flow.build_four_seed_splits(examples, seeds=SEEDS, validation_fraction=.25) assert tuple(first) == SEEDS all_ids = {x.sample_id for x in examples} for seed, split in first.items(): assert seed in SEEDS and split.train_ids and split.validation_ids assert set(split.train_ids).isdisjoint(split.validation_ids) assert set(split.train_ids) | set(split.validation_ids) == all_ids assert split.train_ids == tuple(sorted(split.train_ids)) assert split.validation_ids == tuple(sorted(split.validation_ids)) with pytest.raises((TypeError, ValueError), match="latent|unknown|field"): flow.AudioExample(examples[0].sample_id, examples[0].audio, latent_target=torch.zeros(128, 86)) def test_loss_identity_order_and_heldout_metric_are_audio_only() -> None: decoder, target = TinyFrozenFlowDecoder(), _examples(2)[0].audio.unsqueeze(0) exact = flow.audio_reconstruction_loss(target, target) distorted = flow.audio_reconstruction_loss(target + .1, target) inverted = flow.audio_reconstruction_loss(-target, target) assert exact.item() == pytest.approx(0.0, abs=1e-12) assert distorted > exact and inverted > distorted metric = flow.evaluate_heldout(flow.LearnedFlowEncoder(_config()), decoder, _examples(4), torch.zeros(1, 128, 86)) assert set(metric) == {"encoder_loss", "prior_mean_loss", "improvement_fraction", "sample_count"} assert metric["sample_count"] == 4 and metric["encoder_loss"] >= 0 and metric["prior_mean_loss"] > 0 assert metric["improvement_fraction"] == pytest.approx(1 - metric["encoder_loss"] / metric["prior_mean_loss"]) assert "token" not in " ".join(metric).lower() def test_short_cpu_training_reduces_heldout_loss_below_prior_mean() -> None: torch.manual_seed(7) examples, config, decoder = _examples(), _config(), TinyFrozenFlowDecoder() split = flow.build_four_seed_splits(examples, seeds=SEEDS, validation_fraction=.25)[101] encoder = flow.LearnedFlowEncoder(config) before = flow.evaluate_heldout(encoder, decoder, split.validation_examples(examples), torch.zeros(1, 128, 86)) run = flow.train_encoder(encoder, decoder, split.training_examples(examples), split.validation_examples(examples), config, steps=36) after = flow.evaluate_heldout(encoder, decoder, split.validation_examples(examples), torch.zeros(1, 128, 86)) assert run.training_losses[0] > run.training_losses[-1] assert after["encoder_loss"] < before["encoder_loss"] assert after["encoder_loss"] < after["prior_mean_loss"] and after["improvement_fraction"] > 0 def test_optional_refinement_is_bounded_and_never_claims_native_tokens() -> None: decoder, target = TinyFrozenFlowDecoder(), _examples(1)[0].audio.unsqueeze(0) initial = torch.zeros(1, 128, 86) result = flow.optional_refine_latents(decoder, initial, target, steps=4, learning_rate=.1, max_steps=4) assert result.latents.shape == (1, 128, 86) assert 0 <= result.steps_used <= 4 and result.final_loss <= result.initial_loss assert result.kind == "continuous_flow_latent" and "token" not in result.kind with pytest.raises((TypeError, ValueError), match="max_steps|bound"): flow.optional_refine_latents(decoder, initial, target, steps=5, learning_rate=.1, max_steps=4) def test_checkpoint_contains_only_encoder_config_and_provenance(tmp_path: Path) -> None: config, encoder = _config(), flow.LearnedFlowEncoder(_config()) path = tmp_path / "learned-flow-encoder.pt" provenance = {"base_commit": "9606448", "dataset_ids": tuple(x.sample_id for x in _examples()), "split_seeds": SEEDS, "decoder_identity": "frozen-test-decoder"} flow.save_encoder_checkpoint(path, encoder, config, provenance) payload = torch.load(path, map_location="cpu", weights_only=False) assert set(payload) == {"schema_version", "encoder_state_dict", "config", "provenance"} assert payload["config"] == asdict(config) and payload["provenance"] == provenance serialized = repr(payload).lower() for word in ("decoder_state", "latent_target", "native", "rvq"): assert word not in serialized