nida-tts / src /nida /data /workers /vae_decode.py
Bilal7Dev's picture
Nida v0.5 (research preview): weights, 16 voices, code, model card
c0bce59 verified
Raw History Blame Contribute Delete
3.69 kB
"""VoxCPM2 AudioVAE V2 decoder: latent frames ``.npy`` → 48 kHz float WAV (listening samples).
The input of each request is a latent array ``[num_frames, latent_dim]`` (float32, *VAE*
scale, i.e. already de-normalised) whose path is passed as ``audio_path`` (the worker
protocol's input file). Output: ``params["out_path"]``, 32-bit float mono WAV at the
decoder's output rate, written atomically; with ``params["out_path_16k"]`` also a 16 kHz
16-bit copy (for ASR checks). Same pinned weights as :mod:`vae_encode`.
"""
from __future__ import annotations
import importlib
from collections.abc import Mapping, Sequence
from pathlib import Path
from nida.bench.protocol import WorkRequest
from nida.bench.workers.runner import main_for
from nida.bench.workers.vae_voxcpm import VoxCPMVAEAdapter
__all__ = ["ASR_RATE_HZ", "VAEDecodeAdapter", "wav_out_path"]
#: Sample rate of the optional ASR copy.
ASR_RATE_HZ = 16000
def wav_out_path(request: WorkRequest) -> Path:
"""The ``.wav`` output path named by ``request.params["out_path"]``.
Raises:
ValueError: if the parameter is missing or does not end in ``.wav``.
"""
raw = request.params.get("out_path")
if not raw:
raise ValueError(f"{request.clip_id}: missing params.out_path")
path = Path(raw)
if path.suffix != ".wav":
raise ValueError(f"{request.clip_id}: params.out_path must end in .wav, got {raw!r}")
return path
class VAEDecodeAdapter(VoxCPMVAEAdapter):
name = "vae_decode"
def _decode(self, request: WorkRequest) -> Mapping[str, object]:
out_path = wav_out_path(request)
torch = self._torch
numpy = importlib.import_module("numpy")
soundfile = importlib.import_module("soundfile")
latents = numpy.load(request.audio_path, allow_pickle=False)
if latents.ndim != 2 or latents.shape[0] < 1:
raise ValueError(f"{request.clip_id}: expected [frames, dim], got {latents.shape}")
if not bool(numpy.isfinite(latents).all()):
raise ValueError(f"{request.clip_id}: non-finite latents")
z = torch.from_numpy(latents.astype("float32")).transpose(0, 1).unsqueeze(0)
with torch.inference_mode():
decoded = self._vae.decode(z.to(self._device)) # (1, 1, samples)
out = decoded.reshape(-1).float().cpu().numpy()
rate_hz = int(self._vae.out_sample_rate)
target = Path(out_path)
target.parent.mkdir(parents=True, exist_ok=True)
tmp = target.with_name(f".{target.name}.tmp.wav") # never leave a partial output
soundfile.write(str(tmp), out, rate_hz, subtype="FLOAT")
tmp.replace(target)
payload: dict[str, object] = {}
if request.params.get("out_path_16k"):
functional = importlib.import_module("torchaudio.functional")
low = functional.resample(torch.from_numpy(out), rate_hz, ASR_RATE_HZ).numpy()
target_16k = Path(request.params["out_path_16k"])
tmp_16k = target_16k.with_name(f".{target_16k.name}.tmp.wav")
soundfile.write(str(tmp_16k), numpy.clip(low, -1.0, 1.0), ASR_RATE_HZ, "PCM_16")
tmp_16k.replace(target_16k)
payload["out_path_16k"] = str(target_16k)
return {
**payload,
"out_path": str(out_path),
"out_sample_rate_hz": rate_hz,
"num_samples": int(out.shape[0]),
"num_latent_frames": int(latents.shape[0]),
}
def run_batch(self, batch: Sequence[WorkRequest]) -> list[Mapping[str, object]]:
return [self._decode(r) for r in batch]
if __name__ == "__main__":
raise SystemExit(main_for(VAEDecodeAdapter()))