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..91413eb39 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/data/numbers/magnitudes.tsv @@ -0,0 +1,12 @@ +hundreds_plural వందలు +hundreds_before వందల +thousand వెయ్యి +thousands_plural వేలు +thousands_before వేల +lakh లక్ష +lakh_before_digit లక్షా +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..5ef9ddaf5 --- /dev/null +++ b/nemo_text_processing/text_normalization/te/taggers/cardinal.py @@ -0,0 +1,364 @@ +# 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", "౧") + one_prefix = pynutil.delete(one_digit) + one_as_oka = (one_digit @ ties_one_suffix).optimize() + digit_x1 = (pynini.difference(NEMO_ALL_DIGIT, NEMO_ALL_ZERO | one_digit) @ digit).optimize() + + def U(*parts): + return pynini.union(*parts).optimize() + + def exact_n(n, graph=exact_power): + return pynini.compose(NEMO_ALL_DIGIT**n, graph).optimize() + + def ins(key, space=True): + return pynutil.insert((" " if space else "") + mag[key]) + + i_thou, i_thou_sp = ins("thousand", False), ins("thousand") + i_thous, i_thous_pl = ins("thousands_before"), ins("thousands_plural") + i_lakh, i_lakh_sp = ins("lakh", False), ins("lakh") + i_lakha, i_lakhs, i_lakhs_pl = ( + pynutil.insert(mag["lakh_before_digit"]), + ins("lakhs_before"), + ins("lakhs_plural"), + ) + i_koti, i_koti_sp = ins("crore", False), ins("crore") + i_kotlu, i_kotlu_pl = ins("crores_before"), ins("crores_plural") + i_vandalu, i_vandalu_pl = ins("hundreds_before"), ins("hundreds_plural") + + hp_ten = U( + pynini.compose(NEMO_DIGIT + NEMO_DIGIT, hundred_prefix), + pynini.compose(te_digit + te_digit, hundred_prefix), + ) + hp_one = U(pynini.compose(NEMO_DIGIT, hundred_prefix), pynini.compose(te_digit, hundred_prefix)) + + def teens_ties_of(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_en, dig_te = (NEMO_DIGIT @ digit).optimize(), (te_digit @ digit).optimize() + dig_x1_en = (pynini.difference(NEMO_DIGIT, pynini.union("0", "1")) @ digit).optimize() + dig_x1_te = (pynini.difference(te_digit, pynini.union("౦", "౧")) @ digit).optimize() + teens_ties = U(teens_ties_of(NEMO_DIGIT, "0", dig_en), teens_ties_of(te_digit, "౦", dig_te)) + teens_ties_x1 = U(teens_ties_of(NEMO_DIGIT, "0", dig_x1_en), teens_ties_of(te_digit, "౦", dig_x1_te)) + teens_ties_oka = U( + (NEMO_DIGIT @ ties) + insert_space + (NEMO_DIGIT @ ties_one_suffix), + (te_digit @ ties) + insert_space + (te_digit @ ties_one_suffix), + ) + + self.single_digits_graph = (digit | zero) + pynini.closure(insert_space + (digit | zero)) + + delete_zero = pynutil.delete(NEMO_ALL_ZERO) + z = {0: pynini.accep("")} + for n in range(1, 8): + z[n] = (z[n - 1] + delete_zero).optimize() + + def suffix(prefix, suf, zeros): + return prefix + suf if zeros == 0 else prefix + z[zeros] + suf + + def rung(prefix, suf, zeros, sub): + return prefix + (suf if zeros == 0 else suf + z[zeros]) + insert_space + sub + + def group(prefix, suf, ladder, head=None, head_z=None): + g = suffix(prefix, head, head_z) if head is not None else None + for zeros, sub in ladder: + g = rung(prefix, suf, zeros, sub) if g is None else g | rung(prefix, suf, zeros, sub) + return g + + def prefer(a, b): + return plurals._priority_union(a, b, NEMO_SIGMA) + + def ties_group(oka_suf, other_suf, ladder, head_oka, head_other, head_z): + return U( + group(teens_ties_oka, oka_suf, ladder, head_oka, head_z), + group(teens_ties_x1, other_suf, ladder, head_other, head_z), + ) + + def band(exact, sg, before, head, spaced, zeros, ladder, one_ladder=None, extra=None, ten_oka=None): + """Digit magnitude (+ optional teens ties). head is plural (standalone) or before (crore count).""" + one_ladder = ladder if one_ladder is None else one_ladder + g = exact | group(one_prefix, sg, one_ladder) + if extra is not None: + g = g | extra + g = U(g, group(digit_x1, before, ladder, head, zeros)) + ten = ties_group(ten_oka or before, before, ladder, spaced, head, zeros) + return g, ten + + def oka_count(sg, before, remainders): + """…01 multipliers: 1+sg / N+before / ties+before + zeros + rem→ఒక.""" + + def side(use_digit): + parts = [] + for zeros, rem in remainders: + body = z[zeros] + insert_space + rem + if use_digit: + parts += [one_prefix + sg + body, digit_x1 + before + body] + else: + parts += [teens_ties_oka + before + body, teens_ties_x1 + before + body] + return U(*parts) + + return prefer(side(True), side(False)).optimize() + + def crore_of(oka, other, other_head=None): + """10–14: …01 → కోటి; else → కోట్లు.""" + return prefer( + group(oka, i_koti_sp, crore_ladder, i_koti_sp, 7), + group(other, i_kotlu, crore_ladder, other_head or i_kotlu_pl, 7), + ).optimize() + + def kotlu(count): + """15–19 outer unit: always కోట్లు.""" + return group(count, i_kotlu, crore_ladder, i_kotlu_pl, 7).optimize() + + def oka_koti(*prefs, bare=(), pad=()): + """ఒక → ఒక కోటి (+ koti_ladder / bare / padded zeros).""" + parts = [group(p, i_koti_sp, koti_ladder, i_koti_sp, 7) for p in prefs] + parts += [p + i_koti_sp for p in bare] + parts += [suffix(p, i_koti_sp, n) for p, n in pad] + return U(*parts) + + graph_hundreds = U( + exact_n(3), + hp_ten + digit, + hp_one + teens_ties, + suffix(digit_x1, i_vandalu_pl, 2), + rung(digit_x1, i_vandalu, 1, digit), + rung(digit_x1, i_vandalu, 0, teens_ties), + ) + thousand_ladder = [(2, digit), (1, teens_ties), (0, graph_hundreds)] + graph_thousands, graph_ten_thousands = band( + exact_n(4), i_thou, i_thous, i_thous_pl, i_thou_sp, 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 = band( + exact_n(6), + i_lakh, + i_lakhs, + i_lakhs_pl, + i_lakh_sp, + 5, + lakh_ladder, + one_ladder=lakh_ladder[1:], + extra=rung(one_prefix, i_lakha, 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 = band( + exact_n(8), i_koti, i_kotlu, i_kotlu_pl, i_koti_sp, 7, crore_ladder, ten_oka=i_koti_sp + ) + + hundred_crore = U( + exact_n(3), + hp_ten + digit_x1, + hp_one + teens_ties, + suffix(digit_x1, i_vandalu, 2), + rung(digit_x1, i_vandalu, 1, digit_x1), + rung(digit_x1, i_vandalu, 0, teens_ties), + ) + hundred_one = U( + hp_one + pynutil.delete(NEMO_ALL_ZERO) + one_as_oka, + digit_x1 + pynutil.delete(NEMO_ALL_ZERO) + hundreds_before_one, + ) + thou_crore_ladder = [(2, digit_x1), (1, teens_ties), (0, hundred_crore)] + thousand_one = oka_count(i_thou, i_thous, [(2, one_as_oka), (0, hundred_one)]) + thousand_crore, ten_thousand_crore = band( + exact_n(4), i_thou, i_thous, i_thous, i_thou_sp, 3, thou_crore_ladder + ) + graph_hundred_crores = U(crore_of(hundred_one, hundred_crore), rung(hundred_crore, i_kotlu, 0, graph_crores)) + graph_thousand_crores = crore_of(thousand_one, U(thousand_crore, ten_thousand_crore)) + graph_ten_thousand_crores = crore_of(thousand_one, ten_thousand_crore) + + lakh_crore_ladder = [ + (4, digit_x1), + (3, teens_ties), + (2, hundred_crore), + (1, graph_thousands), + (0, ten_thousand_crore), + ] + lakh_one = oka_count( + i_lakh, + i_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 = band(exact_n(6), i_lakh, i_lakhs, i_lakhs, i_lakh_sp, 5, lakh_crore_ladder) + graph_lakh_crores = crore_of(lakh_one, U(lakh_crore, ten_lakh_crore)) + + ten_lakh_rem = ties_group(i_lakhs, i_lakhs, lakh_ladder, i_lakhs, i_lakhs, 5) + koti_ladder = [ + (6, digit), + (5, teens_ties), + (4, graph_hundreds), + (3, graph_thousands), + (2, graph_ten_thousands), + (1, graph_lakhs), + (0, ten_lakh_rem), + ] + crore_one = oka_count( + i_koti, + i_kotlu, + [(6, one_as_oka), (4, hundred_one), (1, exact_n(6, lakh_one)), (0, exact_n(7, lakh_one))], + ) + except_one_ladder = [(2, digit_x1), (1, teens_ties_x1)] + hundred_amt = U( + exact_n(3), + hp_ten + digit_x1, + hp_one + teens_ties_x1, + suffix(digit_x1, i_vandalu, 2), + rung(digit_x1, i_vandalu, 1, digit_x1), + ) + thousand_amt = U(*band(exact_n(4), i_thou, i_thous, i_thous, i_thou_sp, 3, except_one_ladder)) + ten_thousand_amt = ties_group(i_thous, i_thous, except_one_ladder + [(0, hundred_amt)], i_thou_sp, i_thous, 3) + crore_one_10 = rung(hundred_amt, i_kotlu, 6, one_as_oka).optimize() + crore_one_11 = rung(thousand_amt, i_kotlu, 6, one_as_oka).optimize() + crore_one_12 = rung(ten_thousand_amt, i_kotlu, 6, one_as_oka).optimize() + + ten_lakh_crore_count = prefer( + U( + rung(one_prefix, i_koti, 0, ten_lakh_rem), + rung(digit_x1, i_kotlu, 0, ten_lakh_rem), + rung(teens_ties_oka, i_koti_sp, 0, ten_lakh_rem), + rung(teens_ties_x1, i_kotlu, 0, ten_lakh_rem), + ), + U(graph_crores, graph_ten_crores), + ).optimize() + graph_ten_lakh_crores = prefer( + kotlu(exact_n(8, crore_one) + i_koti_sp), kotlu(ten_lakh_crore_count) + ).optimize() + graph_crore_crores = prefer( + kotlu(exact_n(9, crore_one) + i_koti_sp), + kotlu(ties_group(i_koti_sp, i_kotlu, koti_ladder, i_koti_sp, i_kotlu, 7)), + ).optimize() + + graph_ten_crore_crores = kotlu( + prefer( + oka_koti(hundred_one, crore_one_10, bare=(crore_one_10,)), + group(hundred_crore, i_kotlu, koti_ladder, i_kotlu, 7), + ).optimize() + ) + hcc_oka = group(teens_ties_oka, i_thous, thou_crore_ladder, i_thou_sp, 3) + hcc_other = U(thousand_crore, group(teens_ties_x1, i_thous, thou_crore_ladder, i_thous, 3)) + graph_hundred_crore_crores = kotlu( + prefer( + oka_koti( + thousand_one, + hcc_oka, + crore_one_12, + crore_one_11, + crore_one_10, + bare=(crore_one_12, crore_one_11), + pad=((crore_one_10, 1),), + ), + group(hcc_other, i_kotlu, koti_ladder, i_kotlu, 7), + ).optimize() + ) + + graph = U( + 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( + U( + 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 = U(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..bfce7dabe --- /dev/null +++ b/tests/nemo_text_processing/te/data_text_normalization/test_cases_cardinal.txt @@ -0,0 +1,204 @@ +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~ఇరవై ఒక వెయ్యి +31000~ముప్పై ఒక వెయ్యి +91000~తొంభై ఒక వెయ్యి +21500~ఇరవై ఒక వేల ఐదు వందలు +౨౧౦౦౦~ఇరవై ఒక వెయ్యి +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~యాభై లక్షల రెండు కోట్లు \ 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,