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

155 lines
5.2 KiB
Python

# Copyright (c) 2026, 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.parts.utils.asr_multispeaker_utils import get_hidden_length_from_sample_length
from nemo.collections.asr.parts.utils.sot_speaker_alignment import (
collate_speaker_activity_targets,
ensure_single_speaker_sot,
fix_speaker_activity,
parse_speaker_tokens,
sl_to_wl_sot,
)
@pytest.mark.unit
def test_parse_speaker_tokens_handles_multi_digit_speakers():
assert parse_speaker_tokens("<spk:10> hello <spk:1> world") == [10, 1]
@pytest.mark.unit
def test_sl_and_wl_sot_have_same_speaker_sequence():
sl_text = "<spk:0> hello world <spk:1> yes"
wl_text = sl_to_wl_sot(sl_text)
assert wl_text == "<spk:0> hello <spk:0> world <spk:1> yes"
assert parse_speaker_tokens(sl_text) == parse_speaker_tokens(wl_text)
@pytest.mark.unit
def test_ensure_single_speaker_sot_prefixes_no_token_text():
text, spk_idx, changed = ensure_single_speaker_sot("hello world")
assert text == "<spk:0> hello world"
assert spk_idx == 0
assert changed
@pytest.mark.unit
def test_ensure_single_speaker_sot_keeps_existing_tokens():
text, spk_idx, changed = ensure_single_speaker_sot("<spk:2> hello")
assert text == "<spk:2> hello"
assert spk_idx == -1
assert not changed
@pytest.mark.unit
def test_fix_speaker_activity_swaps_simple_two_speaker_permutation():
activity = torch.tensor(
[
[0.0, 1.0],
[0.0, 1.0],
[1.0, 0.0],
[1.0, 0.0],
]
)
fixed = fix_speaker_activity("<spk:0> hello world <spk:1> yes now", activity, num_speakers=2)
expected = torch.tensor(
[
[1.0, 0.0],
[1.0, 0.0],
[0.0, 1.0],
[0.0, 1.0],
]
)
assert torch.equal(fixed, expected)
@pytest.mark.unit
def test_fix_speaker_activity_empty_text_is_noop():
activity = torch.tensor([[1.0, 0.0]])
fixed = fix_speaker_activity("", activity, num_speakers=2)
assert fixed is activity
@pytest.mark.unit
@pytest.mark.parametrize(
"n_spk_in, num_speakers",
[
(5, 4), # speaker dim > num_speakers -> truncate extra columns
(4, 4), # speaker dim == num_speakers -> unchanged
(2, 4), # speaker dim < num_speakers -> zero-pad missing columns
],
)
def test_collate_speaker_activity_targets_normalizes_speaker_axis(n_spk_in, num_speakers):
num_frames = 2
# Distinct per-element values so truncation/padding is observable.
activity = torch.arange(1, num_frames * n_spk_in + 1, dtype=torch.float32).reshape(num_frames, n_spk_in)
targets, target_length = collate_speaker_activity_targets(
[activity],
audio_lens=torch.tensor([2560]),
num_speakers=num_speakers,
num_sample_per_mel_frame=160,
num_mel_frame_per_target_frame=8,
dtype=torch.float32,
)
# Truncate extras / zero-pad missing so the speaker axis is always num_speakers wide.
expected = torch.zeros(num_frames, num_speakers)
keep = min(n_spk_in, num_speakers)
expected[:, :keep] = activity[:, :keep]
assert targets.shape == (1, num_frames, num_speakers)
assert torch.equal(targets[0], expected)
assert target_length.tolist() == [get_hidden_length_from_sample_length(2560, 160, 8)]
@pytest.mark.unit
def test_collate_speaker_activity_targets_mixed_speaker_counts_and_lengths():
# Batch mixing different speaker counts AND time lengths must not crash inside
# collate_matrices: the speaker axis is normalized first, then the time axis is
# zero-padded to the batch max.
five_spk = torch.ones(2, 5) # (T=2, N=5) -> truncated to (2, 4)
two_spk = torch.full((3, 2), 2.0) # (T=3, N=2) -> padded to (3, 4)
targets, target_length = collate_speaker_activity_targets(
[five_spk, two_spk],
audio_lens=torch.tensor([2560, 3840]),
num_speakers=4,
num_sample_per_mel_frame=160,
num_mel_frame_per_target_frame=8,
dtype=torch.float16,
)
assert targets.shape == (2, 3, 4) # B=2, T_max=3, num_speakers=4
assert targets.dtype == torch.float16
# Truncated example: only first 4 cols kept, and its 3rd time-step is zero-padded.
assert torch.equal(targets[0, :2], torch.ones(2, 4, dtype=torch.float16))
assert torch.equal(targets[0, 2], torch.zeros(4, dtype=torch.float16))
# Padded example: cols 2-3 are zero.
assert torch.equal(targets[1, :, 2:], torch.zeros(3, 2, dtype=torch.float16))
assert torch.equal(targets[1, :, :2], torch.full((3, 2), 2.0, dtype=torch.float16))
assert target_length.tolist() == [
get_hidden_length_from_sample_length(2560, 160, 8),
get_hidden_length_from_sample_length(3840, 160, 8),
]