mirror of
https://github.com/modelscope/FunASR.git
synced 2026-10-11 22:39:57 +00:00
fix(frontend): read cmvn statistics without crashing on blank lines (#3760)
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.
This commit is contained in:
@@ -394,23 +394,25 @@ class MultiChannelFrontend(nn.Module):
|
||||
cmvn_file: TODO.
|
||||
"""
|
||||
with open(cmvn_file, "r", encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
lines = [line for line in f.readlines() if line.split()]
|
||||
means_list = []
|
||||
vars_list = []
|
||||
for i in range(len(lines)):
|
||||
line_item = lines[i].split()
|
||||
if line_item[0] == "<AddShift>":
|
||||
line_item = lines[i + 1].split()
|
||||
if line_item[0] == "<LearnRateCoef>":
|
||||
line_item = lines[i + 1].split() if i + 1 < len(lines) else []
|
||||
if line_item and line_item[0] == "<LearnRateCoef>":
|
||||
add_shift_line = line_item[3 : (len(line_item) - 1)]
|
||||
means_list = list(add_shift_line)
|
||||
continue
|
||||
elif line_item[0] == "<Rescale>":
|
||||
line_item = lines[i + 1].split()
|
||||
if line_item[0] == "<LearnRateCoef>":
|
||||
line_item = lines[i + 1].split() if i + 1 < len(lines) else []
|
||||
if line_item and line_item[0] == "<LearnRateCoef>":
|
||||
rescale_line = line_item[3 : (len(line_item) - 1)]
|
||||
vars_list = list(rescale_line)
|
||||
continue
|
||||
if not means_list or not vars_list:
|
||||
raise ValueError(f"No <AddShift>/<Rescale> statistics found in cmvn file: {cmvn_file}")
|
||||
means = np.array(means_list).astype(np.float64)
|
||||
vars = np.array(vars_list).astype(np.float64)
|
||||
return means, vars
|
||||
|
||||
@@ -19,23 +19,25 @@ def load_cmvn(cmvn_file):
|
||||
cmvn_file: TODO.
|
||||
"""
|
||||
with open(cmvn_file, "r", encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
lines = [line for line in f.readlines() if line.split()]
|
||||
means_list = []
|
||||
vars_list = []
|
||||
for i in range(len(lines)):
|
||||
line_item = lines[i].split()
|
||||
if line_item[0] == "<AddShift>":
|
||||
line_item = lines[i + 1].split()
|
||||
if line_item[0] == "<LearnRateCoef>":
|
||||
line_item = lines[i + 1].split() if i + 1 < len(lines) else []
|
||||
if line_item and line_item[0] == "<LearnRateCoef>":
|
||||
add_shift_line = line_item[3 : (len(line_item) - 1)]
|
||||
means_list = list(add_shift_line)
|
||||
continue
|
||||
elif line_item[0] == "<Rescale>":
|
||||
line_item = lines[i + 1].split()
|
||||
if line_item[0] == "<LearnRateCoef>":
|
||||
line_item = lines[i + 1].split() if i + 1 < len(lines) else []
|
||||
if line_item and line_item[0] == "<LearnRateCoef>":
|
||||
rescale_line = line_item[3 : (len(line_item) - 1)]
|
||||
vars_list = list(rescale_line)
|
||||
continue
|
||||
if not means_list or not vars_list:
|
||||
raise ValueError(f"No <AddShift>/<Rescale> statistics found in cmvn file: {cmvn_file}")
|
||||
means = np.array(means_list).astype(np.float32)
|
||||
vars = np.array(vars_list).astype(np.float32)
|
||||
cmvn = np.array([means, vars])
|
||||
|
||||
@@ -37,6 +37,62 @@ def test_cmvn_load_returns_float64_arrays(cmvn_path):
|
||||
np.testing.assert_array_equal(scales, np.full(8, 1.25, dtype=np.float64))
|
||||
|
||||
|
||||
def test_cmvn_readers_skip_blank_lines(tmp_path):
|
||||
from funasr.frontends.default import MultiChannelFrontend
|
||||
from funasr.frontends.wav_frontend import load_cmvn
|
||||
|
||||
path = tmp_path / "blank_lines.cmvn"
|
||||
path.write_text(
|
||||
"\n<AddShift>\n \t \n<LearnRateCoef> 0 0 "
|
||||
+ " ".join(["0.1"] * 8)
|
||||
+ " </LearnRateCoef>\n</AddShift>\n\n"
|
||||
+ "<Rescale>\n<LearnRateCoef> 0 0 "
|
||||
+ " ".join(["1.25"] * 8)
|
||||
+ " </LearnRateCoef>\n</Rescale>\n\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
means, scales = MultiChannelFrontend._load_cmvn(None, path)
|
||||
np.testing.assert_array_equal(means, np.full(8, 0.1, dtype=np.float64))
|
||||
np.testing.assert_array_equal(scales, np.full(8, 1.25, dtype=np.float64))
|
||||
cmvn = load_cmvn(str(path))
|
||||
assert cmvn.shape == (2, 8)
|
||||
torch.testing.assert_close(cmvn[0], torch.full((8,), 0.1))
|
||||
torch.testing.assert_close(cmvn[1], torch.full((8,), 1.25))
|
||||
|
||||
|
||||
def test_cmvn_readers_reject_files_without_statistics(tmp_path):
|
||||
from funasr.frontends.default import MultiChannelFrontend
|
||||
from funasr.frontends.wav_frontend import load_cmvn
|
||||
|
||||
# Kaldi also emits the whole nnet on a single line; the readers walk lines,
|
||||
# so nothing matches and empty statistics used to be returned silently.
|
||||
compact = tmp_path / "compact.cmvn"
|
||||
compact.write_text(
|
||||
"<Nnet> <AddShift> <LearnRateCoef> 1 [ -1.0 -2.0 ] </LearnRateCoef> "
|
||||
"<Rescale> <LearnRateCoef> 1 [ 0.5 0.5 ] </Rescale> </Nnet>\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
truncated = tmp_path / "truncated.cmvn"
|
||||
truncated.write_text("<AddShift> 2 2\n", encoding="utf-8")
|
||||
for path in (compact, truncated):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
MultiChannelFrontend._load_cmvn(None, path)
|
||||
assert str(path) in str(excinfo.value)
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
load_cmvn(str(path))
|
||||
assert str(path) in str(excinfo.value)
|
||||
|
||||
|
||||
def test_cmvn_reader_loads_the_am_mvn_shipped_by_the_runtime():
|
||||
from funasr.frontends.wav_frontend import load_cmvn
|
||||
|
||||
path = Path(__file__).resolve().parents[1] / (
|
||||
"runtime/triton_gpu/model_repo_sense_voice_small/feature_extractor/am.mvn"
|
||||
)
|
||||
cmvn = load_cmvn(str(path))
|
||||
assert cmvn.shape == (2, 560) and torch.isfinite(cmvn).all()
|
||||
|
||||
|
||||
def test_frontend_applies_cmvn_and_preserves_padding(cmvn_path):
|
||||
from funasr.frontends.default import MultiChannelFrontend
|
||||
|
||||
|
||||
Reference in New Issue
Block a user