项目文件夹

文件
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

298 行
12 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.
from __future__ import annotations
import re
from typing import TYPE_CHECKING, Callable
from omegaconf import DictConfig
from nemo.collections.asr.inference.streaming.state.state import StreamingState
from nemo.collections.asr.inference.utils.constants import POST_WORD_PUNCTUATION
from nemo.collections.asr.inference.utils.pipeline_utils import (
get_leading_punctuation_regex_pattern,
get_repeated_punctuation_regex_pattern,
)
from nemo.collections.asr.inference.utils.text_segment import Word
if TYPE_CHECKING:
from nemo.collections.asr.inference.itn.inverse_normalizer import AlignmentPreservingInverseNormalizer
class StreamingTextProcessor:
"""
A streaming text post-processing module to standardize the ASR output.
"""
def __init__(
self,
itn_cfg: DictConfig,
itn_model: AlignmentPreservingInverseNormalizer | None,
asr_supported_puncts: set,
asr_supports_punctuation: bool,
confidence_aggregator: Callable,
sep: str,
enable_itn: bool = False,
):
"""
Initialize the streaming text processor.
Args:
itn_cfg (DictConfig): ITN parameters.
itn_model (AlignmentPreservingInverseNormalizer | None): Model for inverse text normalization (ITN).
asr_supported_puncts (set): Set of punctuation marks recognized by the ASR model.
asr_supports_punctuation (bool): Boolean indicating if the ASR model outputs punctuation.
confidence_aggregator (Callable): Function for aggregating confidence scores.
sep (str): String separator used in ASR output processing.
enable_itn (bool): Boolean to enable ITN. Default is False.
"""
self.supports_punctuation = asr_supports_punctuation
self.itn_model = itn_model
self.itn_enabled = False
if enable_itn:
self.itn_enabled = itn_model is not None
self.itn_runtime_params = {
"batch_size": itn_cfg.batch_size,
"n_jobs": itn_cfg.n_jobs,
}
self.itn_left_padding_size = itn_cfg.left_padding_size
self.asr_supported_puncts = asr_supported_puncts
self.asr_supported_puncts_str = ''.join(self.asr_supported_puncts)
self.sep = sep
puncts_to_process = self.asr_supported_puncts
self.leading_punctuation_regex_pattern = get_leading_punctuation_regex_pattern(puncts_to_process)
self.repeated_punctuation_regex_pattern = get_repeated_punctuation_regex_pattern(puncts_to_process)
self.alignment_aware_itn_model = None
if self.itn_enabled:
from nemo.collections.asr.inference.itn.batch_inverse_normalizer import (
BatchAlignmentPreservingInverseNormalizer,
)
self.alignment_aware_itn_model = BatchAlignmentPreservingInverseNormalizer(
itn_model=self.itn_model,
sep=self.sep,
asr_supported_puncts=self.asr_supported_puncts,
post_word_punctuation=POST_WORD_PUNCTUATION,
conf_aggregate_fn=confidence_aggregator,
)
def process(self, states: list[StreamingState]) -> None:
"""
Post-process the states.
Args:
states: (list[StreamingState]) List of StreamingState objects
"""
word_boundary_states, segment_boundary_states = [], []
for state in states:
if state.options.is_word_level_output():
word_boundary_states.append(state)
else:
segment_boundary_states.append(state)
# Process states with word boundaries
if word_boundary_states:
self.process_states_with_word_boundaries(word_boundary_states)
# Process states with segment boundaries
if segment_boundary_states:
self.process_states_with_segment_boundaries(segment_boundary_states)
# Generate final transcript
self.generate_final_transcript(word_boundary_states, segment_boundary_states)
def process_states_with_segment_boundaries(self, states: list[StreamingState]) -> None:
"""
Post-process the states with segment boundaries.
Args:
states (list[StreamingState]): List of StreamingState objects that have segments
"""
states_with_text = [state for state in states if len(state.segments) > 0]
if len(states_with_text) == 0:
return
# Apply ITN
if self.itn_enabled:
# collect texts
texts = []
for i, state in enumerate(states_with_text):
# if ITN is disabled for this state
if not state.options.enable_itn:
continue
for j, seg in enumerate(state.segments):
if state.processed_segment_mask[j]: # if the segment is already processed, skip it
continue
texts.append((i, j, seg.text))
if len(texts) > 0:
# apply ITN
processed_texts = self.itn_model.inverse_normalize_list(
texts=[text for _, _, text in texts], params=self.itn_runtime_params
)
# update states with ITN-processed texts
for (i, j, _), processed_text in zip(texts, processed_texts):
states_with_text[i].segments[j].text = processed_text
# mark all segments as processed
for state in states_with_text:
if self.supports_punctuation:
for seg in state.segments:
if self.leading_punctuation_regex_pattern:
seg.text = re.sub(self.leading_punctuation_regex_pattern, r'\1', seg.text)
if self.repeated_punctuation_regex_pattern:
seg.text = re.sub(self.repeated_punctuation_regex_pattern, r'\1', seg.text)
state.processed_segment_mask = [True] * len(state.segments)
def process_states_with_word_boundaries(self, states: list[StreamingState]) -> None:
"""
Post-process the states with word boundaries.
Args:
states: (list[StreamingState]) List of StreamingState objects
"""
# Get the indices of the states that have new words to process
indices, asr_words_list = self.prepare_asr_words(states)
# Keep the words as is
for idx, jdx, z in indices:
states[idx].pnc_words[-z:] = asr_words_list[jdx][-z:]
# If ITN is disabled globally, do nothing
if not self.itn_enabled:
return
# Apply Inverse Text Normalization (ITN)
self.apply_itn(states, indices)
def prepare_asr_words(self, states: list[StreamingState]) -> tuple[list[tuple], list[list[Word]]]:
"""
Find the indices of the states that have words to process.
Args:
states: (list[StreamingState]) List of StreamingState objects
Returns:
tuple[list[tuple], list[list[Word]]]:
indices: list of indices of the states that have words to process
asr_words_list: list of words to process
"""
indices, asr_words_list = [], []
jdx = 0
for idx, state in enumerate(states):
if (n_not_punctuated_words := len(state.words) - len(state.pnc_words)) == 0:
continue
words_list = [word.copy() for word in state.words[-n_not_punctuated_words:]]
asr_words_list.append(words_list)
state.pnc_words.extend([None] * n_not_punctuated_words)
indices.append((idx, jdx, len(words_list)))
jdx += 1
return indices, asr_words_list
def apply_itn(self, states: list[StreamingState], indices: list[tuple]) -> None:
"""
Apply Inverse Text Normalization (ITN) on the states.
Calculates the lookback for ITN and updates the states with the ITN results.
Args:
states: (list[StreamingState]) List of StreamingState objects
indices: (list[tuple]) List of indices of the states that have words to process
"""
itn_indices, asr_words_list, pnc_words_list = [], [], []
jdx = 0
for state_idx, _, _ in indices:
state = states[state_idx]
if not state.options.enable_itn:
continue
s, t, cut_point = self.calculate_itn_lookback(state)
asr_words_list.append([word.copy() for word in state.words[s:]])
pnc_words_list.append([word.copy() for word in state.pnc_words[s:]])
itn_indices.append((state_idx, jdx, s, t, cut_point))
jdx += 1
output = self.alignment_aware_itn_model(
asr_words_list, pnc_words_list, self.itn_runtime_params, return_alignment=True
)
self.update_itn_words(states, output, itn_indices)
def calculate_itn_lookback(self, state: StreamingState) -> tuple[int, int, int]:
"""
Calculate the lookback for ITN.
Args:
state: (StreamingState) StreamingState object
Returns:
Start index (int): Start index of the source (non itn-ed) words
Target index (int): Start index of the target (itn-ed) words
Cut point (int): Index to cut the source words
"""
s, t, cut_point = 0, 0, len(state.itn_words)
word_alignment = list(reversed(state.word_alignment))
for idx, (sidx, tidx, _) in enumerate(word_alignment, start=1):
s, t = sidx[0], tidx[0]
state.word_alignment.pop()
cut_point -= 1
if idx == self.itn_left_padding_size:
break
return s, t, cut_point
@staticmethod
def update_itn_words(states: list[StreamingState], output: list[tuple], indices: list[tuple]) -> None:
"""
Update the states with the ITN results.
Updates the word_alignment and itn_words in the states.
Args:
states: (list[StreamingState]) List of StreamingState objects
output: (list[tuple]) List of output tuples containing the spans and alignment
indices: (list[tuple]) List of indices of the states that have words to process
"""
for state_idx, jdx, s, t, cut_point in indices:
state = states[state_idx]
spans, alignment = output[jdx]
for sidx, tidx, sclass in alignment:
sidx = [k + s for k in sidx]
tidx = [k + t for k in tidx]
state.word_alignment.append((sidx, tidx, sclass))
state.itn_words = state.itn_words[:cut_point] + spans
assert len(state.word_alignment) == len(state.itn_words)
def generate_final_transcript(
self, word_boundary_states: list[StreamingState], segment_boundary_states: list[StreamingState]
) -> None:
"""
Generate final transcript based on enabled features and word count.
Args:
word_boundary_states (list[StreamingState]): The streaming state containing words
segment_boundary_states (list[StreamingState]): The streaming state containing segments
"""
# Generate final transcript for word boundary states
for state in word_boundary_states:
attr_name = "itn_words" if state.options.enable_itn else "pnc_words"
words = getattr(state, attr_name)
for word in words:
state.final_segments.append(word.copy())
state.final_transcript += word.text + self.sep
state.final_transcript = state.final_transcript.rstrip(self.sep)
# Generate final transcript for segment boundary states
for state in segment_boundary_states:
for segment in state.segments:
state.final_segments.append(segment.copy())
state.final_transcript += segment.text + self.sep
state.final_transcript = state.final_transcript.rstrip(self.sep)