Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (c) 2026, 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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ಿ
Comment on lines +1 to +2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

optional · verified + linguistic-claim

Nice use of cdrewrite to strip the cardinal's final vowel before the suffix — and the two entries
are well chosen: covers the digits/teens/ties and ಿ covers the three scale_suffixes.tsv rows
ending that way (ಕೋಟಿ). I checked and every cardinal in data/numbers/ ends in one of the two.

Except zero: ಸೊನ್ನೆ ends in , which isn't here, so nothing gets stripped:

0ನೇ  ->  ಸೊನ್ನೆನೆಯ
೦ನೇ  ->  ಸೊನ್ನೆನೆಯ

Whether that even matters is your call — "zeroth" is a marginal thing to write, and rejecting 0ನೇ
outright (so it falls through to word) may well be better than normalizing it. But right now it
produces output, and I'd rather flag that it's unreviewed than let it through silently. If ಸೊನ್ನೆನೆಯ
isn't the form a Kannada reader expects, either add here or exclude zero from the ordinal input
set.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added 0ನೇ and ೦ನೇ as exceptions in exceptions.tsv with the output ಸೊನ್ನೆಯ, since zero does not follow the existing last-vowel stripping pattern.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
1ನೇ ಮೊದಲನೆಯ
೧ನೇ ಮೊದಲನೆಯ
೦ನೇ ಸೊನ್ನೆಯ
0ನೇ ಸೊನ್ನೆಯ
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ನೆ ನೆಯ
ನೇ ನೆಯ
53 changes: 53 additions & 0 deletions nemo_text_processing/text_normalization/kn/taggers/ordinal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copyright (c) 2026, 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.
#
# 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 pynini
from pynini.lib import pynutil

from nemo_text_processing.text_normalization.kn.graph_utils import NEMO_SIGMA, GraphFst
from nemo_text_processing.text_normalization.kn.taggers.cardinal import CardinalFst
from nemo_text_processing.text_normalization.kn.utils import get_abs_path


class OrdinalFst(GraphFst):
"""
Finite state transducer for classifying Kannada ordinals, e.g.
೧೦ನೇ -> ordinal { integer: "ಹತ್ತನೆಯ" }
12ನೇ -> ordinal { integer: "ಹನ್ನೆರಡನೆಯ" }

Args:
deterministic: if True will provide a single transduction option,
for False multiple transduction are generated (used for audio-based normalization)
"""

def __init__(self, cardinal: CardinalFst, deterministic: bool = True):
super().__init__(name="ordinal", kind="classify", deterministic=deterministic)

exceptions = pynini.string_file(get_abs_path("data/ordinals/exceptions.tsv"))
endings = pynini.string_file(get_abs_path("data/ordinals/ending.tsv"))
kn_suffixes = pynini.string_file(get_abs_path("data/ordinals/kn_suffixes.tsv"))

drop_cardinal_ending = pynini.cdrewrite(pynutil.delete(endings), "", "[EOS]", NEMO_SIGMA).optimize()

kn_ordinal_graph = cardinal.final_graph @ drop_cardinal_ending + kn_suffixes

exception_inputs = pynini.project(exceptions, "input").optimize()
ordinal_input = pynini.project(kn_ordinal_graph, "input").optimize()
ordinal_inputs = pynini.difference(ordinal_input, exception_inputs).optimize()

ordinal_graph = (ordinal_inputs @ kn_ordinal_graph).optimize()

graph = pynini.union(exceptions, ordinal_graph).optimize()

final_graph = pynutil.insert('integer: "') + graph + pynutil.insert('"')
self.fst = self.add_tokens(final_graph).optimize()
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
generator_main,
)
from nemo_text_processing.text_normalization.kn.taggers.cardinal import CardinalFst
from nemo_text_processing.text_normalization.kn.taggers.ordinal import OrdinalFst
from nemo_text_processing.text_normalization.kn.taggers.punctuation import PunctuationFst
from nemo_text_processing.text_normalization.kn.taggers.word import WordFst

Expand Down Expand Up @@ -75,10 +76,13 @@ def __init__(
cardinal = CardinalFst(deterministic=deterministic)
cardinal_graph = cardinal.fst

ordinal = OrdinalFst(cardinal=cardinal, deterministic=deterministic)
ordinal_graph = ordinal.fst

punctuation = PunctuationFst(deterministic=deterministic)
punct_graph = punctuation.fst

classify = pynutil.add_weight(cardinal_graph, 1.1)
classify = pynutil.add_weight(cardinal_graph, 1.1) | pynutil.add_weight(ordinal_graph, 1.1)

word_graph = WordFst().fst

Expand Down
38 changes: 38 additions & 0 deletions nemo_text_processing/text_normalization/kn/verbalizers/ordinal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright (c) 2026, 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 pynini
from pynini.lib import pynutil

from nemo_text_processing.text_normalization.kn.graph_utils import NEMO_NOT_QUOTE, GraphFst, delete_space


class OrdinalFst(GraphFst):
"""
Finite state transducer for verbalizing Kannada ordinals, e.g.
ordinal { integer: "ಮೊದಲನೆಯ" } -> ಮೊದಲನೆಯ
ordinal { integer: "ಹತ್ತನೆಯ" } -> ಹತ್ತನೆಯ

Args:
deterministic: if True will provide a single transduction option,
for False multiple transduction are generated (used for audio-based normalization)
"""

def __init__(self, deterministic: bool = True):
super().__init__(name="ordinal", kind="verbalize", deterministic=deterministic)

integer_value = delete_space + pynutil.delete("\"") + pynini.closure(NEMO_NOT_QUOTE, 1) + pynutil.delete("\"")
graph = pynutil.delete("integer:") + integer_value
delete_tokens = self.delete_tokens(graph)
self.fst = delete_tokens.optimize()
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from nemo_text_processing.text_normalization.kn.graph_utils import GraphFst
from nemo_text_processing.text_normalization.kn.verbalizers.cardinal import CardinalFst
from nemo_text_processing.text_normalization.kn.verbalizers.ordinal import OrdinalFst


class VerbalizeFst(GraphFst):
Expand All @@ -33,6 +34,9 @@ def __init__(self, deterministic: bool = True):
cardinal = CardinalFst(deterministic=deterministic)
cardinal_graph = cardinal.fst

graph = cardinal_graph
ordinal = OrdinalFst(deterministic=deterministic)
ordinal_graph = ordinal.fst

graph = cardinal_graph | ordinal_graph

self.fst = graph
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
೧ನೇ~ಮೊದಲನೆಯ
೨ನೇ~ಎರಡನೆಯ
2ನೇ~ಎರಡನೆಯ
೩ನೇ~ಮೂರನೆಯ
೪ನೇ~ನಾಲ್ಕನೆಯ
೫ನೇ~ಐದನೆಯ
5ನೇ~ಐದನೆಯ
೬ನೇ~ಆರನೆಯ
೭ನೇ~ಏಳನೆಯ
7ನೇ~ಏಳನೆಯ
೮ನೇ~ಎಂಟನೆಯ
೯ನೇ~ಒಂಬತ್ತನೆಯ
೧೦ನೇ~ಹತ್ತನೆಯ
೧೨ನೇ~ಹನ್ನೆರಡನೆಯ
12ನೇ~ಹನ್ನೆರಡನೆಯ
೧೪ನೇ~ಹದಿನಾಲ್ಕನೆಯ
15ನೇ~ಹದಿನೈದನೆಯ
೧೬ನೇ~ಹದಿನಾರನೆಯ
೧೭ನೇ~ಹದಿನೇಳನೆಯ
೧೮ನೇ~ಹದಿನೆಂಟನೆಯ
೧೯ನೇ~ಹತ್ತೊಂಬತ್ತನೆಯ
19ನೇ~ಹತ್ತೊಂಬತ್ತನೆಯ
೨೦ನೇ~ಇಪ್ಪತ್ತನೆಯ
೨೧ನೇ~ಇಪ್ಪತ್ತೊಂದನೆಯ
೨೫ನೇ~ಇಪ್ಪತ್ತೈದನೆಯ
೨೭ನೇ~ಇಪ್ಪತ್ತೇಳನೆಯ
೩೦ನೇ~ಮೂವತ್ತನೆಯ
೩೩ನೇ~ಮೂವತ್ತಮೂರನೆಯ
೪೦ನೇ~ನಲವತ್ತನೆಯ
೪೫ನೇ~ನಲವತ್ತೈದನೆಯ
೫೦ನೇ~ಐವತ್ತನೆಯ
೫೬ನೇ~ಐವತ್ತಾರನೆಯ
೬೦ನೇ~ಅರವತ್ತನೆಯ
೬೭ನೇ~ಅರವತ್ತೇಳನೆಯ
67ನೇ~ಅರವತ್ತೇಳನೆಯ
೭೫ನೇ~ಎಪ್ಪತ್ತೈದನೆಯ
೮೦ನೇ~ಎಂಬತ್ತನೆಯ
೮೮ನೇ~ಎಂಬತ್ತೆಂಟನೆಯ
೯೧ನೇ~ತೊಂಬತ್ತೊಂದನೆಯ
೯೯ನೇ~ತೊಂಬತ್ತೊಂಬತ್ತನೆಯ
೧೦೦ನೇ~ನೂರನೆಯ
100899823000ನೇ~ಹತ್ತು ಸಾವಿರದ ಎಂಬತ್ತೊಂಬತ್ತು ಕೋಟಿಯ ತೊಂಬತ್ತೆಂಟು ಲಕ್ಷದ ಇಪ್ಪತ್ತಮೂರು ಸಾವಿರನೆಯ
೧೦೧ನೇ~ನೂರ ಒಂದನೆಯ
೧೧೧ನೇ~ನೂರ ಹನ್ನೊಂದನೆಯ
೧೨೫ನೇ~ನೂರ ಇಪ್ಪತ್ತೈದನೆಯ
೧೫೩ನೇ~ನೂರ ಐವತ್ತಮೂರನೆಯ
೨೦೦ನೇ~ಇನ್ನೂರನೆಯ
೨೧೯ನೇ~ಇನ್ನೂರ ಹತ್ತೊಂಬತ್ತನೆಯ
೨೪೦ನೇ~ಇನ್ನೂರ ನಲವತ್ತನೆಯ
೩೨೯ನೇ~ಮುನ್ನೂರ ಇಪ್ಪತ್ತೊಂಬತ್ತನೆಯ
೩೬೫ನೇ~ಮುನ್ನೂರ ಅರವತ್ತೈದನೆಯ
೪೫೫ನೇ~ನಾನೂರ ಐವತ್ತೈದನೆಯ
೫೫೫ನೇ~ಐನೂರ ಐವತ್ತೈದನೆಯ
೬೪೦ನೇ~ಆರುನೂರ ನಲವತ್ತನೆಯ
೮೯೦ನೇ~ಎಂಟುನೂರ ತೊಂಬತ್ತನೆಯ
೧೦೦೧ನೇ~ಒಂದು ಸಾವಿರದ ಒಂದನೆಯ
೧೦೯೧ನೇ~ಒಂದು ಸಾವಿರದ ತೊಂಬತ್ತೊಂದನೆಯ
೧೭೮೨ನೆ~ಒಂದು ಸಾವಿರದ ಏಳುನೂರ ಎಂಬತ್ತೆರಡನೆಯ
೧೮೯೦ನೇ~ಒಂದು ಸಾವಿರದ ಎಂಟುನೂರ ತೊಂಬತ್ತನೆಯ
೧೯೮೧ನೇ~ಒಂದು ಸಾವಿರದ ಒಂಬೈನೂರ ಎಂಬತ್ತೊಂದನೆಯ
೯೮೨೬ನೇ~ಒಂಬತ್ತು ಸಾವಿರದ ಎಂಟುನೂರ ಇಪ್ಪತ್ತಾರನೆಯ
6789876ನೆ~ಅರವತ್ತೇಳು ಲಕ್ಷದ ಎಂಬತ್ತೊಂಬತ್ತು ಸಾವಿರದ ಎಂಟುನೂರ ಎಪ್ಪತ್ತಾರನೆಯ
10000000000000ನೆ~ಹತ್ತು ಲಕ್ಷ ಕೋಟನೆಯ
10000001ನೇ~ಒಂದು ಕೋಟಿಯ ಒಂದನೆಯ
1000ನೇ~ಒಂದು ಸಾವಿರನೆಯ
೭೦೦೦೦೦ನೇ~ಏಳು ಲಕ್ಷನೆಯ
ಈ ಮಗುವಿನ ಶೈಕ್ಷಣಿಕ ಫಲಿತಾಂಶವು ಯಾವಾಗಲೂ ಇಡೀ ತರಗತಿಯಲ್ಲಿ ೧ನೇ ಸ್ಥಾನದಲ್ಲಿದೆ.~ಈ ಮಗುವಿನ ಶೈಕ್ಷಣಿಕ ಫಲಿತಾಂಶವು ಯಾವಾಗಲೂ ಇಡೀ ತರಗತಿಯಲ್ಲಿ ಮೊದಲನೆಯ ಸ್ಥಾನದಲ್ಲಿದೆ.
ಈ ಸಾಲಿನಿಂದ ಕೆಳಕ್ಕೆ ಎಣಿಸಿದಾಗ 5ನೇ ರಾಘವೇಂದ್ರ.~ಈ ಸಾಲಿನಿಂದ ಕೆಳಕ್ಕೆ ಎಣಿಸಿದಾಗ ಐದನೆಯ ರಾಘವೇಂದ್ರ.
ಅಭಿನಂದನೆಗಳು! ನೀವು ನಮ್ಮ ಅಂಗಡಿಯ ೧೦೧ನೇ ಗ್ರಾಹಕರಾಗಿದ್ದೀರಿ.~ಅಭಿನಂದನೆಗಳು! ನೀವು ನಮ್ಮ ಅಂಗಡಿಯ ನೂರ ಒಂದನೆಯ ಗ್ರಾಹಕರಾಗಿದ್ದೀರಿ.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

should-fix · verified

81 cases with both scripts, tier boundaries, a 12-digit number and two full sentences — this is
genuinely good coverage, and the whole suite is green (424 passed on a fresh cache). Two gaps, both
of which hide things I've commented on elsewhere:

1. Nothing covers the 15/16 reading boundary. 88th is here but no case sits either side of the
discontinuity, so whichever convention you settle on in exceptions.tsv, these pin it down. Every
expected value below is copied from an actual run against this PR:

Suggested change
ಅಭಿನಂದನೆಗಳು! ನೀವು ನಮ್ಮ ಅಂಗಡಿಯ ೧೦೧ನೇ ಗ್ರಾಹಕರಾಗಿದ್ದೀರಿ.~ಅಭಿನಂದನೆಗಳು! ನೀವು ನಮ್ಮ ಅಂಗಡಿಯ ನೂರ ಒಂದನೆಯ ಗ್ರಾಹಕರಾಗಿದ್ದೀರಿ.
ಅಭಿನಂದನೆಗಳು! ನೀವು ನಮ್ಮ ಅಂಗಡಿಯ ೧೦೧ನೇ ಗ್ರಾಹಕರಾಗಿದ್ದೀರಿ.~ಅಭಿನಂದನೆಗಳು! ನೀವು ನಮ್ಮ ಅಂಗಡಿಯ ನೂರ ಒಂದನೆಯ ಗ್ರಾಹಕರಾಗಿದ್ದೀರಿ.
16th~ಹದಿನಾರನೆಯ
21st~ಇಪ್ಪತ್ತೊಂದನೆಯ
೧೦೦೦ನೇ~ಒಂದು ಸಾವಿರನೆಯ

I've left 15th out on purpose — what it should produce depends on which convention you pick, and
guessing at it would be worse than leaving it to you. (I nearly shipped a guessed thousand-tier form
here too; the real output turned out to be ಒಂದು ಸಾವಿರನೆಯ, not what I'd assumed, which is why it's above verbatim from a run.)

2. Nothing covers malformed English suffixes, which is why 5st and 11st currently normalize
unnoticed. Once en_suffixes.tsv is constrained, a guard belongs here — 11th normalizes today
(ಇಲೆವೆಂತ್) while 11st should ideally be left alone.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added the missing boundary cases to the test file, including 15ನೇ, 16ನೇ using the Kannada convention consistently.

ಈ ಪಟ್ಟಿಯ ಆರಂಭದಿಂದ ೧೦೦ನೇ ವ್ಯಕ್ತಿಯವರೆಗೆ ಇರುವ ಎಲ್ಲರೂ ನಿಮ್ಮ ಗುರಿ ಗ್ರಾಹಕರು.~ಈ ಪಟ್ಟಿಯ ಆರಂಭದಿಂದ ನೂರನೆಯ ವ್ಯಕ್ತಿಯವರೆಗೆ ಇರುವ ಎಲ್ಲರೂ ನಿಮ್ಮ ಗುರಿ ಗ್ರಾಹಕರು.
0ನೇ~ಸೊನ್ನೆಯ
33 changes: 33 additions & 0 deletions tests/nemo_text_processing/kn/test_ordinal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Copyright (c) 2026, 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 pytest
from parameterized import parameterized

from nemo_text_processing.text_normalization.normalize import Normalizer

from ..utils import CACHE_DIR, parse_test_case_file


class TestOrdinal:
normalizer = Normalizer(
input_case='cased', lang='kn', cache_dir=CACHE_DIR, overwrite_cache=False, post_process=False
)

@parameterized.expand(parse_test_case_file('kn/data_text_normalization/test_cases_ordinal.txt'))
@pytest.mark.run_only_on('CPU')
@pytest.mark.unit
def test_norm(self, test_input, expected):
pred = self.normalizer.normalize(test_input, verbose=False)
assert pred == expected
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,10 @@ testTNPunctuation() {
runtest $input
}

testTNOrdinal() {
input=$PROJECT_DIR/kn/data_text_normalization/test_cases_ordinal.txt
runtest $input
}

# Load shUnit2
. $PROJECT_DIR/../shunit2/shunit2