| from __future__ import annotations |
|
|
| import subprocess |
| import sys |
| from pathlib import Path |
|
|
| import pytest |
| import torch |
| from torch import nn |
| from torch.nn import functional as F |
|
|
| from music3lab.codec.flow_encoder import ( |
| ContinuousFlowEncoder, |
| audio_metrics, |
| latent_normalized_mse, |
| learning_rate, |
| load_flow_encoder_config, |
| prior_mean_latents, |
| ) |
| from music3lab.codec.runner import ( |
| FileRecord, |
| TeacherDatasetManifest, |
| TeacherSplitRecord, |
| ) |
| from music3lab.vocoder import FlowVocoderLatents |
|
|
|
|
| ROOT = Path(__file__).resolve().parents[1] |
| CONFIG = ROOT / "configs" / "flow-encoder-v1.yaml" |
|
|
|
|
| class TinyVocoder(nn.Module): |
| def __init__(self) -> None: |
| super().__init__() |
| self.gain = nn.Parameter(torch.ones(2), requires_grad=False) |
|
|
| def forward(self, latents: FlowVocoderLatents) -> torch.Tensor: |
| value = F.interpolate( |
| latents.tensor[:, :2].float(), |
| size=44032, |
| mode="linear", |
| align_corners=False, |
| ) |
| return value * self.gain.view(1, 2, 1) |
|
|
|
|
| def test_frozen_config_has_exact_geometry_and_disjoint_64_16_16_seeds() -> None: |
| config = load_flow_encoder_config(CONFIG).config |
| seeds = config.teacher.split_seeds() |
| assert {key: len(value) for key, value in seeds.items()} == { |
| "train": 64, |
| "validation": 16, |
| "heldout": 16, |
| } |
| assert len({item for values in seeds.values() for item in values}) == 96 |
| assert config.capability.endswith("not_native_rvq") |
| assert config.model.input_samples == 44032 |
| assert config.model.latent_frames == 86 |
|
|
|
|
| def test_encoder_exact_geometry_is_deterministic_and_has_gradients() -> None: |
| loaded = load_flow_encoder_config(CONFIG).config |
| torch.manual_seed(9) |
| encoder = ContinuousFlowEncoder( |
| loaded.model, latent_mean=loaded.loss.latent_mean |
| ) |
| audio = torch.randn(2, 2, 44032) |
| first = encoder(audio) |
| second = encoder(audio) |
| assert first.shape == (2, 128, 86) |
| assert torch.equal(first, second) |
| target = torch.randn_like(first) |
| latent_normalized_mse(first, target, loaded.loss).mean().backward() |
| assert any( |
| parameter.grad is not None and parameter.grad.abs().sum() > 0 |
| for parameter in encoder.parameters() |
| ) |
|
|
|
|
| def test_prior_mean_baseline_and_schedule_are_frozen() -> None: |
| config = load_flow_encoder_config(CONFIG).config |
| prior = prior_mean_latents(3, config.model, config.loss) |
| assert prior.shape == (3, 128, 86) |
| assert float(prior.mean()) == pytest.approx(config.loss.latent_mean) |
| assert learning_rate(0, config.training) == pytest.approx( |
| config.training.maximum_learning_rate |
| ) |
| assert learning_rate(config.training.steps, config.training) == pytest.approx( |
| config.training.minimum_learning_rate |
| ) |
|
|
|
|
| def test_frozen_decoder_has_no_weight_grad_while_encoder_input_does() -> None: |
| decoder = TinyVocoder() |
| latents = torch.randn(2, 128, 86, requires_grad=True) |
| audio = decoder(FlowVocoderLatents(latents)) |
| audio.square().mean().backward() |
| assert latents.grad is not None and latents.grad.abs().sum() > 0 |
| assert all(parameter.grad is None for parameter in decoder.parameters()) |
|
|
|
|
| def test_audio_metrics_order_exact_before_distorted() -> None: |
| target = torch.randn(2, 2, 44032) |
| noise = 0.1 * torch.randn(2, 2, 44032) |
| exact = audio_metrics(target, target) |
| distorted = audio_metrics(target + noise, target) |
| assert exact.mae == 0 |
| assert exact.correlation > distorted.correlation |
| assert exact.unscaled_snr_db > distorted.unscaled_snr_db |
|
|
|
|
| def test_cli_help_is_cpu_safe() -> None: |
| result = subprocess.run( |
| [ |
| sys.executable, |
| "-B", |
| str(ROOT / "scripts" / "run_flow_encoder_pilot.py"), |
| "--help", |
| ], |
| check=True, |
| text=True, |
| capture_output=True, |
| env={ |
| "PATH": __import__("os").environ["PATH"], |
| "PYTHONPATH": str(ROOT / "src"), |
| "CUDA_VISIBLE_DEVICES": "-1", |
| "PYTHONDONTWRITEBYTECODE": "1", |
| }, |
| ) |
| normalized = " ".join(result.stdout.split()) |
| assert "does not produce native RVQ tokens" in normalized |
|
|
|
|
| def test_teacher_manifest_canonicalizes_nested_split_models() -> None: |
| config = load_flow_encoder_config(CONFIG) |
| counts = {"train": 64, "validation": 16, "heldout": 16} |
| starts = {"train": 1000, "validation": 2000, "heldout": 3000} |
| splits = { |
| name: TeacherSplitRecord( |
| count=count, |
| seeds=tuple(range(starts[name], starts[name] + count)), |
| file=FileRecord( |
| path=f"{name}.safetensors", |
| sha256="1" * 64, |
| size=1, |
| ), |
| audio_shape=(count, 2, 44032), |
| latent_shape=(count, 128, 86), |
| audio_content_sha256="2" * 64, |
| latent_content_sha256="3" * 64, |
| ) |
| for name, count in counts.items() |
| } |
| manifest = TeacherDatasetManifest.create( |
| schema_version="music3lab.flow-encoder-teachers.v1", |
| capability="continuous_flow_vocoder_latent_teacher_pairs_not_native_rvq", |
| config_file_sha256=config.file_sha256, |
| config_semantic_digest=config.semantic_digest, |
| model_revision=config.config.model_revision, |
| base_id=config.config.expected_base_id, |
| base_manifest_file_sha256="4" * 64, |
| diffusers_revision=config.config.diffusers_revision, |
| prompt_sha256="5" * 64, |
| lyrics_sha256="6" * 64, |
| persistent_pipeline_load_count=1, |
| replay_exact_count=96, |
| producer_project_git_commit="7" * 40, |
| publication_project_git_commit="8" * 40, |
| recovered_from_complete_quarantine=True, |
| generation_seconds=None, |
| peak_cuda_allocated_bytes=None, |
| splits=splits, |
| ) |
| assert ( |
| TeacherDatasetManifest.model_validate_json( |
| __import__( |
| "music3lab.codec.flow_encoder", |
| fromlist=["canonical_json_bytes"], |
| ).canonical_json_bytes(manifest) |
| ) |
| == manifest |
| ) |
|
|