from __future__ import annotations import os from pathlib import Path import pytest import torch import music3lab.codec.flow_encoder as flow_encoder from music3lab.codec.external_finetune import ( InterimExternalDataConfig, load_external_finetune_config, load_interim_source_splits, ) from music3lab.codec.flow_encoder import load_flow_encoder_config from music3lab.inversion_v2 import mid_side_nmse ROOT = Path(__file__).resolve().parents[1] CONFIG = ROOT / "configs" / "external-flow-encoder-interim-678-v1.yaml" def test_interim_config_versions_counts_without_redefining_final_api() -> None: loaded = load_external_finetune_config(CONFIG) assert ( loaded.config.schema_version == "music3lab.external-flow-encoder-finetune.interim678.v1" ) assert isinstance(loaded.config.data, InterimExternalDataConfig) assert loaded.config.data.split_counts == { "train": 542, "validation": 68, "heldout": 68, } assert loaded.config.data.dataset_status == "not_2k_final" assert loaded.config.training.steps == 12000 assert loaded.config.training.validation_interval_steps == 500 assert loaded.config.loss.external_ruler_weight == pytest.approx(0.70) assert loaded.config.loss.teacher_weight == pytest.approx(0.30) assert loaded.config.loss.teacher_ruler_weight == pytest.approx(0.05) assert loaded.config.loss.external_prior_weight == pytest.approx(0.005) def test_real_frozen_interim_assignment_is_loaded_without_resplitting() -> None: value = os.environ.get("MINIMAX_INTERIM_678_ROOT") if value is None: pytest.skip("MINIMAX_INTERIM_678_ROOT is not set") root = Path(value).resolve(strict=True) loaded = load_external_finetune_config(CONFIG) assert isinstance(loaded.config.data, InterimExternalDataConfig) splits = load_interim_source_splits( root / "splits.json", loaded.config.data, verify_audio_files=False, ) assert {name: len(rows) for name, rows in splits.items()} == { "train": 542, "validation": 68, "heldout": 68, } all_rows = [row for rows in splits.values() for row in rows] assert len({row.source_id for row in all_rows}) == 678 assert len({row.canonical_sha256 for row in all_rows}) == 678 assert all( Path(row.canonical_path).parent == (root / "files") for row in all_rows if row.canonical_path is not None ) def test_interim_loader_rejects_even_reformatted_assignment(tmp_path: Path) -> None: value = os.environ.get("MINIMAX_INTERIM_678_ROOT") if value is None: pytest.skip("MINIMAX_INTERIM_678_ROOT is not set") source = Path(value).resolve(strict=True) (tmp_path / "manifest.jsonl").write_bytes((source / "manifest.jsonl").read_bytes()) (tmp_path / "summary.json").write_bytes((source / "summary.json").read_bytes()) data = (source / "splits.json").read_text(encoding="utf-8") (tmp_path / "splits.json").write_text(data + " ", encoding="utf-8") loaded = load_external_finetune_config(CONFIG) assert isinstance(loaded.config.data, InterimExternalDataConfig) with pytest.raises(ValueError, match="split file SHA-256"): load_interim_source_splits( tmp_path / "splits.json", loaded.config.data, verify_audio_files=False, ) def test_nonfinite_ruler_names_component_bad_row_and_exact_values( monkeypatch: pytest.MonkeyPatch, ) -> None: finite = lambda *args, **kwargs: torch.tensor([0.25, 0.5]) monkeypatch.setattr(flow_encoder, "target_energy_normalized_time_nmse", finite) monkeypatch.setattr( flow_encoder, "complex_stft_nmse", lambda *args, **kwargs: torch.tensor([0.25, float("nan")]), ) monkeypatch.setattr(flow_encoder, "mrstft_magnitude_distance", finite) monkeypatch.setattr(flow_encoder, "mid_side_nmse", finite) monkeypatch.setattr(flow_encoder, "relative_envelope_distance", finite) config = load_flow_encoder_config( ROOT / "configs" / "flow-encoder-v1.yaml" ).config audio = torch.zeros(2, 2, 2048) with pytest.raises(FloatingPointError) as observed: flow_encoder.audio_ruler_components(audio, audio, config.loss) message = str(observed.value) assert "complex_stft_nmse" in message assert "row_finite=[true,false]" in message assert "values=[0.25, nan]" in message assert "time_nmse" not in message def test_mid_side_absolute_epsilon_handles_silence_without_changing_ordinary_targets() -> None: silent_target = torch.zeros(1, 2, 32) nonzero_prediction = torch.full((1, 2, 32), 0.125) silent_score = mid_side_nmse( nonzero_prediction, silent_target, floor_fraction=1e-4, epsilon=1e-8, ) assert bool(torch.isfinite(silent_score).all()) target = torch.stack( ( torch.linspace(-0.75, 0.75, 32), torch.linspace(0.5, -0.25, 32), ) ).unsqueeze(0) predicted = target * 0.8 + 0.03 pred_mid = 0.5 * (predicted[:, 0] + predicted[:, 1]) pred_side = 0.5 * (predicted[:, 0] - predicted[:, 1]) target_mid = 0.5 * (target[:, 0] + target[:, 1]) target_side = 0.5 * (target[:, 0] - target[:, 1]) total = target.square().mean(dim=(1, 2)) legacy_terms = [] for pred_part, target_part in ( (pred_mid, target_mid), (pred_side, target_side), ): numerator = (pred_part - target_part).square().mean(dim=1) denominator = torch.maximum( target_part.square().mean(dim=1), 1e-4 * total ) legacy_terms.append(numerator / denominator) legacy = torch.stack(legacy_terms).mean(dim=0) corrected = mid_side_nmse(predicted, target, floor_fraction=1e-4, epsilon=1e-8) assert torch.equal(corrected, legacy)