Files
wehub-resource-sync ba4be087d5
Create PR to main with cherry-pick from release / cherry-pick (push) Failing after 0s
CICD NeMo / pre-flight (push) Failing after 0s
CICD NeMo / configure (push) Has been skipped
Build, validate, and release Neural Modules / pre-flight (push) Failing after 1s
CICD NeMo / code-linting (push) Has been skipped
Build, validate, and release Neural Modules / release (push) Has been skipped
Build, validate, and release Neural Modules / release-summary (push) Has been cancelled
CICD NeMo / cicd-test-container-build (push) Has been cancelled
CICD NeMo / cicd-import-tests (push) Has been cancelled
CICD NeMo / L0_Setup_Test_Data_And_Models (push) Has been cancelled
CICD NeMo / cicd-main-unit-tests (push) Has been cancelled
CICD NeMo / cicd-main-speech (push) Has been cancelled
CICD NeMo / Nemo_CICD_Test (push) Has been cancelled
CICD NeMo / Coverage (e2e) (push) Has been cancelled
CICD NeMo / Coverage (unit-test) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CICD NeMo / cicd-wait-in-queue (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:28:58 +08:00

182 lines
7.3 KiB
Python

# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import pytest
import torch
from nemo.collections.asr.inference.streaming.buffering.incremental_audio_bufferer import IncrementalAudioBufferer
from nemo.collections.asr.inference.streaming.framing.mono_stream import MonoStream
from nemo.collections.asr.inference.streaming.framing.request import Frame
@pytest.fixture(scope="module")
def test_audios():
return torch.ones(83200), torch.ones(118960)
def _make_frame(samples: torch.Tensor, stream_id: int = 0, is_first: bool = False, is_last: bool = False) -> Frame:
return Frame(
samples=samples,
stream_id=stream_id,
is_first=is_first,
is_last=is_last,
)
class TestIncrementalAudioBufferer:
"""Tests for IncrementalAudioBufferer."""
@pytest.mark.unit
def test_constructor_valid_params(self):
"""Constructor with valid params initializes buffer and capacity."""
sample_rate = 16000
buffer_size_in_secs = 5.0
chunk_size_in_secs = 2.5
overlap_size_in_secs = 2.5
buf = IncrementalAudioBufferer(
sample_rate=sample_rate,
buffer_size_in_secs=buffer_size_in_secs,
chunk_size_in_secs=chunk_size_in_secs,
overlap_size_in_secs=overlap_size_in_secs,
)
assert buf.sample_rate == sample_rate
assert buf.buffer_size == int(buffer_size_in_secs * sample_rate)
assert buf.chunk_size == int(chunk_size_in_secs * sample_rate)
assert buf.overlap_size == int(overlap_size_in_secs * sample_rate)
assert buf.sample_buffer.shape[0] == buf.buffer_size
assert buf.remaining_capacity == buf.buffer_size
assert buf.head == 0
assert not buf.is_full()
@pytest.mark.unit
def test_constructor_overlap_negative_raises(self):
"""Overlap < 0 raises ValueError."""
with pytest.raises(ValueError, match="Overlap size.*must satisfy"):
IncrementalAudioBufferer(
sample_rate=16000,
buffer_size_in_secs=5.0,
chunk_size_in_secs=2.5,
overlap_size_in_secs=-0.1,
)
@pytest.mark.unit
def test_constructor_overlap_exceeds_buffer_raises(self):
"""Overlap > buffer_size raises ValueError."""
with pytest.raises(ValueError, match="Overlap size.*must satisfy"):
IncrementalAudioBufferer(
sample_rate=16000,
buffer_size_in_secs=5.0,
chunk_size_in_secs=2.5,
overlap_size_in_secs=6.0,
)
@pytest.mark.unit
def test_constructor_buffer_not_divisible_by_chunk_raises(self):
"""Buffer size not divisible by chunk size raises ValueError."""
with pytest.raises(ValueError, match="Buffer size.*must be divisible by chunk size"):
IncrementalAudioBufferer(
sample_rate=16000,
buffer_size_in_secs=5.0,
chunk_size_in_secs=1.7,
overlap_size_in_secs=1.7,
)
@pytest.mark.unit
def test_constructor_overlap_not_divisible_by_chunk_raises(self):
"""Overlap not divisible by chunk size raises ValueError."""
with pytest.raises(ValueError, match="Overlap size.*must be divisible by chunk size"):
IncrementalAudioBufferer(
sample_rate=16000,
buffer_size_in_secs=5.0,
chunk_size_in_secs=2.5,
overlap_size_in_secs=2.0,
)
@pytest.mark.unit
def test_update_single_frame(self):
"""Single frame update fills start of buffer and decreases remaining_capacity."""
buf = IncrementalAudioBufferer(
sample_rate=16000,
buffer_size_in_secs=5.0,
chunk_size_in_secs=2.5,
overlap_size_in_secs=2.5,
)
chunk_size = 40000 # 2.5 * 16000
samples = torch.arange(chunk_size, dtype=torch.float32)
frame = _make_frame(samples)
buf.update(frame)
assert buf.head == chunk_size
assert buf.remaining_capacity == buf.buffer_size - chunk_size
assert torch.allclose(buf.sample_buffer[:chunk_size], samples, atol=1e-5)
assert not buf.is_full()
@pytest.mark.unit
def test_update_multiple_frames_until_full(self):
"""Multiple updates fill buffer; is_full() becomes True when capacity is 0."""
buf = IncrementalAudioBufferer(
sample_rate=16000,
buffer_size_in_secs=5.0,
chunk_size_in_secs=2.5,
overlap_size_in_secs=2.5,
)
chunk_size = 40000
for i in range(2):
samples = torch.full((chunk_size,), float(i), dtype=torch.float32)
frame = _make_frame(samples)
buf.update(frame)
assert buf.remaining_capacity == 0
assert buf.is_full()
assert buf.head == buf.buffer_size
assert torch.allclose(buf.sample_buffer[:chunk_size], torch.zeros(chunk_size), atol=1e-5)
assert torch.allclose(buf.sample_buffer[chunk_size:], torch.ones(chunk_size), atol=1e-5)
@pytest.mark.unit
def test_update_frame_exceeds_buffer_raises(self):
"""Frame larger than buffer size raises RuntimeError."""
buf = IncrementalAudioBufferer(
sample_rate=16000,
buffer_size_in_secs=5.0,
chunk_size_in_secs=2.5,
overlap_size_in_secs=2.5,
)
oversized = torch.zeros(buf.buffer_size + 1)
frame = _make_frame(oversized)
with pytest.raises(RuntimeError, match="Frame size.*exceeds buffer size"):
buf.update(frame)
@pytest.mark.unit
def test_incremental_audio_bufferer_with_mono_stream(self, test_audios):
"""Integration: feed frames from MonoStream; buffer contents and paddings are consistent."""
sample_rate = 16000
chunk_size_in_secs = 2.5
buffer_size_in_secs = 5.0
overlap_size_in_secs = 2.5
for audio in test_audios:
stream = MonoStream(sample_rate, frame_size_in_secs=chunk_size_in_secs, stream_id=0, pad_last_frame=False)
stream.load_audio(audio, options=None)
buf = IncrementalAudioBufferer(
sample_rate=sample_rate,
buffer_size_in_secs=buffer_size_in_secs,
chunk_size_in_secs=chunk_size_in_secs,
overlap_size_in_secs=overlap_size_in_secs,
)
for frame in iter(stream):
frame = frame[0]
buf.update(frame)
# Newest frame is at [head - frame.size : head]; after update it's at [head - frame.size : head]
start = buf.head - frame.size
assert torch.allclose(buf.sample_buffer[start : buf.head], frame.samples, atol=1e-5)
assert buf.remaining_capacity == max(0, buf.remaining_capacity)