diff --git a/Jenkinsfile b/Jenkinsfile index 38e05bb00..b85f9c8ce 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' + TE_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 { @@ -112,6 +113,24 @@ pipeline { } } } + stage('L0: Create TE TN Grammars') { + when { + anyOf { + branch 'main' + branch 'staging/**' + branch 'staging_*' + changeRequest target: 'main' + } + } + failFast true + parallel { + stage('L0: TE TN grammars') { + steps { + sh 'CUDA_VISIBLE_DEVICES="" python nemo_text_processing/text_normalization/normalize.py --lang=te --text="౧" --cache_dir ${TE_TN_CACHE}' + } + } + } + } stage('L0: Create DE/ES TN/ITN Grammars') { when { @@ -408,6 +427,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 TE TN/ITN tests (restore grammars from cache)') { + steps { + sh 'CUDA_VISIBLE_DEVICES="" pytest tests/nemo_text_processing/te/ -m "not pleasefixme" --cpu --tn_cache_dir ${TE_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/normalize.py b/nemo_text_processing/text_normalization/normalize.py index d8ebf2f4d..f365dfeb2 100644 --- a/nemo_text_processing/text_normalization/normalize.py +++ b/nemo_text_processing/text_normalization/normalize.py @@ -191,6 +191,9 @@ 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 == 'te': + from nemo_text_processing.text_normalization.te.taggers.tokenize_and_classify import ClassifyFst + from nemo_text_processing.text_normalization.te.verbalizers.verbalize_final import VerbalizeFinalFst else: raise NotImplementedError(f"Language {lang} has not been supported yet.") @@ -737,7 +740,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", "te", "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..1df194042 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', 'te', 'ko', 'vi', 'pt'], default="en", type=str, ) diff --git a/nemo_text_processing/text_normalization/te/__init__.py b/nemo_text_processing/text_normalization/te/__init__.py new file mode 100644 index 000000000..4fc25d0d3 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/__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/te/data/__init__.py b/nemo_text_processing/text_normalization/te/data/__init__.py new file mode 100644 index 000000000..4fc25d0d3 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/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/te/data/numbers/__init__.py b/nemo_text_processing/text_normalization/te/data/numbers/__init__.py new file mode 100644 index 000000000..4fc25d0d3 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/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/te/data/numbers/digit.tsv b/nemo_text_processing/text_normalization/te/data/numbers/digit.tsv new file mode 100644 index 000000000..ab878ec99 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/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/te/data/numbers/exact_power.tsv b/nemo_text_processing/text_normalization/te/data/numbers/exact_power.tsv new file mode 100644 index 000000000..c43f22889 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/data/numbers/exact_power.tsv @@ -0,0 +1,8 @@ +100 వంద +౧౦౦ వంద +1000 వెయ్యి +౧౦౦౦ వెయ్యి +100000 లక్ష +౧౦౦౦౦౦ లక్ష +10000000 కోటి +౧౦౦౦౦౦౦౦ కోటి diff --git a/nemo_text_processing/text_normalization/te/data/numbers/hundred_prefix.tsv b/nemo_text_processing/text_normalization/te/data/numbers/hundred_prefix.tsv new file mode 100644 index 000000000..b08a6aa40 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/data/numbers/hundred_prefix.tsv @@ -0,0 +1,4 @@ +10 నూట +౧౦ నూట +1 నూట +౧ నూట diff --git a/nemo_text_processing/text_normalization/te/data/numbers/hundreds_before_one.tsv b/nemo_text_processing/text_normalization/te/data/numbers/hundreds_before_one.tsv new file mode 100644 index 000000000..f4198c9ce --- /dev/null +++ b/nemo_text_processing/text_normalization/te/data/numbers/hundreds_before_one.tsv @@ -0,0 +1,2 @@ +1 వందల ఒక +౧ వందల ఒక diff --git a/nemo_text_processing/text_normalization/te/data/numbers/magnitudes.tsv b/nemo_text_processing/text_normalization/te/data/numbers/magnitudes.tsv new file mode 100644 index 000000000..ecf379124 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/data/numbers/magnitudes.tsv @@ -0,0 +1,11 @@ +hundreds_plural వందలు +hundreds_before వందల +thousand వెయ్యి +thousands_plural వేలు +thousands_before వేల +lakh లక్ష +lakhs_plural లక్షలు +lakhs_before లక్షల +crore కోటి +crores_plural కోట్లు +crores_before కోట్ల diff --git a/nemo_text_processing/text_normalization/te/data/numbers/teens_and_ties.tsv b/nemo_text_processing/text_normalization/te/data/numbers/teens_and_ties.tsv new file mode 100644 index 000000000..69eadf9f7 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/data/numbers/teens_and_ties.tsv @@ -0,0 +1,20 @@ +౧౦ పది +౧౧ పదకొండు +౧౨ పన్నెండు +౧౩ పదమూడు +౧౪ పద్నాలుగు +౧౫ పదిహేను +౧౬ పదహారు +౧౭ పదిహేడు +౧౮ పధ్ధెనిమిది +౧౯ పంతొమ్మిది +10 పది +11 పదకొండు +12 పన్నెండు +13 పదమూడు +14 పద్నాలుగు +15 పదిహేను +16 పదహారు +17 పదిహేడు +18 పధ్ధెనిమిది +19 పంతొమ్మిది diff --git a/nemo_text_processing/text_normalization/te/data/numbers/ties.tsv b/nemo_text_processing/text_normalization/te/data/numbers/ties.tsv new file mode 100644 index 000000000..a8d7d7227 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/data/numbers/ties.tsv @@ -0,0 +1,16 @@ +2 ఇరవై +3 ముప్పై +4 నలభై +5 యాభై +6 అరవై +7 డెబ్బై +8 ఎనభై +9 తొంభై +౨ ఇరవై +౩ ముప్పై +౪ నలభై +౫ యాభై +౬ అరవై +౭ డెబ్బై +౮ ఎనభై +౯ తొంభై diff --git a/nemo_text_processing/text_normalization/te/data/numbers/ties_one_suffix.tsv b/nemo_text_processing/text_normalization/te/data/numbers/ties_one_suffix.tsv new file mode 100644 index 000000000..d312af237 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/data/numbers/ties_one_suffix.tsv @@ -0,0 +1,2 @@ +1 ఒక +౧ ఒక diff --git a/nemo_text_processing/text_normalization/te/data/numbers/zero.tsv b/nemo_text_processing/text_normalization/te/data/numbers/zero.tsv new file mode 100644 index 000000000..309aab75e --- /dev/null +++ b/nemo_text_processing/text_normalization/te/data/numbers/zero.tsv @@ -0,0 +1,2 @@ +0 సున్నా +౦ సున్నా \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/te/graph_utils.py b/nemo_text_processing/text_normalization/te/graph_utils.py new file mode 100644 index 000000000..6fdea0492 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/graph_utils.py @@ -0,0 +1,130 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# Copyright 2015 and onwards Google, Inc. +# +# 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.te.utils import get_abs_path + +NEMO_CHAR = utf8.VALID_UTF8_CHAR +NEMO_DIGIT = byte.DIGIT + +NEMO_ALL_ZERO = pynini.project(pynini.string_file(get_abs_path("data/numbers/zero.tsv")), "input").optimize() +NEMO_ALL_DIGIT = pynini.union( + pynini.project(pynini.string_file(get_abs_path("data/numbers/digit.tsv")), "input"), + NEMO_ALL_ZERO, +).optimize() + +NEMO_NON_BREAKING_SPACE = u"\u00a0" +NEMO_SPACE = " " +NEMO_WHITE_SPACE = pynini.union(" ", "\t", "\n", "\r", NEMO_NON_BREAKING_SPACE).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(NEMO_NON_BREAKING_SPACE, " "), "", "", NEMO_SIGMA) diff --git a/nemo_text_processing/text_normalization/te/taggers/__init__.py b/nemo_text_processing/text_normalization/te/taggers/__init__.py new file mode 100644 index 000000000..4fc25d0d3 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/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/te/taggers/cardinal.py b/nemo_text_processing/text_normalization/te/taggers/cardinal.py new file mode 100644 index 000000000..fdaab111e --- /dev/null +++ b/nemo_text_processing/text_normalization/te/taggers/cardinal.py @@ -0,0 +1,388 @@ +# 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.examples import plurals +from pynini.lib import pynutil + +from nemo_text_processing.text_normalization.te.graph_utils import ( + NEMO_ALL_DIGIT, + NEMO_ALL_ZERO, + NEMO_DIGIT, + NEMO_SIGMA, + GraphFst, + insert_space, +) +from nemo_text_processing.text_normalization.te.utils import get_abs_path, load_labels + + +class CardinalFst(GraphFst): + """ + Finite state transducer for classifying cardinals, e.g. + -౨౩ -> cardinal { negative: "true" integer: "ఇరవై మూడు" } + + Covers numbers up to 19 digits by composing crore (కోటి) groups + (through hundred crore crores (వంద కోట్ల కోట్లు) / 10^17). + + 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 = pynini.string_file(get_abs_path("data/numbers/teens_and_ties.tsv")) + ties = pynini.string_file(get_abs_path("data/numbers/ties.tsv")) + exact_power = pynini.string_file(get_abs_path("data/numbers/exact_power.tsv")) + hundred_prefix = pynini.string_file(get_abs_path("data/numbers/hundred_prefix.tsv")) + ties_one_suffix = pynini.string_file(get_abs_path("data/numbers/ties_one_suffix.tsv")) + hundreds_before_one = pynini.string_file(get_abs_path("data/numbers/hundreds_before_one.tsv")) + mag = dict(load_labels(get_abs_path("data/numbers/magnitudes.tsv"))) + + te_digit = pynini.difference(NEMO_ALL_DIGIT, NEMO_DIGIT).optimize() + one_digit = pynini.union("1", "౧") + del_one = pynutil.delete(one_digit) + one_as_oka = (one_digit @ ties_one_suffix).optimize() + digit_nx01 = (pynini.difference(NEMO_ALL_DIGIT, NEMO_ALL_ZERO | one_digit) @ digit).optimize() + + def union(*parts): + return pynini.union(*parts).optimize() + + def exact_n(n, graph=exact_power): + return pynini.compose(NEMO_ALL_DIGIT**n, graph).optimize() + + def mag_ins(key, space=True): + return pynutil.insert((" " if space else "") + mag[key]) + + ins_thou = mag_ins("thousand", False) + ins_thous, ins_thous_pl = mag_ins("thousands_before"), mag_ins("thousands_plural") + ins_lakh = mag_ins("lakh", False) + ins_lakhs, ins_lakhs_pl = mag_ins("lakhs_before"), mag_ins("lakhs_plural") + ins_koti, ins_koti_sp = mag_ins("crore", False), mag_ins("crore") + ins_kotlu, ins_kotlu_pl = mag_ins("crores_before"), mag_ins("crores_plural") + ins_hund, ins_hund_pl = mag_ins("hundreds_before"), mag_ins("hundreds_plural") + + hprefix_2d = union( + pynini.compose(NEMO_DIGIT + NEMO_DIGIT, hundred_prefix), + pynini.compose(te_digit + te_digit, hundred_prefix), + ) + hprefix_1d = union(pynini.compose(NEMO_DIGIT, hundred_prefix), pynini.compose(te_digit, hundred_prefix)) + + def make_teens(d_cls, zero_ch, dig_map): + t = pynini.compose(d_cls + d_cls, teens) + ti = (d_cls @ ties).optimize() + return t | (ti + pynutil.delete(zero_ch)) | (ti + insert_space + dig_map) + + dig_ascii, dig_te = (NEMO_DIGIT @ digit).optimize(), (te_digit @ digit).optimize() + dig_nx01_en = (pynini.difference(NEMO_DIGIT, pynini.union("0", "1")) @ digit).optimize() + dig_nx01_te = (pynini.difference(te_digit, pynini.union("౦", "౧")) @ digit).optimize() + teens_ties = union(make_teens(NEMO_DIGIT, "0", dig_ascii), make_teens(te_digit, "౦", dig_te)) + teens_x1 = union(make_teens(NEMO_DIGIT, "0", dig_nx01_en), make_teens(te_digit, "౦", dig_nx01_te)) + teens_oka = union( + (NEMO_DIGIT @ ties) + insert_space + (NEMO_DIGIT @ ties_one_suffix), + (te_digit @ ties) + insert_space + (te_digit @ ties_one_suffix), + ) + teens_before = union(teens_oka, teens_x1) + self.single_digits_graph = (digit | zero) + pynini.closure(insert_space + (digit | zero)) + delete_zero = pynutil.delete(NEMO_ALL_ZERO) + zdel = {0: pynini.accep("")} + + for n in range(1, 8): + zdel[n] = (zdel[n - 1] + delete_zero).optimize() + + def with_unit(prefix, suf, zeros): + return prefix + suf if zeros == 0 else prefix + zdel[zeros] + suf + + def with_rem(prefix, suf, zeros, sub): + return prefix + (suf if zeros == 0 else suf + zdel[zeros]) + insert_space + sub + + def mag_forms(prefix, suf, ladder, head=None, head_z=None): + g = with_unit(prefix, head, head_z) if head is not None else None + for zeros, sub in ladder: + g = with_rem(prefix, suf, zeros, sub) if g is None else g | with_rem(prefix, suf, zeros, sub) + return g + + def prio(a, b): + return plurals._priority_union(a, b, NEMO_SIGMA) + + def ties_scale(oka_suf, other_suf, ladder, head_oka, head_other, head_z): + return union( + mag_forms(teens_oka, oka_suf, ladder, head_oka, head_z), + mag_forms(teens_x1, other_suf, ladder, head_other, head_z), + ) + + def scale(exact, sg, before, head, spaced, zeros, ladder, one_ladder=None, extra=None, ten_oka=None): + one_ladder = ladder if one_ladder is None else one_ladder + g = exact | mag_forms(del_one, sg, one_ladder) + if extra is not None: + g = g | extra + g = union(g, mag_forms(digit_nx01, before, ladder, head, zeros)) + ten = ties_scale(ten_oka or before, before, ladder, spaced, head, zeros) + return g, ten + + def oka_mult(sg, before, remainders): + def side(use_digit): + parts = [] + for zeros, rem in remainders: + body = zdel[zeros] + insert_space + rem + if use_digit: + parts += [del_one + sg + body, digit_nx01 + before + body] + else: + parts += [teens_oka + before + body, teens_x1 + before + body] + return union(*parts) + + return prio(side(True), side(False)).optimize() + + def crore_unit(oka, other, other_head=None, nested=False): + if nested: + oka_suf, oka_head = ins_koti_sp, ins_koti_sp + else: + oka_suf, oka_head = ins_kotlu, ins_kotlu_pl + return prio( + mag_forms(oka, oka_suf, crore_ladder, oka_head, 7), + mag_forms(other, ins_kotlu, crore_ladder, other_head or ins_kotlu_pl, 7), + ).optimize() + + def outer_crores(count): + return mag_forms(count, ins_kotlu, crore_ladder, ins_kotlu_pl, 7).optimize() + + graph_hundreds = union( + exact_n(3), + hprefix_2d + digit, + hprefix_1d + teens_ties, + with_unit(digit_nx01, ins_hund_pl, 2), + with_rem(digit_nx01, ins_hund, 1, digit), + with_rem(digit_nx01, ins_hund, 0, teens_ties), + ) + thousand_ladder = [(2, digit), (1, teens_ties), (0, graph_hundreds)] + graph_thousands, graph_ten_thousands = scale( + exact_n(4), ins_thou, ins_thous, ins_thous_pl, ins_thous_pl, 3, thousand_ladder + ) + lakh_ladder = [ + (4, digit), + (3, teens_ties), + (2, graph_hundreds), + (1, graph_thousands), + (0, graph_ten_thousands), + ] + graph_lakhs, graph_ten_lakhs = scale( + exact_n(6), + ins_lakh, + ins_lakhs, + ins_lakhs_pl, + ins_lakhs_pl, + 5, + lakh_ladder, + one_ladder=lakh_ladder[1:], + extra=with_rem(del_one, ins_lakh, 4, digit), + ) + crore_ladder = [ + (6, digit), + (5, teens_ties), + (4, graph_hundreds), + (3, graph_thousands), + (2, graph_ten_thousands), + (1, graph_lakhs), + (0, graph_ten_lakhs), + ] + graph_crores, graph_ten_crores = scale( + exact_n(8), ins_koti, ins_kotlu, ins_kotlu_pl, ins_kotlu_pl, 7, crore_ladder + ) + hundred_one = union( + hprefix_1d + pynutil.delete(NEMO_ALL_ZERO) + one_as_oka, + digit_nx01 + pynutil.delete(NEMO_ALL_ZERO) + hundreds_before_one, + ) + hundred_crore = union( + exact_n(3), + hundred_one, + hprefix_2d + one_as_oka, + hprefix_2d + digit_nx01, + hprefix_1d + teens_before, + with_unit(digit_nx01, ins_hund, 2), + with_rem(digit_nx01, ins_hund, 1, digit_nx01), + with_rem(digit_nx01, ins_hund, 0, teens_before), + ) + thou_crore_ladder = [(2, digit_nx01), (1, teens_before), (0, hundred_crore)] + thousand_one = oka_mult(ins_thou, ins_thous, [(2, one_as_oka), (0, hundred_one)]) + thousand_crore, ten_thousand_crore = scale( + exact_n(4), ins_thou, ins_thous, ins_thous, ins_thous, 3, thou_crore_ladder + ) + graph_hundred_crores = union( + crore_unit(hundred_one, hundred_crore), with_rem(hundred_crore, ins_kotlu, 0, graph_crores) + ) + graph_thousand_crores = crore_unit(thousand_one, union(thousand_crore, ten_thousand_crore)) + graph_ten_thousand_crores = crore_unit(thousand_one, ten_thousand_crore) + lakh_crore_ladder = [ + (4, digit_nx01), + (3, teens_before), + (2, hundred_crore), + (1, thousand_crore), + (0, ten_thousand_crore), + ] + lakh_one = oka_mult( + ins_lakh, + ins_lakhs, + [(4, one_as_oka), (2, hundred_one), (1, exact_n(4, thousand_one)), (0, exact_n(5, thousand_one))], + ) + lakh_crore, ten_lakh_crore = scale(exact_n(6), ins_lakh, ins_lakhs, ins_lakhs, ins_lakhs, 5, lakh_crore_ladder) + graph_lakh_crores = crore_unit(lakh_one, union(lakh_crore, ten_lakh_crore)) + ten_lakh_rem = ties_scale( + ins_lakhs, + ins_lakhs, + [ + (4, digit), + (3, teens_before), + (2, hundred_crore), + (1, thousand_crore), + (0, ten_thousand_crore), + ], + ins_lakhs, + ins_lakhs, + 5, + ) + koti_ladder = [ + (6, digit), + (5, teens_before), + (4, hundred_crore), + (3, thousand_crore), + (2, ten_thousand_crore), + (1, lakh_crore), + (0, ten_lakh_rem), + ] + crore_one = oka_mult( + ins_koti, + ins_kotlu, + [ + (6, one_as_oka), + (4, hundred_one), + (3, exact_n(4, thousand_one)), + (2, exact_n(5, thousand_one)), + (1, exact_n(6, lakh_one)), + (0, exact_n(7, lakh_one)), + ], + ) + + def nest_one(count): + return union( + with_rem(count, ins_kotlu, 6, one_as_oka), + with_rem(count, ins_kotlu, 4, exact_n(3, hundred_one)), + with_rem(count, ins_kotlu, 3, exact_n(4, thousand_one)), + with_rem(count, ins_kotlu, 2, exact_n(5, thousand_one)), + with_rem(count, ins_kotlu, 1, exact_n(6, lakh_one)), + with_rem(count, ins_kotlu, 0, exact_n(7, lakh_one)), + ).optimize() + + crore_one_10 = nest_one(hundred_crore) + crore_one_11 = nest_one(union(thousand_one, thousand_crore)) + crore_one_12 = nest_one(union(exact_n(5, thousand_one), ten_thousand_crore)) + + crore_before, ten_crore_before = scale(exact_n(8), ins_koti, ins_kotlu, ins_kotlu, ins_kotlu, 7, koti_ladder) + ten_lakh_crore_count = prio( + union( + with_rem(del_one, ins_koti, 0, ten_lakh_rem), + with_rem(digit_nx01, ins_kotlu, 0, ten_lakh_rem), + with_rem(teens_oka, ins_kotlu, 0, ten_lakh_rem), + with_rem(teens_x1, ins_kotlu, 0, ten_lakh_rem), + ), + union(crore_before, ten_crore_before), + ).optimize() + graph_ten_lakh_crores = crore_unit(exact_n(8, crore_one), ten_lakh_crore_count, nested=True) + other_nine = mag_forms(teens_x1, ins_kotlu, koti_ladder, ins_kotlu, 7) + + graph_crore_crores = prio( + union( + mag_forms(exact_n(9, crore_one), ins_koti_sp, crore_ladder, ins_koti_sp, 7), + outer_crores(with_unit(teens_oka, ins_kotlu, 7)), + ), + outer_crores( + union( + mag_forms(teens_oka, ins_kotlu, koti_ladder, ins_kotlu_pl, 7), + other_nine, + ) + ), + ).optimize() + graph_ten_crore_crores = prio( + union( + mag_forms(crore_one_10, ins_koti_sp, crore_ladder, ins_koti_sp, 7), + outer_crores(with_unit(hundred_one, ins_kotlu, 7)), + ), + outer_crores( + union( + mag_forms(hundred_one, ins_kotlu, koti_ladder, ins_kotlu_pl, 7), + mag_forms(hundred_crore, ins_kotlu, koti_ladder, ins_kotlu, 7), + ) + ), + ).optimize() + hcc_oka = mag_forms(teens_oka, ins_thous, thou_crore_ladder, ins_thous, 3) + hcc_x1 = union(thousand_crore, mag_forms(teens_x1, ins_thous, thou_crore_ladder, ins_thous, 3)) + graph_hundred_crore_crores = prio( + union( + mag_forms(crore_one_12, ins_koti_sp, crore_ladder, ins_koti_sp, 7), + mag_forms(crore_one_11, ins_koti_sp, crore_ladder, ins_koti_sp, 7), + outer_crores(with_unit(thousand_one, ins_kotlu, 7)), + outer_crores(with_unit(hcc_oka, ins_kotlu, 7)), + ), + outer_crores( + union( + mag_forms(thousand_one, ins_kotlu, koti_ladder, ins_kotlu_pl, 7), + mag_forms(hcc_oka, ins_kotlu, koti_ladder, ins_kotlu_pl, 7), + mag_forms(hcc_x1, ins_kotlu, koti_ladder, ins_kotlu, 7), + ) + ), + ).optimize() + + graph = union( + digit, + zero, + teens_ties, + graph_hundreds, + graph_thousands, + graph_ten_thousands, + graph_lakhs, + graph_ten_lakhs, + graph_crores, + graph_ten_crores, + exact_n(10, graph_hundred_crores), + exact_n(11, graph_thousand_crores), + exact_n(12, graph_ten_thousand_crores), + exact_n(13, graph_lakh_crores), + exact_n(14, graph_lakh_crores), + exact_n(15, graph_ten_lakh_crores), + exact_n(16, graph_crore_crores), + exact_n(17, graph_ten_crore_crores), + exact_n(18, graph_hundred_crore_crores), + exact_n(19, graph_hundred_crore_crores), + ) + graph = pynini.compose(pynini.closure(NEMO_DIGIT, 1) | pynini.closure(te_digit, 1), graph) + + leading_zeros = pynini.compose( + (pynini.closure("0", 1) + pynini.closure(NEMO_DIGIT)) + | (pynini.closure("౦", 1) + pynini.closure(te_digit)), + self.single_digits_graph, + ) + sep, two, three = pynutil.delete(","), NEMO_ALL_DIGIT**2, NEMO_ALL_DIGIT**3 + grouped = pynini.compose( + union( + pynini.closure(NEMO_ALL_DIGIT, 1, 2) + pynini.closure(sep + two) + sep + three, + pynini.closure(NEMO_ALL_DIGIT, 1, 3) + pynini.closure(sep + three, 1), + ), + graph, + ).optimize() + final = union(graph, leading_zeros, grouped) + minus = pynini.closure(pynutil.insert("negative: ") + pynini.cross("-", '"true" '), 0, 1) + self.final_graph = final.optimize() + self.fst = self.add_tokens(minus + pynutil.insert('integer: "') + self.final_graph + pynutil.insert('"')) diff --git a/nemo_text_processing/text_normalization/te/taggers/punctuation.py b/nemo_text_processing/text_normalization/te/taggers/punctuation.py new file mode 100644 index 000000000..c5811c55a --- /dev/null +++ b/nemo_text_processing/text_normalization/te/taggers/punctuation.py @@ -0,0 +1,61 @@ +# 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.te.graph_utils import NEMO_NOT_SPACE, NEMO_SIGMA, GraphFst + + +class PunctuationFst(GraphFst): + """ + Finite state transducer for classifying punctuation + e.g. a, -> tokens { name: "a" } 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 + ] + + punct_marks = punct_unicode + list(s) + + punct = pynini.union(*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.fst = (pynutil.insert("name: \"") + punct + pynutil.insert("\"")).optimize() diff --git a/nemo_text_processing/text_normalization/te/taggers/tokenize_and_classify.py b/nemo_text_processing/text_normalization/te/taggers/tokenize_and_classify.py new file mode 100644 index 000000000..980275081 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/taggers/tokenize_and_classify.py @@ -0,0 +1,115 @@ +# 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.te.graph_utils import ( + NEMO_SPACE, + NEMO_WHITE_SPACE, + GraphFst, + delete_extra_space, + delete_space, + generator_main, +) +from nemo_text_processing.text_normalization.te.taggers.cardinal import CardinalFst +from nemo_text_processing.text_normalization.te.taggers.punctuation import PunctuationFst +from nemo_text_processing.text_normalization.te.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"te_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(deterministic=deterministic).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/te/taggers/word.py b/nemo_text_processing/text_normalization/te/taggers/word.py new file mode 100644 index 000000000..5ef0b0778 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/taggers/word.py @@ -0,0 +1,34 @@ +# 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.te.graph_utils import NEMO_NOT_SPACE, GraphFst + + +class WordFst(GraphFst): + """ + Finite state transducer for classifying Telugu 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/te/utils.py b/nemo_text_processing/text_normalization/te/utils.py new file mode 100644 index 000000000..5e7e96c27 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/utils.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 csv +import os + + +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 + + +def load_labels(abs_path): + """ + loads relative path file as dictionary + + Args: + abs_path: absolute path + + Returns dictionary of mappings + """ + with open(abs_path, encoding="utf-8") as label_tsv: + return list(csv.reader(label_tsv, delimiter="\t")) diff --git a/nemo_text_processing/text_normalization/te/verbalizers/__init__.py b/nemo_text_processing/text_normalization/te/verbalizers/__init__.py new file mode 100644 index 000000000..4fc25d0d3 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/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/te/verbalizers/cardinal.py b/nemo_text_processing/text_normalization/te/verbalizers/cardinal.py new file mode 100644 index 000000000..b70c957f3 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/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.te.graph_utils import NEMO_NOT_QUOTE, GraphFst + + +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/te/verbalizers/verbalize.py b/nemo_text_processing/text_normalization/te/verbalizers/verbalize.py new file mode 100644 index 000000000..f2564e504 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/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.te.graph_utils import GraphFst +from nemo_text_processing.text_normalization.te.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/te/verbalizers/verbalize_final.py b/nemo_text_processing/text_normalization/te/verbalizers/verbalize_final.py new file mode 100644 index 000000000..beb01924b --- /dev/null +++ b/nemo_text_processing/text_normalization/te/verbalizers/verbalize_final.py @@ -0,0 +1,75 @@ +# 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.te.graph_utils import ( + GraphFst, + delete_extra_space, + delete_space, + generator_main, +) +from nemo_text_processing.text_normalization.te.verbalizers.verbalize import VerbalizeFst +from nemo_text_processing.text_normalization.te.verbalizers.word import WordFst + + +class VerbalizeFinalFst(GraphFst): + """ + Finite state transducer that verbalizes an entire sentence, e.g. + + 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"te_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/te/verbalizers/word.py b/nemo_text_processing/text_normalization/te/verbalizers/word.py new file mode 100644 index 000000000..ece2e9b76 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/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.te.graph_utils import NEMO_CHAR, GraphFst, delete_space + + +class WordFst(GraphFst): + """ + Finite state transducer for verbalizing Telugu 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/tests/nemo_text_processing/te/__init__.py b/tests/nemo_text_processing/te/__init__.py new file mode 100644 index 000000000..4fc25d0d3 --- /dev/null +++ b/tests/nemo_text_processing/te/__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/te/data_text_normalization/test_cases_cardinal.txt b/tests/nemo_text_processing/te/data_text_normalization/test_cases_cardinal.txt new file mode 100644 index 000000000..ca47a9006 --- /dev/null +++ b/tests/nemo_text_processing/te/data_text_normalization/test_cases_cardinal.txt @@ -0,0 +1,267 @@ +4 నాలుగులు~నాలుగు నాలుగులు +౬ మంది ఆటగాళ్లు బయటకు~ఆరు మంది ఆటగాళ్లు బయటకు +4 ఓవర్లలో 17 పరుగులు~నాలుగు ఓవర్లలో పదిహేడు పరుగులు +౫ చాక్లెట్లు ౯ టాఫీలు~ఐదు చాక్లెట్లు తొమ్మిది టాఫీలు +5551 అరటిపండ్లు~ఐదు వేల ఐదు వందల యాభై ఒకటి అరటిపండ్లు +౫౫౫౧ అరటిపండ్లు~ఐదు వేల ఐదు వందల యాభై ఒకటి అరటిపండ్లు +4 మంది మృతి 18 మందికి గాయాలు~నాలుగు మంది మృతి పధ్ధెనిమిది మందికి గాయాలు +అధ్యాయం ౧౦ శ్లోకం ౨~అధ్యాయం పది శ్లోకం రెండు +ఆదాయం -5000 రూపాయలు~ఆదాయం మైనస్ ఐదు వేలు రూపాయలు +₹ -100~₹ మైనస్ వంద +10~పది +౧౦~పది +100~వంద +౧౦౦~వంద +101~నూట ఒకటి +111~నూట పదకొండు +130~నూట ముప్పై +200~రెండు వందలు +203~రెండు వందల మూడు +312~మూడు వందల పన్నెండు +425~నాలుగు వందల ఇరవై ఐదు +525~ఐదు వందల ఇరవై ఐదు +౫౨౫~ఐదు వందల ఇరవై ఐదు +85~ఎనభై ఐదు +౯౧~తొంభై ఒకటి +1001~వెయ్యి ఒకటి +10099~పది వేల తొంభై తొమ్మిది +100001~లక్ష ఒకటి +12346~పన్నెండు వేల మూడు వందల నలభై ఆరు +1345~వెయ్యి మూడు వందల నలభై ఐదు +123456~లక్ష ఇరవై మూడు వేల నాలుగు వందల యాభై ఆరు +౧౩౪౫౬~పదమూడు వేల నాలుగు వందల యాభై ఆరు +872987~ఎనిమిది లక్షల డెబ్బై రెండు వేల తొమ్మిది వందల ఎనభై ఏడు +9876789~తొంభై ఎనిమిది లక్షల డెబ్బై ఆరు వేల ఏడు వందల ఎనభై తొమ్మిది +2345567~ఇరవై మూడు లక్షల నలభై ఐదు వేల ఐదు వందల అరవై ఏడు +12345567~కోటి ఇరవై మూడు లక్షల నలభై ఐదు వేల ఐదు వందల అరవై ఏడు +12121212~కోటి ఇరవై ఒక లక్షల ఇరవై ఒక వేల రెండు వందల పన్నెండు +1122345567~నూట పన్నెండు కోట్ల ఇరవై మూడు లక్షల నలభై ఐదు వేల ఐదు వందల అరవై ఏడు +1022345567~నూట రెండు కోట్ల ఇరవై మూడు లక్షల నలభై ఐదు వేల ఐదు వందల అరవై ఏడు +11022345567~వెయ్యి నూట రెండు కోట్ల ఇరవై మూడు లక్షల నలభై ఐదు వేల ఐదు వందల అరవై ఏడు +51022345567~ఐదు వేల నూట రెండు కోట్ల ఇరవై మూడు లక్షల నలభై ఐదు వేల ఐదు వందల అరవై ఏడు +10000000000~వెయ్యి కోట్లు +100000000000~పది వేల కోట్లు +1000000000000~లక్ష కోట్లు +10000000000000~పది లక్షల కోట్లు +100000000000000~కోటి కోట్లు +999999999999999~తొమ్మిది కోట్ల తొంభై తొమ్మిది లక్షల తొంభై తొమ్మిది వేల తొమ్మిది వందల తొంభై తొమ్మిది కోట్ల తొంభై తొమ్మిది లక్షల తొంభై తొమ్మిది వేల తొమ్మిది వందల తొంభై తొమ్మిది +1000000000000000~పది కోట్ల కోట్లు +999999999999999999~తొమ్మిది వేల తొమ్మిది వందల తొంభై తొమ్మిది కోట్ల తొంభై తొమ్మిది లక్షల తొంభై తొమ్మిది వేల తొమ్మిది వందల తొంభై తొమ్మిది కోట్ల తొంభై తొమ్మిది లక్షల తొంభై తొమ్మిది వేల తొమ్మిది వందల తొంభై తొమ్మిది +9999999999999999999~తొంభై తొమ్మిది వేల తొమ్మిది వందల తొంభై తొమ్మిది కోట్ల తొంభై తొమ్మిది లక్షల తొంభై తొమ్మిది వేల తొమ్మిది వందల తొంభై తొమ్మిది కోట్ల తొంభై తొమ్మిది లక్షల తొంభై తొమ్మిది వేల తొమ్మిది వందల తొంభై తొమ్మిది +జనాభా 999999999999999~జనాభా తొమ్మిది కోట్ల తొంభై తొమ్మిది లక్షల తొంభై తొమ్మిది వేల తొమ్మిది వందల తొంభై తొమ్మిది కోట్ల తొంభై తొమ్మిది లక్షల తొంభై తొమ్మిది వేల తొమ్మిది వందల తొంభై తొమ్మిది +౯౯౯౯౯౯౯౯౯౯౯౯౯౯౯~తొమ్మిది కోట్ల తొంభై తొమ్మిది లక్షల తొంభై తొమ్మిది వేల తొమ్మిది వందల తొంభై తొమ్మిది కోట్ల తొంభై తొమ్మిది లక్షల తొంభై తొమ్మిది వేల తొమ్మిది వందల తొంభై తొమ్మిది +౯౯౯౯౯౯౯౯౯౯౯౯౯౯౯౯౯౯౯~తొంభై తొమ్మిది వేల తొమ్మిది వందల తొంభై తొమ్మిది కోట్ల తొంభై తొమ్మిది లక్షల తొంభై తొమ్మిది వేల తొమ్మిది వందల తొంభై తొమ్మిది కోట్ల తొంభై తొమ్మిది లక్షల తొంభై తొమ్మిది వేల తొమ్మిది వందల తొంభై తొమ్మిది +05~సున్నా ఐదు +01~సున్నా ఒకటి +073~సున్నా ఏడు మూడు +0001~సున్నా సున్నా సున్నా ఒకటి +000~సున్నా సున్నా సున్నా +-23~మైనస్ ఇరవై మూడు +-100~మైనస్ వంద +-12345567~మైనస్ కోటి ఇరవై మూడు లక్షల నలభై ఐదు వేల ఐదు వందల అరవై ఏడు +-51022345567~మైనస్ ఐదు వేల నూట రెండు కోట్ల ఇరవై మూడు లక్షల నలభై ఐదు వేల ఐదు వందల అరవై ఏడు +1,23,456~లక్ష ఇరవై మూడు వేల నాలుగు వందల యాభై ఆరు +12,345~పన్నెండు వేల మూడు వందల నలభై ఐదు +1,00,00,000~కోటి +5,50,00,000~ఐదు కోట్ల యాభై లక్షలు +32,45,000~ముప్పై రెండు లక్షల నలభై ఐదు వేలు +5,56,320~ఐదు లక్షల యాభై ఆరు వేల మూడు వందల ఇరవై +32,450,000~మూడు కోట్ల ఇరవై నాలుగు లక్షల యాభై వేలు +4,99,99,000~నాలుగు కోట్ల తొంభై తొమ్మిది లక్షల తొంభై తొమ్మిది వేలు +11,220~పదకొండు వేల రెండు వందల ఇరవై +౧,౨౩,౪౫౬~లక్ష ఇరవై మూడు వేల నాలుగు వందల యాభై ఆరు +-1,23,456~మైనస్ లక్ష ఇరవై మూడు వేల నాలుగు వందల యాభై ఆరు +10050~పది వేల యాభై +10100~పది వేల వంద +100100~లక్ష వంద +101000~లక్ష వెయ్యి +100010~లక్ష పది +1005000~పది లక్షల ఐదు వేలు +1002003~పది లక్షల రెండు వేల మూడు +2050607~ఇరవై లక్షల యాభై వేల ఆరు వందల ఏడు +10000050~కోటి యాభై +10010000~కోటి పది వేలు +10100000~కోటి లక్ష +10001001~కోటి వెయ్యి ఒకటి +100000000001~పది వేల కోట్ల ఒకటి +1,00,100~లక్ష వంద +10,05,000~పది లక్షల ఐదు వేలు +1,00,01,001~కోటి వెయ్యి ఒకటి +50000000001~ఐదు వేల కోట్ల ఒకటి +2100000000000~రెండు లక్షల పది వేల కోట్లు +210000000000000~రెండు కోట్ల పది లక్షల కోట్లు +2200000000000000~ఇరవై రెండు కోట్ల కోట్లు +10000000000000000~వంద కోట్ల కోట్లు +10200000000000000~నూట రెండు కోట్ల కోట్లు +990000000000000000~తొమ్మిది వేల తొమ్మిది వందల కోట్ల కోట్లు +9999999990000000~తొంభై తొమ్మిది కోట్ల తొంభై తొమ్మిది లక్షల తొంభై తొమ్మిది వేల తొమ్మిది వందల తొంభై తొమ్మిది కోట్లు +99999999999999999~తొమ్మిది వందల తొంభై తొమ్మిది కోట్ల తొంభై తొమ్మిది లక్షల తొంభై తొమ్మిది వేల తొమ్మిది వందల తొంభై తొమ్మిది కోట్ల తొంభై తొమ్మిది లక్షల తొంభై తొమ్మిది వేల తొమ్మిది వందల తొంభై తొమ్మిది +12345678901234567~నూట ఇరవై మూడు కోట్ల నలభై ఐదు లక్షల అరవై ఏడు వేల ఎనిమిది వందల తొంభై కోట్ల పన్నెండు లక్షల ముప్పై నాలుగు వేల ఐదు వందల అరవై ఏడు +2100000000000000~ఇరవై ఒక కోట్ల కోట్లు +20100000000000000~రెండు వందల ఒక కోట్ల కోట్లు +99000000000000~తొంభై తొమ్మిది లక్షల కోట్లు +990000000000000~తొమ్మిది కోట్ల తొంభై లక్షల కోట్లు +1010000000~నూట ఒక కోట్లు +1012345567~నూట ఒక కోట్ల ఇరవై మూడు లక్షల నలభై ఐదు వేల ఐదు వందల అరవై ఏడు +1,01,00,00,000~నూట ఒక కోట్లు +2010000000~రెండు వందల ఒక కోట్లు +9010000000~తొమ్మిది వందల ఒక కోట్లు +1020000000~నూట రెండు కోట్లు +12౩45~12౩45 +1౨345~1౨345 +౧2345~౧2345 +1234౫~1234౫ +123౪56789~123౪56789 +14~పద్నాలుగు +15~పదిహేను +16~పదహారు +19~పంతొమ్మిది +114~నూట పద్నాలుగు +1015~వెయ్యి పదిహేను +216~రెండు వందల పదహారు +౧౪~పద్నాలుగు +౧౯~పంతొమ్మిది +999999999~తొంభై తొమ్మిది కోట్ల తొంభై తొమ్మిది లక్షల తొంభై తొమ్మిది వేల తొమ్మిది వందల తొంభై తొమ్మిది +220000000~ఇరవై రెండు కోట్లు +210000000~ఇరవై ఒక కోట్లు +900000000~తొంభై కోట్లు +123456789~పన్నెండు కోట్ల ముప్పై నాలుగు లక్షల యాభై ఆరు వేల ఏడు వందల ఎనభై తొమ్మిది +1000~వెయ్యి +100000~లక్ష +10000000~కోటి +౧౦౦౦~వెయ్యి +౧౦౦౦౦౦~లక్ష +౧౦౦౦౦౦౦౦~కోటి +౧౦౧~నూట ఒకటి +౧౧౧~నూట పదకొండు +౧౩౦~నూట ముప్పై +21000~ఇరవై ఒక వేలు +2100000~ఇరవై ఒక లక్షలు +3100000~ముప్పై ఒక లక్షలు +9100000~తొంభై ఒక లక్షలు +2100001~ఇరవై ఒక లక్షల ఒకటి +21000000000000~ఇరవై ఒక లక్షల కోట్లు +2040000000000~రెండు లక్షల నాలుగు వేల కోట్లు +3014000000000~మూడు లక్షల వెయ్యి నాలుగు వందల కోట్లు +9040000000000~తొమ్మిది లక్షల నాలుగు వేల కోట్లు +2000400000000000~ఇరవై కోట్ల నలభై వేల కోట్లు +200000000000000~రెండు కోట్ల కోట్లు +100040000000000~కోటి నాలుగు వేల కోట్లు +104000000000000~కోటి నాలుగు లక్షల కోట్లు +100004000000000~కోటి నాలుగు వందల కోట్లు +500000000000001~ఐదు కోట్ల కోట్ల ఒకటి +12100000000000000~నూట ఇరవై ఒక కోట్ల కోట్లు +12140000000000000~నూట ఇరవై ఒక కోట్ల నలభై లక్షల కోట్లు +1210000000~నూట ఇరవై ఒక కోట్లు +101010101010101010~వెయ్యి పది కోట్ల పది లక్షల పది వేల నూట ఒక కోటి లక్ష వెయ్యి పది +123456789012345678~వెయ్యి రెండు వందల ముప్పై నాలుగు కోట్ల యాభై ఆరు లక్షల డెబ్బై ఎనిమిది వేల తొమ్మిది వందల ఒక కోటి ఇరవై మూడు లక్షల నలభై ఐదు వేల ఆరు వందల డెబ్బై ఎనిమిది +12100000~కోటి ఇరవై ఒక లక్షలు +31000~ముప్పై ఒక వేలు +91000~తొంభై ఒక వేలు +21500~ఇరవై ఒక వేల ఐదు వందలు +౨౧౦౦౦~ఇరవై ఒక వేలు +210000000000~ఇరవై ఒక వేల కోట్లు +2100000000000000000~ఇరవై ఒక వేల కోట్ల కోట్లు +0~సున్నా +౦~సున్నా +-0~మైనస్ సున్నా +99999999999999999999~99999999999999999999 +1,0000~ఒకటి , సున్నా సున్నా సున్నా సున్నా +12,34~పన్నెండు , ముప్పై నాలుగు +1,2,3~ఒకటి , రెండు , మూడు +51010000000~ఐదు వేల నూట ఒక కోట్లు +34018285364~మూడు వేల నాలుగు వందల ఒక కోట్ల ఎనభై రెండు లక్షల ఎనభై ఐదు వేల మూడు వందల అరవై నాలుగు +711010000000~డెబ్బై ఒక వేల నూట ఒక కోట్లు +465017762066~నలభై ఆరు వేల ఐదు వందల ఒక కోట్ల డెబ్బై ఏడు లక్షల అరవై రెండు వేల అరవై ఆరు +3164010000000~మూడు లక్షల పదహారు వేల నాలుగు వందల ఒక కోట్లు +97931010000000~తొంభై ఏడు లక్షల తొంభై మూడు వేల నూట ఒక కోట్లు +90140000000000000~తొమ్మిది వందల ఒక కోట్ల నలభై లక్షల కోట్లు +340100000000000000~మూడు వేల నాలుగు వందల ఒక కోట్ల కోట్లు +340182853640000000~మూడు వేల నాలుగు వందల ఒక కోట్ల ఎనభై రెండు లక్షల ఎనభై ఐదు వేల మూడు వందల అరవై నాలుగు కోట్లు +4650177620660000000~నలభై ఆరు వేల ఐదు వందల ఒక కోట్ల డెబ్బై ఏడు లక్షల అరవై రెండు వేల అరవై ఆరు కోట్లు +51020000000~ఐదు వేల నూట రెండు కోట్లు +34028285364~మూడు వేల నాలుగు వందల రెండు కోట్ల ఎనభై రెండు లక్షల ఎనభై ఐదు వేల మూడు వందల అరవై నాలుగు +90240000000000000~తొమ్మిది వందల రెండు కోట్ల నలభై లక్షల కోట్లు +90100000000000000~తొమ్మిది వందల ఒక కోట్ల కోట్లు +10010000000~వెయ్యి ఒక కోట్లు +20010000000~రెండు వేల ఒక కోట్లు +50010000000~ఐదు వేల ఒక కోట్లు +10001010000000~పది లక్షల నూట ఒక కోట్లు +10000000101~వెయ్యి కోట్ల నూట ఒకటి +10000000201~వెయ్యి కోట్ల రెండు వందల ఒకటి +220000000000000~రెండు కోట్ల ఇరవై లక్షల కోట్లు +230000000000000~రెండు కోట్ల ముప్పై లక్షల కోట్లు +410000000000000~నాలుగు కోట్ల పది లక్షల కోట్లు +941302654706146~తొమ్మిది కోట్ల నలభై ఒక లక్షల ముప్పై వేల రెండు వందల అరవై ఐదు కోట్ల నలభై ఏడు లక్షల ఆరు వేల నూట నలభై ఆరు +851750346234890~ఎనిమిది కోట్ల యాభై ఒక లక్షల డెబ్బై ఐదు వేల ముప్పై నాలుగు కోట్ల అరవై రెండు లక్షల ముప్పై నాలుగు వేల ఎనిమిది వందల తొంభై +0౫~0౫ +౦5~౦5 +00౭~00౭ +0౦5~0౦5 +05~సున్నా ఐదు +౦౫~సున్నా ఐదు +007~సున్నా సున్నా ఏడు +౦౦౭~సున్నా సున్నా ఏడు +1000010000000~లక్ష ఒక కోట్లు +2000010000000~రెండు లక్షల ఒక కోట్లు +5000010000000~ఐదు లక్షల ఒక కోట్లు +9000010000000~తొమ్మిది లక్షల ఒక కోట్లు +11000010000000~పదకొండు లక్షల ఒక కోట్లు +99000010000000~తొంభై తొమ్మిది లక్షల ఒక కోట్లు +1000014261933~లక్ష ఒక కోట్ల నలభై రెండు లక్షల అరవై ఒక వేల తొమ్మిది వందల ముప్పై మూడు +10000010000000~పది లక్షల ఒక కోట్లు +50000010000000~యాభై లక్షల ఒక కోట్లు +90000010701028~తొంభై లక్షల ఒక కోట్ల ఏడు లక్షల వెయ్యి ఇరవై ఎనిమిది +1010010000000~లక్ష వెయ్యి ఒక కోట్లు +3010010000000~మూడు లక్షల వెయ్యి ఒక కోట్లు +5010010000000~ఐదు లక్షల వెయ్యి ఒక కోట్లు +1010012900770~లక్ష వెయ్యి ఒక కోట్ల ఇరవై తొమ్మిది లక్షల ఏడు వందల డెబ్బై +5010012900770~ఐదు లక్షల వెయ్యి ఒక కోట్ల ఇరవై తొమ్మిది లక్షల ఏడు వందల డెబ్బై +5001010000000~ఐదు లక్షల నూట ఒక కోట్లు +90410010000205~తొంభై లక్షల నలభై ఒక వేల ఒక కోట్ల రెండు వందల ఐదు +30920010793149~ముప్పై లక్షల తొంభై రెండు వేల ఒక కోట్ల ఏడు లక్షల తొంభై మూడు వేల నూట నలభై తొమ్మిది +10601010405114~పది లక్షల అరవై వేల నూట ఒక కోట్ల నాలుగు లక్షల ఐదు వేల నూట పద్నాలుగు +1000020000000~లక్ష రెండు కోట్లు +10000020000000~పది లక్షల రెండు కోట్లు +50000020000000~యాభై లక్షల రెండు కోట్లు +500000010000000~ఐదు కోట్ల ఒక కోటి +5000000010000000~యాభై కోట్ల ఒక కోటి +50000000010000000~ఐదు వందల కోట్ల ఒక కోటి +500000000010000000~ఐదు వేల కోట్ల ఒక కోటి +5000000000010000000~యాభై వేల కోట్ల ఒక కోటి +500000015724739~ఐదు కోట్ల ఒక కోటి యాభై ఏడు లక్షల ఇరవై నాలుగు వేల ఏడు వందల ముప్పై తొమ్మిది +50000000015724739~ఐదు వందల కోట్ల ఒక కోటి యాభై ఏడు లక్షల ఇరవై నాలుగు వేల ఏడు వందల ముప్పై తొమ్మిది +268828015724739~రెండు కోట్ల అరవై ఎనిమిది లక్షల ఎనభై రెండు వేల ఎనిమిది వందల ఒక కోటి యాభై ఏడు లక్షల ఇరవై నాలుగు వేల ఏడు వందల ముప్పై తొమ్మిది +558073012157368~ఐదు కోట్ల యాభై ఎనిమిది లక్షల ఏడు వేల మూడు వందల ఒక కోటి ఇరవై ఒక లక్షల యాభై ఏడు వేల మూడు వందల అరవై ఎనిమిది +6690674013350484~అరవై ఆరు కోట్ల తొంభై లక్షల అరవై ఏడు వేల నాలుగు వందల ఒక కోటి ముప్పై మూడు లక్షల యాభై వేల నాలుగు వందల ఎనభై నాలుగు +500000020000000~ఐదు కోట్ల రెండు కోట్లు +500000110000000~ఐదు కోట్ల పదకొండు కోట్లు +268828025724739~రెండు కోట్ల అరవై ఎనిమిది లక్షల ఎనభై రెండు వేల ఎనిమిది వందల రెండు కోట్ల యాభై ఏడు లక్షల ఇరవై నాలుగు వేల ఏడు వందల ముప్పై తొమ్మిది +100010010000000~కోటి వెయ్యి ఒక కోటి +40100000010000000~నాలుగు వందల ఒక కోట్ల ఒక కోటి +100100000010000000~వెయ్యి ఒక కోట్ల ఒక కోటి +40010009010002012~నాలుగు వందల కోట్ల పది లక్షల తొమ్మిది వందల ఒక కోటి రెండు వేల పన్నెండు +400100090100002012~నాలుగు వేల ఒక కోట్ల తొమ్మిది వేల పది కోట్ల రెండు వేల పన్నెండు +200100090100002012~రెండు వేల ఒక కోట్ల తొమ్మిది వేల పది కోట్ల రెండు వేల పన్నెండు +౪౦౦౧౦౦౦౯౦౧౦౦౦౦౨౦౧౨~నాలుగు వేల ఒక కోట్ల తొమ్మిది వేల పది కోట్ల రెండు వేల పన్నెండు +40001000010002012~నాలుగు వందల కోట్ల లక్ష ఒక కోటి రెండు వేల పన్నెండు +40002000010002012~నాలుగు వందల కోట్ల రెండు లక్షల ఒక కోటి రెండు వేల పన్నెండు +40002100010002012~నాలుగు వందల కోట్ల రెండు లక్షల పది వేల ఒక కోటి రెండు వేల పన్నెండు +400101000010002012~నాలుగు వేల ఒక కోట్ల లక్ష ఒక కోటి రెండు వేల పన్నెండు +4000101000010002012~నలభై వేల ఒక కోట్ల లక్ష ఒక కోటి రెండు వేల పన్నెండు +౪౦౦౦౧౦౦౦౦౧౦౦౦౨౦౧౨~నాలుగు వందల కోట్ల లక్ష ఒక కోటి రెండు వేల పన్నెండు +40000001010002012~నాలుగు వందల కోట్ల నూట ఒక కోటి రెండు వేల పన్నెండు +40000002010002012~నాలుగు వందల కోట్ల రెండు వందల ఒక కోటి రెండు వేల పన్నెండు +40000009010002012~నాలుగు వందల కోట్ల తొమ్మిది వందల ఒక కోటి రెండు వేల పన్నెండు +40000001010000000~నాలుగు వందల కోట్ల నూట ఒక కోటి +400100001010002012~నాలుగు వేల ఒక కోట్ల నూట ఒక కోటి రెండు వేల పన్నెండు +4000100001010002012~నలభై వేల ఒక కోట్ల నూట ఒక కోటి రెండు వేల పన్నెండు +౪౦౦౦౦౦౦౧౦౧౦౦౦౨౦౧౨~నాలుగు వందల కోట్ల నూట ఒక కోటి రెండు వేల పన్నెండు +210000~రెండు లక్షల పది వేలు +40000010010002012~నాలుగు వందల కోట్ల వెయ్యి ఒక కోటి రెండు వేల పన్నెండు +40000100010002012~నాలుగు వందల కోట్ల పది వేల ఒక కోటి రెండు వేల పన్నెండు +40000002100002012~నాలుగు వందల కోట్ల రెండు వందల పది కోట్ల రెండు వేల పన్నెండు +40000001210002012~నాలుగు వందల కోట్ల నూట ఇరవై ఒక కోట్ల రెండు వేల పన్నెండు +200001010002012~రెండు కోట్ల నూట ఒక కోటి రెండు వేల పన్నెండు +2100001010002012~ఇరవై ఒక కోట్ల నూట ఒక కోటి రెండు వేల పన్నెండు +123,456,789~పన్నెండు కోట్ల ముప్పై నాలుగు లక్షల యాభై ఆరు వేల ఏడు వందల ఎనభై తొమ్మిది \ No newline at end of file diff --git a/tests/nemo_text_processing/te/data_text_normalization/test_cases_punctuation.txt b/tests/nemo_text_processing/te/data_text_normalization/test_cases_punctuation.txt new file mode 100644 index 000000000..1e2426eb7 --- /dev/null +++ b/tests/nemo_text_processing/te/data_text_normalization/test_cases_punctuation.txt @@ -0,0 +1,38 @@ +ఒక `కోట్` ఉదాహరణ~ఒక `కోట్` ఉదాహరణ +ఒక `25` ఉదాహరణ~ఒక ` ఇరవై ఐదు ` ఉదాహరణ +సింగిల్ కోట్.'~సింగిల్ కోట్.' +సింగిల్ కోట్. '4~సింగిల్ కోట్. ' నాలుగు +పరీక్ష -~పరీక్ష - +-5 పరీక్ష -~మైనస్ ఐదు పరీక్ష - +సంక్షిప్తం యు.ఎస్.ఏ.~సంక్షిప్తం యు.ఎస్.ఏ. +డా. రాము~డా. రాము +ఎ.పి.~ఎ.పి. +1-4=5~ఒకటి - నాలుగు = ఐదు +?,~?, +?,లేదు~?,లేదు +అవును, సరే.~అవును, సరే. +మీరు ఎలా ఉన్నారు?~మీరు ఎలా ఉన్నారు? +ఇది (12 జాతులు) మరియు...~ఇది ( పన్నెండు జాతులు) మరియు... +జంతువులు: (1), (2), (3)~జంతువులు: ( ఒకటి ), ( రెండు ), ( మూడు ) +జంతువులు: ``పిల్లి``, ``కుక్క``, ``సింహం``~జంతువులు: ``పిల్లి``, ``కుక్క``, ``సింహం`` +జంతువులు: ``4~జంతువులు: `` నాలుగు +‘హలో’~‘హలో’ +“తెలుగు”~“తెలుగు” +(పరీక్ష)~(పరీక్ష) +హలో।~హలో। +హలో॥~హలో॥ +హలో…~హలో… +హలో—~హలో— +114...48~నూట పద్నాలుగు ... నలభై ఎనిమిది +1961–1965~వెయ్యి తొమ్మిది వందల అరవై ఒకటి – వెయ్యి తొమ్మిది వందల అరవై ఐదు +1961–1965 and (2006–2012)~వెయ్యి తొమ్మిది వందల అరవై ఒకటి – వెయ్యి తొమ్మిది వందల అరవై ఐదు and ( రెండు వేల ఆరు – రెండు వేల పన్నెండు ) +2!~రెండు ! +౧!~ఒకటి ! +1!!!!~ఒకటి !!!! +1, ~ఒకటి , +(1)హలో~(1)హలో +123,000012~నూట ఇరవై మూడు , సున్నా సున్నా సున్నా సున్నా ఒకటి రెండు +₹100~₹100 +హలో~హలో +test -~test - +-5 test -~మైనస్ ఐదు test - \ No newline at end of file diff --git a/tests/nemo_text_processing/te/data_text_normalization/test_cases_punctuation_match_input.txt b/tests/nemo_text_processing/te/data_text_normalization/test_cases_punctuation_match_input.txt new file mode 100644 index 000000000..7eef277e7 --- /dev/null +++ b/tests/nemo_text_processing/te/data_text_normalization/test_cases_punctuation_match_input.txt @@ -0,0 +1,16 @@ +2!~రెండు! +౧!~ఒకటి! +1!!!!~ఒకటి!!!! +1,~ఒకటి, +(1)హలో~(1)హలో +1!హలో~1!హలో +?,లేదు~?,లేదు +ఇది (12 జాతులు) మరియు...~ఇది (పన్నెండు జాతులు) మరియు... +జంతువులు: (1), (2), (3)~జంతువులు: (ఒకటి), (రెండు), (మూడు) +ఒక `25` ఉదాహరణ~ఒక `ఇరవై ఐదు` ఉదాహరణ +సింగిల్ కోట్. '4~సింగిల్ కోట్. 'నాలుగు +114...48~నూట పద్నాలుగు...నలభై ఎనిమిది +123,000012~నూట ఇరవై మూడు,సున్నా సున్నా సున్నా సున్నా ఒకటి రెండు +జంతువులు: ``4~జంతువులు: ``నాలుగు +1961–1965 and (2006–2012)~వెయ్యి తొమ్మిది వందల అరవై ఒకటి – వెయ్యి తొమ్మిది వందల అరవై ఐదు and (రెండు వేల ఆరు – రెండు వేల పన్నెండు) +హలో...ప్రపంచం~హలో...ప్రపంచం \ No newline at end of file diff --git a/tests/nemo_text_processing/te/data_text_normalization/test_cases_word.txt b/tests/nemo_text_processing/te/data_text_normalization/test_cases_word.txt new file mode 100644 index 000000000..6ce3d5ef7 --- /dev/null +++ b/tests/nemo_text_processing/te/data_text_normalization/test_cases_word.txt @@ -0,0 +1,33 @@ +~ + ~ +తెలుగు~తెలుగు +మీరు లోపలికి రావచ్చా?~మీరు లోపలికి రావచ్చా? +అవును మీరు రావచ్చు.~అవును మీరు రావచ్చు. +మార్.~మార్. +మార్~మార్ +తె3~తె3 +।~। +॥~॥ +₹~₹ +…~… +·~· +నిద్ర~నిద్ర +బాగుంది~బాగుంది +కచ్చా-పక్కా~కచ్చా-పక్కా +ఆంధ్ర-తెలంగాణ~ఆంధ్ర-తెలంగాణ + 1~ఒకటి +1~ఒకటి +!1~! ఒకటి +౧~ఒకటి +౧!~ఒకటి ! +లోహం కా,~లోహం కా, +యాహూ!~యాహూ! +‘హలో’~‘హలో’ +“తెలుగు”~“తెలుగు” +(పరీక్ష)~(పరీక్ష) +తెలుగు।~తెలుగు। +నమస్కారం॥~నమస్కారం॥ +హలో…~హలో… +రూ. 100~రూ. వంద +నేను (22) బాతు పిల్లలను చూశాను~నేను ( ఇరవై రెండు ) బాతు పిల్లలను చూశాను +ఇరవై !~ఇరవై ! \ No newline at end of file diff --git a/tests/nemo_text_processing/te/test_cardinal.py b/tests/nemo_text_processing/te/test_cardinal.py new file mode 100644 index 000000000..aacab9485 --- /dev/null +++ b/tests/nemo_text_processing/te/test_cardinal.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 TestCardinal: + normalizer = Normalizer( + input_case='cased', + lang='te', + cache_dir=CACHE_DIR, + overwrite_cache=False, + post_process=False, + ) + + @parameterized.expand(parse_test_case_file('te/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/te/test_punctuation.py b/tests/nemo_text_processing/te/test_punctuation.py new file mode 100644 index 000000000..fc78711eb --- /dev/null +++ b/tests/nemo_text_processing/te/test_punctuation.py @@ -0,0 +1,44 @@ +# 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='te', + cache_dir=CACHE_DIR, + overwrite_cache=False, + post_process=False, + ) + + @parameterized.expand(parse_test_case_file('te/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, punct_post_process=False) + assert pred == expected, f"input: {test_input} != {expected}" + + @parameterized.expand(parse_test_case_file('te/data_text_normalization/test_cases_punctuation_match_input.txt')) + @pytest.mark.run_only_on('CPU') + @pytest.mark.unit + def test_norm_python_punct_post_process(self, test_input, expected): + pred = self.normalizer.normalize(test_input, verbose=False, punct_post_process=True) + assert pred == expected, f"for input |{test_input}|: pred: |{pred}| != expected: |{expected}|" diff --git a/tests/nemo_text_processing/te/test_sparrowhawk_normalization.sh b/tests/nemo_text_processing/te/test_sparrowhawk_normalization.sh new file mode 100644 index 000000000..edb3a32f6 --- /dev/null +++ b/tests/nemo_text_processing/te/test_sparrowhawk_normalization.sh @@ -0,0 +1,42 @@ +#! /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/te/data_text_normalization/test_cases_cardinal.txt + runtest $input +} + +testTNWord() { + input=$PROJECT_DIR/te/data_text_normalization/test_cases_word.txt + runtest $input +} + +testTNPunctuation() { + input=$PROJECT_DIR/te/data_text_normalization/test_cases_punctuation.txt + runtest $input +} + +# Load shUnit2 +. $PROJECT_DIR/../shunit2/shunit2 diff --git a/tests/nemo_text_processing/te/test_word.py b/tests/nemo_text_processing/te/test_word.py new file mode 100644 index 000000000..2d9fe6476 --- /dev/null +++ b/tests/nemo_text_processing/te/test_word.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 TestWord: + normalizer = Normalizer( + input_case='cased', + lang='te', + cache_dir=CACHE_DIR, + overwrite_cache=False, + post_process=False, + ) + + @parameterized.expand(parse_test_case_file('te/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..9fa9ac8d8 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', + 'te', ], type=str, default='en', @@ -224,6 +225,11 @@ def parse_args(): PostProcessingFst as TNPostProcessingFst, ) from nemo_text_processing.text_normalization.hi.verbalizers.verbalize import VerbalizeFst as TNVerbalizeFst + elif args.language == 'te': + from nemo_text_processing.text_normalization.te.taggers.tokenize_and_classify import ( + ClassifyFst as TNClassifyFst, + ) + from nemo_text_processing.text_normalization.te.verbalizers.verbalize import VerbalizeFst as TNVerbalizeFst elif args.language == 'hu': from nemo_text_processing.text_normalization.hu.taggers.tokenize_and_classify import ( ClassifyFst as TNClassifyFst,