Repository navigation
Conversation
Adds nemo_text_processing/inverse_text_normalization/bantu/, a shared inverse text normalization kernel that Bantu languages configure with a language profile. It supports cardinal and money, resolved in a sentence-level classifier; the verbalizers only format. The kernel is not listed in the language codes. Tests build the classifier and verbalizer directly with a made-up test language (currency code XTS), run through NeMo's inverse_normalize runtime, and check that every test input has exactly one best reading. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Cal3bdev <[email protected]>
- Pass any other word (capitals, digits, punctuation, any script) through
unchanged and collapse repeated spaces, instead of failing on such input.
- Split the taggers into CardinalFst, MoneyFst and WordFst classes, with
the place-value builders in number.py.
- Move test cases into test_cases_*.txt files read by parse_test_case_file.
- Add a Jenkinsfile stage for the kernel tests.
- Use typing.Optional/Tuple, NeMo's logger and the {lang}_itn_lower_cased.far
cache name; move the vowel list into the profile.
- Fix the money-signal docstring and remove project-internal notes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Cal3bdev <[email protected]>
The test language now has a ten-thousands number place and counts a listed noun with its ten-thousands scale, as for millions, so a count of 10 or more after a listed noun is read like after any other word. Corrects the comment on how listed nouns are counted and adds test cases. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Cal3bdev <[email protected]>
tbartley94
left a comment
There was a problem hiding this comment.
Minor changes needed:
- Expansion of cardinal cases
- attribution for crane labs (optional)
- Left questions about reasoning for semiotic class distinctions. Not necessarily a problem but curious about justification.
I'm also uncertain if the profile approach should be used. It's rather clean but departs from our normal convention so adds maintenance overhead. Any further explanation on the approach?
| @@ -0,0 +1,13 @@ | |||
| # Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |||
| # | |||
There was a problem hiding this comment.
You'll want to add Crane Labs contribution here just so it doesn't all go to Nvidia.
| @@ -0,0 +1,69 @@ | |||
| # Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |||
| # | |||
| bakopa emina deka k'epi~bakopa 120,000 | ||
| bakopa emina pideka vo ema~bakopa 210,000 | ||
| bakopa emina pideka vo milo fu~bakopa 205,000 | ||
| bakopa emina pideka vo baima~bakopa emina pideka vo baima |
There was a problem hiding this comment.
please add a random sample for a million or so
| zikopa pideka vo kasa~zikopa 20 vo kasa | ||
| rana bakopa bako vo kasa lena~rana bakopa 3 vo kasa lena | ||
| rana bakopa bapi lena zikopa ziko~rana bakopa 2 lena zikopa 3 | ||
| bakopa bapi zikopa ziko~bakopa 2 zikopa 3 |
There was a problem hiding this comment.
2 is a bit too frequent here, please just enumerate cases up to 20 for a basic sanity checks
| @@ -0,0 +1,39 @@ | |||
| koin zipi~2 XTS | |||
There was a problem hiding this comment.
let's add base cases for dollars, pound, euros, yen
| @@ -0,0 +1,88 @@ | |||
| # Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |||
There was a problem hiding this comment.
why is this distinct from a cardinal?
There was a problem hiding this comment.
number.py holds the place-value builders that cardinal, money and scale all use. CardinalFst can't be the shared piece as it stands, because it also decides the context (the noun before the number). Other languages pass a CardinalFst into MoneyFst and reuse its graph, and we can follow that: move these builders into CardinalFst and have MoneyFst take the cardinal. I'd do that as part of the restructure in my main comment.
|
|
||
| from nemo_text_processing.inverse_text_normalization.bantu.profile import CardinalProfile, Forms, NounContext | ||
|
|
||
| DIGIT = pynini.union(*"0123456789").optimize() |
| from nemo_text_processing.inverse_text_normalization.bantu.profile import CardinalProfile, Forms, NounContext | ||
|
|
||
| DIGIT = pynini.union(*"0123456789").optimize() | ||
| NONZERO = pynini.union(*"123456789").optimize() |
| for place in profile.places: | ||
| if place.power != expected_power: | ||
| raise ValueError("places must be consecutive ascending decimal powers") | ||
| if any(value not in "123456789" or len(value) != 1 for _, value in place.forms): |
There was a problem hiding this comment.
string already defined as global
| return amount.optimize() | ||
|
|
||
|
|
||
| def money_token(amount: pynini.Fst, code: str, marker: str = "") -> pynini.Fst: |
There was a problem hiding this comment.
believe you can reuse the token context helper from other cases no?
- add Crane Labs to the licence headers - move taggers/sentence.py to bantu/graph_utils.py - use NEMO_SPACE, string.digits and GraphFst.add_tokens - move the thousands separator into the profile - cardinal cases: enumerate 1-20, add million-range samples - money cases: base cases for USD, GBP, EUR and JPY Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Cal3bdev <[email protected]>
|
Thanks for the comments, @tbartley94. I've pushed d2752d1 with the smaller fixes and replied to the questions inline. On the profile: it was simply the quickest way for each language to hand its words and settings to the shared kernel. You're right that it departs from the usual interface: the profile argument on the constructors, a language written in Python instead of data files, and the test helper that builds the normalizer by hand. Proposal: keep the kernel's builders in
Adding a Bantu language would then mean adding its TSVs and tests. Does this work for you? If so, I'll restructure the PR in one pass. |
What does this PR do ?
Adds a shared inverse text normalization (ITN) kernel for Bantu languages in
nemo_text_processing/inverse_text_normalization/bantu/, tested with a small made-uptest language. Luganda and Swahili will be built on it by supplying a language profile
(word lists and settings), instead of each writing its own grammars.
This is stage 1 of the staged plan agreed with @tbartley94: the kernel and a small
test-only language profile, with tests that check the kernel on its own. Luganda and
Swahili follow in their own PRs.
The kernel is laid out like a regular language but is not added to the language
codes, so it cannot be selected with
--lang. The tests build the classifier andverbalizer directly and run them through NeMo's
inverse_normalizeruntime.Collection: ITN
Why a shared kernel
Bantu languages share the hard parts of number reading:
different spoken forms (Luganda: abantu babiri "2 people", amagi abiri "2 eggs").
after amagi "eggs", but 20 after a currency word).
"twenty mu five" but also means "in").
"two ten-thousands" = 20,000).
The kernel handles these once; a language supplies only its words and settings.
How it works
then each run is read in the context of the word before it: a counted noun
(
CardinalFst), a currency phrase or money cue (MoneyFst), or nothing, in whichcase the words stay unchanged (
WordFst). Output is the standard token format:tokens { name: "bakopa" } tokens { cardinal { integer: "21" } }.(digit grouping,
<amount> <ISO code>).leaving words unchanged. Readings are made unique by structure, and every test also
checks that its input has exactly one best reading. (NeMo's runtime picks one reading
silently when there is a tie, so this check matters.)
punctuation, any script) passes through unchanged, and repeated spaces are collapsed.
Punctuation attached to a number word (
bapi.) leaves that number unchanged; fullcased and punctuation handling comes with each language.
cardinal,moneyand plain words.Suggested reading order
tests/nemo_text_processing/bantu/toy_profile.py: the test language, with theLuganda pattern each feature mirrors.
bantu/profile.py: what a language supplies.bantu/taggers/tokenize_and_classify.py, thencardinal.py,money.py,word.py.number.py(place values),scale.py(scale nouns),sentence.py(number-word runs and the money-signal check).bantu/verbalizers/.Files
The test-only language
toy_profile.pyis a small made-up language. Every word is invented, but it is shapedlike a Bantu language so that each kernel feature is used: noun classes with agreement,
a join word that is also an ordinary word, a currency phrase that licenses a special
reading, ten-thousands and millions counted with scale nouns, implied currency with
money cues, contracted joins, and a word that continues a number but is not handled
yet. Its currency code is
XTS, the ISO 4217 code reserved for testing.Because the words are invented, a failing test points at the kernel, not at real
language data. The test profile is written in Python because it exists only for the
tests; real languages will keep their word lists in
data/*.tsvfiles.Tests
test_cases_*.txt, as for other languages: countednumbers, money, words left unchanged, numbers inside sentences, malformed number
runs, and input with capitals, digits, punctuation and other scripts.
Jenkinsfilestage runs these tests in CI.Outside this PR, we also ran our 205 Luganda development cases (cardinals and money)
through this kernel in NeMo's runtime: all pass, each with exactly one best reading.
That data will come with the Luganda PR after native-speaker review.
Questions for the reviewer
main. Other languages use a staging branch (e.g.staging/telugu_itn_v1). Should we retarget to one, e.g.staging/bantu_itn_v1?pynini_export.pycannotexport it as-is. How would you like the test language exported for the Sparrowhawk tests?
Not in this PR
(see question 2).
its data and tests. Swahili test cases are being reviewed by Ernest Omondi.
Before your PR is "Ready for review"
Pre checks:
git commit -sto sign.pytestor (if your machine does not have GPU)pytest --cpufrom the root folder (given you marked your test cases accordingly@pytest.mark.run_only_on('CPU')).bash tools/text_processing_deployment/export_grammars.sh --MODE=test ...: next step, see abovepytestand Sparrowhawk here. pytest added; Sparrowhawk is the next step__init__.pyfor every folder and subfolder, includingdatafolder which has .TSV files?Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES. All rights reserved.to all newly added Python files? (year 2026)Copyright 2015 and onwards Google, Inc.. n/a,graph_utilsis imported, not copiedtry import: ... except: ...) if not already done. None presentPR Type:
If you haven't finished some of the above items you can still open "Draft" PR.
🤖 Generated with Claude Code