Download convert.py from Moipee/MossFormer2-SS-16K-CoreML: direct link, hf CLI and curl.
- Browser
- Download file 4.31 kB
-
https://huggingface.co/Moipee/MossFormer2-SS-16K-CoreML/resolve/main/convert.py
- Command line
-
hf download hf://Moipee/MossFormer2-SS-16K-CoreML/convert.py
-
curl -L -o convert.py https://huggingface.co/Moipee/MossFormer2-SS-16K-CoreML/resolve/main/convert.py
4.31 kB
| """Converts Alibaba's MossFormer2_SS_16K (ClearerVoice-Studio) to Core ML, then checks the Core ML model against the | |
| PyTorch one on the same input. | |
| pip install clearvoice coremltools | |
| python convert.py | |
| Writes MossFormer2_SS_16K.mlpackage. Converted with clearvoice 0.1.2, torch 2.15 and coremltools 9.0 on macOS 26. | |
| """ | |
| import time | |
| import numpy as np | |
| import torch | |
| import torch.backends.mps | |
| torch.backends.mps.is_available = lambda: False | |
| import coremltools as ct | |
| from clearvoice import ClearVoice | |
| # 4,096 encoder steps (kernel 16, stride 8): 16 whole attention groups of 256, so attention needs no padding mask, | |
| # which Core ML can't convert. | |
| WINDOW = 32776 | |
| network = ClearVoice(task="speech_separation", model_names=["MossFormer2_SS_16K"]).models[0].model.eval().cpu() | |
| # The token shift pads by -1 at one end, and Core ML's pad takes only non-negative amounts: cropping where an amount | |
| # is negative, then padding the rest, gives the same tensor. | |
| _pad = torch.nn.functional.pad | |
| def pad(x, amounts, mode="constant", value=None): | |
| if all(a >= 0 for a in amounts): | |
| return _pad(x, amounts, mode=mode, value=value) | |
| keep = [slice(None)] * x.dim() | |
| positive = [] | |
| for i in range(0, len(amounts), 2): | |
| dim, before, after = x.dim() - 1 - i // 2, amounts[i], amounts[i + 1] | |
| keep[dim] = slice(-before if before < 0 else 0, x.shape[dim] + after if after < 0 else x.shape[dim]) | |
| positive += [max(before, 0), max(after, 0)] | |
| return _pad(x[tuple(keep)], positive, mode=mode, value=value) | |
| # Core ML converts only some einsum forms; each one MossFormer2 uses is a plain or a matrix product. | |
| _einsum = torch.einsum | |
| def einsum(equation, *operands): | |
| a, b = operands[0], operands[1] if len(operands) > 1 else None | |
| form = equation.replace(" ", "") | |
| if form == "...d,hd->...hd": | |
| return a.unsqueeze(-2) * b | |
| if form == "...id,...jd->...ij": | |
| return a @ b.transpose(-1, -2) | |
| if form == "...ij,...jd->...id": | |
| return a @ b | |
| if form == "bgnd,bgne->bde": | |
| batch, groups, n, d = a.shape | |
| return a.reshape(batch, groups * n, d).transpose(1, 2) @ b.reshape(batch, groups * n, -1) | |
| if form == "bgnd,bde->bgne": | |
| return a @ b.unsqueeze(1) | |
| if form in ("i,j->ij", "...,f->...f"): | |
| return a.unsqueeze(-1) * b | |
| raise ValueError(f"einsum form not handled: {equation}") | |
| from clearvoice.models.mossformer2_ss import mossformer2_block | |
| import rotary_embedding_torch.rotary_embedding_torch as rotary | |
| torch.nn.functional.pad = pad | |
| mossformer2_block.einsum = einsum | |
| if hasattr(rotary, "einsum"): | |
| rotary.einsum = einsum | |
| class Separate(torch.nn.Module): | |
| """One tensor in and one out: audio [1, WINDOW] -> voices [1, 2, WINDOW].""" | |
| def __init__(self, network): | |
| super().__init__() | |
| self.network = network | |
| def forward(self, audio): | |
| return torch.stack(self.network(audio), dim=1) | |
| model = Separate(network).eval() | |
| example = torch.from_numpy(np.random.default_rng(0).standard_normal((1, WINDOW)).astype(np.float32) * 0.05) | |
| with torch.no_grad(): | |
| reference = model(example).numpy()[0] | |
| started = time.time() | |
| traced = torch.jit.trace(model, example, check_trace=False) | |
| mlmodel = ct.convert( | |
| traced, | |
| inputs=[ct.TensorType(name="audio", shape=(1, WINDOW), dtype=np.float32)], | |
| outputs=[ct.TensorType(name="voices", dtype=np.float32)], | |
| convert_to="mlprogram", | |
| compute_precision=ct.precision.FLOAT16, | |
| minimum_deployment_target=ct.target.macOS15, | |
| ) | |
| mlmodel.short_description = "MossFormer2 (ClearerVoice-Studio) speech separation, 16 kHz, two voices, 2.05 s windows" | |
| mlmodel.license = "Apache-2.0" | |
| mlmodel.save("MossFormer2_SS_16K.mlpackage") | |
| print(f"converted in {time.time() - started:.0f} s") | |
| def si_sdr(estimate, target): | |
| estimate, target = estimate - estimate.mean(), target - target.mean() | |
| projection = np.dot(estimate, target) / np.dot(target, target) * target | |
| return 10 * np.log10(np.sum(projection ** 2) / np.sum((estimate - projection) ** 2)) | |
| loaded = ct.models.MLModel("MossFormer2_SS_16K.mlpackage", compute_units=ct.ComputeUnit.CPU_AND_GPU) | |
| voices = loaded.predict({"audio": example.numpy()})["voices"][0] | |
| for k in range(2): | |
| print(f"voice {k + 1}: Core ML vs PyTorch {si_sdr(voices[k], reference[k]):.1f} dB SI-SDR") | |