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
162 lines
5.9 KiB
Python
162 lines
5.9 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 os
|
|
import tempfile
|
|
|
|
import numpy as np
|
|
import pytest
|
|
import torch
|
|
from lightning.pytorch import Trainer
|
|
|
|
from nemo.collections.asr.models import EncDecCTCModelBPE
|
|
from nemo.collections.asr.parts import context_biasing
|
|
from nemo.collections.asr.parts.context_biasing.ctc_based_word_spotter import WSHyp
|
|
from nemo.collections.asr.parts.utils import rnnt_utils
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def conformer_ctc_bpe_model():
|
|
model = EncDecCTCModelBPE.from_pretrained(model_name="stt_en_conformer_ctc_small")
|
|
model.set_trainer(Trainer(devices=1, accelerator="cpu"))
|
|
model = model.eval()
|
|
return model
|
|
|
|
|
|
class TestContextGraphCTC:
|
|
@pytest.mark.unit
|
|
def test_graph_building(self):
|
|
context_biasing_list = [["gpu", [['▁g', 'p', 'u'], ['▁g', '▁p', '▁u']]]]
|
|
context_graph = context_biasing.ContextGraphCTC(blank_id=1024)
|
|
context_graph.add_to_graph(context_biasing_list)
|
|
assert context_graph.num_nodes == 8
|
|
assert context_graph.blank_token == 1024
|
|
assert not context_graph.root.next['▁g'].is_end
|
|
assert context_graph.root.next['▁g'].next['p'].next['u'].is_end
|
|
assert context_graph.root.next['▁g'].next['p'].next['u'].word == 'gpu'
|
|
assert context_graph.root.next['▁g'].next['▁p'].next['▁u'].is_end
|
|
assert context_graph.root.next['▁g'].next['▁p'].next['▁u'].word == 'gpu'
|
|
|
|
|
|
class TestCTCWordSpotter:
|
|
@pytest.mark.unit
|
|
@pytest.mark.with_downloads
|
|
def test_run_word_spotter(self, test_data_dir, conformer_ctc_bpe_model):
|
|
asr_model = conformer_ctc_bpe_model
|
|
audio_file_path = os.path.join(test_data_dir, "asr/test/an4/wav/cen3-mjwl-b.wav")
|
|
target_text = "nineteen"
|
|
target_tokenization = asr_model.tokenizer.text_to_ids(target_text)
|
|
ctc_logprobs = (
|
|
asr_model.transcribe([audio_file_path], batch_size=1, return_hypotheses=True)[0].alignments.cpu().numpy()
|
|
)
|
|
context_biasing_list = [[target_text, [target_tokenization]]]
|
|
context_graph = context_biasing.ContextGraphCTC(blank_id=asr_model.decoding.blank_id)
|
|
context_graph.add_to_graph(context_biasing_list)
|
|
|
|
# without context biasing
|
|
ws_results = context_biasing.run_word_spotter(
|
|
ctc_logprobs,
|
|
context_graph,
|
|
asr_model,
|
|
blank_idx=asr_model.decoding.blank_id,
|
|
beam_threshold=5.0,
|
|
cb_weight=0.0,
|
|
ctc_ali_token_weight=0.6,
|
|
)
|
|
assert len(ws_results) == 0
|
|
|
|
# with context biasing
|
|
ws_results = context_biasing.run_word_spotter(
|
|
ctc_logprobs,
|
|
context_graph,
|
|
asr_model,
|
|
blank_idx=asr_model.decoding.blank_id,
|
|
beam_threshold=5.0,
|
|
cb_weight=3.0,
|
|
ctc_ali_token_weight=0.6,
|
|
)
|
|
assert len(ws_results) == 1
|
|
assert ws_results[0].word == target_text
|
|
assert ws_results[0].start_frame == 9
|
|
assert ws_results[0].end_frame == 19
|
|
torch.testing.assert_close(ws_results[0].score, 8.9967, atol=1e-3, rtol=1e-4)
|
|
|
|
|
|
class TestContextBiasingUtils:
|
|
@pytest.mark.unit
|
|
@pytest.mark.with_downloads
|
|
def test_merge_alignment_with_ws_hyps(self, conformer_ctc_bpe_model):
|
|
asr_model = conformer_ctc_bpe_model
|
|
blank_idx = asr_model.decoding.blank_id
|
|
ws_results = [WSHyp(word="gpu", score=6.0, start_frame=0, end_frame=2)]
|
|
|
|
# ctc argmax predictions
|
|
preds = np.array([120, 29, blank_idx, blank_idx])
|
|
pred_text, raw_text = context_biasing.merge_alignment_with_ws_hyps(
|
|
preds,
|
|
asr_model,
|
|
ws_results,
|
|
decoder_type="ctc",
|
|
blank_idx=blank_idx,
|
|
)
|
|
assert raw_text == "gp"
|
|
assert pred_text == "gpu"
|
|
|
|
# rnnt token predictions
|
|
preds = rnnt_utils.Hypothesis(
|
|
y_sequence=torch.tensor([120, 29]),
|
|
score=0.0,
|
|
timestamp=torch.tensor([0, 1, 2, 3]),
|
|
)
|
|
pred_text, raw_text = context_biasing.merge_alignment_with_ws_hyps(
|
|
preds,
|
|
asr_model,
|
|
ws_results,
|
|
decoder_type="rnnt",
|
|
blank_idx=blank_idx,
|
|
)
|
|
assert raw_text == "gp"
|
|
assert pred_text == "gpu"
|
|
|
|
# rnnt empty token predictions
|
|
preds = rnnt_utils.Hypothesis(
|
|
y_sequence=[],
|
|
score=0.0,
|
|
timestamp=[],
|
|
)
|
|
pred_text, raw_text = context_biasing.merge_alignment_with_ws_hyps(
|
|
preds,
|
|
asr_model,
|
|
ws_results,
|
|
decoder_type="rnnt",
|
|
blank_idx=blank_idx,
|
|
)
|
|
assert raw_text == ""
|
|
assert pred_text == "gpu"
|
|
|
|
@pytest.mark.unit
|
|
def test_compute_fscore(self):
|
|
recog_manifest = """{"audio_filepath": "test.wav", "duration": 1.0, "text": "a new gpu for nvidia", "pred_text": "a new gpu for invidia"}\n"""
|
|
context_words = ["gpu", "cpu", "nvidia"]
|
|
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8') as f:
|
|
f.write(recog_manifest)
|
|
f.seek(0)
|
|
fscore_stats = context_biasing.compute_fscore(f.name, context_words)
|
|
assert (round(fscore_stats[0], 4), round(fscore_stats[1], 4), round(fscore_stats[2], 4)) == (
|
|
1.0,
|
|
0.5,
|
|
0.6667,
|
|
)
|