diff --git a/Jenkinsfile b/Jenkinsfile
index 38e05bb00..60412126c 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -30,6 +30,7 @@ pipeline {
JA_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/10-17-24-1'
HI_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/06-04-26-5'
KO_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/06-04-25-6'
+ KN_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/07-21-26-0'
DEFAULT_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/06-08-23-0'
}
stages {
@@ -48,7 +49,6 @@ pipeline {
}
}
-
stage('L0: Create EN TN/ITN Grammars') {
when {
anyOf {
@@ -113,6 +113,26 @@ pipeline {
}
}
+ stage('L0: Create KN TN Grammars') {
+ when {
+ anyOf {
+ branch 'main'
+ branch 'staging/**'
+ branch 'staging_*'
+ changeRequest target: 'main'
+ }
+ }
+ failFast true
+ parallel {
+ stage('L0: KN TN grammars') {
+ steps{
+ sh 'CUDA_VISIBLE_DEVICES="" python nemo_text_processing/text_normalization/normalize.py --lang=kn --text="೧" --cache_dir ${KN_TN_CACHE}'
+ }
+ }
+ }
+ }
+
+
stage('L0: Create DE/ES TN/ITN Grammars') {
when {
anyOf {
@@ -408,6 +428,11 @@ pipeline {
sh 'CUDA_VISIBLE_DEVICES="" pytest tests/nemo_text_processing/hi/ -m "not pleasefixme" --cpu --tn_cache_dir ${HI_TN_CACHE}'
}
}
+ stage('L1: Run all KN TN tests (restore grammars from cache)') {
+ steps {
+ sh 'CUDA_VISIBLE_DEVICES="" pytest tests/nemo_text_processing/kn/ -m "not pleasefixme" --cpu --tn_cache_dir ${KN_TN_CACHE}'
+ }
+ }
stage('L1: Run all Codeswitched ES/EN TN/ITN tests (restore grammars from cache)') {
steps {
sh 'CUDA_VISIBLE_DEVICES="" pytest tests/nemo_text_processing/es_en/ -m "not pleasefixme" --cpu --tn_cache_dir ${ES_EN_TN_CACHE}'
diff --git a/nemo_text_processing/text_normalization/kn/__init__.py b/nemo_text_processing/text_normalization/kn/__init__.py
new file mode 100644
index 000000000..4fc25d0d3
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/__init__.py
@@ -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.
diff --git a/nemo_text_processing/text_normalization/kn/data/__init__.py b/nemo_text_processing/text_normalization/kn/data/__init__.py
new file mode 100644
index 000000000..4fc25d0d3
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/data/__init__.py
@@ -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.
diff --git a/nemo_text_processing/text_normalization/kn/data/numbers/__init__.py b/nemo_text_processing/text_normalization/kn/data/numbers/__init__.py
new file mode 100644
index 000000000..4fc25d0d3
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/data/numbers/__init__.py
@@ -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.
diff --git a/nemo_text_processing/text_normalization/kn/data/numbers/digit.tsv b/nemo_text_processing/text_normalization/kn/data/numbers/digit.tsv
new file mode 100644
index 000000000..d4e5a1171
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/data/numbers/digit.tsv
@@ -0,0 +1,18 @@
+1 ಒಂದು
+2 ಎರಡು
+3 ಮೂರು
+4 ನಾಲ್ಕು
+5 ಐದು
+6 ಆರು
+7 ಏಳು
+8 ಎಂಟು
+9 ಒಂಬತ್ತು
+೧ ಒಂದು
+೨ ಎರಡು
+೩ ಮೂರು
+೪ ನಾಲ್ಕು
+೫ ಐದು
+೬ ಆರು
+೭ ಏಳು
+೮ ಎಂಟು
+೯ ಒಂಬತ್ತು
\ No newline at end of file
diff --git a/nemo_text_processing/text_normalization/kn/data/numbers/hundreds.tsv b/nemo_text_processing/text_normalization/kn/data/numbers/hundreds.tsv
new file mode 100644
index 000000000..b3aff9b13
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/data/numbers/hundreds.tsv
@@ -0,0 +1,18 @@
+೧ ನೂರ
+೨ ಇನ್ನೂರ
+೩ ಮುನ್ನೂರ
+೪ ನಾನೂರ
+೫ ಐನೂರ
+೬ ಆರುನೂರ
+೭ ಏಳುನೂರ
+೮ ಎಂಟುನೂರ
+೯ ಒಂಬೈನೂರ
+1 ನೂರ
+2 ಇನ್ನೂರ
+3 ಮುನ್ನೂರ
+4 ನಾನೂರ
+5 ಐನೂರ
+6 ಆರುನೂರ
+7 ಏಳುನೂರ
+8 ಎಂಟುನೂರ
+9 ಒಂಬೈನೂರ
\ No newline at end of file
diff --git a/nemo_text_processing/text_normalization/kn/data/numbers/scale_suffixes.tsv b/nemo_text_processing/text_normalization/kn/data/numbers/scale_suffixes.tsv
new file mode 100644
index 000000000..4af1789ec
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/data/numbers/scale_suffixes.tsv
@@ -0,0 +1,11 @@
+thousand ಸಾವಿರ
+thousand_gen ಸಾವಿರದ
+lakh ಲಕ್ಷ
+lakh_gen ಲಕ್ಷದ
+crore ಕೋಟಿ
+crore_gen ಕೋಟಿಯ
+thousand_crore ಸಾವಿರ ಕೋಟಿ
+thousand_crore_gen ಸಾವಿರ ಕೋಟಿಯ
+lakh_crore ಲಕ್ಷ ಕೋಟಿ
+lakh_crore_gen ಲಕ್ಷ ಕೋಟಿಯ
+hundred_suffix ು
\ No newline at end of file
diff --git a/nemo_text_processing/text_normalization/kn/data/numbers/teens_and_ties.tsv b/nemo_text_processing/text_normalization/kn/data/numbers/teens_and_ties.tsv
new file mode 100644
index 000000000..d7cc5a427
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/data/numbers/teens_and_ties.tsv
@@ -0,0 +1,90 @@
+೧೦ ಹತ್ತು
+೧೧ ಹನ್ನೊಂದು
+೧೨ ಹನ್ನೆರಡು
+೧೩ ಹದಿಮೂರು
+೧೪ ಹದಿನಾಲ್ಕು
+೧೫ ಹದಿನೈದು
+೧೬ ಹದಿನಾರು
+೧೭ ಹದಿನೇಳು
+೧೮ ಹದಿನೆಂಟು
+೧೯ ಹತ್ತೊಂಬತ್ತು
+೨೦ ಇಪ್ಪತ್ತು
+೨೧ ಇಪ್ಪತ್ತೊಂದು
+೨೨ ಇಪ್ಪತ್ತೆರಡು
+೨೩ ಇಪ್ಪತ್ತಮೂರು
+೨೪ ಇಪ್ಪತ್ತನಾಲ್ಕು
+೨೫ ಇಪ್ಪತ್ತೈದು
+೨೬ ಇಪ್ಪತ್ತಾರು
+೨೭ ಇಪ್ಪತ್ತೇಳು
+೨೮ ಇಪ್ಪತ್ತೆಂಟು
+೨೯ ಇಪ್ಪತ್ತೊಂಬತ್ತು
+೩೦ ಮೂವತ್ತು
+೩೧ ಮೂವತ್ತೊಂದು
+೩೨ ಮೂವತ್ತೆರಡು
+೩೩ ಮೂವತ್ತಮೂರು
+೩೪ ಮೂವತ್ತನಾಲ್ಕು
+೩೫ ಮೂವತ್ತೈದು
+೩೬ ಮೂವತ್ತಾರು
+೩೭ ಮೂವತ್ತೇಳು
+೩೮ ಮೂವತ್ತೆಂಟು
+೩೯ ಮೂವತ್ತೊಂಬತ್ತು
+೪೦ ನಲವತ್ತು
+೪೧ ನಲವತ್ತೊಂದು
+೪೨ ನಲವತ್ತೆರಡು
+೪೩ ನಲವತ್ತಮೂರು
+೪೪ ನಲವತ್ತನಾಲ್ಕು
+೪೫ ನಲವತ್ತೈದು
+೪೬ ನಲವತ್ತಾರು
+೪೭ ನಲವತ್ತೇಳು
+೪೮ ನಲವತ್ತೆಂಟು
+೪೯ ನಲವತ್ತೊಂಬತ್ತು
+೫೦ ಐವತ್ತು
+೫೧ ಐವತ್ತೊಂದು
+೫೨ ಐವತ್ತೆರಡು
+೫೩ ಐವತ್ತಮೂರು
+೫೪ ಐವತ್ತನಾಲ್ಕು
+೫೫ ಐವತ್ತೈದು
+೫೬ ಐವತ್ತಾರು
+೫೭ ಐವತ್ತೇಳು
+೫೮ ಐವತ್ತೆಂಟು
+೫೯ ಐವತ್ತೊಂಬತ್ತು
+೬೦ ಅರವತ್ತು
+೬೧ ಅರವತ್ತೊಂದು
+೬೨ ಅರವತ್ತೆರಡು
+೬೩ ಅರವತ್ತಮೂರು
+೬೪ ಅರವತ್ತನಾಲ್ಕು
+೬೫ ಅರವತ್ತೈದು
+೬೬ ಅರವತ್ತಾರು
+೬೭ ಅರವತ್ತೇಳು
+೬೮ ಅರವತ್ತೆಂಟು
+೬೯ ಅರವತ್ತೊಂಬತ್ತು
+೭೦ ಎಪ್ಪತ್ತು
+೭೧ ಎಪ್ಪತ್ತೊಂದು
+೭೨ ಎಪ್ಪತ್ತೆರಡು
+೭೩ ಎಪ್ಪತ್ತಮೂರು
+೭೪ ಎಪ್ಪತ್ತನಾಲ್ಕು
+೭೫ ಎಪ್ಪತ್ತೈದು
+೭೬ ಎಪ್ಪತ್ತಾರು
+೭೭ ಎಪ್ಪತ್ತೇಳು
+೭೮ ಎಪ್ಪತ್ತೆಂಟು
+೭೯ ಎಪ್ಪತ್ತೊಂಬತ್ತು
+೮೦ ಎಂಬತ್ತು
+೮೧ ಎಂಬತ್ತೊಂದು
+೮೨ ಎಂಬತ್ತೆರಡು
+೮೩ ಎಂಬತ್ತಮೂರು
+೮೪ ಎಂಬತ್ತನಾಲ್ಕು
+೮೫ ಎಂಬತ್ತೈದು
+೮೬ ಎಂಬತ್ತಾರು
+೮೭ ಎಂಬತ್ತೇಳು
+೮೮ ಎಂಬತ್ತೆಂಟು
+೮೯ ಎಂಬತ್ತೊಂಬತ್ತು
+೯೦ ತೊಂಬತ್ತು
+೯೧ ತೊಂಬತ್ತೊಂದು
+೯೨ ತೊಂಬತ್ತೆರಡು
+೯೩ ತೊಂಬತ್ತಮೂರು
+೯೪ ತೊಂಬತ್ತನಾಲ್ಕು
+೯೫ ತೊಂಬತ್ತೈದು
+೯೬ ತೊಂಬತ್ತಾರು
+೯೭ ತೊಂಬತ್ತೇಳು
+೯೮ ತೊಂಬತ್ತೆಂಟು
+೯೯ ತೊಂಬತ್ತೊಂಬತ್ತು
\ No newline at end of file
diff --git a/nemo_text_processing/text_normalization/kn/data/numbers/teens_and_ties_en.tsv b/nemo_text_processing/text_normalization/kn/data/numbers/teens_and_ties_en.tsv
new file mode 100644
index 000000000..1a4a6fbd8
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/data/numbers/teens_and_ties_en.tsv
@@ -0,0 +1,90 @@
+10 ಹತ್ತು
+11 ಹನ್ನೊಂದು
+12 ಹನ್ನೆರಡು
+13 ಹದಿಮೂರು
+14 ಹದಿನಾಲ್ಕು
+15 ಹದಿನೈದು
+16 ಹದಿನಾರು
+17 ಹದಿನೇಳು
+18 ಹದಿನೆಂಟು
+19 ಹತ್ತೊಂಬತ್ತು
+20 ಇಪ್ಪತ್ತು
+21 ಇಪ್ಪತ್ತೊಂದು
+22 ಇಪ್ಪತ್ತೆರಡು
+23 ಇಪ್ಪತ್ತಮೂರು
+24 ಇಪ್ಪತ್ತನಾಲ್ಕು
+25 ಇಪ್ಪತ್ತೈದು
+26 ಇಪ್ಪತ್ತಾರು
+27 ಇಪ್ಪತ್ತೇಳು
+28 ಇಪ್ಪತ್ತೆಂಟು
+29 ಇಪ್ಪತ್ತೊಂಬತ್ತು
+30 ಮೂವತ್ತು
+31 ಮೂವತ್ತೊಂದು
+32 ಮೂವತ್ತೆರಡು
+33 ಮೂವತ್ತಮೂರು
+34 ಮೂವತ್ತನಾಲ್ಕು
+35 ಮೂವತ್ತೈದು
+36 ಮೂವತ್ತಾರು
+37 ಮೂವತ್ತೇಳು
+38 ಮೂವತ್ತೆಂಟು
+39 ಮೂವತ್ತೊಂಬತ್ತು
+40 ನಲವತ್ತು
+41 ನಲವತ್ತೊಂದು
+42 ನಲವತ್ತೆರಡು
+43 ನಲವತ್ತಮೂರು
+44 ನಲವತ್ತನಾಲ್ಕು
+45 ನಲವತ್ತೈದು
+46 ನಲವತ್ತಾರು
+47 ನಲವತ್ತೇಳು
+48 ನಲವತ್ತೆಂಟು
+49 ನಲವತ್ತೊಂಬತ್ತು
+50 ಐವತ್ತು
+51 ಐವತ್ತೊಂದು
+52 ಐವತ್ತೆರಡು
+53 ಐವತ್ತಮೂರು
+54 ಐವತ್ತನಾಲ್ಕು
+55 ಐವತ್ತೈದು
+56 ಐವತ್ತಾರು
+57 ಐವತ್ತೇಳು
+58 ಐವತ್ತೆಂಟು
+59 ಐವತ್ತೊಂಬತ್ತು
+60 ಅರವತ್ತು
+61 ಅರವತ್ತೊಂದು
+62 ಅರವತ್ತೆರಡು
+63 ಅರವತ್ತಮೂರು
+64 ಅರವತ್ತನಾಲ್ಕು
+65 ಅರವತ್ತೈದು
+66 ಅರವತ್ತಾರು
+67 ಅರವತ್ತೇಳು
+68 ಅರವತ್ತೆಂಟು
+69 ಅರವತ್ತೊಂಬತ್ತು
+70 ಎಪ್ಪತ್ತು
+71 ಎಪ್ಪತ್ತೊಂದು
+72 ಎಪ್ಪತ್ತೆರಡು
+73 ಎಪ್ಪತ್ತಮೂರು
+74 ಎಪ್ಪತ್ತನಾಲ್ಕು
+75 ಎಪ್ಪತ್ತೈದು
+76 ಎಪ್ಪತ್ತಾರು
+77 ಎಪ್ಪತ್ತೇಳು
+78 ಎಪ್ಪತ್ತೆಂಟು
+79 ಎಪ್ಪತ್ತೊಂಬತ್ತು
+80 ಎಂಬತ್ತು
+81 ಎಂಬತ್ತೊಂದು
+82 ಎಂಬತ್ತೆರಡು
+83 ಎಂಬತ್ತಮೂರು
+84 ಎಂಬತ್ತನಾಲ್ಕು
+85 ಎಂಬತ್ತೈದು
+86 ಎಂಬತ್ತಾರು
+87 ಎಂಬತ್ತೇಳು
+88 ಎಂಬತ್ತೆಂಟು
+89 ಎಂಬತ್ತೊಂಬತ್ತು
+90 ತೊಂಬತ್ತು
+91 ತೊಂಬತ್ತೊಂದು
+92 ತೊಂಬತ್ತೆರಡು
+93 ತೊಂಬತ್ತಮೂರು
+94 ತೊಂಬತ್ತನಾಲ್ಕು
+95 ತೊಂಬತ್ತೈದು
+96 ತೊಂಬತ್ತಾರು
+97 ತೊಂಬತ್ತೇಳು
+98 ತೊಂಬತ್ತೆಂಟು
+99 ತೊಂಬತ್ತೊಂಬತ್ತು
\ No newline at end of file
diff --git a/nemo_text_processing/text_normalization/kn/data/numbers/zero.tsv b/nemo_text_processing/text_normalization/kn/data/numbers/zero.tsv
new file mode 100644
index 000000000..316f71da0
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/data/numbers/zero.tsv
@@ -0,0 +1,2 @@
+0 ಸೊನ್ನೆ
+೦ ಸೊನ್ನೆ
\ No newline at end of file
diff --git a/nemo_text_processing/text_normalization/kn/graph_utils.py b/nemo_text_processing/text_normalization/kn/graph_utils.py
new file mode 100644
index 000000000..2f2a2515d
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/graph_utils.py
@@ -0,0 +1,128 @@
+# 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 logging
+import os
+from pathlib import Path
+from typing import Dict
+
+import pynini
+from pynini import Far
+from pynini.export import export
+from pynini.lib import byte, pynutil, utf8
+from nemo_text_processing.text_normalization.kn.utils import get_abs_path
+
+NEMO_CHAR = utf8.VALID_UTF8_CHAR
+
+kn_digit_map = pynini.string_file(get_abs_path("data/numbers/digit.tsv"))
+kn_zero_map = pynini.string_file(get_abs_path("data/numbers/zero.tsv"))
+
+kn_digit_only = pynini.project(kn_digit_map, "input").optimize()
+NEMO_ALL_ZERO = pynini.project(kn_zero_map, "input").optimize()
+NEMO_ALL_DIGIT = pynini.union(kn_digit_only, NEMO_ALL_ZERO).optimize()
+
+NEMO_NON_BREAKING_SPACE = u"\u00a0"
+NEMO_SPACE = " "
+NEMO_WHITE_SPACE = pynini.union(" ", "\t", "\n", "\r", u"\u00a0").optimize()
+NEMO_NOT_SPACE = pynini.difference(NEMO_CHAR, NEMO_WHITE_SPACE).optimize()
+NEMO_NOT_QUOTE = pynini.difference(NEMO_CHAR, r'"').optimize()
+NEMO_SIGMA = pynini.closure(NEMO_CHAR)
+
+delete_space = pynutil.delete(pynini.closure(NEMO_WHITE_SPACE))
+insert_space = pynutil.insert(" ")
+delete_extra_space = pynini.cross(pynini.closure(NEMO_WHITE_SPACE, 1), " ")
+
+
+def generator_main(file_name: str, graphs: Dict[str, 'pynini.FstLike']):
+ """
+ Exports graph as OpenFst finite state archive (FAR) file with given file name and rule name.
+
+ Args:
+ file_name: exported file name
+ graphs: Mapping of a rule name and Pynini WFST graph to be exported
+ """
+ exporter = export.Exporter(file_name)
+ for rule, graph in graphs.items():
+ exporter[rule] = graph.optimize()
+ exporter.close()
+ logging.info(f'Created {file_name}')
+
+
+class GraphFst:
+ """
+ Base class for all grammar fsts.
+
+ Args:
+ name: name of grammar class
+ kind: either 'classify' or 'verbalize'
+ deterministic: if True will provide a single transduction option,
+ for False multiple transduction are generated (used for audio-based normalization)
+ """
+
+ def __init__(self, name: str, kind: str, deterministic: bool = True):
+ self.name = name
+ self.kind = kind
+ self._fst = None
+ self.deterministic = deterministic
+
+ self.far_path = Path(os.path.dirname(__file__) + '/grammars/' + kind + '/' + name + '.far')
+ if self.far_exist():
+ self._fst = Far(self.far_path, mode="r", arc_type="standard", far_type="default").get_fst()
+
+ def far_exist(self) -> bool:
+ """
+ Returns true if FAR can be loaded
+ """
+ return self.far_path.exists()
+
+ @property
+ def fst(self) -> 'pynini.FstLike':
+ return self._fst
+
+ @fst.setter
+ def fst(self, fst):
+ self._fst = fst
+
+ def add_tokens(self, fst) -> 'pynini.FstLike':
+ """
+ Wraps class name around to given fst
+
+ Args:
+ fst: input fst
+
+ Returns:
+ Fst: fst
+ """
+ return pynutil.insert(f"{self.name} {{ ") + fst + pynutil.insert(" }")
+
+ def delete_tokens(self, fst) -> 'pynini.FstLike':
+ """
+ Deletes class name wrap around output of given fst
+
+ Args:
+ fst: input fst
+
+ Returns:
+ Fst: fst
+ """
+ res = (
+ pynutil.delete(f"{self.name}")
+ + delete_space
+ + pynutil.delete("{")
+ + delete_space
+ + fst
+ + delete_space
+ + pynutil.delete("}")
+ )
+ return res @ pynini.cdrewrite(pynini.cross(u"\u00a0", " "), "", "", NEMO_SIGMA)
diff --git a/nemo_text_processing/text_normalization/kn/taggers/__init__.py b/nemo_text_processing/text_normalization/kn/taggers/__init__.py
new file mode 100644
index 000000000..4fc25d0d3
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/taggers/__init__.py
@@ -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.
diff --git a/nemo_text_processing/text_normalization/kn/taggers/cardinal.py b/nemo_text_processing/text_normalization/kn/taggers/cardinal.py
new file mode 100644
index 000000000..bdcc662e6
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/taggers/cardinal.py
@@ -0,0 +1,202 @@
+# 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_ALL_DIGIT,
+ NEMO_ALL_ZERO,
+ GraphFst,
+ insert_space,
+)
+from nemo_text_processing.text_normalization.kn.utils import get_abs_path
+
+
+class CardinalFst(GraphFst):
+ """
+ Finite state transducer for classifying cardinals, e.g.
+ -99999999999999 -> cardinal { negative: "true" 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="cardinal", kind="classify", deterministic=deterministic)
+
+ digit = pynini.string_file(get_abs_path("data/numbers/digit.tsv"))
+ zero = pynini.string_file(get_abs_path("data/numbers/zero.tsv"))
+ teens_and_ties = pynini.union(
+ pynini.string_file(get_abs_path("data/numbers/teens_and_ties.tsv")),
+ pynini.string_file(get_abs_path("data/numbers/teens_and_ties_en.tsv")),
+ )
+ hundreds = pynini.string_file(get_abs_path("data/numbers/hundreds.tsv"))
+
+ single_digit = digit | zero
+ self.single_digits_graph = single_digit + pynini.closure(insert_space + single_digit)
+
+ delete_zero = pynutil.delete(NEMO_ALL_ZERO)
+ EMPTY = pynini.accep("")
+
+ scale_suffixes = pynini.string_file(get_abs_path("data/numbers/scale_suffixes.tsv"))
+
+ def suffix_insert(key, leading_space=True):
+ value = pynini.compose(key, scale_suffixes).string()
+ text = (" " + value) if leading_space else value
+ return pynutil.insert(text)
+
+ suf_thousand = suffix_insert("thousand")
+ suf_thousand_gen = suffix_insert("thousand_gen")
+ suf_lakh = suffix_insert("lakh")
+ suf_lakh_gen = suffix_insert("lakh_gen")
+ suf_crore = suffix_insert("crore")
+ suf_crore_gen = suffix_insert("crore_gen")
+ suf_thousand_crore = suffix_insert("thousand_crore")
+ suf_thousand_crore_gen = suffix_insert("thousand_crore_gen")
+ suf_lakh_crore = suffix_insert("lakh_crore")
+ suf_lakh_crore_gen = suffix_insert("lakh_crore_gen")
+ suf_hundred = suffix_insert("hundred_suffix", leading_space=False)
+
+ def scale_parts(coeff, num_zeros, suf_standalone, suf_gen, remainders):
+ standalone = coeff + (delete_zero**num_zeros) + suf_standalone
+ with_rem = None
+
+ for rem_graph, width in remainders:
+ zeros = num_zeros - width
+ middle = (delete_zero**zeros) if zeros > 0 else EMPTY
+ branch = coeff + middle + suf_gen + insert_space + rem_graph
+ with_rem = branch if with_rem is None else with_rem | branch
+
+ return standalone, with_rem
+
+ def scale(coeff, num_zeros, suf_standalone, suf_gen, remainders):
+ standalone, with_rem = scale_parts(coeff, num_zeros, suf_standalone, suf_gen, remainders)
+ return (standalone | with_rem).optimize()
+
+ graph_hundreds = (
+ hundreds + (delete_zero**2) + suf_hundred
+ | hundreds + delete_zero + insert_space + digit
+ | hundreds + insert_space + teens_and_ties
+ ).optimize()
+
+ self.graph_hundreds = graph_hundreds
+
+ rem_thousand = [(digit, 1), (teens_and_ties, 2), (graph_hundreds, 3)]
+
+ th_standalone, th_rem = scale_parts(digit, 3, suf_thousand, suf_thousand_gen, rem_thousand)
+ tth_standalone, tth_rem = scale_parts(teens_and_ties, 3, suf_thousand, suf_thousand_gen, rem_thousand)
+
+ graph_thousands = (th_standalone | th_rem).optimize()
+ graph_ten_thousands = (tth_standalone | tth_rem).optimize()
+
+ self.graph_thousands = graph_thousands
+ self.graph_ten_thousands = graph_ten_thousands
+
+ rem_lakh = rem_thousand + [(graph_thousands, 4), (graph_ten_thousands, 5)]
+
+ l_standalone, l_rem = scale_parts(digit, 5, suf_lakh, suf_lakh_gen, rem_lakh)
+ tl_standalone, tl_rem = scale_parts(teens_and_ties, 5, suf_lakh, suf_lakh_gen, rem_lakh)
+ graph_lakhs = (l_standalone | l_rem).optimize()
+ graph_ten_lakhs = (tl_standalone | tl_rem).optimize()
+
+ self.graph_lakhs = graph_lakhs
+ self.graph_ten_lakhs = graph_ten_lakhs
+
+ rem_crore = rem_lakh + [(graph_lakhs, 6), (graph_ten_lakhs, 7)]
+
+ sub_crore_rem = (
+ (delete_zero**6) + digit
+ | (delete_zero**5) + teens_and_ties
+ | (delete_zero**4) + graph_hundreds
+ | (delete_zero**3) + graph_thousands
+ | (delete_zero**2) + graph_ten_thousands
+ | delete_zero + graph_lakhs
+ | graph_ten_lakhs
+ ).optimize()
+
+ graph_crores = scale(digit, 7, suf_crore, suf_crore_gen, rem_crore)
+ graph_ten_crores = scale(teens_and_ties, 7, suf_crore, suf_crore_gen, rem_crore)
+ graph_hundred_crores = scale(graph_hundreds, 7, suf_crore, suf_crore_gen, rem_crore)
+ self.graph_crores = graph_crores
+ self.graph_ten_crores = graph_ten_crores
+
+ def append_crore(coeff_remainder, with_sub_rem):
+ g = coeff_remainder + (delete_zero**7) + suf_crore
+
+ if with_sub_rem:
+ g |= coeff_remainder + suf_crore_gen + insert_space + sub_crore_rem
+ return g
+
+ th_crore = scale(digit, 10, suf_thousand_crore, suf_thousand_crore_gen, rem_crore)
+ graph_thousand_crores = (th_crore | append_crore(th_rem, with_sub_rem=True)).optimize()
+
+ th_crore = scale(teens_and_ties, 10, suf_thousand_crore, suf_thousand_crore_gen, rem_crore)
+ graph_ten_thousand_crores = (th_crore | append_crore(tth_rem, with_sub_rem=True)).optimize()
+
+ th_crore = scale(digit, 12, suf_lakh_crore, suf_lakh_crore_gen, rem_crore)
+ graph_lakh_crores = (th_crore | append_crore(l_rem, with_sub_rem=True)).optimize()
+
+ th_crore = scale(teens_and_ties, 12, suf_lakh_crore, suf_lakh_crore_gen, rem_crore)
+ graph_ten_lakh_crores = (th_crore | append_crore(tl_rem, with_sub_rem=True)).optimize()
+
+ graph_without_leading_zeros = pynini.union(
+ digit,
+ zero,
+ teens_and_ties,
+ graph_hundreds,
+ graph_thousands,
+ graph_ten_thousands,
+ graph_lakhs,
+ graph_ten_lakhs,
+ graph_crores,
+ graph_ten_crores,
+ graph_hundred_crores,
+ graph_thousand_crores,
+ graph_ten_thousand_crores,
+ graph_lakh_crores,
+ graph_ten_lakh_crores,
+ )
+
+ self.graph_without_leading_zeros = graph_without_leading_zeros.optimize()
+
+ cardinal_with_leading_zeros = pynini.compose(
+ NEMO_ALL_ZERO + pynini.closure(NEMO_ALL_DIGIT), self.single_digits_graph
+ )
+
+ graph_no_commas = graph_without_leading_zeros | cardinal_with_leading_zeros
+ delete_comma = pynutil.delete(",")
+
+ western_format = pynini.closure(NEMO_ALL_DIGIT, 1, 3) + pynini.closure(
+ delete_comma + pynini.closure(NEMO_ALL_DIGIT, 3, 3), 1
+ )
+
+ indian_format = (
+ pynini.closure(NEMO_ALL_DIGIT, 1, 2)
+ + pynini.closure(delete_comma + pynini.closure(NEMO_ALL_DIGIT, 2, 2))
+ + delete_comma
+ + pynini.closure(NEMO_ALL_DIGIT, 3, 3)
+ )
+
+ comma_number = western_format | indian_format
+
+ cardinal_with_commas = pynini.compose(comma_number, graph_without_leading_zeros)
+
+ final_graph = graph_no_commas | cardinal_with_commas
+ self.final_graph = final_graph.optimize()
+
+ optional_minus_graph = pynini.closure(pynutil.insert("negative: ") + pynini.cross("-", "\"true\" "), 0, 1)
+ final_graph = optional_minus_graph + pynutil.insert("integer: \"") + self.final_graph + pynutil.insert("\"")
+ self.fst = self.add_tokens(final_graph)
diff --git a/nemo_text_processing/text_normalization/kn/taggers/punctuation.py b/nemo_text_processing/text_normalization/kn/taggers/punctuation.py
new file mode 100644
index 000000000..463463753
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/taggers/punctuation.py
@@ -0,0 +1,62 @@
+# 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 sys
+from unicodedata import category
+
+import pynini
+from pynini.examples import plurals
+from pynini.lib import pynutil
+
+from nemo_text_processing.text_normalization.kn.graph_utils import NEMO_NOT_SPACE, NEMO_SIGMA, GraphFst
+
+
+class PunctuationFst(GraphFst):
+ """
+ Finite state transducer for classifying punctuation
+ e.g. , -> tokens { name: "," }
+
+ Args:
+ deterministic: if True will provide a single transduction option,
+ for False multiple transductions are generated (used for audio-based normalization)
+ """
+
+ def __init__(self, deterministic: bool = True):
+ super().__init__(name="punctuation", kind="classify", deterministic=deterministic)
+ s = "!#%&\'()*+,-./:;<=>?@^_`{|}~\""
+
+ punct_symbols_to_exclude = ["[", "]"]
+ punct_unicode = [
+ chr(i)
+ for i in range(sys.maxunicode)
+ if category(chr(i)).startswith("P") and chr(i) not in punct_symbols_to_exclude
+ ]
+
+ self.punct_marks = [p for p in punct_unicode + list(s)]
+
+ punct = pynini.union(*self.punct_marks)
+ punct = pynini.closure(punct, 1)
+
+ emphasis = (
+ pynini.accep("<")
+ + pynini.union(
+ (pynini.closure(NEMO_NOT_SPACE - pynini.union("<", ">"), 1) + pynini.closure(pynini.accep("/"), 0, 1)),
+ (pynini.accep("/") + pynini.closure(NEMO_NOT_SPACE - pynini.union("<", ">"), 1)),
+ )
+ + pynini.accep(">")
+ )
+ punct = plurals._priority_union(emphasis, punct, NEMO_SIGMA)
+
+ self.graph = punct
+ self.fst = (pynutil.insert("name: \"") + self.graph + pynutil.insert("\"")).optimize()
diff --git a/nemo_text_processing/text_normalization/kn/taggers/tokenize_and_classify.py b/nemo_text_processing/text_normalization/kn/taggers/tokenize_and_classify.py
new file mode 100644
index 000000000..fd279b6bb
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/taggers/tokenize_and_classify.py
@@ -0,0 +1,117 @@
+# 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 logging
+import os
+
+import pynini
+from pynini.lib import pynutil
+
+from nemo_text_processing.text_normalization.kn.graph_utils import (
+ NEMO_SPACE,
+ NEMO_WHITE_SPACE,
+ GraphFst,
+ delete_extra_space,
+ delete_space,
+ generator_main,
+)
+from nemo_text_processing.text_normalization.kn.taggers.cardinal import CardinalFst
+from nemo_text_processing.text_normalization.kn.taggers.punctuation import PunctuationFst
+from nemo_text_processing.text_normalization.kn.taggers.word import WordFst
+
+
+class ClassifyFst(GraphFst):
+ """
+ Final class that composes all other classification grammars. This class can process an entire sentence including punctuation.
+ For deployment, this grammar will be compiled and exported to OpenFst Finite State Archive (FAR) File.
+ More details to deployment at NeMo/tools/text_processing_deployment.
+
+ Args:
+ input_case: accepting either "lower_cased" or "cased" input.
+ deterministic: if True will provide a single transduction option,
+ for False multiple options (used for audio-based normalization)
+ cache_dir: path to a dir with .far grammar file. Set to None to avoid using cache.
+ overwrite_cache: set to True to overwrite .far files
+ whitelist: path to a file with whitelist replacements
+ """
+
+ def __init__(
+ self,
+ input_case: str,
+ deterministic: bool = True,
+ cache_dir: str = None,
+ overwrite_cache: bool = False,
+ whitelist: str = None,
+ ):
+ super().__init__(name="tokenize_and_classify", kind="classify", deterministic=deterministic)
+
+ far_file = None
+
+ if cache_dir is not None and cache_dir != "None":
+ os.makedirs(cache_dir, exist_ok=True)
+ whitelist_file = os.path.basename(whitelist) if whitelist else ""
+ far_file = os.path.join(
+ cache_dir,
+ f"kn_tn_{deterministic}_deterministic_{input_case}_{whitelist_file}_tokenize.far",
+ )
+
+ if not overwrite_cache and far_file and os.path.exists(far_file):
+ self.fst = pynini.Far(far_file, mode="r")["tokenize_and_classify"]
+ logging.info(f"ClassifyFst.fst was restored from {far_file}.")
+ else:
+ logging.info(f"Creating ClassifyFst grammars.")
+
+ cardinal = CardinalFst(deterministic=deterministic)
+ cardinal_graph = cardinal.fst
+
+ punctuation = PunctuationFst(deterministic=deterministic)
+ punct_graph = punctuation.fst
+
+ classify = pynutil.add_weight(cardinal_graph, 1.1)
+
+ word_graph = WordFst().fst
+
+ punct = pynutil.insert("tokens { ") + pynutil.add_weight(punct_graph, weight=2.1) + pynutil.insert(" }")
+ punct = pynini.closure(
+ pynini.union(
+ pynini.compose(pynini.closure(NEMO_WHITE_SPACE, 1), delete_extra_space),
+ (pynutil.insert(NEMO_SPACE) + punct),
+ ),
+ 1,
+ )
+
+ classify = pynini.union(classify, pynutil.add_weight(word_graph, 100))
+ token = pynutil.insert("tokens { ") + classify + pynutil.insert(" }")
+ token_plus_punct = (
+ pynini.closure(punct + pynutil.insert(NEMO_SPACE))
+ + token
+ + pynini.closure(pynutil.insert(NEMO_SPACE) + punct)
+ )
+
+ graph = token_plus_punct + pynini.closure(
+ pynini.union(
+ pynini.compose(pynini.closure(NEMO_WHITE_SPACE, 1), delete_extra_space),
+ (pynutil.insert(NEMO_SPACE) + punct + pynutil.insert(NEMO_SPACE)),
+ )
+ + token_plus_punct
+ )
+
+ graph = delete_space + graph + delete_space
+ graph = pynini.union(graph, punct)
+
+ self.fst = graph.optimize()
+
+ if far_file:
+ generator_main(far_file, {"tokenize_and_classify": self.fst})
+ logging.info(f"ClassifyFst grammars are saved to {far_file}.")
diff --git a/nemo_text_processing/text_normalization/kn/taggers/word.py b/nemo_text_processing/text_normalization/kn/taggers/word.py
new file mode 100644
index 000000000..6ca4d7e60
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/taggers/word.py
@@ -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 pynini
+from pynini.lib import pynutil
+from nemo_text_processing.text_normalization.kn.graph_utils import NEMO_NOT_SPACE, GraphFst
+
+
+class WordFst(GraphFst):
+ """
+ Finite state transducer for classifying Kannada words.
+ e.g. ಚಿನ್ನ -> tokens { name: "ಚಿನ್ನ" }
+
+ Args:
+ deterministic: if True will provide a single transduction option,
+ for False multiple transductions are generated (used for audio-based normalization)
+ """
+
+ def __init__(self, deterministic: bool = True):
+ super().__init__(name="word", kind="classify", deterministic=deterministic)
+ word = pynutil.insert("name: \"") + pynini.closure(NEMO_NOT_SPACE, 1) + pynutil.insert("\"")
+ self.fst = word.optimize()
diff --git a/nemo_text_processing/text_normalization/kn/utils.py b/nemo_text_processing/text_normalization/kn/utils.py
new file mode 100644
index 000000000..1de2952c2
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/utils.py
@@ -0,0 +1,28 @@
+# 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 csv
+import os
+import pynini
+
+
+def get_abs_path(rel_path):
+ """
+ Get absolute path
+
+ Args:
+ rel_path: relative path to this file
+
+ Returns absolute path
+ """
+ return os.path.dirname(os.path.abspath(__file__)) + '/' + rel_path
diff --git a/nemo_text_processing/text_normalization/kn/verbalizers/__init__.py b/nemo_text_processing/text_normalization/kn/verbalizers/__init__.py
new file mode 100644
index 000000000..4fc25d0d3
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/verbalizers/__init__.py
@@ -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.
diff --git a/nemo_text_processing/text_normalization/kn/verbalizers/cardinal.py b/nemo_text_processing/text_normalization/kn/verbalizers/cardinal.py
new file mode 100644
index 000000000..18e524c42
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/verbalizers/cardinal.py
@@ -0,0 +1,41 @@
+# 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 CardinalFst(GraphFst):
+ """
+ Finite state transducer for verbalizing cardinals, e.g.
+ cardinal { integer: "ಐದು" } -> ಐದು
+ cardinal { negative: "true" 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="cardinal", kind="verbalize", deterministic=deterministic)
+
+ optional_sign = pynini.closure(pynini.cross("negative: \"true\" ", "ಮೈನಸ್ "), 0, 1)
+
+ integer = pynini.closure(NEMO_NOT_QUOTE, 1)
+ integer = pynutil.delete("integer:") + pynutil.delete(" \"") + integer + pynutil.delete("\"")
+
+ graph = optional_sign + integer
+ self.fst = self.delete_tokens(graph).optimize()
diff --git a/nemo_text_processing/text_normalization/kn/verbalizers/verbalize.py b/nemo_text_processing/text_normalization/kn/verbalizers/verbalize.py
new file mode 100644
index 000000000..2f68662c4
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/verbalizers/verbalize.py
@@ -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.
+
+from nemo_text_processing.text_normalization.kn.graph_utils import GraphFst
+from nemo_text_processing.text_normalization.kn.verbalizers.cardinal import CardinalFst
+
+
+class VerbalizeFst(GraphFst):
+ """
+ Composes other verbalizer grammars.
+ For deployment, this grammar will be compiled and exported to OpenFst Finite State Archive (FAR) File.
+ More details to deployment at NeMo/tools/text_processing_deployment.
+
+ Args:
+ deterministic: if True will provide a single transduction option,
+ for False multiple options (used for audio-based normalization)
+ """
+
+ def __init__(self, deterministic: bool = True):
+ super().__init__(name="verbalize", kind="verbalize", deterministic=deterministic)
+
+ cardinal = CardinalFst(deterministic=deterministic)
+ cardinal_graph = cardinal.fst
+
+ graph = cardinal_graph
+
+ self.fst = graph
diff --git a/nemo_text_processing/text_normalization/kn/verbalizers/verbalize_final.py b/nemo_text_processing/text_normalization/kn/verbalizers/verbalize_final.py
new file mode 100644
index 000000000..17887ff31
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/verbalizers/verbalize_final.py
@@ -0,0 +1,76 @@
+# 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 logging
+import os
+
+import pynini
+from pynini.lib import pynutil
+
+from nemo_text_processing.text_normalization.kn.graph_utils import (
+ GraphFst,
+ delete_extra_space,
+ delete_space,
+ generator_main,
+)
+from nemo_text_processing.text_normalization.kn.verbalizers.verbalize import VerbalizeFst
+from nemo_text_processing.text_normalization.kn.verbalizers.word import WordFst
+
+
+class VerbalizeFinalFst(GraphFst):
+ """
+ Finite state transducer that verbalizes an entire sentence
+ tokens { cardinal { integer: "ಎರಡು" } } tokens { name: "ಷಟ್ಕಗಳು" } tokens { cardinal { integer: "ಹನ್ನೆರಡು" } } tokens { name: "ಓಟಗಳು" } -> ಎರಡು ಷಟ್ಕಗಳು ಹನ್ನೆರಡು ಓಟಗಳು
+
+ Args:
+ deterministic: if True will provide a single transduction option,
+ for False multiple options (used for audio-based normalization)
+ cache_dir: path to a dir with .far grammar file. Set to None to avoid using cache.
+ overwrite_cache: set to True to overwrite .far files
+ """
+
+ def __init__(self, deterministic: bool = True, cache_dir: str = None, overwrite_cache: bool = False):
+ super().__init__(name="verbalize_final", kind="verbalize", deterministic=deterministic)
+
+ far_file = None
+ if cache_dir is not None and cache_dir != "None":
+ os.makedirs(cache_dir, exist_ok=True)
+ far_file = os.path.join(cache_dir, f"kn_tn_{deterministic}_deterministic_verbalizer.far")
+ if not overwrite_cache and far_file and os.path.exists(far_file):
+ self.fst = pynini.Far(far_file, mode="r")["verbalize"]
+ logging.info(f'VerbalizeFinalFst graph was restored from {far_file}.')
+ else:
+ verbalize = VerbalizeFst(deterministic=deterministic).fst
+ word = WordFst(deterministic=deterministic).fst
+ types = verbalize | word
+
+ if deterministic:
+ graph = (
+ pynutil.delete("tokens")
+ + delete_space
+ + pynutil.delete("{")
+ + delete_space
+ + types
+ + delete_space
+ + pynutil.delete("}")
+ )
+ else:
+ graph = delete_space + types + delete_space
+
+ graph = delete_space + pynini.closure(graph + delete_extra_space) + graph + delete_space
+
+ self.fst = graph.optimize()
+ if far_file:
+ generator_main(far_file, {"verbalize": self.fst})
+ logging.info(f"VerbalizeFinalFst grammars are saved to {far_file}.")
diff --git a/nemo_text_processing/text_normalization/kn/verbalizers/word.py b/nemo_text_processing/text_normalization/kn/verbalizers/word.py
new file mode 100644
index 000000000..d8b3b3993
--- /dev/null
+++ b/nemo_text_processing/text_normalization/kn/verbalizers/word.py
@@ -0,0 +1,35 @@
+# 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_CHAR, GraphFst, delete_space
+
+
+class WordFst(GraphFst):
+ """
+ Finite state transducer for verbalizing Kannada words.
+ e.g. tokens { name: "ಚಿನ್ನ" } -> ಚಿನ್ನ
+
+ 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="word", kind="verbalize", deterministic=deterministic)
+ chars = pynini.closure(NEMO_CHAR - " ", 1)
+ char = pynutil.delete("name:") + delete_space + pynutil.delete("\"") + chars + pynutil.delete("\"")
+
+ self.fst = char.optimize()
diff --git a/nemo_text_processing/text_normalization/normalize.py b/nemo_text_processing/text_normalization/normalize.py
index d8ebf2f4d..548d7a0c8 100644
--- a/nemo_text_processing/text_normalization/normalize.py
+++ b/nemo_text_processing/text_normalization/normalize.py
@@ -166,6 +166,7 @@ def __init__(
if post_process:
self.post_processor = PostProcessingFst(cache_dir=cache_dir, overwrite_cache=overwrite_cache)
+
elif lang == 'it':
from nemo_text_processing.text_normalization.it.taggers.tokenize_and_classify import ClassifyFst
from nemo_text_processing.text_normalization.it.verbalizers.verbalize_final import VerbalizeFinalFst
@@ -191,6 +192,10 @@ def __init__(
elif lang == 'ko':
from nemo_text_processing.text_normalization.ko.taggers.tokenize_and_classify import ClassifyFst
from nemo_text_processing.text_normalization.ko.verbalizers.verbalize_final import VerbalizeFinalFst
+ elif lang == 'kn':
+ from nemo_text_processing.text_normalization.kn.taggers.tokenize_and_classify import ClassifyFst
+ from nemo_text_processing.text_normalization.kn.verbalizers.verbalize_final import VerbalizeFinalFst
+
else:
raise NotImplementedError(f"Language {lang} has not been supported yet.")
@@ -737,7 +742,7 @@ def parse_args():
parser.add_argument(
"--language",
help="language",
- choices=["en", "de", "es", "fr", "hu", "sv", "zh", "ar", "it", "hy", "ja", "hi", "ko", "vi", "pt"],
+ choices=["en", "de", "es", "fr", "hu", "sv", "zh", "ar", "it", "hy", "ja", "hi", "kn", "ko", "vi", "pt"],
default="en",
type=str,
)
diff --git a/nemo_text_processing/text_normalization/run_evaluate.py b/nemo_text_processing/text_normalization/run_evaluate.py
index 3a1964bbd..163b20648 100644
--- a/nemo_text_processing/text_normalization/run_evaluate.py
+++ b/nemo_text_processing/text_normalization/run_evaluate.py
@@ -35,7 +35,7 @@ def parse_args():
parser.add_argument(
"--lang",
help="language",
- choices=['ar', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ru', 'sv', 'zh', 'hy', 'hi', 'ko', 'vi', 'pt'],
+ choices=['ar', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ru', 'sv', 'zh', 'hy', 'hi', 'kn', 'ko', 'vi', 'pt'],
default="en",
type=str,
)
diff --git a/tests/nemo_text_processing/kn/__init__.py b/tests/nemo_text_processing/kn/__init__.py
new file mode 100644
index 000000000..4fc25d0d3
--- /dev/null
+++ b/tests/nemo_text_processing/kn/__init__.py
@@ -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.
diff --git a/tests/nemo_text_processing/kn/data_text_normalization/test_cases_cardinal.txt b/tests/nemo_text_processing/kn/data_text_normalization/test_cases_cardinal.txt
new file mode 100644
index 000000000..f9762efb0
--- /dev/null
+++ b/tests/nemo_text_processing/kn/data_text_normalization/test_cases_cardinal.txt
@@ -0,0 +1,266 @@
+4 ಚೌಕೆ~ನಾಲ್ಕು ಚೌಕೆ
+೬ ಆಟಗಾರ ಔಟ್~ಆರು ಆಟಗಾರ ಔಟ್
+ವನ್ಪ್ಲಸ್ 8 ಪ್ರೊ~ವನ್ಪ್ಲಸ್ ಎಂಟು ಪ್ರೊ
+೫ ಚಾರ್ಜರ್~ಐದು ಚಾರ್ಜರ್
+4 ಓವರ್ನಲ್ಲಿ 17 ಓಟ~ನಾಲ್ಕು ಓವರ್ನಲ್ಲಿ ಹದಿನೇಳು ಓಟ
+೫ ಚಾಕೊಲೇಟ್ಗಳು ೯ ಟಾಫಿಗಳು~ಐದು ಚಾಕೊಲೇಟ್ಗಳು ಒಂಬತ್ತು ಟಾಫಿಗಳು
+10099~ಹತ್ತು ಸಾವಿರದ ತೊಂಬತ್ತೊಂಬತ್ತು
+೧೦೦೦೦೧~ಒಂದು ಲಕ್ಷದ ಒಂದು
+100001~ಒಂದು ಲಕ್ಷದ ಒಂದು
+4 ಷಟ್ಕಗಳು 14 ಚೌಕೆಗಳು~ನಾಲ್ಕು ಷಟ್ಕಗಳು ಹದಿನಾಲ್ಕು ಚೌಕೆಗಳು
+೬ ಓಟ ಗಳಿಸಿದರು~ಆರು ಓಟ ಗಳಿಸಿದರು
+3 ಗೋಲು ಹೊಡೆದರು~ಮೂರು ಗೋಲು ಹೊಡೆದರು
+೬ ಓಟ ಗಳಿಸಿದರು~ಆರು ಓಟ ಗಳಿಸಿದರು
+3 ಗೋಲು ಹೊಡೆದರು~ಮೂರು ಗೋಲು ಹೊಡೆದರು
+೫ ಚೌಕೆ~ಐದು ಚೌಕೆ
+2 ಷಟ್ಕಗಳು 12 ಓಟ~ಎರಡು ಷಟ್ಕಗಳು ಹನ್ನೆರಡು ಓಟ
+೯ ಅಂಕಗಳು~ಒಂಬತ್ತು ಅಂಕಗಳು
+4 ಮೃತರು 18 ಗಾಯಾಳುಗಳು~ನಾಲ್ಕು ಮೃತರು ಹದಿನೆಂಟು ಗಾಯಾಳುಗಳು
+೫ ಗೋಲು ಹೊಡೆ~ಐದು ಗೋಲು ಹೊಡೆ
+3 ಬ್ಯಾಟ್ಸ್ಮನ್ಗಳು 12 ಆಟಗಾರರು~ಮೂರು ಬ್ಯಾಟ್ಸ್ಮನ್ಗಳು ಹನ್ನೆರಡು ಆಟಗಾರರು
+೫ ಸೋಲು ೧ ಡ್ರಾ ೧೭ ಜಯ~ಐದು ಸೋಲು ಒಂದು ಡ್ರಾ ಹದಿನೇಳು ಜಯ
+51022345567~ಐದು ಸಾವಿರದ ನೂರ ಎರಡು ಕೋಟಿಯ ಇಪ್ಪತ್ತಮೂರು ಲಕ್ಷದ ನಲವತ್ತೈದು ಸಾವಿರದ ಐನೂರ ಅರವತ್ತೇಳು
+ಪಾಠ ೧೦ ಶ್ಲೋಕ ೨~ಪಾಠ ಹತ್ತು ಶ್ಲೋಕ ಎರಡು
+೧೧೦~ನೂರ ಹತ್ತು
+೫೫೫೫೨ ಸೇಬುಗಳು~ಐವತ್ತೈದು ಸಾವಿರದ ಐನೂರ ಐವತ್ತೆರಡು ಸೇಬುಗಳು
+-53~ಮೈನಸ್ ಐವತ್ತಮೂರು
+-100001~ಮೈನಸ್ ಒಂದು ಲಕ್ಷದ ಒಂದು
+-೬~ಮೈನಸ್ ಆರು
+೫೪~ಐವತ್ತನಾಲ್ಕು
+99~ತೊಂಬತ್ತೊಂಬತ್ತು
+೧೩೨೩~ಒಂದು ಸಾವಿರದ ಮುನ್ನೂರ ಇಪ್ಪತ್ತಮೂರು
+12,346~ಹನ್ನೆರಡು ಸಾವಿರದ ಮುನ್ನೂರ ನಲವತ್ತಾರು
+೧೨೩೪೫೬~ಒಂದು ಲಕ್ಷದ ಇಪ್ಪತ್ತಮೂರು ಸಾವಿರದ ನಾನೂರ ಐವತ್ತಾರು
+872987~ಎಂಟು ಲಕ್ಷದ ಎಪ್ಪತ್ತೆರಡು ಸಾವಿರದ ಒಂಬೈನೂರ ಎಂಬತ್ತೇಳು
+೯೮೭೬೦೯~ಒಂಬತ್ತು ಲಕ್ಷದ ಎಂಬತ್ತೇಳು ಸಾವಿರದ ಆರುನೂರ ಒಂಬತ್ತು
+9,876,789~ತೊಂಬತ್ತೆಂಟು ಲಕ್ಷದ ಎಪ್ಪತ್ತಾರು ಸಾವಿರದ ಏಳುನೂರ ಎಂಬತ್ತೊಂಬತ್ತು
+೨೩೪೫೫೬೭~ಇಪ್ಪತ್ತಮೂರು ಲಕ್ಷದ ನಲವತ್ತೈದು ಸಾವಿರದ ಐನೂರ ಅರವತ್ತೇಳು
+12345567~ಒಂದು ಕೋಟಿಯ ಇಪ್ಪತ್ತಮೂರು ಲಕ್ಷದ ನಲವತ್ತೈದು ಸಾವಿರದ ಐನೂರ ಅರವತ್ತೇಳು
+೧೨೧೨೧೨೧೨~ಒಂದು ಕೋಟಿಯ ಇಪ್ಪತ್ತೊಂದು ಲಕ್ಷದ ಇಪ್ಪತ್ತೊಂದು ಸಾವಿರದ ಇನ್ನೂರ ಹನ್ನೆರಡು
+೧೦೨೨೩೪೫೫೬೭~ನೂರ ಎರಡು ಕೋಟಿಯ ಇಪ್ಪತ್ತಮೂರು ಲಕ್ಷದ ನಲವತ್ತೈದು ಸಾವಿರದ ಐನೂರ ಅರವತ್ತೇಳು
+2 ಅಂಕಗಳು 12 ಗೋಲ ~ಎರಡು ಅಂಕಗಳು ಹನ್ನೆರಡು ಗೋಲ
+01~ಸೊನ್ನೆ ಒಂದು
+೦೭೩~ಸೊನ್ನೆ ಏಳು ಮೂರು
+0001~ಸೊನ್ನೆ ಸೊನ್ನೆ ಸೊನ್ನೆ ಒಂದು
+೦೦೦~ಸೊನ್ನೆ ಸೊನ್ನೆ ಸೊನ್ನೆ
+೧೦೦೦೦೦೩~ಹತ್ತು ಲಕ್ಷದ ಮೂರು
+ಪುಟ ೧೯೯ ಪಾಠ ೬~ಪುಟ ನೂರ ತೊಂಬತ್ತೊಂಬತ್ತು ಪಾಠ ಆರು
+೧೯ ರಲ್ಲಿ ೩ ಗ್ಲಾಸ್~ಹತ್ತೊಂಬತ್ತು ರಲ್ಲಿ ಮೂರು ಗ್ಲಾಸ್
+ರುಕು ೪ ಆಯತ್ ೬ ರ ಸಾಲು ೩೩~ರುಕು ನಾಲ್ಕು ಆಯತ್ ಆರು ರ ಸಾಲು ಮೂವತ್ತಮೂರು
+10000000000~ಒಂದು ಸಾವಿರ ಕೋಟಿ
+1,23,45,00,00,000~ಹನ್ನೆರಡು ಸಾವಿರದ ಮುನ್ನೂರ ನಲವತ್ತೈದು ಕೋಟಿ
+1,001,000,000,000~ಒಂದು ಲಕ್ಷದ ನೂರು ಕೋಟಿ
+1,00,00,000~ಒಂದು ಕೋಟಿ
+007~ಸೊನ್ನೆ ಸೊನ್ನೆ ಏಳು
+020~ಸೊನ್ನೆ ಎರಡು ಸೊನ್ನೆ
+12,342,300,000~ಒಂದು ಸಾವಿರದ ಇನ್ನೂರ ಮೂವತ್ತನಾಲ್ಕು ಕೋಟಿಯ ಇಪ್ಪತ್ತಮೂರು ಲಕ್ಷ
+12,342,300,001~ಒಂದು ಸಾವಿರದ ಇನ್ನೂರ ಮೂವತ್ತನಾಲ್ಕು ಕೋಟಿಯ ಇಪ್ಪತ್ತಮೂರು ಲಕ್ಷದ ಒಂದು
+1001000012345~ಒಂದು ಲಕ್ಷದ ನೂರು ಕೋಟಿಯ ಹನ್ನೆರಡು ಸಾವಿರದ ಮುನ್ನೂರ ನಲವತ್ತೈದು
+99999999999999~ತೊಂಬತ್ತೊಂಬತ್ತು ಲಕ್ಷದ ತೊಂಬತ್ತೊಂಬತ್ತು ಸಾವಿರದ ಒಂಬೈನೂರ ತೊಂಬತ್ತೊಂಬತ್ತು ಕೋಟಿಯ ತೊಂಬತ್ತೊಂಬತ್ತು ಲಕ್ಷದ ತೊಂಬತ್ತೊಂಬತ್ತು ಸಾವಿರದ ಒಂಬೈನೂರ ತೊಂಬತ್ತೊಂಬತ್ತು
+100000000000000~100000000000000
+700~ಏಳುನೂರು
+೭೦೩~ಏಳುನೂರ ಮೂರು
+7000~ಏಳು ಸಾವಿರ
+೭೦೦೩~ಏಳು ಸಾವಿರದ ಮೂರು
+7047~ಏಳು ಸಾವಿರದ ನಲವತ್ತೇಳು
+೩೪೦೦೦~ಮೂವತ್ತನಾಲ್ಕು ಸಾವಿರ
+34003~ಮೂವತ್ತನಾಲ್ಕು ಸಾವಿರದ ಮೂರು
+೭೦೦೦೦೦~ಏಳು ಲಕ್ಷ
+700047~ಏಳು ಲಕ್ಷದ ನಲವತ್ತೇಳು
+೭೦೦೬೦೮~ಏಳು ಲಕ್ಷದ ಆರುನೂರ ಎಂಟು
+705019~ಏಳು ಲಕ್ಷದ ಐದು ಸಾವಿರದ ಹತ್ತೊಂಬತ್ತು
+೩೪೦೦೦೦೦~ಮೂವತ್ತನಾಲ್ಕು ಲಕ್ಷ
+3400047~ಮೂವತ್ತನಾಲ್ಕು ಲಕ್ಷದ ನಲವತ್ತೇಳು
+೩೪೦೦೬೦೮~ಮೂವತ್ತನಾಲ್ಕು ಲಕ್ಷದ ಆರುನೂರ ಎಂಟು
+3405019~ಮೂವತ್ತನಾಲ್ಕು ಲಕ್ಷದ ಐದು ಸಾವಿರದ ಹತ್ತೊಂಬತ್ತು
+೭೦೦೦೦೦೦೩~ಏಳು ಕೋಟಿಯ ಮೂರು
+70000047~ಏಳು ಕೋಟಿಯ ನಲವತ್ತೇಳು
+೭೦೦೦೦೬೦೮~ಏಳು ಕೋಟಿಯ ಆರುನೂರ ಎಂಟು
+70005019~ಏಳು ಕೋಟಿಯ ಐದು ಸಾವಿರದ ಹತ್ತೊಂಬತ್ತು
+೭೦೦೭೦೪೦೨~ಏಳು ಕೋಟಿಯ ಎಪ್ಪತ್ತು ಸಾವಿರದ ನಾನೂರ ಎರಡು
+70230045~ಏಳು ಕೋಟಿಯ ಎರಡು ಲಕ್ಷದ ಮೂವತ್ತು ಸಾವಿರದ ನಲವತ್ತೈದು
+೩೪೦೦೦೦೦೦೦~ಮೂವತ್ತನಾಲ್ಕು ಕೋಟಿ
+340000003~ಮೂವತ್ತನಾಲ್ಕು ಕೋಟಿಯ ಮೂರು
+೩೪೦೦೦೦೦೪೭~ಮೂವತ್ತನಾಲ್ಕು ಕೋಟಿಯ ನಲವತ್ತೇಳು
+340000608~ಮೂವತ್ತನಾಲ್ಕು ಕೋಟಿಯ ಆರುನೂರ ಎಂಟು
+೩೪೦೦೦೫೦೧೯~ಮೂವತ್ತನಾಲ್ಕು ಕೋಟಿಯ ಐದು ಸಾವಿರದ ಹತ್ತೊಂಬತ್ತು
+340070402~ಮೂವತ್ತನಾಲ್ಕು ಕೋಟಿಯ ಎಪ್ಪತ್ತು ಸಾವಿರದ ನಾನೂರ ಎರಡು
+೩೪೦೨೩೦೦೪೫~ಮೂವತ್ತನಾಲ್ಕು ಕೋಟಿಯ ಎರಡು ಲಕ್ಷದ ಮೂವತ್ತು ಸಾವಿರದ ನಲವತ್ತೈದು
+348000059~ಮೂವತ್ತನಾಲ್ಕು ಕೋಟಿಯ ಎಂಬತ್ತು ಲಕ್ಷದ ಐವತ್ತೊಂಬತ್ತು
+೫೦೬೦೦೦೦೦೦೦~ಐನೂರ ಆರು ಕೋಟಿ
+5060000003~ಐನೂರ ಆರು ಕೋಟಿಯ ಮೂರು
+೫೦೬೦೦೦೦೦೪೭~ಐನೂರ ಆರು ಕೋಟಿಯ ನಲವತ್ತೇಳು
+5060000608~ಐನೂರ ಆರು ಕೋಟಿಯ ಆರುನೂರ ಎಂಟು
+೫೦೬೦೦೦೫೦೧೯~ಐನೂರ ಆರು ಕೋಟಿಯ ಐದು ಸಾವಿರದ ಹತ್ತೊಂಬತ್ತು
+5060070402~ಐನೂರ ಆರು ಕೋಟಿಯ ಎಪ್ಪತ್ತು ಸಾವಿರದ ನಾನೂರ ಎರಡು
+೫೦೬೦೨೩೦೦೪೫~ಐನೂರ ಆರು ಕೋಟಿಯ ಎರಡು ಲಕ್ಷದ ಮೂವತ್ತು ಸಾವಿರದ ನಲವತ್ತೈದು
+70000000003~ಏಳು ಸಾವಿರ ಕೋಟಿಯ ಮೂರು
+೭೦೦೦೦೦೦೦೦೪೭~ಏಳು ಸಾವಿರ ಕೋಟಿಯ ನಲವತ್ತೇಳು
+70000000608~ಏಳು ಸಾವಿರ ಕೋಟಿಯ ಆರುನೂರ ಎಂಟು
+೭೦೦೦೦೦೦೫೦೧೯~ಏಳು ಸಾವಿರ ಕೋಟಿಯ ಐದು ಸಾವಿರದ ಹತ್ತೊಂಬತ್ತು
+70000070402~ಏಳು ಸಾವಿರ ಕೋಟಿಯ ಎಪ್ಪತ್ತು ಸಾವಿರದ ನಾನೂರ ಎರಡು
+೭೦೦೦೦೨೩೦೦೪೫~ಏಳು ಸಾವಿರ ಕೋಟಿಯ ಎರಡು ಲಕ್ಷದ ಮೂವತ್ತು ಸಾವಿರದ ನಲವತ್ತೈದು
+70008000059~ಏಳು ಸಾವಿರ ಕೋಟಿಯ ಎಂಬತ್ತು ಲಕ್ಷದ ಐವತ್ತೊಂಬತ್ತು
+೨೦೮೧೦೦೦೦೦೦೦~ಎರಡು ಸಾವಿರದ ಎಂಬತ್ತೊಂದು ಕೋಟಿ
+20810000003~ಎರಡು ಸಾವಿರದ ಎಂಬತ್ತೊಂದು ಕೋಟಿಯ ಮೂರು
+೨೦೮೧೦೦೦೦೦೪೭~ಎರಡು ಸಾವಿರದ ಎಂಬತ್ತೊಂದು ಕೋಟಿಯ ನಲವತ್ತೇಳು
+20810000608~ಎರಡು ಸಾವಿರದ ಎಂಬತ್ತೊಂದು ಕೋಟಿಯ ಆರುನೂರ ಎಂಟು
+೨೦೮೧೦೦೦೫೦೧೯~ಎರಡು ಸಾವಿರದ ಎಂಬತ್ತೊಂದು ಕೋಟಿಯ ಐದು ಸಾವಿರದ ಹತ್ತೊಂಬತ್ತು
+20810070402~ಎರಡು ಸಾವಿರದ ಎಂಬತ್ತೊಂದು ಕೋಟಿಯ ಎಪ್ಪತ್ತು ಸಾವಿರದ ನಾನೂರ ಎರಡು
+೨೦೮೧೦೨೩೦೦೪೫~ಎರಡು ಸಾವಿರದ ಎಂಬತ್ತೊಂದು ಕೋಟಿಯ ಎರಡು ಲಕ್ಷದ ಮೂವತ್ತು ಸಾವಿರದ ನಲವತ್ತೈದು
+340000000000~ಮೂವತ್ತನಾಲ್ಕು ಸಾವಿರ ಕೋಟಿ
+೩೪೦೦೦೦೦೦೦೦೦೩~ಮೂವತ್ತನಾಲ್ಕು ಸಾವಿರ ಕೋಟಿಯ ಮೂರು
+340000000047~ಮೂವತ್ತನಾಲ್ಕು ಸಾವಿರ ಕೋಟಿಯ ನಲವತ್ತೇಳು
+೩೪೦೦೦೦೦೦೦೬೦೮~ಮೂವತ್ತನಾಲ್ಕು ಸಾವಿರ ಕೋಟಿಯ ಆರುನೂರ ಎಂಟು
+340000005019~ಮೂವತ್ತನಾಲ್ಕು ಸಾವಿರ ಕೋಟಿಯ ಐದು ಸಾವಿರದ ಹತ್ತೊಂಬತ್ತು
+೩೪೦೦೦೦೦೭೦೪೦೨~ಮೂವತ್ತನಾಲ್ಕು ಸಾವಿರ ಕೋಟಿಯ ಎಪ್ಪತ್ತು ಸಾವಿರದ ನಾನೂರ ಎರಡು
+340000230045~ಮೂವತ್ತನಾಲ್ಕು ಸಾವಿರ ಕೋಟಿಯ ಎರಡು ಲಕ್ಷದ ಮೂವತ್ತು ಸಾವಿರದ ನಲವತ್ತೈದು
+೩೪೦೦೦೮೦೦೦೦೫೯~ಮೂವತ್ತನಾಲ್ಕು ಸಾವಿರ ಕೋಟಿಯ ಎಂಬತ್ತು ಲಕ್ಷದ ಐವತ್ತೊಂಬತ್ತು
+406070000003~ನಲವತ್ತು ಸಾವಿರದ ಆರುನೂರ ಏಳು ಕೋಟಿಯ ಮೂರು
+೪೦೬೦೭೦೦೦೦೦೪೭~ನಲವತ್ತು ಸಾವಿರದ ಆರುನೂರ ಏಳು ಕೋಟಿಯ ನಲವತ್ತೇಳು
+406070000608~ನಲವತ್ತು ಸಾವಿರದ ಆರುನೂರ ಏಳು ಕೋಟಿಯ ಆರುನೂರ ಎಂಟು
+೪೦೬೦೭೦೦೦೫೦೧೯~ನಲವತ್ತು ಸಾವಿರದ ಆರುನೂರ ಏಳು ಕೋಟಿಯ ಐದು ಸಾವಿರದ ಹತ್ತೊಂಬತ್ತು
+406070070402~ನಲವತ್ತು ಸಾವಿರದ ಆರುನೂರ ಏಳು ಕೋಟಿಯ ಎಪ್ಪತ್ತು ಸಾವಿರದ ನಾನೂರ ಎರಡು
+೪೦೬೦೭೦೨೩೦೦೪೫~ನಲವತ್ತು ಸಾವಿರದ ಆರುನೂರ ಏಳು ಕೋಟಿಯ ಎರಡು ಲಕ್ಷದ ಮೂವತ್ತು ಸಾವಿರದ ನಲವತ್ತೈದು
+406078000059~ನಲವತ್ತು ಸಾವಿರದ ಆರುನೂರ ಏಳು ಕೋಟಿಯ ಎಂಬತ್ತು ಲಕ್ಷದ ಐವತ್ತೊಂಬತ್ತು
+೭೦೦೦೦೦೦೦೦೦೦೦೦~ಏಳು ಲಕ್ಷ ಕೋಟಿ
+7000000000003~ಏಳು ಲಕ್ಷ ಕೋಟಿಯ ಮೂರು
+೭೦೦೦೦೦೦೦೦೦೦೪೭~ಏಳು ಲಕ್ಷ ಕೋಟಿಯ ನಲವತ್ತೇಳು
+7000000000608~ಏಳು ಲಕ್ಷ ಕೋಟಿಯ ಆರುನೂರ ಎಂಟು
+೭೦೦೦೦೦೦೦೦೫೦೧೯~ಏಳು ಲಕ್ಷ ಕೋಟಿಯ ಐದು ಸಾವಿರದ ಹತ್ತೊಂಬತ್ತು
+7000000070402~ಏಳು ಲಕ್ಷ ಕೋಟಿಯ ಎಪ್ಪತ್ತು ಸಾವಿರದ ನಾನೂರ ಎರಡು
+೭೦೦೦೦೦೦೨೩೦೦೪೫~ಏಳು ಲಕ್ಷ ಕೋಟಿಯ ಎರಡು ಲಕ್ಷದ ಮೂವತ್ತು ಸಾವಿರದ ನಲವತ್ತೈದು
+7000008000059~ಏಳು ಲಕ್ಷ ಕೋಟಿಯ ಎಂಬತ್ತು ಲಕ್ಷದ ಐವತ್ತೊಂಬತ್ತು
+೩೦೮೦೫೨೦೦೦೦೦೦೩~ಮೂರು ಲಕ್ಷದ ಎಂಟು ಸಾವಿರದ ಐವತ್ತೆರಡು ಕೋಟಿಯ ಮೂರು
+3080520000047~ಮೂರು ಲಕ್ಷದ ಎಂಟು ಸಾವಿರದ ಐವತ್ತೆರಡು ಕೋಟಿಯ ನಲವತ್ತೇಳು
+೩೦೮೦೫೨೦೦೦೦೬೦೮~ಮೂರು ಲಕ್ಷದ ಎಂಟು ಸಾವಿರದ ಐವತ್ತೆರಡು ಕೋಟಿಯ ಆರುನೂರ ಎಂಟು
+3080520005019~ಮೂರು ಲಕ್ಷದ ಎಂಟು ಸಾವಿರದ ಐವತ್ತೆರಡು ಕೋಟಿಯ ಐದು ಸಾವಿರದ ಹತ್ತೊಂಬತ್ತು
+೩೦೮೦೫೨೦೨೩೦೦೪೫~ಮೂರು ಲಕ್ಷದ ಎಂಟು ಸಾವಿರದ ಐವತ್ತೆರಡು ಕೋಟಿಯ ಎರಡು ಲಕ್ಷದ ಮೂವತ್ತು ಸಾವಿರದ ನಲವತ್ತೈದು
+3080528000059~ಮೂರು ಲಕ್ಷದ ಎಂಟು ಸಾವಿರದ ಐವತ್ತೆರಡು ಕೋಟಿಯ ಎಂಬತ್ತು ಲಕ್ಷದ ಐವತ್ತೊಂಬತ್ತು
+೩೪೦೦೦೦೦೦೦೦೦೦೦೦~ಮೂವತ್ತನಾಲ್ಕು ಲಕ್ಷ ಕೋಟಿ
+34000000000003~ಮೂವತ್ತನಾಲ್ಕು ಲಕ್ಷ ಕೋಟಿಯ ಮೂರು
+೩೪೦೦೦೦೦೦೦೦೦೦೪೭~ಮೂವತ್ತನಾಲ್ಕು ಲಕ್ಷ ಕೋಟಿಯ ನಲವತ್ತೇಳು
+34000000000608~ಮೂವತ್ತನಾಲ್ಕು ಲಕ್ಷ ಕೋಟಿಯ ಆರುನೂರ ಎಂಟು
+೩೪೦೦೦೦೦೦೦೦೫೦೧೯~ಮೂವತ್ತನಾಲ್ಕು ಲಕ್ಷ ಕೋಟಿಯ ಐದು ಸಾವಿರದ ಹತ್ತೊಂಬತ್ತು
+34000000070402~ಮೂವತ್ತನಾಲ್ಕು ಲಕ್ಷ ಕೋಟಿಯ ಎಪ್ಪತ್ತು ಸಾವಿರದ ನಾನೂರ ಎರಡು
+೩೪೦೦೦೦೦೦೨೩೦೦೪೫~ಮೂವತ್ತನಾಲ್ಕು ಲಕ್ಷ ಕೋಟಿಯ ಎರಡು ಲಕ್ಷದ ಮೂವತ್ತು ಸಾವಿರದ ನಲವತ್ತೈದು
+34000008000059~ಮೂವತ್ತನಾಲ್ಕು ಲಕ್ಷ ಕೋಟಿಯ ಎಂಬತ್ತು ಲಕ್ಷದ ಐವತ್ತೊಂಬತ್ತು
+೬೦೭೦೮೦೯೦೦೦೦೦೦೦~ಅರವತ್ತು ಲಕ್ಷದ ಎಪ್ಪತ್ತು ಸಾವಿರದ ಎಂಟುನೂರ ಒಂಬತ್ತು ಕೋಟಿ
+60708090000003~ಅರವತ್ತು ಲಕ್ಷದ ಎಪ್ಪತ್ತು ಸಾವಿರದ ಎಂಟುನೂರ ಒಂಬತ್ತು ಕೋಟಿಯ ಮೂರು
+೬೦೭೦೮೦೯೦೦೦೦೦೪೭~ಅರವತ್ತು ಲಕ್ಷದ ಎಪ್ಪತ್ತು ಸಾವಿರದ ಎಂಟುನೂರ ಒಂಬತ್ತು ಕೋಟಿಯ ನಲವತ್ತೇಳು
+60708090000608~ಅರವತ್ತು ಲಕ್ಷದ ಎಪ್ಪತ್ತು ಸಾವಿರದ ಎಂಟುನೂರ ಒಂಬತ್ತು ಕೋಟಿಯ ಆರುನೂರ ಎಂಟು
+೬೦೭೦೮೦೯೦೦೦೫೦೧೯~ಅರವತ್ತು ಲಕ್ಷದ ಎಪ್ಪತ್ತು ಸಾವಿರದ ಎಂಟುನೂರ ಒಂಬತ್ತು ಕೋಟಿಯ ಐದು ಸಾವಿರದ ಹತ್ತೊಂಬತ್ತು
+60708090070402~ಅರವತ್ತು ಲಕ್ಷದ ಎಪ್ಪತ್ತು ಸಾವಿರದ ಎಂಟುನೂರ ಒಂಬತ್ತು ಕೋಟಿಯ ಎಪ್ಪತ್ತು ಸಾವಿರದ ನಾನೂರ ಎರಡು
+೬೦೭೦೮೦೯೦೨೩೦೦೪೫~ಅರವತ್ತು ಲಕ್ಷದ ಎಪ್ಪತ್ತು ಸಾವಿರದ ಎಂಟುನೂರ ಒಂಬತ್ತು ಕೋಟಿಯ ಎರಡು ಲಕ್ಷದ ಮೂವತ್ತು ಸಾವಿರದ ನಲವತ್ತೈದು
+100~ನೂರು
+200~ಇನ್ನೂರು
+300~ಮುನ್ನೂರು
+400~ನಾನೂರು
+500~ಐನೂರು
+600~ಆರುನೂರು
+700~ಏಳುನೂರು
+800~ಎಂಟುನೂರು
+900~ಒಂಬೈನೂರು
+೧೦೧~ನೂರ ಒಂದು
+೨೦೨~ಇನ್ನೂರ ಎರಡು
+೩೦೩~ಮುನ್ನೂರ ಮೂರು
+೪೦೪~ನಾನೂರ ನಾಲ್ಕು
+೫೦೫~ಐನೂರ ಐದು
+೬೦೬~ಆರುನೂರ ಆರು
+೭೦೭~ಏಳುನೂರ ಏಳು
+೮೦೮~ಎಂಟುನೂರ ಎಂಟು
+೯೦೯~ಒಂಬೈನೂರ ಒಂಬತ್ತು
+11~ಹನ್ನೊಂದು
+೧೩~ಹದಿಮೂರು
+15~ಹದಿನೈದು
+೧೬~ಹದಿನಾರು
+20~ಇಪ್ಪತ್ತು
+೨೨~ಇಪ್ಪತ್ತೆರಡು
+24~ಇಪ್ಪತ್ತನಾಲ್ಕು
+೨೫~ಇಪ್ಪತ್ತೈದು
+26~ಇಪ್ಪತ್ತಾರು
+೨೭~ಇಪ್ಪತ್ತೇಳು
+28~ಇಪ್ಪತ್ತೆಂಟು
+೨೯~ಇಪ್ಪತ್ತೊಂಬತ್ತು
+30~ಮೂವತ್ತು
+೩೧~ಮೂವತ್ತೊಂದು
+32~ಮೂವತ್ತೆರಡು
+೩೫~ಮೂವತ್ತೈದು
+36~ಮೂವತ್ತಾರು
+೩೭~ಮೂವತ್ತೇಳು
+38~ಮೂವತ್ತೆಂಟು
+೩೯~ಮೂವತ್ತೊಂಬತ್ತು
+40~ನಲವತ್ತು
+೪೧~ನಲವತ್ತೊಂದು
+42~ನಲವತ್ತೆರಡು
+೪೩~ನಲವತ್ತಮೂರು
+44~ನಲವತ್ತನಾಲ್ಕು
+೪೭~ನಲವತ್ತೇಳು
+48~ನಲವತ್ತೆಂಟು
+೪೯~ನಲವತ್ತೊಂಬತ್ತು
+50~ಐವತ್ತು
+೫೧~ಐವತ್ತೊಂದು
+57~ಐವತ್ತೇಳು
+೫೮~ಐವತ್ತೆಂಟು
+59~ಐವತ್ತೊಂಬತ್ತು
+೬೦~ಅರವತ್ತು
+61~ಅರವತ್ತೊಂದು
+೬೨~ಅರವತ್ತೆರಡು
+63~ಅರವತ್ತಮೂರು
+೬೪~ಅರವತ್ತನಾಲ್ಕು
+65~ಅರವತ್ತೈದು
+೬೬~ಅರವತ್ತಾರು
+68~ಅರವತ್ತೆಂಟು
+೬೯~ಅರವತ್ತೊಂಬತ್ತು
+70~ಎಪ್ಪತ್ತು
+೭೧~ಎಪ್ಪತ್ತೊಂದು
+73~ಎಪ್ಪತ್ತಮೂರು
+೭೪~ಎಪ್ಪತ್ತನಾಲ್ಕು
+75~ಎಪ್ಪತ್ತೈದು
+೭೭~ಎಪ್ಪತ್ತೇಳು
+78~ಎಪ್ಪತ್ತೆಂಟು
+೭೯~ಎಪ್ಪತ್ತೊಂಬತ್ತು
+80~ಎಂಬತ್ತು
+೮೧~ಎಂಬತ್ತೊಂದು
+82~ಎಂಬತ್ತೆರಡು
+೮೩~ಎಂಬತ್ತಮೂರು
+84~ಎಂಬತ್ತನಾಲ್ಕು
+೮೫~ಎಂಬತ್ತೈದು
+86~ಎಂಬತ್ತಾರು
+೮೮~ಎಂಬತ್ತೆಂಟು
+90~ತೊಂಬತ್ತು
+೯೧~ತೊಂಬತ್ತೊಂದು
+92~ತೊಂಬತ್ತೆರಡು
+೯೩~ತೊಂಬತ್ತಮೂರು
+94~ತೊಂಬತ್ತನಾಲ್ಕು
+೯೫~ತೊಂಬತ್ತೈದು
+96~ತೊಂಬತ್ತಾರು
+೯೭~ತೊಂಬತ್ತೇಳು
+0~ಸೊನ್ನೆ
+೦~ಸೊನ್ನೆ
+-0~ಮೈನಸ್ ಸೊನ್ನೆ
++5~+ ಐದು
+--5~- ಮೈನಸ್ ಐದು
+- 5~- ಐದು
+5-~ಐದು -
+1,0000~ಒಂದು , ಸೊನ್ನೆ ಸೊನ್ನೆ ಸೊನ್ನೆ ಸೊನ್ನೆ
+1,23,4567~ಒಂದು , ಇಪ್ಪತ್ತಮೂರು , ನಾಲ್ಕು ಸಾವಿರದ ಐನೂರ ಅರವತ್ತೇಳು
+1000,000~ಒಂದು ಸಾವಿರ , ಸೊನ್ನೆ ಸೊನ್ನೆ ಸೊನ್ನೆ
+12,34~ಹನ್ನೆರಡು , ಮೂವತ್ತನಾಲ್ಕು
+1೨~1೨
+೧2~೧2
+1000000000000000~1000000000000000
+999999999999999~999999999999999
+10000000000~ಒಂದು ಸಾವಿರ ಕೋಟಿ
+10000000001~ಒಂದು ಸಾವಿರ ಕೋಟಿಯ ಒಂದು
+10010000000~ಒಂದು ಸಾವಿರದ ಒಂದು ಕೋಟಿ
+12340000000~ಒಂದು ಸಾವಿರದ ಇನ್ನೂರ ಮೂವತ್ತನಾಲ್ಕು ಕೋಟಿ
+100000000000~ಹತ್ತು ಸಾವಿರ ಕೋಟಿ
+100000000001~ಹತ್ತು ಸಾವಿರ ಕೋಟಿಯ ಒಂದು
+123450000000~ಹನ್ನೆರಡು ಸಾವಿರದ ಮುನ್ನೂರ ನಲವತ್ತೈದು ಕೋಟಿ
+1000000000000~ಒಂದು ಲಕ್ಷ ಕೋಟಿ
+1000000000001~ಒಂದು ಲಕ್ಷ ಕೋಟಿಯ ಒಂದು
+1001000000000~ಒಂದು ಲಕ್ಷದ ನೂರು ಕೋಟಿ
+10000000000000~ಹತ್ತು ಲಕ್ಷ ಕೋಟಿ
+10000000000001~ಹತ್ತು ಲಕ್ಷ ಕೋಟಿಯ ಒಂದು
+12340000000000~ಹನ್ನೆರಡು ಲಕ್ಷದ ಮೂವತ್ತನಾಲ್ಕು ಸಾವಿರ ಕೋಟಿ
\ No newline at end of file
diff --git a/tests/nemo_text_processing/kn/data_text_normalization/test_cases_punctuation.txt b/tests/nemo_text_processing/kn/data_text_normalization/test_cases_punctuation.txt
new file mode 100644
index 000000000..4cc2e977b
--- /dev/null
+++ b/tests/nemo_text_processing/kn/data_text_normalization/test_cases_punctuation.txt
@@ -0,0 +1,74 @@
+.~.
+,~,
+!~!
+?~?
+;~;
+:~:
+।~।
+॥~॥
+॰~॰
+—~—
+–~–
+…~…
+‥~‥
+·~·
+•~•
+₹~₹
+°~°
+′~′
+″~″
+|~|
+?,~?,
+...~...
+???~???
+ಹಲೋ.~ಹಲೋ.
+ಹಲೋ!~ಹಲೋ!
+ಹಲೋ?~ಹಲೋ?
+ಹಲೋ,~ಹಲೋ,
+ಹಲೋ;~ಹಲೋ;
+ಹಲೋ:~ಹಲೋ:
+ಹಲೋ।~ಹಲೋ।
+ಹಲೋ॥~ಹಲೋ॥
+ಹಲೋ…~ಹಲೋ…
+ಹಲೋ—~ಹಲೋ—
+ಹಲೋ!!~ಹಲೋ!!
+ಹಲೋ?!~ಹಲೋ?!
+ಹಲೋ .~ಹಲೋ .
+ಹಲೋ !~ಹಲೋ !
+ಫೇಸ್ಬುಕ್!~ಫೇಸ್ಬುಕ್!
+ಶ್ರೀ॰~ಶ್ರೀ॰
+‘ಹಲೋ’~‘ಹಲೋ’
+“ಕನ್ನಡ”~“ಕನ್ನಡ”
+'ಹಲೋ'~'ಹಲೋ'
+«ಕನ್ನಡ»~«ಕನ್ನಡ»
+‹ಹಲೋ›~‹ಹಲೋ›
+„ಹಲೋ“~„ಹಲೋ“
+(ಪರೀಕ್ಷೆ)~(ಪರೀಕ್ಷೆ)
+[ಪರೀಕ್ಷೆ]~[ಪರೀಕ್ಷೆ]
+{ಪರೀಕ್ಷೆ}~{ಪರೀಕ್ಷೆ}
+`ಕೋಟ್`~`ಕೋಟ್`
+ಹೌದು, ಸರಿ.~ಹೌದು, ಸರಿ.
+ನೀವು ಹೇಗಿದ್ದೀರಿ?~ನೀವು ಹೇಗಿದ್ದೀರಿ?
+ಡಾ. ರಾಮು~ಡಾ. ರಾಮು
+ಎ.ಪಿ.~ಎ.ಪಿ.
+ರೂ. ನೂರು~ರೂ. ನೂರು
+2!~ಎರಡು !
+೧!~ಒಂದು !
+೨೩?~ಇಪ್ಪತ್ತಮೂರು ?
+1!!!!~ಒಂದು !!!!
+1, ~ಒಂದು ,
+-೫~ಮೈನಸ್ ಐದು
+(1)ಹಲೋ~(1)ಹಲೋ
+ಪ್ರಾಣಿಗಳು: (1), (2), (3)~ಪ್ರಾಣಿಗಳು: ( ಒಂದು ), ( ಎರಡು ), ( ಮೂರು )
+114...48~ನೂರ ಹದಿನಾಲ್ಕು ... ನಲವತ್ತೆಂಟು
+1961–1965~ಒಂದು ಸಾವಿರದ ಒಂಬೈನೂರ ಅರವತ್ತೊಂದು – ಒಂದು ಸಾವಿರದ ಒಂಬೈನೂರ ಅರವತ್ತೈದು
+2006—2012~ಎರಡು ಸಾವಿರದ ಆರು — ಎರಡು ಸಾವಿರದ ಹನ್ನೆರಡು
+100%~ನೂರು %
+₹100~₹100
+ಹಲೋ~ಹಲೋ
+test -~test -
+-5 test -~ಮೈನಸ್ ಐದು test -
+?,ಇಲ್ಲ~?,ಇಲ್ಲ
+ಹಲೋ...ಪ್ರಪಂಚ~ಹಲೋ...ಪ್ರಪಂಚ
+ಹಲೋ & ಅಲ್ಲಿ~ಹಲೋ & ಅಲ್ಲಿ
+• ಐಟಂ~• ಐಟಂ
\ No newline at end of file
diff --git a/tests/nemo_text_processing/kn/data_text_normalization/test_cases_word.txt b/tests/nemo_text_processing/kn/data_text_normalization/test_cases_word.txt
new file mode 100644
index 000000000..9a6d22578
--- /dev/null
+++ b/tests/nemo_text_processing/kn/data_text_normalization/test_cases_word.txt
@@ -0,0 +1,20 @@
+ನಿದ್ರೆ~ನಿದ್ರೆ
+ಯಾಹೂ!~ಯಾಹೂ!
+।~।
+ಆಆಆ~ಆಆಆ
+ಆಕಾಶಗಂಗೆ~ಆಕಾಶಗಂಗೆ
+ಲಟರ್ಪಟರ್~ಲಟರ್ಪಟರ್
+ಕಚ್ಚಾ-ಪಕ್ಕಾ~ಕಚ್ಚಾ-ಪಕ್ಕಾ
+ಬಲೂನು~ಬಲೂನು
+ಚೀಟಿ~ಚೀಟಿ
+ಹುಡುಕುವುದು~ಹುಡುಕುವುದು
+ಕಬ್ಬಿಣದ!~ಕಬ್ಬಿಣದ!
+ವಾಹ್!~ವಾಹ್!
+ಟಾಟಾ~ಟಾಟಾ
+~
+ಝ~ಝ
+ಸಂಗೀತ~ಸಂಗೀತ
+ಮರ~ಮರ
+ನದಿ~ನದಿ
+ಬೆಟ್ಟ~ಬೆಟ್ಟ
+ಹೂವು~ಹೂವು
\ No newline at end of file
diff --git a/tests/nemo_text_processing/kn/test_cardinal.py b/tests/nemo_text_processing/kn/test_cardinal.py
new file mode 100644
index 000000000..da86b05d6
--- /dev/null
+++ b/tests/nemo_text_processing/kn/test_cardinal.py
@@ -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 TestCardinal:
+ 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_cardinal.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
diff --git a/tests/nemo_text_processing/kn/test_punctuation.py b/tests/nemo_text_processing/kn/test_punctuation.py
new file mode 100644
index 000000000..2040ec07b
--- /dev/null
+++ b/tests/nemo_text_processing/kn/test_punctuation.py
@@ -0,0 +1,37 @@
+# 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 TestPunctuation:
+ 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_punctuation.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
diff --git a/tests/nemo_text_processing/kn/test_sparrowhawk_normalization.sh b/tests/nemo_text_processing/kn/test_sparrowhawk_normalization.sh
new file mode 100644
index 000000000..d652a756e
--- /dev/null
+++ b/tests/nemo_text_processing/kn/test_sparrowhawk_normalization.sh
@@ -0,0 +1,41 @@
+#! /bin/sh
+
+PROJECT_DIR=/workspace/tests
+
+runtest () {
+ input=$1
+ cd /workspace/sparrowhawk/documentation/grammars
+
+ # read test file
+ while IFS= read -r testcase; do
+ IFS='~' read -r written spoken <<< "$testcase"
+
+ # Escape backslashes and replace non breaking space with breaking space
+ escaped_written=$(printf '%s' "$written" | sed 's/\\/\\\\/g')
+ denorm_pred=$(echo "$escaped_written" | normalizer_main --config=sparrowhawk_configuration.ascii_proto 2>&1 | tail -n 1 | sed 's/\xC2\xA0/ /g')
+
+ # trim white space and remove space before punctuation
+ spoken="$(echo -e "${spoken}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/ \([!?.]\)/\1/g')"
+ denorm_pred="$(echo -e "${denorm_pred}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/ \([!?.]\)/\1/g')"
+
+ # input expected actual
+ assertEquals "$written" "$spoken" "$denorm_pred"
+ done < "$input"
+}
+
+testTNCardinal() {
+ input=$PROJECT_DIR/kn/data_text_normalization/test_cases_cardinal.txt
+ runtest $input
+}
+testTNWord() {
+ input=$PROJECT_DIR/kn/data_text_normalization/test_cases_word.txt
+ runtest $input
+}
+
+testTNPunctuation() {
+ input=$PROJECT_DIR/kn/data_text_normalization/test_cases_punctuation.txt
+ runtest $input
+}
+
+# Load shUnit2
+. $PROJECT_DIR/../shunit2/shunit2
\ No newline at end of file
diff --git a/tests/nemo_text_processing/kn/test_word.py b/tests/nemo_text_processing/kn/test_word.py
new file mode 100644
index 000000000..b9431fc27
--- /dev/null
+++ b/tests/nemo_text_processing/kn/test_word.py
@@ -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 TestWord:
+ 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_word.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
diff --git a/tools/text_processing_deployment/pynini_export.py b/tools/text_processing_deployment/pynini_export.py
index 73a4fc138..20f055837 100644
--- a/tools/text_processing_deployment/pynini_export.py
+++ b/tools/text_processing_deployment/pynini_export.py
@@ -109,6 +109,7 @@ def parse_args():
'ja',
'rw',
'ko',
+ 'kn',
],
type=str,
default='en',
@@ -352,6 +353,11 @@ def parse_args():
ClassifyFst as TNClassifyFst,
)
from nemo_text_processing.text_normalization.ko.verbalizers.verbalize import VerbalizeFst as TNVerbalizeFst
+ elif args.language == 'kn':
+ from nemo_text_processing.text_normalization.kn.taggers.tokenize_and_classify import (
+ ClassifyFst as TNClassifyFst,
+ )
+ from nemo_text_processing.text_normalization.kn.verbalizers.verbalize import VerbalizeFst as TNVerbalizeFst
else:
raise KeyError(f"Language {args.language} is not defined for export.")
output_dir = os.path.join(args.output_dir, f"{args.language}_{args.grammars}_{args.input_case}")