diff --git a/families/personaplex/reference/adapter.py b/families/personaplex/reference/adapter.py index 0d2836b6ca..18ce5d7c33 100644 --- a/families/personaplex/reference/adapter.py +++ b/families/personaplex/reference/adapter.py @@ -62,8 +62,8 @@ def run() -> list[Any]: tokens = self.generator.step(encoded[:, :, index:index + 1]) if tokens is None: continue + # Decode the returned stream once; the other codec is only needed for official warmup. chunks.append(self.mimi.decode(tokens[:, 1:9])[0, 0].float()) - self.other_mimi.decode(tokens[:, 1:9]) if len(chunks) >= frames: return chunks return chunks diff --git a/families/personaplex/tests/test_reference_decode_work.py b/families/personaplex/tests/test_reference_decode_work.py new file mode 100644 index 0000000000..3cc9ffd663 --- /dev/null +++ b/families/personaplex/tests/test_reference_decode_work.py @@ -0,0 +1,70 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""The timed Native task decodes the audio it returns, once per frame.""" + +from __future__ import annotations + +import sys +from types import ModuleType, SimpleNamespace + +import numpy as np +import pytest + +from families.personaplex.reference.adapter import Adapter + + +def test_native_speech_does_not_decode_discarded_audio(monkeypatch, tmp_path): + torch = pytest.importorskip("torch") + + class Codec: + sample_rate = 24_000 + + def __init__(self): + self.decode_calls = 0 + + def reset_streaming(self): + pass + + def decode(self, tokens): + self.decode_calls += 1 + value = float(tokens[0, 0, 0]) + return torch.tensor([[[value, value + 0.5]]]) + + class Generator: + def reset_streaming(self): + pass + + def step(self, encoded): + return encoded[:, :1].expand(1, 17, 1) + + # Exercise the real invoke loop without loading a checkpoint or using CUDA. + lm = ModuleType("moshi.models.lm") + lm._iterate_audio = lambda *args, **kwargs: iter(()) + lm.encode_from_sphn = lambda *args, **kwargs: iter( + [torch.tensor([[[1, 2, 3]]], dtype=torch.int64)] + ) + monkeypatch.setitem(sys.modules, "moshi.models.lm", lm) + + adapter = Adapter.__new__(Adapter) + adapter.mimi = Codec() + adapter.other_mimi = Codec() # retained for the pinned official warmup + adapter.generator = Generator() + adapter.frame_size = 1920 + adapter.host = SimpleNamespace( + required=lambda request, name: request[name], + load_audio=lambda *args: np.zeros(5760, dtype=np.float32), + timed=lambda run: (run(), 1.0), + tensor_observation=lambda audio, base: {"audio": audio}, + invocation=lambda observation, ms: (observation, ms), + ) + + observation, _ = adapter.invoke( + {"audio_path": "input.wav", "max_new_tokens": 2}, tmp_path / "result" + ) + + np.testing.assert_array_equal(observation["audio"], [1.0, 1.5, 2.0, 2.5]) + assert observation["output_tokens"] == 2 + assert observation["sample_rate"] == 24_000 + assert adapter.mimi.decode_calls == 2 + assert adapter.other_mimi.decode_calls == 0