Skip to content

Commit d0b9cc9

Browse files
committed
fix(frontend): read cmvn statistics without crashing on blank lines
Both package cmvn readers index line_item[0] and lines[i + 1] without checking, so a blank or whitespace-only line anywhere in the file (trailing newline at EOF, a separator between sections) aborts frontend construction with IndexError, and a file whose tags the walker does not recognize returns empty statistics that only fail much later at inference. Skip blank lines, bound the lookahead, and raise a ValueError naming the file when neither <AddShift> nor <Rescale> parsed.
1 parent 66d7a4c commit d0b9cc9

3 files changed

Lines changed: 70 additions & 10 deletions

File tree

‎funasr/frontends/default.py‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -394,23 +394,25 @@ def _load_cmvn(self, cmvn_file):
394394
cmvn_file: TODO.
395395
"""
396396
with open(cmvn_file, "r", encoding="utf-8") as f:
397-
lines = f.readlines()
397+
lines = [line for line in f.readlines() if line.split()]
398398
means_list = []
399399
vars_list = []
400400
for i in range(len(lines)):
401401
line_item = lines[i].split()
402402
if line_item[0] == "<AddShift>":
403-
line_item = lines[i + 1].split()
404-
if line_item[0] == "<LearnRateCoef>":
403+
line_item = lines[i + 1].split() if i + 1 < len(lines) else []
404+
if line_item and line_item[0] == "<LearnRateCoef>":
405405
add_shift_line = line_item[3 : (len(line_item) - 1)]
406406
means_list = list(add_shift_line)
407407
continue
408408
elif line_item[0] == "<Rescale>":
409-
line_item = lines[i + 1].split()
410-
if line_item[0] == "<LearnRateCoef>":
409+
line_item = lines[i + 1].split() if i + 1 < len(lines) else []
410+
if line_item and line_item[0] == "<LearnRateCoef>":
411411
rescale_line = line_item[3 : (len(line_item) - 1)]
412412
vars_list = list(rescale_line)
413413
continue
414+
if not means_list or not vars_list:
415+
raise ValueError(f"No <AddShift>/<Rescale> statistics found in cmvn file: {cmvn_file}")
414416
means = np.array(means_list).astype(np.float64)
415417
vars = np.array(vars_list).astype(np.float64)
416418
return means, vars

‎funasr/frontends/wav_frontend.py‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,25 @@ def load_cmvn(cmvn_file):
1919
cmvn_file: TODO.
2020
"""
2121
with open(cmvn_file, "r", encoding="utf-8") as f:
22-
lines = f.readlines()
22+
lines = [line for line in f.readlines() if line.split()]
2323
means_list = []
2424
vars_list = []
2525
for i in range(len(lines)):
2626
line_item = lines[i].split()
2727
if line_item[0] == "<AddShift>":
28-
line_item = lines[i + 1].split()
29-
if line_item[0] == "<LearnRateCoef>":
28+
line_item = lines[i + 1].split() if i + 1 < len(lines) else []
29+
if line_item and line_item[0] == "<LearnRateCoef>":
3030
add_shift_line = line_item[3 : (len(line_item) - 1)]
3131
means_list = list(add_shift_line)
3232
continue
3333
elif line_item[0] == "<Rescale>":
34-
line_item = lines[i + 1].split()
35-
if line_item[0] == "<LearnRateCoef>":
34+
line_item = lines[i + 1].split() if i + 1 < len(lines) else []
35+
if line_item and line_item[0] == "<LearnRateCoef>":
3636
rescale_line = line_item[3 : (len(line_item) - 1)]
3737
vars_list = list(rescale_line)
3838
continue
39+
if not means_list or not vars_list:
40+
raise ValueError(f"No <AddShift>/<Rescale> statistics found in cmvn file: {cmvn_file}")
3941
means = np.array(means_list).astype(np.float32)
4042
vars = np.array(vars_list).astype(np.float32)
4143
cmvn = np.array([means, vars])

‎tests/test_numpy_compatibility.py‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,62 @@ def test_cmvn_load_returns_float64_arrays(cmvn_path):
3737
np.testing.assert_array_equal(scales, np.full(8, 1.25, dtype=np.float64))
3838

3939

40+
def test_cmvn_readers_skip_blank_lines(tmp_path):
41+
from funasr.frontends.default import MultiChannelFrontend
42+
from funasr.frontends.wav_frontend import load_cmvn
43+
44+
path = tmp_path / "blank_lines.cmvn"
45+
path.write_text(
46+
"\n<AddShift>\n \t \n<LearnRateCoef> 0 0 "
47+
+ " ".join(["0.1"] * 8)
48+
+ " </LearnRateCoef>\n</AddShift>\n\n"
49+
+ "<Rescale>\n<LearnRateCoef> 0 0 "
50+
+ " ".join(["1.25"] * 8)
51+
+ " </LearnRateCoef>\n</Rescale>\n\n",
52+
encoding="utf-8",
53+
)
54+
means, scales = MultiChannelFrontend._load_cmvn(None, path)
55+
np.testing.assert_array_equal(means, np.full(8, 0.1, dtype=np.float64))
56+
np.testing.assert_array_equal(scales, np.full(8, 1.25, dtype=np.float64))
57+
cmvn = load_cmvn(str(path))
58+
assert cmvn.shape == (2, 8)
59+
torch.testing.assert_close(cmvn[0], torch.full((8,), 0.1))
60+
torch.testing.assert_close(cmvn[1], torch.full((8,), 1.25))
61+
62+
63+
def test_cmvn_readers_reject_files_without_statistics(tmp_path):
64+
from funasr.frontends.default import MultiChannelFrontend
65+
from funasr.frontends.wav_frontend import load_cmvn
66+
67+
# Kaldi also emits the whole nnet on a single line; the readers walk lines,
68+
# so nothing matches and empty statistics used to be returned silently.
69+
compact = tmp_path / "compact.cmvn"
70+
compact.write_text(
71+
"<Nnet> <AddShift> <LearnRateCoef> 1 [ -1.0 -2.0 ] </LearnRateCoef> "
72+
"<Rescale> <LearnRateCoef> 1 [ 0.5 0.5 ] </Rescale> </Nnet>\n",
73+
encoding="utf-8",
74+
)
75+
truncated = tmp_path / "truncated.cmvn"
76+
truncated.write_text("<AddShift> 2 2\n", encoding="utf-8")
77+
for path in (compact, truncated):
78+
with pytest.raises(ValueError) as excinfo:
79+
MultiChannelFrontend._load_cmvn(None, path)
80+
assert str(path) in str(excinfo.value)
81+
with pytest.raises(ValueError) as excinfo:
82+
load_cmvn(str(path))
83+
assert str(path) in str(excinfo.value)
84+
85+
86+
def test_cmvn_reader_loads_the_am_mvn_shipped_by_the_runtime():
87+
from funasr.frontends.wav_frontend import load_cmvn
88+
89+
path = Path(__file__).resolve().parents[1] / (
90+
"runtime/triton_gpu/model_repo_sense_voice_small/feature_extractor/am.mvn"
91+
)
92+
cmvn = load_cmvn(str(path))
93+
assert cmvn.shape == (2, 560) and torch.isfinite(cmvn).all()
94+
95+
4096
def test_frontend_applies_cmvn_and_preserves_padding(cmvn_path):
4197
from funasr.frontends.default import MultiChannelFrontend
4298

0 commit comments

Comments
 (0)