| from __future__ import annotations |
|
|
| import importlib.util |
| import sys |
| import types |
| from pathlib import Path |
|
|
| import pytest |
| import torch |
|
|
| ROOT = Path(__file__).resolve().parents[1] |
| SRC = ROOT / "src" |
|
|
|
|
| def _load_module(name: str, path: Path): |
| spec = importlib.util.spec_from_file_location(name, path) |
| module = importlib.util.module_from_spec(spec) |
| assert spec.loader is not None |
| sys.modules[name] = module |
| spec.loader.exec_module(module) |
| return module |
|
|
|
|
| def bootstrap_repo_modules(monkeypatch): |
| for name, path in [ |
| ("voxcpm", SRC / "voxcpm"), |
| ("voxcpm.model", SRC / "voxcpm" / "model"), |
| ("voxcpm.modules", SRC / "voxcpm" / "modules"), |
| ]: |
| pkg = types.ModuleType(name) |
| pkg.__path__ = [str(path)] |
| monkeypatch.setitem(sys.modules, name, pkg) |
|
|
| hh = types.ModuleType("huggingface_hub") |
| hh.snapshot_download = lambda *a, **k: "/tmp/fake" |
| monkeypatch.setitem(sys.modules, "huggingface_hub", hh) |
|
|
| pydantic = types.ModuleType("pydantic") |
|
|
| class BaseModel: |
| @classmethod |
| def model_rebuild(cls): |
| return None |
|
|
| @classmethod |
| def model_validate_json(cls, s): |
| return cls() |
|
|
| def model_dump(self): |
| return {} |
|
|
| pydantic.BaseModel = BaseModel |
| monkeypatch.setitem(sys.modules, "pydantic", pydantic) |
|
|
| torchaudio = types.ModuleType("torchaudio") |
| monkeypatch.setitem(sys.modules, "torchaudio", torchaudio) |
|
|
| librosa = types.ModuleType("librosa") |
| librosa.effects = types.SimpleNamespace(trim=lambda *a, **k: (None, (0, 0))) |
| monkeypatch.setitem(sys.modules, "librosa", librosa) |
|
|
| einops = types.ModuleType("einops") |
| einops.rearrange = lambda x, *a, **k: x |
| monkeypatch.setitem(sys.modules, "einops", einops) |
|
|
| tqdm_pkg = types.ModuleType("tqdm") |
| tqdm_pkg.__path__ = ["/nonexistent"] |
| tqdm_pkg.tqdm = lambda x, *a, **k: x |
| monkeypatch.setitem(sys.modules, "tqdm", tqdm_pkg) |
|
|
| tqdm_auto = types.ModuleType("tqdm.auto") |
| tqdm_auto.tqdm = lambda x, *a, **k: x |
| monkeypatch.setitem(sys.modules, "tqdm.auto", tqdm_auto) |
|
|
| transformers = types.ModuleType("transformers") |
|
|
| class LlamaTokenizerFast: |
| pass |
|
|
| class PreTrainedTokenizer: |
| pass |
|
|
| transformers.LlamaTokenizerFast = LlamaTokenizerFast |
| transformers.PreTrainedTokenizer = PreTrainedTokenizer |
| monkeypatch.setitem(sys.modules, "transformers", transformers) |
|
|
| internal_mods = { |
| "voxcpm.modules.audiovae": ["AudioVAE", "AudioVAEConfig", "AudioVAEV2", "AudioVAEConfigV2"], |
| "voxcpm.modules.layers": ["ScalarQuantizationLayer"], |
| "voxcpm.modules.locdit": ["CfmConfig", "UnifiedCFM", "VoxCPMLocDiT", "VoxCPMLocDiTV2"], |
| "voxcpm.modules.locenc": ["VoxCPMLocEnc"], |
| "voxcpm.modules.minicpm4": ["MiniCPM4Config", "MiniCPMModel"], |
| "voxcpm.modules.layers.lora": ["apply_lora_to_named_linear_modules", "LoRALinear"], |
| } |
| for modname, names in internal_mods.items(): |
| module = types.ModuleType(modname) |
| for name in names: |
| if name == "apply_lora_to_named_linear_modules": |
| setattr(module, name, lambda *a, **k: None) |
| else: |
| setattr(module, name, type(name, (), {})) |
| monkeypatch.setitem(sys.modules, modname, module) |
|
|
| _load_module("voxcpm.model.utils", SRC / "voxcpm" / "model" / "utils.py") |
| voxcpm = _load_module("voxcpm.model.voxcpm", SRC / "voxcpm" / "model" / "voxcpm.py") |
| voxcpm2 = _load_module("voxcpm.model.voxcpm2", SRC / "voxcpm" / "model" / "voxcpm2.py") |
| return voxcpm.VoxCPMModel, voxcpm2.VoxCPM2Model |
|
|
|
|
| class DummyModel: |
| device = "cpu" |
|
|
| def named_parameters(self): |
| return [] |
|
|
|
|
| @pytest.mark.parametrize("module_name", ["v1", "v2"]) |
| def test_load_lora_weights_accepts_tensor_only_legacy_checkpoints(monkeypatch, tmp_path, module_name): |
| VoxCPMModel, VoxCPM2Model = bootstrap_repo_modules(monkeypatch) |
| cls = VoxCPMModel if module_name == "v1" else VoxCPM2Model |
|
|
| ckpt_path = tmp_path / "lora_weights.ckpt" |
| torch.save({"state_dict": {"fake": torch.zeros(1)}}, ckpt_path) |
|
|
| loaded, skipped = cls.load_lora_weights(DummyModel(), str(ckpt_path), device="cpu") |
|
|
| assert loaded == [] |
| assert skipped == ["fake"] |
|
|
|
|
| @pytest.mark.parametrize("module_name", ["v1", "v2"]) |
| def test_load_lora_weights_rejects_malicious_pickle_payloads(monkeypatch, tmp_path, module_name): |
| VoxCPMModel, VoxCPM2Model = bootstrap_repo_modules(monkeypatch) |
| cls = VoxCPMModel if module_name == "v1" else VoxCPM2Model |
|
|
| ckpt_path = tmp_path / "lora_weights.ckpt" |
| marker_path = tmp_path / f"{module_name}-marker.txt" |
|
|
| class Exploit: |
| def __reduce__(self): |
| import pathlib |
|
|
| return (pathlib.Path.write_text, (marker_path, f"{module_name} executed\n")) |
|
|
| torch.save({"state_dict": {"fake": torch.zeros(1)}, "boom": Exploit()}, ckpt_path) |
|
|
| with pytest.raises(Exception, match="Weights only load failed"): |
| cls.load_lora_weights(DummyModel(), str(ckpt_path), device="cpu") |
|
|
| assert not marker_path.exists() |
|
|