Skip to content

Commit f767ae5

Browse files
ja longform fix and StrEnum import fix (#15499)
Signed-off-by: subhankar-ghosh <subhankar2321@gmail.com>
1 parent be64db1 commit f767ae5

3 files changed

Lines changed: 149 additions & 6 deletions

File tree

nemo/collections/tts/metrics/eou_classifier.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,14 @@
4141

4242
import math
4343
from dataclasses import dataclass, field
44-
from enum import StrEnum
44+
45+
# StrEnum is part of enum in python >= 3.11, for backward compatibility
46+
# to python < 3.11 we import StrEnum from strenum. Use-case: Huggignface
47+
# demo works on python version 3.10
48+
try:
49+
from enum import StrEnum
50+
except ImportError:
51+
from strenum import StrEnum
4552
from typing import Union
4653

4754
import librosa

nemo/collections/tts/models/magpietts.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4731,11 +4731,10 @@ def _initialize_chunked_attn_prior(
47314731
current_starting_point = batch_text_lens[_idx] - current_chunk_len[_idx]
47324732
prior_weights = self.chunked_inference_config.prior_weights_init
47334733
_attn_prior[_idx, :, :current_starting_point] = prior_epsilon * prior_epsilon
4734-
_attn_prior[_idx, :, current_starting_point] = prior_weights[0]
4735-
_attn_prior[_idx, :, current_starting_point + 1] = prior_weights[1]
4736-
_attn_prior[_idx, :, current_starting_point + 2] = prior_weights[2]
4737-
_attn_prior[_idx, :, current_starting_point + 3] = prior_weights[3]
4738-
_attn_prior[_idx, :, current_starting_point + 4] = prior_weights[4]
4734+
for offset, weight in enumerate(prior_weights[:5]):
4735+
idx = current_starting_point + offset
4736+
if idx < max_text_len:
4737+
_attn_prior[_idx, :, idx] = weight
47394738

47404739
return _attn_prior
47414740

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Unit tests for MagpieTTSModel._initialize_chunked_attn_prior bounds-check fix.
17+
18+
PR #15499 introduced a bounds check to prevent out-of-bounds access when
19+
prior_weights offsets exceed max_text_len (e.g. during Japanese longform TTS).
20+
"""
21+
22+
import pytest
23+
import torch
24+
25+
from nemo.collections.tts.models.magpietts import ChunkedInferenceConfig, ChunkState
26+
27+
28+
class _StubModel:
29+
"""Minimal stub that exposes only what _initialize_chunked_attn_prior needs."""
30+
31+
def __init__(self):
32+
self.chunked_inference_config = ChunkedInferenceConfig()
33+
34+
@staticmethod
35+
def _to_int(x):
36+
return int(x)
37+
38+
# Bind the real method to this stub class so we can call it without a full model.
39+
from nemo.collections.tts.models.magpietts import MagpieTTSModel
40+
41+
_initialize_chunked_attn_prior = MagpieTTSModel._initialize_chunked_attn_prior
42+
43+
44+
def _make_chunk_state(batch_size, previous_attn_len, left_offset=None):
45+
"""Helper: create a ChunkState with previous_attn_len populated (non-empty triggers logic)."""
46+
state = ChunkState(batch_size=batch_size)
47+
state.previous_attn_len = list(previous_attn_len)
48+
state.left_offset = list(left_offset) if left_offset is not None else [0] * batch_size
49+
return state
50+
51+
52+
class TestInitializeChunkedAttnPrior:
53+
"""Tests for the bounds-check in _initialize_chunked_attn_prior."""
54+
55+
prior_epsilon = 1e-8
56+
prior_weights = ChunkedInferenceConfig().prior_weights_init # (0.5, 1.0, 0.8, 0.2, 0.2)
57+
58+
def _call(self, chunk_state, current_chunk_len, batch_text_lens, max_text_len, batch_size, use_cfg=False):
59+
model = _StubModel()
60+
return model._initialize_chunked_attn_prior(
61+
chunk_state=chunk_state,
62+
current_chunk_len=torch.tensor(current_chunk_len, dtype=torch.long),
63+
batch_text_lens=torch.tensor(batch_text_lens, dtype=torch.long),
64+
max_text_len=max_text_len,
65+
batch_size=batch_size,
66+
use_cfg=use_cfg,
67+
prior_epsilon=self.prior_epsilon,
68+
device=torch.device('cpu'),
69+
)
70+
71+
@pytest.mark.run_only_on('CPU')
72+
@pytest.mark.unit
73+
def test_normal_case_all_weights_written(self):
74+
"""All 5 prior weights are written when starting point leaves enough room."""
75+
batch_size = 1
76+
max_text_len = 10
77+
batch_text_lens = [8]
78+
current_chunk_len = [3]
79+
# current_starting_point = 8 - 3 = 5; offsets 0..4 → indices 5..9, all < 10
80+
state = _make_chunk_state(batch_size, previous_attn_len=[5])
81+
82+
result = self._call(state, current_chunk_len, batch_text_lens, max_text_len, batch_size)
83+
84+
assert result is not None
85+
assert result.shape == (batch_size, 1, max_text_len)
86+
starting = batch_text_lens[0] - current_chunk_len[0] # 5
87+
for offset, weight in enumerate(self.prior_weights[:5]):
88+
assert result[0, 0, starting + offset].item() == pytest.approx(
89+
weight
90+
), f"Weight mismatch at offset {offset}"
91+
92+
@pytest.mark.run_only_on('CPU')
93+
@pytest.mark.unit
94+
def test_bounds_check_prevents_index_error_near_boundary(self):
95+
"""No IndexError when current_starting_point + some offsets exceed max_text_len."""
96+
batch_size = 1
97+
max_text_len = 7
98+
batch_text_lens = [7]
99+
current_chunk_len = [2]
100+
# current_starting_point = 7 - 2 = 5; offsets 0..4 → indices 5,6,7,8,9
101+
# indices 7,8,9 are >= max_text_len=7 and must be skipped
102+
state = _make_chunk_state(batch_size, previous_attn_len=[5])
103+
104+
# Before the fix this would raise an IndexError; now it must succeed silently.
105+
result = self._call(state, current_chunk_len, batch_text_lens, max_text_len, batch_size)
106+
107+
assert result is not None
108+
assert result.shape == (batch_size, 1, max_text_len)
109+
# Weights at valid indices only
110+
assert result[0, 0, 5].item() == pytest.approx(self.prior_weights[0])
111+
assert result[0, 0, 6].item() == pytest.approx(self.prior_weights[1])
112+
113+
@pytest.mark.run_only_on('CPU')
114+
@pytest.mark.unit
115+
def test_batch_mixed_boundary_conditions(self):
116+
"""Batch with one item having room for all weights and one near the boundary."""
117+
batch_size = 2
118+
max_text_len = 10
119+
batch_text_lens = [10, 10]
120+
current_chunk_len = [5, 1]
121+
# Item 0: starting = 10-5 = 5; offsets 0..4 → indices 5..9, all valid
122+
# Item 1: starting = 10-1 = 9; offset 0 → idx 9 (valid), offsets 1..4 → OOB
123+
state = _make_chunk_state(batch_size, previous_attn_len=[5, 9])
124+
125+
result = self._call(state, current_chunk_len, batch_text_lens, max_text_len, batch_size)
126+
127+
assert result is not None
128+
assert result.shape == (batch_size, 1, max_text_len)
129+
130+
# Item 0: all 5 weights present
131+
for offset, weight in enumerate(self.prior_weights[:5]):
132+
assert result[0, 0, 5 + offset].item() == pytest.approx(
133+
weight
134+
), f"Item 0: weight mismatch at offset {offset}"
135+
136+
# Item 1: only first weight at index 9; offsets 1..4 were out of bounds
137+
assert result[1, 0, 9].item() == pytest.approx(self.prior_weights[0])

0 commit comments

Comments
 (0)