Skip to content

feat(audio): add transcript normalization stages - #2267

Merged
mohammadaaftabv merged 8 commits into
NVIDIA-NeMo:mainfrom
mohammadaaftabv:agent/granary-regex-substitution
Aug 27, 2026
Merged

feat(audio): add transcript normalization stages#2267
mohammadaaftabv merged 8 commits into
NVIDIA-NeMo:mainfrom
mohammadaaftabv:agent/granary-regex-substitution

Conversation

@mohammadaaftabv

@mohammadaaftabv mohammadaaftabv commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add ordered YAML-driven regex substitution for ASR transcripts (pred_texttext) with strict setup-time rule validation, whitespace normalization, skip preservation, and empty-after-cleaning handling.
  • Add language-aware concatenation of ASR-spelled abbreviations with contraction, particle, and Unicode plural handling plus normalization notes.
  • Export both stages through nemo_curator.stages.audio.text_filtering and the lazy nemo_curator.stages.audio API.
  • Use the current-main stage lifecycle with inline process() and inherited batch processing.

Validation

  • 79 focused tests pass.
  • A 492,274-case generated abbreviation probe is idempotent and preserves character sequences apart from intended space removal.
  • Target/reference differential checks cover abbreviation text and metadata, regex rules, and the default chained stages.
  • Ruff and pre-commit pass.

Includes and supersedes #2268.

@copy-pr-bot

copy-pr-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@mohammadaaftabv
mohammadaaftabv force-pushed the agent/granary-regex-substitution branch from 4f06835 to f45fc2f Compare August 17, 2026 05:30
@mohammadaaftabv mohammadaaftabv changed the title Audio: add regex transcript normalization feat(audio): add transcript normalization stages Aug 17, 2026
@mohammadaaftabv
mohammadaaftabv force-pushed the agent/granary-regex-substitution branch 2 times, most recently from 7fc15d1 to cb63d2b Compare August 19, 2026 05:21
@mohammadaaftabv
mohammadaaftabv marked this pull request as ready for review August 19, 2026 06:37
@mohammadaaftabv
mohammadaaftabv requested a review from a team as a code owner August 19, 2026 06:37
@mohammadaaftabv
mohammadaaftabv requested review from suiyoubi and removed request for a team August 19, 2026 06:37
@greptile-apps

greptile-apps Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds YAML-driven transcript regex substitution and language-aware abbreviation concatenation stages, including public exports and focused tests.

  • Validates ordered regex rules during worker setup and normalizes resulting whitespace.
  • Concatenates ASR-spelled abbreviations while preserving skips and recording normalization notes.
  • Adds multilingual, contraction, plural, lifecycle, and public-export coverage.

Confidence Score: 4/5

The PR is not yet safe to merge because supported-language transcripts containing non-ASCII uppercase vowel-plus-s tokens can still be normalized incorrectly.

The abbreviation stage still classifies vowels through an ASCII-only set, so inputs such as German A Ös are joined into AÖs rather than preserving the vowel-led word.

Files Needing Attention: nemo_curator/stages/audio/text_filtering/abbreviation_concat.py

Important Files Changed

Filename Overview
nemo_curator/stages/audio/text_filtering/abbreviation_concat.py Adds multilingual abbreviation concatenation, but its ASCII-only vowel classification still corrupts supported non-ASCII vowel-plus-s tokens.
nemo_curator/stages/audio/text_filtering/regex_substitution.py Adds ordered YAML substitutions with comprehensive setup-time rule validation and empty-result handling.
tests/stages/audio/text_filtering/test_abbreviation_concat.py Covers multilingual abbreviation, contraction, plural, metadata, skip, and export behavior, though the outstanding Unicode-vowel case remains uncovered.
tests/stages/audio/text_filtering/test_regex_substitution.py Thoroughly covers rule validation, ordering, lifecycle behavior, custom fields, skips, and stage chaining.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[AudioTask pred_text] --> B[RegexSubstitutionStage]
  B --> C[text]
  C --> D[AbbreviationConcatStage]
  D --> E[Normalized text and notes]
  B --> F{Empty after cleaning?}
  F -->|Yes| G[Set skip reason]
Loading

Reviews (7): Last reviewed commit: "fix(audio): address normalization review..." | Re-trigger Greptile

def _pattern(language: str) -> re.Pattern[str]:
char_class = _LANG_CHAR_CLASS.get(language, _LANG_CHAR_CLASS["en"])
return re.compile(
rf"(?<![\w’’’ʼ])({char_class}(?: {char_class}){{1,}}(?:(?<=[A-Z])s)?)(?!\w)" # noqa: RUF001

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Non-ASCII plural matching breaks

When a supported-language abbreviation contains non-ASCII uppercase letters and a consonant plural token, the ASCII-only [A-Z] condition can match only a suffix of the sequence, producing partially normalized transcript text instead of joining the complete abbreviation.

Knowledge Base Used: Audio Stages

Comment on lines +58 to +63
for index, rule in enumerate(raw_rules):
if not isinstance(rule, dict) or "pattern" not in rule or "repl" not in rule:
msg = f"Regex rule {index} must define pattern and repl"
raise ValueError(msg)
re.compile(str(rule["pattern"]))
self._rules = raw_rules

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Rule value types remain unchecked

YAML setup validates key presence and regex syntax but not repl or count types, so an invalid count fails later during transcript processing while a null replacement silently inserts the literal string None. Validate these values once during worker setup to reject malformed normalization configuration before processing data.

Knowledge Base Used: Audio Stages

@mohammadaaftabv
mohammadaaftabv marked this pull request as draft August 19, 2026 07:29
len(parts) >= _MIN_PARTS
and len(parts[-1]) == _PLURAL_SUFFIX_LEN
and parts[-1][1] == "s"
and (parts[-1][0] in _VOWELS or not parts[-1][0].isupper())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Unicode vowels treated as consonants

When a supported-language abbreviation ends with a non-ASCII uppercase vowel-plus-s token such as German A Ös, the ASCII-only _VOWELS set classifies Ös as a consonant plural and emits AÖs instead of preserving A Ös, corrupting the normalized transcript.

Knowledge Base Used: Audio Stages

@mohammadaaftabv
mohammadaaftabv marked this pull request as ready for review August 19, 2026 09:08
@mohammadaaftabv

mohammadaaftabv commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

@nithinraok, could you please confirm which of the following remaining behavioural differences from nkoluguri/integration-test should be kept?

Comparison anchors

PR head: 3977493
Reference: nithinraok/Curator:nkoluguri/integration-test at a1df5ec

  1. Contraction boundaries also differ:

    Input PR Reference
    I'm A B I'm AB I'mAB
    A B I'm AB I'm ABI'm
    A B I’m AB I’m AB’m
  2. Rejected matches and edge particles are preserved instead of deleting tokens.

    Input PR Reference
    A A Is A A Is A A
    a a B a aB aB
    B a a Ba a Ba
  3. An ASCII s after an uppercase non-ASCII letter is accepted as an abbreviation plural. This is also the outstanding Greptile concern because the ASCII-only vowel check classifies uppercase accented vowels as consonants.

    Input PR Reference
    А Б Вs АБВs АБ Вs
    Ä Ö Üs ÄÖÜs ÄÖ Üs
    A Ös AÖs A Ös
    Α Β Γs ΑΒΓs ΑΒ Γs
  4. Language resolution is more permissive. A missing or falsey source_lang falls back to English, and supplied language codes are stripped and lowercased. The reference requires the key and uses its value unchanged.

  5. Regex YAML validation is strict. The document must be a list of mappings; pattern and repl must be strings; and count must be a non-boolean, non-negative integer.

  6. Missing or non-string regex input stabilizes the output schema. The PR materializes an empty output field when it is absent; the reference raises KeyError for a missing input and returns a non-string row without creating the output.

Please confirm whether all six differences are acceptable, or identify the item numbers that should be aligned with the reference.

[
("A P I and G P U", ["API", "GPU"]),
("a A P I", ["a API"]),
("A P Is", ["AP I"]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should expected be APIs ?

@mohammadaaftabv mohammadaaftabv Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APIs is the more natural expectation here. However, the current test was matching nithinraok/Curator:nkoluguri/integration-test.
The reference treats terminal Is as a separate word, producing AP Is, and reports it using replaced.strip().rstrip("’s").rstrip("’s"), which produces AP I because rstrip() treats its argument as a character set rather than an exact suffix.
@nkoluguri, can you confirm whether we should intentionally diverge from the reference and make both the normalized text and reported abbreviation APIs, or retain the reference behavior?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be APIs as correctly pointed by Viraj.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it should be APIs

def outputs(self) -> tuple[list[str], list[str]]:
return [], [self.output_text_key, self.notes_key]

def process(self, task: AudioTask) -> AudioTask:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to have process_batch as well?

result = re.sub(r"\s+", " ", result).strip()
task.data[self.output_text_key] = result
if not result and had_text:
task.data[self.skip_me_key] = "Empty after regex cleaning"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check is _skipme flag was already there and not to overwrite it?

@mohammadaaftabv

Copy link
Copy Markdown
Contributor Author

/ok to test a55b89a

Signed-off-by: aaftaabv@gmail.com <aaftaabv@gmail.com>
Signed-off-by: aaftaabv@gmail.com <aaftaabv@gmail.com>
Signed-off-by: aaftaabv@gmail.com <aaftaabv@gmail.com>
Signed-off-by: aaftaabv@gmail.com <aaftaabv@gmail.com>
Signed-off-by: aaftaabv@gmail.com <aaftaabv@gmail.com>
Signed-off-by: aaftaabv@gmail.com <aaftaabv@gmail.com>
Signed-off-by: aaftaabv@gmail.com <aaftaabv@gmail.com>
@mohammadaaftabv
mohammadaaftabv force-pushed the agent/granary-regex-substitution branch from a55b89a to 47d7a38 Compare August 27, 2026 03:26
@mohammadaaftabv

Copy link
Copy Markdown
Contributor Author

/ok to test 47d7a38

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants