diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cbc636f1a..a2886d56e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,4 +50,4 @@ repos: - id: black name: Format code args: [--skip-string-normalization, --line-length=119] - additional_dependencies: ['click==8.0.2'] + additional_dependencies: ['click>=8.0.2'] diff --git a/Jenkinsfile b/Jenkinsfile index 51ce37a10..4527bbf1c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,8 +1,8 @@ pipeline { agent { docker { - image 'tnitn_ci:py310' - args '--user 0:128 -v /home/jenkinsci:/home/jenkinsci -v $HOME/.cache:/root/.cache --shm-size=4g --entrypoint=""' + image 'tnitn_ci_py310:24.07' + args '-v /mnt/jenkins/jenkinsci:/home/jenkins -v $HOME/.cache:/root/.cache --shm-size=4g --entrypoint=""' } } options { @@ -11,33 +11,27 @@ pipeline { } environment { - AR_TN_CACHE='/home/jenkinsci/TestData/text_norm/ci/grammars/04-24-24-0' - DE_TN_CACHE='/home/jenkinsci/TestData/text_norm/ci/grammars/10-23-24-0' - EN_TN_CACHE='/home/jenkinsci/TestData/text_norm/ci/grammars/09-04-24-0' - ES_TN_CACHE='/home/jenkinsci/TestData/text_norm/ci/grammars/09-25-24-0' - ES_EN_TN_CACHE='/home/jenkinsci/TestData/text_norm/ci/grammars/08-30-24-0' - FR_TN_CACHE='/home/jenkinsci/TestData/text_norm/ci/grammars/04-07-25-0' - HU_TN_CACHE='/home/jenkinsci/TestData/text_norm/ci/grammars/07-16-24-0' - PT_TN_CACHE='/home/jenkinsci/TestData/text_norm/ci/grammars/06-08-23-0' - RU_TN_CACHE='/home/jenkinsci/TestData/text_norm/ci/grammars/06-08-23-0' - VI_TN_CACHE='/home/jenkinsci/TestData/text_norm/ci/grammars/06-08-23-0' - SV_TN_CACHE='/home/jenkinsci/TestData/text_norm/ci/grammars/06-08-23-0' - ZH_TN_CACHE='/home/jenkinsci/TestData/text_norm/ci/grammars/11-13-24-0' - IT_TN_CACHE='/home/jenkinsci/TestData/text_norm/ci/grammars/08-22-24-0' - HY_TN_CACHE='/home/jenkinsci/TestData/text_norm/ci/grammars/03-12-24-0' - MR_TN_CACHE='/home/jenkinsci/TestData/text_norm/ci/grammars/03-12-24-1' - JA_TN_CACHE='/home/jenkinsci/TestData/text_norm/ci/grammars/10-17-24-1' - HI_TN_CACHE='/home/jenkinsci/TestData/text_norm/ci/grammars/04-22-25-0' - DEFAULT_TN_CACHE='/home/jenkinsci/TestData/text_norm/ci/grammars/06-08-23-0' + AR_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/04-24-24-0' + DE_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/10-23-24-0' + EN_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/09-04-24-0' + ES_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/09-25-24-0' + ES_EN_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/08-30-24-0' + FR_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/04-07-25-0' + HU_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/07-16-24-0' + PT_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/06-08-23-0' + RU_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/06-08-23-0' + VI_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/06-08-23-0' + SV_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/06-08-23-0' + ZH_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/11-13-24-0' + IT_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/08-22-24-0' + HY_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/03-12-24-0' + MR_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/03-12-24-1' + JA_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/10-17-24-1' + HI_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/04-22-25-0' + DEFAULT_TN_CACHE='/home/jenkins/TestData/text_norm/ci/grammars/06-08-23-0' } stages { - stage('Add git safe directory'){ - steps{ - sh 'git config --global --add safe.directory /var/lib/jenkins/workspace/NTP_$GIT_BRANCH' - sh 'git config --global --add safe.directory /home/jenkinsci/workspace/NTP_$GIT_BRANCH' - } - } stage('PyTorch version') { steps { @@ -46,14 +40,6 @@ pipeline { } } - stage('Install test requirements') { - steps { - sh 'apt-get update && apt-get install -y bc' - } - } - - - stage('NeMo Installation') { steps { sh './reinstall.sh release' @@ -65,7 +51,10 @@ pipeline { when { anyOf { branch 'main' + branch 'staging/**' + branch 'staging_*' changeRequest target: 'main' + } } failFast true @@ -97,6 +86,8 @@ pipeline { when { anyOf { branch 'main' + branch 'staging/**' + branch 'staging_*' changeRequest target: 'main' } } @@ -120,6 +111,8 @@ pipeline { when { anyOf { branch 'main' + branch 'staging/**' + branch 'staging_*' changeRequest target: 'main' } } @@ -156,7 +149,9 @@ pipeline { stage('L0: Create AR TN/ITN Grammars') { when { anyOf { - branch 'main' + branch 'main' + branch 'staging/**' + branch 'staging_*' changeRequest target: 'main' } } @@ -176,10 +171,12 @@ pipeline { } } - stage('L0: Create FR TN/ITN & VI ITN & HU TN & IT TN') { + stage('L0: Create FR TN/ITN & VI TN/ITN & HU TN & IT TN') { when { anyOf { - branch 'main' + branch 'main' + branch 'staging/**' + branch 'staging_*' changeRequest target: 'main' } } @@ -200,6 +197,11 @@ pipeline { sh 'CUDA_VISIBLE_DEVICES="" python nemo_text_processing/inverse_text_normalization/inverse_normalize.py --lang=vi --text="một ngàn " --cache_dir ${VI_TN_CACHE}' } } + stage('L0: VI TN grammars') { + steps { + sh 'CUDA_VISIBLE_DEVICES="" python nemo_text_processing/text_normalization/normalize.py --lang=vi --text="100" --cache_dir ${VI_TN_CACHE}' + } + } stage('L0: HU TN grammars') { steps { sh 'CUDA_VISIBLE_DEVICES="" python nemo_text_processing/text_normalization/normalize.py --lang=hu --text="100" --cache_dir ${HU_TN_CACHE}' @@ -216,7 +218,9 @@ pipeline { stage('L0: Create RU TN/ITN Grammars & SV & PT') { when { anyOf { - branch 'main' + branch 'main' + branch 'staging/**' + branch 'staging_*' changeRequest target: 'main' } } @@ -258,7 +262,9 @@ pipeline { stage('L0: Create HY TN/ITN Grammars & MR') { when { anyOf { - branch 'main' + branch 'main' + branch 'staging/**' + branch 'staging_*' changeRequest target: 'main' } } @@ -284,7 +290,9 @@ pipeline { stage('L0: Create ZH TN/ITN Grammar') { when { anyOf { - branch 'main' + branch 'main' + branch 'staging/**' + branch 'staging_*' changeRequest target: 'main' } } @@ -305,7 +313,9 @@ pipeline { stage('L0: Create JA ITN Grammars') { when { anyOf { - branch 'main' + branch 'main' + branch 'staging/**' + branch 'staging_*' changeRequest target: 'main' } } @@ -325,7 +335,9 @@ pipeline { stage('L1: TN/ITN Tests CPU') { when { anyOf { - branch 'main' + branch 'main' + branch 'staging/**' + branch 'staging_*' changeRequest target: 'main' } } @@ -409,10 +421,12 @@ pipeline { } } - stage('L2: Sparrowhawk Tests') { + stage('L2: EN Sparrowhawk Tests') { when { anyOf { - branch 'main' + branch 'main' + branch 'staging/**' + branch 'staging_*' changeRequest target: 'main' } } @@ -441,11 +455,13 @@ pipeline { } } - + stage('L2: NeMo text processing') { when { anyOf { - branch 'main' + branch 'main' + branch 'staging/**' + branch 'staging_*' changeRequest target: 'main' } } @@ -453,23 +469,23 @@ pipeline { parallel { stage('L2: Eng TN') { steps { - sh 'TIME=`date +"%Y-%m-%d-%T"` && NORM_OUTPUT_DIR=/home/jenkinsci/TestData/text_norm/output_${TIME} && \ + sh 'TIME=`date +"%Y-%m-%d-%T"` && NORM_OUTPUT_DIR=/home/jenkins/TestData/text_norm/output_${TIME} && \ cd tools/text_processing_deployment && python pynini_export.py --output=$NORM_OUTPUT_DIR --grammars=tn_grammars --cache_dir ${EN_TN_CACHE} --language=en && ls -R $NORM_OUTPUT_DIR && echo ".far files created "|| exit 1' - sh 'TIME=`date +"%Y-%m-%d-%T"` && NORM_OUTPUT_DIR=/home/jenkinsci/TestData/text_norm/output_${TIME} && mkdir $NORM_OUTPUT_DIR && \ - cd nemo_text_processing/text_normalization/ && python normalize.py --input_file=/home/jenkinsci/TestData/text_norm/ci/test.txt --input_case="lower_cased" --language=en --output_file=$NORM_OUTPUT_DIR/test.pynini.txt --verbose && \ + sh 'TIME=`date +"%Y-%m-%d-%T"` && NORM_OUTPUT_DIR=/home/jenkins/TestData/text_norm/output_${TIME} && mkdir $NORM_OUTPUT_DIR && \ + cd nemo_text_processing/text_normalization/ && python normalize.py --input_file=/home/jenkins/TestData/text_norm/ci/test.txt --input_case="lower_cased" --language=en --output_file=$NORM_OUTPUT_DIR/test.pynini.txt --verbose && \ cat $NORM_OUTPUT_DIR/test.pynini.txt && \ - cmp --silent $NORM_OUTPUT_DIR/test.pynini.txt /home/jenkinsci/TestData/text_norm/ci/test_goal_py.txt || exit 1 && \ + cmp --silent $NORM_OUTPUT_DIR/test.pynini.txt /home/jenkins/TestData/text_norm/ci/test_goal_py.txt || exit 1 && \ rm -rf $NORM_OUTPUT_DIR' } } stage('L2: Eng ITN export') { steps { - sh 'TIME=`date +"%Y-%m-%d-%T"` && DENORM_OUTPUT_DIR=/home/jenkinsci/TestData/text_denorm/output_${TIME} && \ + sh 'TIME=`date +"%Y-%m-%d-%T"` && DENORM_OUTPUT_DIR=/home/jenkins/TestData/text_denorm/output_${TIME} && \ cd tools/text_processing_deployment && python pynini_export.py --output=$DENORM_OUTPUT_DIR --grammars=itn_grammars --cache_dir ${EN_TN_CACHE} --language=en && ls -R $DENORM_OUTPUT_DIR && echo ".far files created "|| exit 1' - sh 'TIME=`date +"%Y-%m-%d-%T"` && DENORM_OUTPUT_DIR=/home/jenkinsci/TestData/text_denorm/output_${TIME} && mkdir $DENORM_OUTPUT_DIR && \ - cd nemo_text_processing/inverse_text_normalization/ && python inverse_normalize.py --input_file=/home/jenkinsci/TestData/text_denorm/ci/test.txt --language=en --output_file=$DENORM_OUTPUT_DIR/test.pynini.txt --verbose && \ - cmp --silent $DENORM_OUTPUT_DIR/test.pynini.txt /home/jenkinsci/TestData/text_denorm/ci/test_goal_py.txt || exit 1 && \ + sh 'TIME=`date +"%Y-%m-%d-%T"` && DENORM_OUTPUT_DIR=/home/jenkins/TestData/text_denorm/output_${TIME} && mkdir $DENORM_OUTPUT_DIR && \ + cd nemo_text_processing/inverse_text_normalization/ && python inverse_normalize.py --input_file=/home/jenkins/TestData/text_denorm/ci/test.txt --language=en --output_file=$DENORM_OUTPUT_DIR/test.pynini.txt --verbose && \ + cmp --silent $DENORM_OUTPUT_DIR/test.pynini.txt /home/jenkins/TestData/text_denorm/ci/test_goal_py.txt || exit 1 && \ rm -rf $DENORM_OUTPUT_DIR' } } @@ -477,18 +493,18 @@ pipeline { stage('L2: Eng alignment TN') { steps { - sh 'TIME=`date +"%Y-%m-%d-%T"` && NORM_OUTPUT_DIR=/home/jenkinsci/TestData/text_norm/output_${TIME} && mkdir $NORM_OUTPUT_DIR && \ + sh 'TIME=`date +"%Y-%m-%d-%T"` && NORM_OUTPUT_DIR=/home/jenkins/TestData/text_norm/output_${TIME} && mkdir $NORM_OUTPUT_DIR && \ cd nemo_text_processing/fst_alignment && python alignment.py --text="2615 Forest Av, 90501 CA, Santa Clara. 10kg, 12/16/2018" --grammar=tn --rule=tokenize_and_classify --fst=${EN_TN_CACHE}/en_tn_True_deterministic_cased__tokenize.far 2>&1 | tee $NORM_OUTPUT_DIR/pred.txt && \ - cmp --silent $NORM_OUTPUT_DIR/pred.txt /home/jenkinsci/TestData/text_norm/ci/alignment_gold.txt || exit 1 && \ + cmp --silent $NORM_OUTPUT_DIR/pred.txt /home/jenkins/TestData/text_norm/ci/alignment_gold.txt || exit 1 && \ rm -rf $NORM_OUTPUT_DIR' } } stage('L2: Eng alignment ITN') { steps { - sh 'TIME=`date +"%Y-%m-%d-%T"` && DENORM_OUTPUT_DIR=/home/jenkinsci/TestData/text_denorm/output_${TIME} && mkdir $DENORM_OUTPUT_DIR && \ + sh 'TIME=`date +"%Y-%m-%d-%T"` && DENORM_OUTPUT_DIR=/home/jenkins/TestData/text_denorm/output_${TIME} && mkdir $DENORM_OUTPUT_DIR && \ cd nemo_text_processing/fst_alignment && python alignment.py --text="one million twenty three thousand two hundred eleven ten kilograms one hundred twenty three dollars and twenty five cents" --grammar=itn --rule=tokenize_and_classify --fst=${EN_TN_CACHE}/en_itn_lower_cased.far 2>&1 | tee $DENORM_OUTPUT_DIR/pred.txt && \ - cmp --silent $DENORM_OUTPUT_DIR/pred.txt /home/jenkinsci/TestData/text_denorm/ci/alignment_gold.txt || exit 1 && \ + cmp --silent $DENORM_OUTPUT_DIR/pred.txt /home/jenkins/TestData/text_denorm/ci/alignment_gold.txt || exit 1 && \ rm -rf $DENORM_OUTPUT_DIR' } } diff --git a/nemo_text_processing/inverse_text_normalization/vi/data/currency.tsv b/nemo_text_processing/inverse_text_normalization/vi/data/currency.tsv index ce65d4420..0e14edf4f 100644 --- a/nemo_text_processing/inverse_text_normalization/vi/data/currency.tsv +++ b/nemo_text_processing/inverse_text_normalization/vi/data/currency.tsv @@ -8,4 +8,4 @@ $ đô la mỹ ₩ won ₩ uôn RM ringgit -₫ đồng \ No newline at end of file +£ bảng anh \ No newline at end of file diff --git a/nemo_text_processing/inverse_text_normalization/vi/data/electronic/symbols.tsv b/nemo_text_processing/inverse_text_normalization/vi/data/electronic/symbols.tsv index eccbe3d47..f8f2fc3d2 100644 --- a/nemo_text_processing/inverse_text_normalization/vi/data/electronic/symbols.tsv +++ b/nemo_text_processing/inverse_text_normalization/vi/data/electronic/symbols.tsv @@ -2,6 +2,7 @@ - gạch _ gạch dưới _ shift gạch +_ shift trừ _ síp gạch ! chấm than # thăng diff --git a/nemo_text_processing/inverse_text_normalization/vi/data/electronic/url_symbols.tsv b/nemo_text_processing/inverse_text_normalization/vi/data/electronic/url_symbols.tsv new file mode 100644 index 000000000..99f2059a2 --- /dev/null +++ b/nemo_text_processing/inverse_text_normalization/vi/data/electronic/url_symbols.tsv @@ -0,0 +1,8 @@ +. chấm +- gạch +- gạch ngang +_ gạch dưới +_ shift gạch +_ shift trừ +_ síp gạch +/ sẹc diff --git a/nemo_text_processing/inverse_text_normalization/vi/graph_utils.py b/nemo_text_processing/inverse_text_normalization/vi/graph_utils.py index 4e58ff475..12f8e8277 100644 --- a/nemo_text_processing/inverse_text_normalization/vi/graph_utils.py +++ b/nemo_text_processing/inverse_text_normalization/vi/graph_utils.py @@ -37,7 +37,7 @@ NEMO_SPACE = " " NEMO_WHITE_SPACE = pynini.union(" ", "\t", "\n", "\r", "\u00a0").optimize() NEMO_NOT_SPACE = pynini.difference(NEMO_CHAR, NEMO_WHITE_SPACE).optimize() -NEMO_NOT_QUOTE = pynini.difference(NEMO_CHAR, r'"').optimize() +NEMO_NOT_QUOTE = pynini.difference(NEMO_CHAR, '"').optimize() NEMO_PUNCT = pynini.union(*map(pynini.escape, string.punctuation)).optimize() NEMO_GRAPH = pynini.union(NEMO_ALNUM, NEMO_PUNCT).optimize() diff --git a/nemo_text_processing/inverse_text_normalization/vi/taggers/cardinal.py b/nemo_text_processing/inverse_text_normalization/vi/taggers/cardinal.py index 155513937..ddd4bdcd1 100644 --- a/nemo_text_processing/inverse_text_normalization/vi/taggers/cardinal.py +++ b/nemo_text_processing/inverse_text_normalization/vi/taggers/cardinal.py @@ -36,117 +36,118 @@ class CardinalFst(GraphFst): def __init__(self): super().__init__(name="cardinal", kind="classify") - graph_zero = pynini.string_file(get_abs_path("data/numbers/zero.tsv")) - graph_digit = pynini.string_file(get_abs_path("data/numbers/digit.tsv")) graph_ties = pynini.string_file(get_abs_path("data/numbers/ties.tsv")) graph_teen = pynini.string_file(get_abs_path("data/numbers/teen.tsv")) + thousand_words = pynini.union("ngàn", "nghìn") + negative_words = pynini.union("âm", "trừ") + + graph_hundred = pynini.cross("trăm", "") + graph_ten = pynini.cross("mươi", "") + zero = pynini.cross(pynini.union("linh", "lẻ"), "0") + + graph_zero = pynini.string_file(get_abs_path("data/numbers/zero.tsv")) + graph_digit = pynini.string_file(get_abs_path("data/numbers/digit.tsv")) graph_one = pynini.cross("mốt", "1") graph_four = pynini.cross("tư", "4") graph_five = pynini.cross("lăm", "5") graph_half = pynini.cross("rưỡi", "5") - graph_hundred = pynini.cross("trăm", "") - graph_ten = pynini.cross("mươi", "") - zero = pynini.cross(pynini.union("linh", "lẻ"), "0") optional_ten = pynini.closure(delete_space + graph_ten, 0, 1) last_digit_exception = pynini.project(pynini.cross("năm", "5"), "input") - last_digit = pynini.union( + self.last_digit = pynini.union( (pynini.project(graph_digit, "input") - last_digit_exception.arcsort()) @ graph_digit, graph_one, graph_four, graph_five, ) - - graph_hundred_ties_component = (graph_digit | graph_zero) + delete_space + graph_hundred - graph_hundred_ties_component += delete_space - graph_hundred_ties_component += pynini.union( + last_digit = self.last_digit + # Build hundreds component (e.g., "một trăm", "hai trăm") + graph_hundreds_component = (graph_digit | graph_zero) + delete_space + graph_hundred + graph_hundreds_component += delete_space + graph_hundreds_component += pynini.union( graph_teen, - (graph_half | graph_four | graph_one) + pynutil.insert("0"), - graph_ties + optional_ten + ((delete_space + last_digit) | pynutil.insert("0")), - zero + delete_space + (graph_digit | graph_four), - pynutil.insert("00"), - ) - graph_hundred_ties_component |= ( + (graph_half | graph_four | graph_one) + pynutil.insert("0", weight=0.1), + graph_ties + optional_ten + ((delete_space + last_digit) | pynutil.insert("0", weight=0.1)), + zero + delete_space + (graph_digit | graph_four | graph_five), + pynutil.insert("00", weight=0.1), + ).optimize() + graph_hundreds_component |= ( pynutil.insert("0") + delete_space + pynini.union( graph_teen, graph_ties + optional_ten + delete_space + last_digit, - graph_ties + delete_space + graph_ten + pynutil.insert("0"), - zero + delete_space + (graph_digit | graph_four), - ) + graph_ties + delete_space + graph_ten + pynutil.insert("0", weight=0.1), + zero + delete_space + (graph_digit | graph_four | graph_five), + ).optimize() + ) + graph_hundred_component = graph_hundreds_component | ( + pynutil.insert("00", weight=0.1) + delete_space + graph_digit ) - graph_hundred_component = graph_hundred_ties_component | (pynutil.insert("00") + delete_space + graph_digit) graph_hundred_component_at_least_one_none_zero_digit = graph_hundred_component @ ( pynini.closure(NEMO_DIGIT) + (NEMO_DIGIT - "0") + pynini.closure(NEMO_DIGIT) ) self.graph_hundred_component_at_least_one_none_zero_digit = ( - graph_hundred_component_at_least_one_none_zero_digit + graph_hundred_component_at_least_one_none_zero_digit.optimize() ) - graph_hundred_ties_zero = graph_hundred_ties_component | pynutil.insert("000") + graph_hundreds_zero = graph_hundreds_component | pynutil.insert("000", weight=0.1) graph_thousands = pynini.union( - graph_hundred_component_at_least_one_none_zero_digit - + delete_space - + pynutil.delete(pynini.union("nghìn", "ngàn")), + graph_hundred_component_at_least_one_none_zero_digit + delete_space + pynutil.delete(thousand_words), pynutil.insert("000", weight=0.1), - ) - - graph_ten_thousand = pynini.union( - graph_hundred_component_at_least_one_none_zero_digit + delete_space + pynutil.delete("vạn"), - pynutil.insert("0000", weight=0.1), - ) - - graph_ten_thousand_suffix = pynini.union( - graph_digit + delete_space + pynutil.delete(pynini.union("nghìn", "ngàn")), - pynutil.insert("0", weight=0.1), - ) + ).optimize() graph_million = pynini.union( graph_hundred_component_at_least_one_none_zero_digit + delete_space + pynutil.delete("triệu"), pynutil.insert("000", weight=0.1), - ) + ).optimize() graph_billion = pynini.union( graph_hundred_component_at_least_one_none_zero_digit + delete_space + pynutil.delete(pynini.union("tỉ", "tỷ")), pynutil.insert("000", weight=0.1), - ) + ).optimize() + # Main graph combining all magnitude levels graph = pynini.union( + # Full format: billion + million + thousand + hundred graph_billion + delete_space + graph_million + delete_space + graph_thousands + delete_space - + graph_hundred_ties_zero, - graph_ten_thousand + delete_space + graph_ten_thousand_suffix + delete_space + graph_hundred_ties_zero, + + graph_hundreds_zero, + # Special thousand format with last digit or "rưỡi" (half) graph_hundred_component_at_least_one_none_zero_digit + delete_space - + pynutil.delete(pynini.union("nghìn", "ngàn")) + + pynutil.delete(thousand_words) + delete_space - + (((last_digit | graph_half) + pynutil.insert("00")) | graph_hundred_ties_zero), + + (((last_digit | graph_half) + pynutil.insert("00", weight=0.1)) | graph_hundreds_zero), + # Single digits (for non-exception cases) graph_digit, graph_zero, ) - graph = graph @ pynini.union( - pynutil.delete(pynini.closure("0")) + pynini.difference(NEMO_DIGIT, "0") + pynini.closure(NEMO_DIGIT), - "0", + graph = ( + graph + @ pynini.union( + pynutil.delete(pynini.closure("0")) + pynini.difference(NEMO_DIGIT, "0") + pynini.closure(NEMO_DIGIT), + "0", + ).optimize() ) # don't convert cardinals from zero to nine inclusive - graph_exception = pynini.project(pynini.union(graph_digit, graph_zero), "input") + single_digits = pynini.project(pynini.union(graph_digit, graph_zero), "input").optimize() self.graph_no_exception = graph - self.graph = (pynini.project(graph, "input") - graph_exception.arcsort()) @ graph + self.graph = pynini.difference(pynini.project(graph, "input"), single_digits) @ graph optional_minus_graph = pynini.closure( - pynutil.insert("negative: ") + pynini.cross(pynini.union("âm", "trừ"), '"-"') + NEMO_SPACE, + pynutil.insert("negative: ") + pynini.cross(negative_words, '"-"') + NEMO_SPACE, 0, 1, ) diff --git a/nemo_text_processing/inverse_text_normalization/vi/taggers/date.py b/nemo_text_processing/inverse_text_normalization/vi/taggers/date.py index 21576efd5..56e6b007b 100644 --- a/nemo_text_processing/inverse_text_normalization/vi/taggers/date.py +++ b/nemo_text_processing/inverse_text_normalization/vi/taggers/date.py @@ -19,143 +19,143 @@ from nemo_text_processing.inverse_text_normalization.vi.graph_utils import GraphFst, delete_extra_space, delete_space from nemo_text_processing.inverse_text_normalization.vi.utils import get_abs_path -graph_teen = pynini.string_file(get_abs_path("data/numbers/teen.tsv")).optimize() -graph_digit = pynini.string_file(get_abs_path("data/numbers/digit.tsv")).optimize() -graph_zero = pynini.string_file(get_abs_path("data/numbers/zero.tsv")).optimize() -ties_graph = pynini.string_file(get_abs_path("data/numbers/ties.tsv")).optimize() - -def _get_month_graph(): - """ - Transducer for month, e.g. march -> march +class DateFst(GraphFst): """ - month_graph = pynini.string_file(get_abs_path("data/months.tsv")).optimize() - return month_graph - + Finite state transducer for classifying date, + e.g. mười lăm tháng một năm hai nghìn mười hai -> date { day: "15" month: "1" year: "2012" preserve_order: true } + e.g. ngày ba mốt tháng mười hai năm một chín chín chín -> date { day: "31" month: "12" year: "2012" preserve_order: true } + e.g. năm hai không hai mốt -> date { year: "2021" preserve_order: true } -def _get_ties_graph(): - """ - Transducer for 20-99 e.g - hai ba -> 23 + Args: + cardinal: CardinalFst """ - graph_one = pynini.cross("mốt", "1") - graph_four = pynini.cross("tư", "4") - graph_five = pynini.cross("lăm", "5") - graph_ten = pynini.cross("mươi", "") - optional_ten = pynini.closure(delete_space + graph_ten, 0, 1) - graph = pynini.union( - ties_graph + optional_ten + delete_space + (graph_digit | graph_one | graph_four | graph_five), - ties_graph + delete_space + graph_ten + pynutil.insert("0"), - ) - return graph + def __init__(self, cardinal: GraphFst): + super().__init__(name="date", kind="classify") + cardinal_graph = cardinal.graph_no_exception + YEAR_WEIGHT = 0.001 -def _get_year_graph(): - """ - Transducer for year, e.g. hai không hai mươi -> 2020 - """ + graph_teen = pynini.string_file(get_abs_path("data/numbers/teen.tsv")).optimize() + graph_digit = pynini.string_file(get_abs_path("data/numbers/digit.tsv")).optimize() + graph_zero = pynini.string_file(get_abs_path("data/numbers/zero.tsv")).optimize() + ties_graph = pynini.string_file(get_abs_path("data/numbers/ties.tsv")).optimize() + + # Special digit mappings for Vietnamese + graph_one = pynini.cross("mốt", "1") + graph_four = pynini.cross("tư", "4") + graph_five = pynini.cross("lăm", "5") + graph_ten = pynini.cross("mươi", "") + optional_ten = pynini.closure(delete_space + graph_ten, 0, 1) + # Ties graph for 20-99 (e.g., "hai ba" -> "23") + graph_ties = pynini.union( + ties_graph + optional_ten + delete_space + pynini.union(graph_digit, graph_one, graph_four, graph_five), + ties_graph + delete_space + graph_ten + pynutil.insert("0"), + ) - def _get_digits_graph(): + # Zero prefix patterns (e.g., "linh năm" -> "05") zero = pynini.cross((pynini.union("linh", "lẻ")), "0") - four = pynini.cross("tư", "4") - graph = pynini.union( - zero + delete_space + (graph_digit | four), + graph_digits = pynini.union( + zero + delete_space + pynini.union(graph_digit, graph_four), graph_zero + delete_space + graph_digit, - ) - graph.optimize() - return graph + ).optimize() - def _get_hundreds_graph(graph_ties, graph_digits): - graph = ( + # Year component builders + # Hundreds pattern (e.g., "hai trăm mười hai" -> "212") + graph_hundreds = ( graph_digit + delete_space + pynutil.delete("trăm") + delete_space - + (graph_teen | graph_ties | graph_digits) + + pynini.union(graph_teen, graph_ties, graph_digits) ) - return graph - def _get_thousands_graph(graph_ties, graph_digits): - graph_hundred_component = ( - (graph_digit | graph_zero) + delete_space + pynutil.delete("trăm") - ) | pynutil.insert("0") - graph = ( + # Thousands pattern (e.g., "hai nghìn không ba" -> "2003") + graph_hundred_component = pynini.union( + pynini.union(graph_digit, graph_zero) + delete_space + pynutil.delete("trăm"), pynutil.insert("0") + ) + graph_thousands = ( graph_digit + delete_space + pynutil.delete(pynini.union("nghìn", "ngàn")) + delete_space + graph_hundred_component + delete_space - + (graph_teen | graph_ties | graph_digits) + + pynini.union(graph_teen, graph_ties, graph_digits) ) - return graph - - graph_ties = _get_ties_graph() - graph_digits = _get_digits_graph() - graph_hundreds = _get_hundreds_graph(graph_ties, graph_digits) - graph_thousands = _get_thousands_graph(graph_ties, graph_digits) - year_graph = ( - # 20 19, 40 12, 2012, 2 0 0 5, 2 0 17, 938 - assuming no limit on the year - graph_digit - + delete_space - + (graph_digit | graph_zero) - + delete_space - + (graph_teen | graph_ties | graph_digits) - | graph_thousands - | graph_hundreds - | (graph_digit + pynutil.insert("0") + delete_space + (graph_ties | graph_digits | graph_teen)) - ) - year_graph.optimize() - return year_graph - - -class DateFst(GraphFst): - """ - Finite state transducer for classifying date, - e.g. mười lăm tháng một năm hai nghìn mười hai -> date { day: "15" month: "1" year: "2012" preserve_order: true } - e.g. ngày ba mốt tháng mười hai năm một chín chín chín -> date { day: "31" month: "12" year: "2012" preserve_order: true } - e.g. năm hai không hai mốt -> date { year: "2021" preserve_order: true } - - Args: - cardinal: CardinalFst - """ - - def __init__(self, cardinal: GraphFst): - super().__init__(name="date", kind="classify") - - cardinal_graph = cardinal.graph_no_exception - year_graph = _get_year_graph() - YEAR_WEIGHT = 0.001 - year_graph = pynutil.add_weight(year_graph, YEAR_WEIGHT) - month_graph = _get_month_graph() - month_graph = pynutil.insert('month: "') + month_graph + pynutil.insert('"') + # Complete year graph with all supported patterns + year_graph_raw = pynini.union( + # Standard patterns: 2019, 2012, 2005, etc. + graph_digit + + delete_space + + pynini.union(graph_digit, graph_zero) + + delete_space + + pynini.union(graph_teen, graph_ties, graph_digits), + graph_thousands, + graph_hundreds, + (graph_digit + pynutil.insert("0") + delete_space + pynini.union(graph_ties, graph_digits, graph_teen)), + ( + pynini.union(graph_digit, graph_zero) + + delete_space + + pynini.union(graph_digit, graph_zero) + + delete_space + + pynini.union(graph_digit, graph_zero) + + delete_space + + pynini.union(graph_digit, graph_zero) + ), + ).optimize() + + year_graph = pynutil.add_weight(year_graph_raw, YEAR_WEIGHT) + + # Month graph with special handling for "năm" (means "5" in months but "year" in other contexts) + month_graph = ( + pynutil.insert('month: "') + + pynini.string_file(get_abs_path("data/months.tsv")).optimize() + + pynutil.insert('"') + ) month_exception = pynini.project(pynini.cross("năm", "5"), "input") month_graph_exception = (pynini.project(month_graph, "input") - month_exception.arcsort()) @ month_graph day_graph = pynutil.insert('day: "') + cardinal_graph + pynutil.insert('"') - # day_suffix = pynini.union("ngày", "mùng") - # optional_day = pynini.closure(day_suffix + delete_space, 0, 1) - graph_month = pynutil.delete("tháng") + delete_space + month_graph_exception - graph_year = ( + + graph_year = pynutil.add_weight( delete_extra_space + pynutil.delete("năm") + delete_extra_space + pynutil.insert('year: "') + pynutil.add_weight(year_graph, -YEAR_WEIGHT) - + pynutil.insert('"') + + pynutil.insert('"'), + -0.1, ) - optional_graph_year = pynini.closure(graph_year, 0, 1) - graph_my = pynutil.delete("tháng") + delete_space + month_graph + graph_year + + # Date pattern combinations + # Pattern 1: Day-Month-Year (e.g., "ngày 15 tháng 1 năm 2024") graph_dmy = ( - day_graph + delete_space + pynutil.delete("tháng") + delete_extra_space + month_graph + optional_graph_year + day_graph + + delete_space + + pynutil.delete("tháng") + + delete_extra_space + + month_graph + + pynini.closure(graph_year, 0, 1) ) - graph_year = ( - pynutil.delete("năm") + delete_extra_space + pynutil.insert('year: "') + year_graph + pynutil.insert('"') + + # Pattern 2: Month-Year (e.g., "tháng 1 năm 2024") + graph_my = pynutil.delete("tháng") + delete_space + month_graph + graph_year + + # Pattern 3: Standalone year (e.g., "năm 2024") + graph_year_standalone = pynutil.add_weight( + pynutil.delete("năm") + delete_extra_space + pynutil.insert('year: "') + year_graph + pynutil.insert('"'), + -0.1, ) - final_graph = (graph_dmy | graph_my | graph_month | graph_year) + pynutil.insert(" preserve_order: true") - final_graph = self.add_tokens(final_graph) - self.fst = final_graph.optimize() + final_graph = pynini.union( + graph_dmy, # Day-Month-Year + graph_my, # Month-Year + graph_month, # Month only + graph_year_standalone, # Year only + ) + pynutil.insert(" preserve_order: true") + + self.fst = self.add_tokens(final_graph).optimize() diff --git a/nemo_text_processing/inverse_text_normalization/vi/taggers/decimal.py b/nemo_text_processing/inverse_text_normalization/vi/taggers/decimal.py index 60c550228..7c2bd2151 100644 --- a/nemo_text_processing/inverse_text_normalization/vi/taggers/decimal.py +++ b/nemo_text_processing/inverse_text_normalization/vi/taggers/decimal.py @@ -21,64 +21,10 @@ GraphFst, delete_extra_space, delete_space, + insert_space, ) from nemo_text_processing.inverse_text_normalization.vi.utils import get_abs_path -graph_digit = pynini.string_file(get_abs_path("data/numbers/digit.tsv")) - - -def get_quantity(decimal: "pynini.FstLike", cardinal_up_to_hundred: "pynini.FstLike") -> "pynini.FstLike": - """ - Returns FST that transforms either a cardinal or decimal followed by a quantity into a numeral, - e.g. một triệu -> integer_part: "1" quantity: "triệu" - e.g. một tỷ rưỡi -> integer_part: "1" fractional_part: "5" quantity: "tỷ" - - Args: - decimal: decimal FST - cardinal_up_to_hundred: cardinal FST - """ - numbers = cardinal_up_to_hundred @ ( - pynutil.delete(pynini.closure("0")) + pynini.difference(NEMO_DIGIT, "0") + pynini.closure(NEMO_DIGIT) - ) - suffix = pynini.union("triệu", "tỉ", "tỷ", "vạn") - graph_four = pynini.cross("tư", "4") - graph_one = pynini.cross("mốt", "1") - graph_half = pynini.cross("rưỡi", "5") - last_digit_exception = pynini.project(pynini.cross("năm", "5"), "input") - last_digit = pynini.union( - (pynini.project(graph_digit, "input") - last_digit_exception.arcsort()) @ graph_digit, - graph_one, - graph_four, - graph_half, - ) - optional_fraction_graph = pynini.closure( - delete_extra_space - + pynutil.insert('fractional_part: "') - + (last_digit | graph_half | graph_one | graph_four) - + pynutil.insert('"'), - 0, - 1, - ) - - res = ( - pynutil.insert('integer_part: "') - + numbers - + pynutil.insert('"') - + delete_extra_space - + pynutil.insert('quantity: "') - + suffix - + pynutil.insert('"') - + optional_fraction_graph - ) - res |= ( - decimal - + delete_extra_space - + pynutil.insert('quantity: "') - + (suffix | "ngàn" | "nghìn") - + pynutil.insert('"') - ) - return res - class DecimalFst(GraphFst): """ @@ -95,40 +41,88 @@ def __init__(self, cardinal: GraphFst): cardinal_graph = cardinal.graph_no_exception - graph_decimal = graph_digit | pynini.string_file(get_abs_path("data/numbers/zero.tsv")) - graph_one = pynini.cross("mốt", "1") + graph_zero = pynini.string_file(get_abs_path("data/numbers/zero.tsv")) + graph_digit = pynini.string_file(get_abs_path("data/numbers/digit.tsv")) + base_decimal = graph_digit | graph_zero graph_four = pynini.cross("tư", "4") graph_five = pynini.cross("lăm", "5") + graph_one = pynini.cross("mốt", "1") + negative_words = pynini.union("âm", "trừ") graph_decimal = pynini.union( - graph_decimal, + base_decimal, graph_four, - pynini.closure(graph_decimal + delete_space, 1) + (graph_decimal | graph_four | graph_five | graph_one), - ) + pynini.closure(base_decimal + delete_space, 1) + (base_decimal | graph_four | graph_five | graph_one), + ).optimize() self.graph = graph_decimal point = pynutil.delete("chấm") | pynutil.delete("phẩy") - optional_graph_negative = pynini.closure( - pynutil.insert("negative: ") + pynini.cross(pynini.union("âm", "trừ"), '"true"') + delete_extra_space, + pynutil.insert("negative:") + insert_space + pynini.cross(negative_words, '"true"') + delete_extra_space, 0, 1, ) - graph_fractional = pynutil.insert('fractional_part: "') + graph_decimal + pynutil.insert('"') + graph_fractional = ( + pynutil.insert('fractional_part:') + + insert_space + + pynutil.insert('"') + + graph_decimal + + pynutil.insert('"') + ) graph_integer = pynutil.insert('integer_part: "') + cardinal_graph + pynutil.insert('"') final_graph_wo_sign = ( pynini.closure(graph_integer + delete_extra_space, 0, 1) + point + delete_extra_space + graph_fractional ) - final_graph = optional_graph_negative + final_graph_wo_sign + # Build quantity handling - reuse magnitude words from cardinal context + # e.g. một triệu -> integer_part: "1" quantity: "triệu" + # e.g. một tỷ rưỡi -> integer_part: "1" fractional_part: "5" quantity: "tỷ" + numbers = cardinal.graph_hundred_component_at_least_one_none_zero_digit @ ( + pynutil.delete(pynini.closure("0")) + pynini.difference(NEMO_DIGIT, "0") + pynini.closure(NEMO_DIGIT) + ) - self.final_graph_wo_negative = final_graph_wo_sign | get_quantity( - final_graph_wo_sign, - cardinal.graph_hundred_component_at_least_one_none_zero_digit, + magnitude_words = pynini.union("triệu", "tỉ", "tỷ", "vạn") + thousand_words = pynini.union("ngàn", "nghìn") + + last_digit = cardinal.last_digit + optional_fraction_graph = pynini.closure( + delete_extra_space + + pynutil.insert('fractional_part:') + + insert_space + + pynutil.insert('"') + + (last_digit | pynini.cross("rưỡi", "5") | graph_one | graph_four) + + pynutil.insert('"'), + 0, + 1, ) - final_graph |= optional_graph_negative + get_quantity( - final_graph_wo_sign, - cardinal.graph_hundred_component_at_least_one_none_zero_digit, + + quantity_graph = ( + pynutil.insert('integer_part:') + + insert_space + + pynutil.insert('"') + + numbers + + pynutil.insert('"') + + delete_extra_space + + pynutil.insert('quantity:') + + insert_space + + pynutil.insert('"') + + magnitude_words + + pynutil.insert('"') + + optional_fraction_graph + ) + quantity_graph |= ( + final_graph_wo_sign + + delete_extra_space + + pynutil.insert('quantity:') + + insert_space + + pynutil.insert('"') + + (magnitude_words | thousand_words) + + pynutil.insert('"') ) + + final_graph = optional_graph_negative + final_graph_wo_sign + + self.final_graph_wo_negative = final_graph_wo_sign | quantity_graph + final_graph |= optional_graph_negative + quantity_graph final_graph = self.add_tokens(final_graph) self.fst = final_graph.optimize() diff --git a/nemo_text_processing/inverse_text_normalization/vi/taggers/electronic.py b/nemo_text_processing/inverse_text_normalization/vi/taggers/electronic.py index e7f5b3697..6ec3b1641 100644 --- a/nemo_text_processing/inverse_text_normalization/vi/taggers/electronic.py +++ b/nemo_text_processing/inverse_text_normalization/vi/taggers/electronic.py @@ -30,15 +30,17 @@ def __init__(self): super().__init__(name="electronic", kind="classify") delete_extra_space = pynutil.delete(" ") - alpha_num = ( - NEMO_ALPHA - | pynini.string_file(get_abs_path("data/numbers/digit.tsv")) - | pynini.string_file(get_abs_path("data/numbers/zero.tsv")) + alpha_num = pynini.union( + NEMO_ALPHA, + pynini.string_file(get_abs_path("data/numbers/digit.tsv")), + pynini.string_file(get_abs_path("data/numbers/zero.tsv")), ) symbols = pynini.string_file(get_abs_path("data/electronic/symbols.tsv")).invert() + url_symbols = pynini.string_file(get_abs_path("data/electronic/url_symbols.tsv")).invert() - accepted_username = alpha_num | symbols + accepted_username = pynini.union(alpha_num, symbols) + accepted_url_chars = pynini.union(alpha_num, url_symbols) process_dot = pynini.cross("chấm", ".") username = ( pynutil.insert('username: "') @@ -47,8 +49,16 @@ def __init__(self): + pynutil.insert('"') ) single_alphanum = pynini.closure(alpha_num + delete_extra_space) + alpha_num - server = single_alphanum | pynini.string_file(get_abs_path("data/electronic/server_name.tsv")) - domain = single_alphanum | pynini.string_file(get_abs_path("data/electronic/domain.tsv")) + server = pynini.union( + single_alphanum, + pynini.string_file(get_abs_path("data/electronic/server_name.tsv")), + pynini.closure(NEMO_ALPHA, 2), # At least 2 letters for server name + ) + domain = pynini.union( + single_alphanum, + pynini.string_file(get_abs_path("data/electronic/domain.tsv")), + pynini.closure(NEMO_ALPHA, 2), # At least 2 letters for domain + ) multi_domain = ( pynini.closure(process_dot + delete_extra_space + domain + delete_extra_space) + process_dot @@ -67,27 +77,31 @@ def __init__(self): ############# url ### protocol_end = pynini.cross(pynini.union("w w w", "www"), "www") - protocol_start = (pynini.cross("h t t p", "http") | pynini.cross("h t t p s", "https")) + pynini.cross( - " hai chấm sẹc sẹc ", "://" - ) - # .com, - ending = ( + protocol_start = pynini.union( + pynini.cross("h t t p", "http"), pynini.cross("h t t p s", "https") + ) + pynini.cross(" hai chấm sẹc sẹc ", "://") + + # Domain part: server.domain (e.g., nvidia.com, www.nvidia.com) + url_domain = server + delete_extra_space + process_dot + delete_extra_space + domain + + # Optional endings: /path or .vn or .com.vn + url_ending = ( delete_extra_space - + symbols + + url_symbols + delete_extra_space - + (domain | pynini.closure(accepted_username + delete_extra_space) + accepted_username) + + pynini.union(domain, pynini.closure(accepted_url_chars + delete_extra_space) + accepted_url_chars) ) - protocol = ( - pynini.closure(protocol_start, 0, 1) - + protocol_end - + delete_extra_space - + process_dot - + pynini.closure(delete_extra_space + accepted_username, 1) - + pynini.closure(ending, 1, 2) + pynini.closure(protocol_start, 0, 1) # Optional http:// + + pynini.closure( + protocol_end + delete_extra_space + process_dot + delete_extra_space, 0, 1 + ) # Optional www. + + url_domain # Required: server.domain + + pynini.closure(url_ending, 0) # Optional: /path or .vn ) + protocol = pynutil.insert('protocol: "') + protocol + pynutil.insert('"') - graph |= protocol + graph = pynini.union(graph, protocol) ######## final_graph = self.add_tokens(graph) diff --git a/nemo_text_processing/inverse_text_normalization/vi/taggers/fraction.py b/nemo_text_processing/inverse_text_normalization/vi/taggers/fraction.py index 9aacf93bd..4fe845569 100644 --- a/nemo_text_processing/inverse_text_normalization/vi/taggers/fraction.py +++ b/nemo_text_processing/inverse_text_normalization/vi/taggers/fraction.py @@ -16,7 +16,12 @@ import pynini from pynini.lib import pynutil -from nemo_text_processing.inverse_text_normalization.vi.graph_utils import GraphFst, delete_extra_space, delete_space +from nemo_text_processing.inverse_text_normalization.vi.graph_utils import ( + GraphFst, + delete_extra_space, + delete_space, + insert_space, +) class FractionFst(GraphFst): @@ -32,14 +37,21 @@ class FractionFst(GraphFst): def __init__(self, cardinal: GraphFst): super().__init__(name="fraction", kind="classify") - # integer_part # numerator # denominator graph_cardinal = cardinal.graph_no_exception graph_four = pynini.cross("tư", "4") - numerator = pynutil.insert('numerator: "') + graph_cardinal + pynutil.insert('"') + numerator = ( + pynutil.insert('numerator:') + insert_space + pynutil.insert('"') + graph_cardinal + pynutil.insert('"') + ) fraction_component = pynutil.delete(pynini.union("phần", "trên", "chia")) - denominator = pynutil.insert('denominator: "') + (graph_cardinal | graph_four) + pynutil.insert('"') + denominator = ( + pynutil.insert('denominator:') + + insert_space + + pynutil.insert('"') + + (graph_cardinal | graph_four) + + pynutil.insert('"') + ) graph_fraction_component = numerator + delete_space + fraction_component + delete_extra_space + denominator self.graph_fraction_component = graph_fraction_component @@ -49,7 +61,10 @@ def __init__(self, cardinal: GraphFst): self.final_graph_wo_negative = graph optional_graph_negative = pynini.closure( - pynutil.insert("negative: ") + pynini.cross(pynini.union("âm", "trừ"), '"true"') + delete_extra_space, + pynutil.insert("negative:") + + insert_space + + pynini.cross(pynini.union("âm", "trừ"), '"true"') + + delete_extra_space, 0, 1, ) diff --git a/nemo_text_processing/inverse_text_normalization/vi/taggers/measure.py b/nemo_text_processing/inverse_text_normalization/vi/taggers/measure.py index 6ffa64b04..fef7d6d93 100644 --- a/nemo_text_processing/inverse_text_normalization/vi/taggers/measure.py +++ b/nemo_text_processing/inverse_text_normalization/vi/taggers/measure.py @@ -59,7 +59,9 @@ def __init__(self, cardinal: GraphFst, decimal: GraphFst): unit_singular = ( pynutil.insert('units: "') - + (unit_singular | unit_misc | pynutil.add_weight(unit_singular + delete_space + unit_misc, 0.01)) + + pynini.union( + unit_singular, unit_misc, pynutil.add_weight(unit_singular + delete_space + unit_misc, 0.01) + ) + pynutil.insert('"') ) @@ -85,20 +87,23 @@ def __init__(self, cardinal: GraphFst, decimal: GraphFst): fraction_graph = ( delete_extra_space + pynutil.insert('fractional_part: "') - + (graph_digit | graph_half | graph_one | graph_four) + + pynini.union(graph_digit, graph_half, graph_one, graph_four) + pynutil.insert('"') ) - subgraph_cardinal |= ( - pynutil.insert("cardinal { ") - + optional_graph_negative - + pynutil.insert('integer: "') - + cardinal_graph - + pynutil.insert('" }') - + delete_extra_space - + unit_singular - + fraction_graph + subgraph_cardinal = pynini.union( + subgraph_cardinal, + ( + pynutil.insert("cardinal { ") + + optional_graph_negative + + pynutil.insert('integer: "') + + cardinal_graph + + pynutil.insert('" }') + + delete_extra_space + + unit_singular + + fraction_graph + ), ) - final_graph = subgraph_decimal | subgraph_cardinal + final_graph = pynini.union(subgraph_decimal, subgraph_cardinal) final_graph = self.add_tokens(final_graph) self.fst = final_graph.optimize() diff --git a/nemo_text_processing/inverse_text_normalization/vi/taggers/money.py b/nemo_text_processing/inverse_text_normalization/vi/taggers/money.py index 414beab1e..b2aacc6e5 100644 --- a/nemo_text_processing/inverse_text_normalization/vi/taggers/money.py +++ b/nemo_text_processing/inverse_text_normalization/vi/taggers/money.py @@ -49,13 +49,13 @@ def __init__(self, cardinal: GraphFst, decimal: GraphFst): graph_unit_singular = pynutil.insert('currency: "') + convert_space(unit_singular) + pynutil.insert('"') - add_leading_zero_to_double_digit = (NEMO_DIGIT + NEMO_DIGIT) | (pynutil.insert("0") + NEMO_DIGIT) + add_leading_zero_to_double_digit = pynini.union((NEMO_DIGIT + NEMO_DIGIT), (pynutil.insert("0") + NEMO_DIGIT)) # twelve dollars fifty, only after integer optional_cents_suffix = pynini.closure( delete_extra_space + pynutil.insert('fractional_part: "') - + (pynutil.add_weight(cardinal_graph @ add_leading_zero_to_double_digit, -0.7) | graph_half) + + pynini.union(pynutil.add_weight(cardinal_graph @ add_leading_zero_to_double_digit, -0.7), graph_half) + pynutil.insert('"'), 0, 1, @@ -71,6 +71,6 @@ def __init__(self, cardinal: GraphFst, decimal: GraphFst): ) graph_decimal = graph_decimal_final + delete_extra_space + graph_unit_singular + optional_cents_suffix - final_graph = graph_integer | graph_decimal + final_graph = pynini.union(graph_integer, graph_decimal) final_graph = self.add_tokens(final_graph) self.fst = final_graph.optimize() diff --git a/nemo_text_processing/inverse_text_normalization/vi/taggers/ordinal.py b/nemo_text_processing/inverse_text_normalization/vi/taggers/ordinal.py index 98b3ac981..62b1c1205 100644 --- a/nemo_text_processing/inverse_text_normalization/vi/taggers/ordinal.py +++ b/nemo_text_processing/inverse_text_normalization/vi/taggers/ordinal.py @@ -16,7 +16,7 @@ import pynini from pynini.lib import pynutil -from nemo_text_processing.inverse_text_normalization.vi.graph_utils import GraphFst, delete_space +from nemo_text_processing.inverse_text_normalization.vi.graph_utils import GraphFst, delete_space, insert_space from nemo_text_processing.inverse_text_normalization.vi.utils import get_abs_path @@ -34,6 +34,14 @@ def __init__(self): graph = graph_digit self.graph = graph - final_graph = pynutil.insert('integer: "') + graph_ordinal + delete_space + self.graph + pynutil.insert('"') + final_graph = ( + pynutil.insert('integer:') + + insert_space + + pynutil.insert('"') + + graph_ordinal + + delete_space + + self.graph + + pynutil.insert('"') + ) final_graph = self.add_tokens(final_graph) self.fst = final_graph.optimize() diff --git a/nemo_text_processing/inverse_text_normalization/vi/taggers/telephone.py b/nemo_text_processing/inverse_text_normalization/vi/taggers/telephone.py index 52b1a6124..6a71d918a 100644 --- a/nemo_text_processing/inverse_text_normalization/vi/taggers/telephone.py +++ b/nemo_text_processing/inverse_text_normalization/vi/taggers/telephone.py @@ -16,26 +16,118 @@ import pynini from pynini.lib import pynutil -from nemo_text_processing.inverse_text_normalization.vi.graph_utils import GraphFst, delete_space +from nemo_text_processing.inverse_text_normalization.vi.graph_utils import ( + NEMO_DIGIT, + GraphFst, + delete_space, + insert_space, +) from nemo_text_processing.inverse_text_normalization.vi.utils import get_abs_path class TelephoneFst(GraphFst): """ - Finite state transducer for classifying telephone numbers, e.g. - một hai ba một hai ba năm sáu bảy tám -> { number_part: "1231235678" } + Finite state transducer for classifying telephone numbers and IP addresses. + + Supported formats: + + 1. Basic telephone (Vietnamese mobile with formatting): + "không chín ba sáu năm năm năm bốn bốn chín" + -> telephone { number_part: "093-655-5449" } + + 2. International format with country code: + "cộng tám mươi bốn không chín ba sáu năm năm năm bốn bốn chín" + -> telephone { country_code: "+84" number_part: "093-655-5449" } + + 3. IP addresses (using "chấm" for dot): + "một chín hai chấm một sáu tám chấm không chấm một" + -> telephone { number_part: "192.168.0.1" } + + 4. Emergency/hotline numbers: + "một một hai" -> telephone { number_part: "112" } + + 5. Credit card (15-16 digits): + "một hai ba bốn năm sáu bảy tám chín mười một hai ba bốn năm sáu" + -> telephone { number_part: "1234 5678 9101 2345" } + + Args: + cardinal: CardinalFst - required for parsing multi-digit numbers like "tám mươi bốn" """ - def __init__(self): + def __init__(self, cardinal: GraphFst): super().__init__(name="telephone", kind="classify") + graph_zero = pynini.string_file(get_abs_path("data/numbers/zero.tsv")) graph_digit = pynini.string_file(get_abs_path("data/numbers/digit.tsv")) - digit = graph_digit | graph_zero - last_digit = digit | pynini.cross("mốt", "1") | pynini.cross("tư", "4") | pynini.cross("lăm", "5") + digit = pynini.union(graph_digit, graph_zero) + last_digit = pynini.union(digit, pynini.cross("mốt", "1"), pynini.cross("tư", "4"), pynini.cross("lăm", "5")) + cardinal_two_digit = pynini.compose(cardinal.graph_no_exception, NEMO_DIGIT**2) + + vietnamese_mobile = pynini.compose( + pynini.cross("không", "0") + delete_space + pynini.closure(digit + delete_space, 8) + last_digit, + pynini.accep("0") + + NEMO_DIGIT**2 + + pynutil.insert("-") + + NEMO_DIGIT**3 + + pynutil.insert("-") + + NEMO_DIGIT**4, + ) + + basic_digits = pynini.closure(digit + delete_space, 2) + last_digit + + basic_phone = pynini.union(pynutil.add_weight(vietnamese_mobile, -0.01), basic_digits) + + country_code_digits = pynini.union( + pynutil.add_weight(cardinal_two_digit, -0.001), digit + delete_space + digit, digit + ) + + phone_with_country_code = ( + pynutil.insert('country_code: "') + + pynini.cross("cộng ", "+") + + country_code_digits + + pynutil.insert('"') + + delete_space + + insert_space + + pynutil.insert('number_part: "') + + basic_phone + + pynutil.insert('"') + ) + + phone_basic = pynutil.insert('number_part: "') + basic_phone + pynutil.insert('"') + + basic_phone_graph = pynini.union(pynutil.add_weight(phone_with_country_code, -0.1), phone_basic) + + ip_octet = pynini.union(pynini.closure(digit + delete_space, 0, 2) + digit, cardinal_two_digit) + + ip_graph = ip_octet + (delete_space + pynini.cross("chấm", ".") + delete_space + ip_octet) ** 3 + + ip_with_tag = pynutil.insert('number_part: "') + ip_graph + pynutil.insert('"') + + sixteen_digits = pynini.closure(digit + delete_space, 15) + digit + card_16 = pynini.compose( + sixteen_digits, + NEMO_DIGIT**4 + + pynutil.insert(" ") + + NEMO_DIGIT**4 + + pynutil.insert(" ") + + NEMO_DIGIT**4 + + pynutil.insert(" ") + + NEMO_DIGIT**4, + ) + + fifteen_digits = pynini.closure(digit + delete_space, 14) + digit + card_15 = pynini.compose( + fifteen_digits, NEMO_DIGIT**4 + pynutil.insert(" ") + NEMO_DIGIT**6 + pynutil.insert(" ") + NEMO_DIGIT**5 + ) + + card_graph = pynini.union(card_16, card_15) + card_with_tag = pynutil.insert('number_part: "') + card_graph + pynutil.insert('"') - graph_number_part = pynini.closure(digit + delete_space, 2) + last_digit - number_part = pynutil.insert('number_part: "') + graph_number_part + pynutil.insert('"') + graph = pynini.union( + pynutil.add_weight(ip_with_tag, weight=-0.01), + pynutil.add_weight(card_with_tag, weight=-0.005), + basic_phone_graph, + ) - graph = number_part final_graph = self.add_tokens(graph) self.fst = final_graph.optimize() diff --git a/nemo_text_processing/inverse_text_normalization/vi/taggers/time.py b/nemo_text_processing/inverse_text_normalization/vi/taggers/time.py index 529744b10..93fb579ae 100644 --- a/nemo_text_processing/inverse_text_normalization/vi/taggers/time.py +++ b/nemo_text_processing/inverse_text_normalization/vi/taggers/time.py @@ -38,7 +38,6 @@ class TimeFst(GraphFst): def __init__(self): super().__init__(name="time", kind="classify") - # hours, minutes, seconds, suffix, zone, style, speak_period graph_hours_to = pynini.string_file(get_abs_path("data/time/hours_to.tsv")) graph_minutes_to = pynini.string_file(get_abs_path("data/time/minutes_to.tsv")) @@ -52,9 +51,17 @@ def __init__(self): optional_minute = pynini.closure(delete_space + minute, 0, 1) second = pynini.cross("giây", "") + # Zero prefix patterns for minutes (linh, lẻ, không) + # Examples: "linh năm" -> "05", "không tám" -> "08" + zero_prefix = pynini.union(pynini.cross("linh", "0"), pynini.cross("lẻ", "0"), pynini.cross("không", "0")) + graph_zero_minute = zero_prefix + delete_space + graph_minutes + graph_minute_extended = graph_minutes | graph_zero_minute + final_graph_hour = pynutil.insert('hours: "') + graph_hours + pynutil.insert('"') + delete_space + oclock - graph_minute = graph_minutes + optional_minute - graph_second = graph_minutes + delete_space + second + graph_minute = graph_minute_extended + optional_minute + graph_second = graph_minute_extended + delete_space + second + + # Optional time zone support final_time_zone_optional = pynini.closure( delete_space + insert_space @@ -65,6 +72,8 @@ def __init__(self): 1, ) + # Time pattern combinations + # Pattern 1: Hour + Minutes (e.g., "tám giờ hai mươi" -> 8:20) graph_hm = ( final_graph_hour + delete_extra_space @@ -73,6 +82,7 @@ def __init__(self): + pynutil.insert('"') ) + # Pattern 2: Hour + Minutes + Seconds (e.g., "tám giờ hai mươi phút ba mươi giây" -> 8:20:30) graph_hms = ( final_graph_hour + delete_extra_space @@ -87,6 +97,7 @@ def __init__(self): + pynutil.insert('"') ) + # Pattern 3: Minutes + Seconds only (e.g., "ba phút hai mươi giây" -> 3p20s) graph_ms = ( pynutil.insert('minutes: "') + graph_minutes @@ -99,9 +110,22 @@ def __init__(self): + pynutil.insert('"') ) + # Pattern 4: Hour + Seconds only (e.g., "ba giờ mười giây" -> 3:00:10) + graph_hs = ( + final_graph_hour + + delete_extra_space + + pynutil.insert('minutes: "0"') + + delete_extra_space + + pynutil.insert('seconds: "') + + graph_second + + pynutil.insert('"') + ) + + # "Kém" pattern components (e.g., "chín giờ kém hai mươi" -> 8:40) graph_hours_to_component = graph_hours @ graph_hours_to graph_minutes_to_component = graph_minutes @ graph_minutes_to + # Pattern 5: "Kém" time format (hour minus minutes) graph_time_to = ( pynutil.insert('hours: "') + graph_hours_to_component @@ -117,10 +141,18 @@ def __init__(self): + optional_minute ) - final_graph = (final_graph_hour | graph_hm | graph_hms) + final_time_zone_optional - final_graph |= graph_ms - final_graph |= graph_time_to + # Combine all time patterns + final_graph = ( + pynini.union( + final_graph_hour, # Hour only + graph_hm, # Hour + Minutes + graph_hms, # Hour + Minutes + Seconds + graph_hs, # Hour + Seconds + graph_ms, # Minutes + Seconds only + graph_time_to, # "Kém" pattern + ) + + final_time_zone_optional + ) final_graph = self.add_tokens(final_graph) - self.fst = final_graph.optimize() diff --git a/nemo_text_processing/inverse_text_normalization/vi/taggers/tokenize_and_classify.py b/nemo_text_processing/inverse_text_normalization/vi/taggers/tokenize_and_classify.py index 04edee6bd..0caf2171b 100644 --- a/nemo_text_processing/inverse_text_normalization/vi/taggers/tokenize_and_classify.py +++ b/nemo_text_processing/inverse_text_normalization/vi/taggers/tokenize_and_classify.py @@ -92,20 +92,20 @@ def __init__( whitelist_graph = WhiteListFst(input_file=whitelist).fst punct_graph = PunctuationFst().fst electronic_graph = ElectronicFst().fst - telephone_graph = TelephoneFst().fst + telephone_graph = TelephoneFst(cardinal=cardinal).fst classify = ( pynutil.add_weight(whitelist_graph, 1.01) - | pynutil.add_weight(time_graph, 1.05) - | pynutil.add_weight(date_graph, 1.09) - | pynutil.add_weight(decimal_graph, 1.08) + | pynutil.add_weight(time_graph, 1.09) + | pynutil.add_weight(date_graph, 1.1) + | pynutil.add_weight(decimal_graph, 1.1) | pynutil.add_weight(measure_graph, 1.1) | pynutil.add_weight(cardinal_graph, 1.1) | pynutil.add_weight(ordinal_graph, 1.1) - | pynutil.add_weight(fraction_graph, 1.09) - | pynutil.add_weight(money_graph, 1.07) + | pynutil.add_weight(fraction_graph, 1.1) + | pynutil.add_weight(money_graph, 1.1) | pynutil.add_weight(telephone_graph, 1.1) - | pynutil.add_weight(electronic_graph, 1.1) + | pynutil.add_weight(electronic_graph, 1.11) | pynutil.add_weight(word_graph, 100) ) diff --git a/nemo_text_processing/inverse_text_normalization/vi/verbalizers/date.py b/nemo_text_processing/inverse_text_normalization/vi/verbalizers/date.py index c7d3f21b6..13e5db8d4 100644 --- a/nemo_text_processing/inverse_text_normalization/vi/verbalizers/date.py +++ b/nemo_text_processing/inverse_text_normalization/vi/verbalizers/date.py @@ -68,7 +68,9 @@ def __init__(self): + delete_space ) - final_graph = (graph_y | graph_m | graph_dm | graph_dmy | graph_my) + delete_space + optional_preserve_order + final_graph = ( + pynini.union(graph_y, graph_m, graph_dm, graph_dmy, graph_my) + delete_space + optional_preserve_order + ) delete_tokens = self.delete_tokens(final_graph) self.fst = delete_tokens.optimize() diff --git a/nemo_text_processing/inverse_text_normalization/vi/verbalizers/decimal.py b/nemo_text_processing/inverse_text_normalization/vi/verbalizers/decimal.py index 1d039ea1d..d25621f66 100644 --- a/nemo_text_processing/inverse_text_normalization/vi/verbalizers/decimal.py +++ b/nemo_text_processing/inverse_text_normalization/vi/verbalizers/decimal.py @@ -15,7 +15,12 @@ import pynini from pynini.lib import pynutil -from nemo_text_processing.inverse_text_normalization.vi.graph_utils import NEMO_NOT_QUOTE, GraphFst, delete_space +from nemo_text_processing.inverse_text_normalization.vi.graph_utils import ( + NEMO_NOT_QUOTE, + NEMO_SPACE, + GraphFst, + delete_space, +) class DecimalFst(GraphFst): @@ -51,7 +56,7 @@ def __init__(self): + pynini.closure(NEMO_NOT_QUOTE, 1) + pynutil.delete('"') ) - optional_quantity = pynini.closure(pynutil.insert(" ") + quantity + delete_space, 0, 1) + optional_quantity = pynini.closure(pynutil.insert(NEMO_SPACE) + quantity + delete_space, 0, 1) graph = optional_integer + optional_fractional + optional_quantity self.numbers = graph graph = optionl_sign + graph diff --git a/nemo_text_processing/inverse_text_normalization/vi/verbalizers/electronic.py b/nemo_text_processing/inverse_text_normalization/vi/verbalizers/electronic.py index 582a1e1b3..443051a73 100644 --- a/nemo_text_processing/inverse_text_normalization/vi/verbalizers/electronic.py +++ b/nemo_text_processing/inverse_text_normalization/vi/verbalizers/electronic.py @@ -50,7 +50,7 @@ def __init__(self): ) graph = user_name + delete_space + pynutil.insert("@") + domain - graph |= protocol + graph = pynini.union(graph, protocol) delete_tokens = self.delete_tokens(graph) self.fst = delete_tokens.optimize() diff --git a/nemo_text_processing/inverse_text_normalization/vi/verbalizers/measure.py b/nemo_text_processing/inverse_text_normalization/vi/verbalizers/measure.py index 9cf68abb8..6342bc1e9 100644 --- a/nemo_text_processing/inverse_text_normalization/vi/verbalizers/measure.py +++ b/nemo_text_processing/inverse_text_normalization/vi/verbalizers/measure.py @@ -17,10 +17,11 @@ from pynini.lib import pynutil from nemo_text_processing.inverse_text_normalization.vi.graph_utils import ( - NEMO_CHAR, NEMO_NOT_QUOTE, + NEMO_NOT_SPACE, GraphFst, delete_space, + insert_space, ) @@ -37,11 +38,23 @@ class MeasureFst(GraphFst): def __init__(self, decimal: GraphFst, cardinal: GraphFst): super().__init__(name="measure", kind="verbalize") optional_sign = pynini.closure(pynini.cross('negative: "true"', "-"), 0, 1) - unit = ( + # Units that don't need space (time units) + no_space_units = pynini.union("s", "ms", "ns", "μs", "h", "min", "%") + + unit_no_space = ( + pynutil.delete("units:") + + delete_space + + pynutil.delete('"') + + no_space_units + + pynutil.delete('"') + + delete_space + ) + + unit_with_space = ( pynutil.delete("units:") + delete_space + pynutil.delete('"') - + pynini.closure(NEMO_CHAR - " ", 1) + + (pynini.closure(NEMO_NOT_SPACE, 1) - no_space_units) + pynutil.delete('"') + delete_space ) @@ -72,13 +85,25 @@ def __init__(self, decimal: GraphFst, cardinal: GraphFst): + pynutil.delete('"') ) optional_fractional = pynini.closure(fractional + delete_space, 0, 1) - graph = ( - (graph_cardinal | graph_decimal) + # Graph with no space for time units + graph_no_space = ( + pynini.union(graph_cardinal, graph_decimal) + delete_space + optional_fractional - + pynutil.insert(" ") - + unit + + unit_no_space + delete_space ) + + # Graph with space for other units + graph_with_space = ( + pynini.union(graph_cardinal, graph_decimal) + + delete_space + + optional_fractional + + insert_space + + unit_with_space + + delete_space + ) + + graph = pynini.union(graph_no_space, graph_with_space) delete_tokens = self.delete_tokens(graph) self.fst = delete_tokens.optimize() diff --git a/nemo_text_processing/inverse_text_normalization/vi/verbalizers/telephone.py b/nemo_text_processing/inverse_text_normalization/vi/verbalizers/telephone.py index 74d28decf..f1e2f9356 100644 --- a/nemo_text_processing/inverse_text_normalization/vi/verbalizers/telephone.py +++ b/nemo_text_processing/inverse_text_normalization/vi/verbalizers/telephone.py @@ -22,13 +22,30 @@ class TelephoneFst(GraphFst): """ Finite state transducer for verbalizing telephone, e.g. - telephone { number_part: "1231235678" } - -> 1231235678 + telephone { number_part: "123-123-5678" } -> 123-123-5678 + telephone { country_code: "+84" number_part: "936-555-449" } -> +84 936-555-449 + telephone { number_part: "192.168.0.1" } -> 192.168.0.1 + telephone { number_part: "1234 5678 9101 2345" } -> 1234 5678 9101 2345 + telephone { number_part: "x86" } -> x86 """ def __init__(self): super().__init__(name="telephone", kind="verbalize") number_part = pynutil.delete('number_part: "') + pynini.closure(NEMO_NOT_QUOTE, 1) + pynutil.delete('"') - delete_tokens = self.delete_tokens(number_part) + + # Optional country code + country_code = ( + pynutil.delete('country_code: "') + + pynini.closure(NEMO_NOT_QUOTE, 1) + + pynutil.delete('"') + + pynutil.delete(' ') + + pynutil.insert(' ') + ) + + optional_country_code = pynini.closure(country_code, 0, 1) + + graph = optional_country_code + number_part + + delete_tokens = self.delete_tokens(graph) self.fst = delete_tokens.optimize() diff --git a/nemo_text_processing/inverse_text_normalization/vi/verbalizers/time.py b/nemo_text_processing/inverse_text_normalization/vi/verbalizers/time.py index 2ad4d5bbf..d0492d3d0 100644 --- a/nemo_text_processing/inverse_text_normalization/vi/verbalizers/time.py +++ b/nemo_text_processing/inverse_text_normalization/vi/verbalizers/time.py @@ -87,6 +87,6 @@ def __init__(self): + pynutil.insert("s") ) - graph = (graph_h | graph_ms | graph_hms) + optional_zone + graph = pynini.union(graph_h, graph_ms, graph_hms) + optional_zone delete_tokens = self.delete_tokens(graph) self.fst = delete_tokens.optimize() diff --git a/nemo_text_processing/text_normalization/data_loader_utils.py b/nemo_text_processing/text_normalization/data_loader_utils.py index 5e7fa1892..9335a9745 100644 --- a/nemo_text_processing/text_normalization/data_loader_utils.py +++ b/nemo_text_processing/text_normalization/data_loader_utils.py @@ -45,6 +45,8 @@ "FRACTION", "TIME", "ADDRESS", + "ROMAN", + "RANGE", ] @@ -140,9 +142,9 @@ def evaluate(preds: List[str], labels: List[str], input: Optional[List[str]] = N acc = acc + 1 else: if input: - print(f"inpu: {json.dumps(input[i])}") - print(f"gold: {json.dumps(label_norm)}") - print(f"pred: {json.dumps(pred_norm)}") + print(f"input: {json.dumps(input[i], ensure_ascii=True)}") + print(f"gold: {json.dumps(label_norm, ensure_ascii=True)}") + print(f"pred: {json.dumps(pred_norm, ensure_ascii=True)}") return acc / nums diff --git a/nemo_text_processing/text_normalization/normalize.py b/nemo_text_processing/text_normalization/normalize.py index 82f8f43d2..4ce71fa2b 100644 --- a/nemo_text_processing/text_normalization/normalize.py +++ b/nemo_text_processing/text_normalization/normalize.py @@ -174,6 +174,13 @@ def __init__( elif lang == 'ja': from nemo_text_processing.text_normalization.ja.taggers.tokenize_and_classify import ClassifyFst from nemo_text_processing.text_normalization.ja.verbalizers.verbalize_final import VerbalizeFinalFst + elif lang == 'vi': + from nemo_text_processing.text_normalization.vi.taggers.tokenize_and_classify import ClassifyFst + from nemo_text_processing.text_normalization.vi.verbalizers.post_processing import PostProcessingFst + from nemo_text_processing.text_normalization.vi.verbalizers.verbalize_final import VerbalizeFinalFst + + if post_process: + self.post_processor = PostProcessingFst(cache_dir=cache_dir, overwrite_cache=overwrite_cache) else: raise NotImplementedError(f"Language {lang} has not been supported yet.") @@ -374,7 +381,7 @@ def normalize( return text output = SPACE_DUP.sub(' ', output[1:]) - if self.lang == "en" and hasattr(self, 'post_processor'): + if self.lang in ["en", "vi"] and hasattr(self, 'post_processor'): output = self.post_process(output) if punct_post_process: diff --git a/nemo_text_processing/text_normalization/run_evaluate.py b/nemo_text_processing/text_normalization/run_evaluate.py index 0438579a7..ae3160f78 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'], + choices=['ar', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ru', 'sv', 'zh', 'hy', 'hi', 'vi'], default="en", type=str, ) @@ -104,8 +104,6 @@ def parse_args(): print("- Accuracy: " + str(sum(token_weighted_accuracy) / sum(token_count_per_type.values()))) print(" - Total: " + str(sum(token_count_per_type.values())), '\n') - print(" - Total: " + str(sum(token_count_per_type.values())), '\n') - for token_type in token_accuracy: if token_type not in known_types: raise ValueError("Unexpected token type: " + token_type) diff --git a/nemo_text_processing/text_normalization/vi/__init__.py b/nemo_text_processing/text_normalization/vi/__init__.py new file mode 100644 index 000000000..b2de1dca7 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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/vi/data/__init__.py b/nemo_text_processing/text_normalization/vi/data/__init__.py new file mode 100644 index 000000000..b2de1dca7 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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/vi/data/date/__init__.py b/nemo_text_processing/text_normalization/vi/data/date/__init__.py new file mode 100644 index 000000000..b2de1dca7 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/date/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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/vi/data/date/days.tsv b/nemo_text_processing/text_normalization/vi/data/date/days.tsv new file mode 100644 index 000000000..5b70479a6 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/date/days.tsv @@ -0,0 +1,40 @@ +01 một +02 hai +03 ba +04 bốn +05 năm +06 sáu +07 bảy +08 tám +09 chín +1 một +2 hai +3 ba +4 bốn +5 năm +6 sáu +7 bảy +8 tám +9 chín +10 mười +11 mười một +12 mười hai +13 mười ba +14 mười bốn +15 mười lăm +16 mười sáu +17 mười bảy +18 mười tám +19 mười chín +20 hai mươi +21 hai mươi mốt +22 hai mươi hai +23 hai mươi ba +24 hai mươi bốn +25 hai mươi lăm +26 hai mươi sáu +27 hai mươi bảy +28 hai mươi tám +29 hai mươi chín +30 ba mươi +31 ba mươi mốt \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/date/months.tsv b/nemo_text_processing/text_normalization/vi/data/date/months.tsv new file mode 100644 index 000000000..fb836fba1 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/date/months.tsv @@ -0,0 +1,21 @@ +1 một +2 hai +3 ba +4 tư +5 năm +6 sáu +7 bảy +8 tám +9 chín +10 mười +11 mười một +12 mười hai +01 một +02 hai +03 ba +04 tư +05 năm +06 sáu +07 bảy +08 tám +09 chín \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/date/year_suffix.tsv b/nemo_text_processing/text_normalization/vi/data/date/year_suffix.tsv new file mode 100644 index 000000000..31b49f955 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/date/year_suffix.tsv @@ -0,0 +1,4 @@ +tcn trước công nguyên +scn sau công nguyên +TCN trước công nguyên +SCN sau công nguyên \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/fraction/__init__.py b/nemo_text_processing/text_normalization/vi/data/fraction/__init__.py new file mode 100644 index 000000000..b2de1dca7 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/fraction/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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/vi/data/fraction/denominator_exceptions.tsv b/nemo_text_processing/text_normalization/vi/data/fraction/denominator_exceptions.tsv new file mode 100644 index 000000000..7b305e655 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/fraction/denominator_exceptions.tsv @@ -0,0 +1 @@ +4 tư \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/measure/__init__.py b/nemo_text_processing/text_normalization/vi/data/measure/__init__.py new file mode 100644 index 000000000..b2de1dca7 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/measure/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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/vi/data/measure/base_units.tsv b/nemo_text_processing/text_normalization/vi/data/measure/base_units.tsv new file mode 100644 index 000000000..eb9faf2f5 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/measure/base_units.tsv @@ -0,0 +1,20 @@ +m mét +m2 mét vuông +m3 mét khối +m² mét vuông +m³ mét khối +g gam +l lít +s giây +v vôn +w oát +hz hẹc +A am pe +b bai +B byte +pa pascal +ω ohm +Ω ôm +h giờ +min phút +hr giờ \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/measure/measurements_minimal.tsv b/nemo_text_processing/text_normalization/vi/data/measure/measurements_minimal.tsv new file mode 100644 index 000000000..66c0a166b --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/measure/measurements_minimal.tsv @@ -0,0 +1,33 @@ +°f độ f +°F độ F +°c độ c +°C độ C +°k độ k +°K độ K +° độ +°E độ đông +°N độ bắc +°S độ nam +°W độ tây +ha héc ta +mi mile +ft foot +inch inch +yd yard +% phần trăm +hp mã lực +rad radian +kwh ki lô oát giờ +kbps kilobit trên giây +mbps megabit trên giây +ghz gi ga hẹc +mhz mê ga hẹc +tw tê ra oát +kcal ki lô calo +gb gi ga bai +mb mê ga bai +mV mi li vôn +MV mê ga vôn +tb terabyte +pb petabyte +g gam \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/measure/prefixes.tsv b/nemo_text_processing/text_normalization/vi/data/measure/prefixes.tsv new file mode 100644 index 000000000..649ce73a7 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/measure/prefixes.tsv @@ -0,0 +1,17 @@ +k ki lô +M mê ga +G gi ga +T tê ra +P pê ta +E ex xa +h hếc tô +da đề ca +d đề xi +c xăng ti +m mi li +µ mi crô +μ mi cờ rô +n na nô +p pi cô +f fem tô +a át tô \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/money/__init__.py b/nemo_text_processing/text_normalization/vi/data/money/__init__.py new file mode 100644 index 000000000..b2de1dca7 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/money/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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/vi/data/money/currency.tsv b/nemo_text_processing/text_normalization/vi/data/money/currency.tsv new file mode 100644 index 000000000..95e1eb2ca --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/money/currency.tsv @@ -0,0 +1,51 @@ +$ đô la +đô la đô la +€ ơ rô +¥ yên nhật +₩ won +₫ đồng +đ đồng +đồng đồng +£ bảng anh +₹ rupee +¢ xu +₣ franc pháp +cent xu +cents xu +CHF franc thụy sĩ +FF franc pháp +JPY yên nhật +KRW won hàn quốc +CNY nhân dân tệ +USD đô la +usd đô la +SGD đô la singapore +MYR ringgit malaysia +THB baht thái lan +IDR rupiah indonesia +PHP peso philippines +AUD đô la úc +CAD đô la canada +NZD đô la new zealand +HKD đô la hồng kông +TWD đô la đài loan +ff franc pháp +chf franc thụy sĩ +jpy yên nhật +krw won hàn quốc +cny nhân dân tệ +usd đô la +vnd đồng +vnđ đồng +sgd đô la singapore +myr ringgit malaysia +thb baht thái lan +idr rupiah indonesia +php peso philippines +aud đô la úc +cad đô la canada +nzd đô la new zealand +hkd đô la hồng kông +twd đô la đài loan +VND đồng +VNĐ đồng \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/money/currency_minor.tsv b/nemo_text_processing/text_normalization/vi/data/money/currency_minor.tsv new file mode 100644 index 000000000..d83858714 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/money/currency_minor.tsv @@ -0,0 +1,7 @@ +$ xu +€ xu +£ penny +¢ xu +cent xu +cents xu +pence penny \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/money/per_unit_bases.tsv b/nemo_text_processing/text_normalization/vi/data/money/per_unit_bases.tsv new file mode 100644 index 000000000..feb1808d6 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/money/per_unit_bases.tsv @@ -0,0 +1,8 @@ +g gam +m mét +m² mét vuông +m2 mét vuông +m³ mét khối +m3 mét khối +l lít +B bai \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/money/per_unit_non_metric.tsv b/nemo_text_processing/text_normalization/vi/data/money/per_unit_non_metric.tsv new file mode 100644 index 000000000..c1ccbdf69 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/money/per_unit_non_metric.tsv @@ -0,0 +1,28 @@ +/giờ trên giờ +/h trên giờ +/ngày trên ngày +/d trên ngày +/tuần trên tuần +/tháng trên tháng +/năm trên năm +/phút trên phút +/p trên phút +/giây trên giây +/s trên giây +/lần một lần +/cái một cái +/chiếc một chiếc +/người một người +/chỗ một chỗ +/bài một bài +/trang một trang +/từ một từ +/đồng một đồng +/đêm một đêm +/buổi một buổi +/ca một ca +/dự án một dự án +/lớp một lớp +/khóa một khóa +/suất một suất +/tấn một tấn \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/money/per_unit_prefixes.tsv b/nemo_text_processing/text_normalization/vi/data/money/per_unit_prefixes.tsv new file mode 100644 index 000000000..154aa7306 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/money/per_unit_prefixes.tsv @@ -0,0 +1,6 @@ +k ki lô +M mê ga +G gi ga +c xăng ti +m mi li +T tê ra \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/numbers/__init__.py b/nemo_text_processing/text_normalization/vi/data/numbers/__init__.py new file mode 100644 index 000000000..b2de1dca7 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/numbers/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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/vi/data/numbers/digit.tsv b/nemo_text_processing/text_normalization/vi/data/numbers/digit.tsv new file mode 100644 index 000000000..573c20bd4 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/numbers/digit.tsv @@ -0,0 +1,9 @@ +1 một +2 hai +3 ba +4 bốn +5 năm +6 sáu +7 bảy +8 tám +9 chín \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/numbers/digit_special.tsv b/nemo_text_processing/text_normalization/vi/data/numbers/digit_special.tsv new file mode 100644 index 000000000..3c3421528 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/numbers/digit_special.tsv @@ -0,0 +1,3 @@ +1 một mốt +4 bốn tư +5 năm lăm \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/numbers/magnitudes.tsv b/nemo_text_processing/text_normalization/vi/data/numbers/magnitudes.tsv new file mode 100644 index 000000000..da60cb686 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/numbers/magnitudes.tsv @@ -0,0 +1,8 @@ +thousand nghìn +million triệu +billion tỷ +trillion nghìn tỷ +quadrillion triệu tỷ +quintillion tỷ tỷ +hundred trăm +linh linh \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/numbers/quantity_abbr.tsv b/nemo_text_processing/text_normalization/vi/data/numbers/quantity_abbr.tsv new file mode 100644 index 000000000..9660bf9bb --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/numbers/quantity_abbr.tsv @@ -0,0 +1,7 @@ +k nghìn +K nghìn +tr triệu +TR triệu +Tr triệu +t tỷ +T tỷ \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/numbers/teen.tsv b/nemo_text_processing/text_normalization/vi/data/numbers/teen.tsv new file mode 100644 index 000000000..8d99f8a69 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/numbers/teen.tsv @@ -0,0 +1,10 @@ +10 mười +11 mười một +12 mười hai +13 mười ba +14 mười bốn +15 mười lăm +16 mười sáu +17 mười bảy +18 mười tám +19 mười chín \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/numbers/ties.tsv b/nemo_text_processing/text_normalization/vi/data/numbers/ties.tsv new file mode 100644 index 000000000..da88b8ab8 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/numbers/ties.tsv @@ -0,0 +1,8 @@ +2 hai mươi +3 ba mươi +4 bốn mươi +5 năm mươi +6 sáu mươi +7 bảy mươi +8 tám mươi +9 chín mươi \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/numbers/zero.tsv b/nemo_text_processing/text_normalization/vi/data/numbers/zero.tsv new file mode 100644 index 000000000..df062e38c --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/numbers/zero.tsv @@ -0,0 +1 @@ +0 không \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/ordinal/__init__.py b/nemo_text_processing/text_normalization/vi/data/ordinal/__init__.py new file mode 100644 index 000000000..6ebc808fa --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/ordinal/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES. 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/vi/data/ordinal/ordinal_exceptions.tsv b/nemo_text_processing/text_normalization/vi/data/ordinal/ordinal_exceptions.tsv new file mode 100644 index 000000000..5aae90801 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/ordinal/ordinal_exceptions.tsv @@ -0,0 +1,2 @@ +1 nhất +4 tư \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/roman/__init__.py b/nemo_text_processing/text_normalization/vi/data/roman/__init__.py new file mode 100644 index 000000000..b2de1dca7 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/roman/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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/vi/data/roman/key_word.tsv b/nemo_text_processing/text_normalization/vi/data/roman/key_word.tsv new file mode 100644 index 000000000..73b822342 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/roman/key_word.tsv @@ -0,0 +1,16 @@ +thế kỉ +thế kỷ +thứ +chương +phần +mục +đoạn +năm +khoản +phụ lục +khóa +số +điều +tiểu mục +bài +khối \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/roman/roman_numerals.tsv b/nemo_text_processing/text_normalization/vi/data/roman/roman_numerals.tsv new file mode 100644 index 000000000..d4d8ad20b --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/roman/roman_numerals.tsv @@ -0,0 +1,13 @@ +I 1 +V 5 +X 10 +L 50 +C 100 +D 500 +M 1000 +IV 4 +IX 9 +XL 40 +XC 90 +CD 400 +CM 900 \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/time/__init__.py b/nemo_text_processing/text_normalization/vi/data/time/__init__.py new file mode 100644 index 000000000..b2de1dca7 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/time/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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/vi/data/time/time_units.tsv b/nemo_text_processing/text_normalization/vi/data/time/time_units.tsv new file mode 100644 index 000000000..66e912f4e --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/time/time_units.tsv @@ -0,0 +1,4 @@ +h giờ +g giờ +p phút +s giây \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/time/time_zones.tsv b/nemo_text_processing/text_normalization/vi/data/time/time_zones.tsv new file mode 100644 index 000000000..74c39849a --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/data/time/time_zones.tsv @@ -0,0 +1,18 @@ +GMT GMT +UTC UTC +CST CST +PST PST +EST EST +JST JST +PT PT +ET ET +CET CET +gmt GMT +utc UTC +cst CST +pst PST +est EST +jst JST +pt PT +et ET +cet CET \ No newline at end of file diff --git a/nemo_text_processing/text_normalization/vi/data/whitelist.tsv b/nemo_text_processing/text_normalization/vi/data/whitelist.tsv new file mode 100644 index 000000000..e69de29bb diff --git a/nemo_text_processing/text_normalization/vi/graph_utils.py b/nemo_text_processing/text_normalization/vi/graph_utils.py new file mode 100644 index 000000000..1c0c1a0ab --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/graph_utils.py @@ -0,0 +1,165 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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 os +import string +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.utils.logging import logger + +NEMO_CHAR = utf8.VALID_UTF8_CHAR + +NEMO_DIGIT = byte.DIGIT +NEMO_LOWER = pynini.union(*string.ascii_lowercase).optimize() +NEMO_UPPER = pynini.union(*string.ascii_uppercase).optimize() +NEMO_ALPHA = pynini.union(NEMO_LOWER, NEMO_UPPER).optimize() +NEMO_ALNUM = pynini.union(NEMO_DIGIT, NEMO_ALPHA).optimize() +NEMO_HEX = pynini.union(*string.hexdigits).optimize() +NEMO_NON_BREAKING_SPACE = "\u00a0" +NEMO_SPACE = " " +NEMO_WHITE_SPACE = pynini.union(" ", "\t", "\n", "\r", "\u00a0").optimize() +NEMO_NOT_SPACE = pynini.difference(NEMO_CHAR, NEMO_WHITE_SPACE).optimize() +NEMO_NOT_QUOTE = pynini.difference(NEMO_CHAR, r'"').optimize() + +NEMO_PUNCT = pynini.union(*map(pynini.escape, string.punctuation)).optimize() +NEMO_GRAPH = pynini.union(NEMO_ALNUM, NEMO_PUNCT).optimize() + +NEMO_SIGMA = pynini.closure(NEMO_CHAR) +NEMO_COMMA = "," +NEMO_COMMA_VI = "phẩy" + +delete_space = pynutil.delete(pynini.closure(NEMO_WHITE_SPACE)) +delete_zero_or_one_space = pynutil.delete(pynini.closure(NEMO_WHITE_SPACE, 0, 1)) +insert_space = pynutil.insert(" ") +delete_extra_space = pynini.cross(pynini.closure(NEMO_WHITE_SPACE, 1), " ") +delete_preserve_order = pynini.closure( + pynutil.delete(" preserve_order: true") + | (pynutil.delete(' field_order: "') + NEMO_NOT_QUOTE + pynutil.delete('"')) +) + +quoted_text = pynini.closure(NEMO_NOT_QUOTE) + + +def extract_field(field_name): + return pynutil.delete(f"{field_name}:") + delete_space + pynutil.delete("\"") + quoted_text + pynutil.delete("\"") + + +def extract_wrapper_content(wrapper_type: str, content_graph): + """Helper to extract content from wrapper like 'decimal { ... }'""" + return pynutil.delete(f"{wrapper_type} {{") + delete_space + content_graph + delete_space + pynutil.delete("}") + + +def convert_space(fst) -> "pynini.FstLike": + """ + Converts space to nonbreaking space. + Used only in tagger grammars for transducing token values within quotes, e.g. name: "hello kitty" + This is making transducer significantly slower, so only use when there could be potential spaces within quotes, otherwise leave it. + + Args: + fst: input fst + + Returns output fst where breaking spaces are converted to non breaking spaces + """ + return fst @ pynini.cdrewrite(pynini.cross(NEMO_SPACE, NEMO_NON_BREAKING_SPACE), "", "", NEMO_SIGMA) + + +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() + logger.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("\u00a0", " "), "", "", NEMO_SIGMA) diff --git a/nemo_text_processing/text_normalization/vi/taggers/__init__.py b/nemo_text_processing/text_normalization/vi/taggers/__init__.py new file mode 100644 index 000000000..b2de1dca7 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/taggers/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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/vi/taggers/cardinal.py b/nemo_text_processing/text_normalization/vi/taggers/cardinal.py new file mode 100644 index 000000000..59bb86d26 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/taggers/cardinal.py @@ -0,0 +1,235 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import NEMO_DIGIT, GraphFst, insert_space +from nemo_text_processing.text_normalization.vi.utils import get_abs_path, load_labels + + +class CardinalFst(GraphFst): + def __init__(self, deterministic: bool = True): + super().__init__(name="cardinal", kind="classify", deterministic=deterministic) + + resources = { + 'zero': pynini.string_file(get_abs_path("data/numbers/zero.tsv")), + 'digit': pynini.string_file(get_abs_path("data/numbers/digit.tsv")), + 'teen': pynini.string_file(get_abs_path("data/numbers/teen.tsv")), + 'ties': pynini.string_file(get_abs_path("data/numbers/ties.tsv")), + } + self.zero, self.digit, self.teen, self.ties = resources.values() + + magnitudes_labels = load_labels(get_abs_path("data/numbers/magnitudes.tsv")) + self.magnitudes = {parts[0]: parts[1] for parts in magnitudes_labels if len(parts) == 2} + + digit_special_labels = load_labels(get_abs_path("data/numbers/digit_special.tsv")) + special = {parts[0]: {'std': parts[1], 'alt': parts[2]} for parts in digit_special_labels if len(parts) >= 3} + + self.special_digits = pynini.union( + *[pynini.cross(k, v["alt"]) for k, v in special.items() if k in ["1", "4", "5"]] + ) + self.linh_digits = pynini.union(*[pynini.cross(k, special[k]["std"]) for k in ["1", "4", "5"]], self.digit) + + self.single_digit = self.digit + + self.two_digit = pynini.union( + self.teen, + self.ties + pynutil.delete("0"), + self.ties + + insert_space + + pynini.union(self.special_digits, pynini.union("2", "3", "6", "7", "8", "9") @ self.digit), + ) + + hundred_word = self.magnitudes["hundred"] + linh_word = self.magnitudes["linh"] + + self.hundreds_pattern = pynini.union( + # X00: một trăm, hai trăm, etc. + self.single_digit + insert_space + pynutil.insert(hundred_word) + pynutil.delete("00"), + # X0Y: một trăm linh một, hai trăm linh năm, etc. + self.single_digit + + insert_space + + pynutil.insert(hundred_word) + + pynutil.delete("0") + + insert_space + + pynutil.insert(linh_word) + + insert_space + + self.linh_digits, + # XYZ: một trăm hai mười ba, etc. + self.single_digit + insert_space + pynutil.insert(hundred_word) + insert_space + self.two_digit, + # 0YZ: Handle numbers starting with 0 (e.g., 087 -> tám mươi bảy) + pynutil.delete("0") + self.two_digit, + # 00Z: Handle numbers starting with 00 (e.g., 008 -> tám) + pynutil.delete("00") + self.single_digit, + ) + + self.hundreds = pynini.closure(NEMO_DIGIT, 3, 3) @ self.hundreds_pattern + + self.magnitude_patterns = self._build_all_magnitude_patterns() + custom_patterns = self._build_all_patterns() + + all_patterns = [ + *custom_patterns, + *self.magnitude_patterns.values(), + self.hundreds, + self.two_digit, + self.single_digit, + self.zero, + ] + self.graph = pynini.union(*all_patterns).optimize() + + self.single_digits_graph = self.single_digit | self.zero + self.graph_with_and = self.graph + + negative = pynini.closure(pynutil.insert("negative: ") + pynini.cross("-", "\"true\" "), 0, 1) + final_graph = negative + pynutil.insert("integer: \"") + self.graph + pynutil.insert("\"") + self.fst = self.add_tokens(final_graph).optimize() + + def _build_magnitude_pattern(self, name, min_digits, max_digits, zero_count, prev_pattern=None): + magnitude_word = self.magnitudes[name] + linh_word = self.magnitudes["linh"] + patterns = [] + + for digits in range(min_digits, max_digits + 1): + leading_digits = digits - zero_count + if leading_digits == 1: + leading_fst = self.single_digit + elif leading_digits == 2: + leading_fst = self.two_digit + else: + leading_fst = self.hundreds_pattern + + prefix = leading_fst + insert_space + pynutil.insert(magnitude_word) + digit_patterns = [prefix + pynutil.delete("0" * zero_count)] + + if prev_pattern and name not in ["quadrillion", "quintillion"]: + digit_patterns.append(prefix + insert_space + prev_pattern) + + for trailing_zeros in range(zero_count): + remaining_digits = zero_count - trailing_zeros + trailing_prefix = prefix + pynutil.delete("0" * trailing_zeros) + + if remaining_digits == 1: + linh_pattern = ( + trailing_prefix + insert_space + pynutil.insert(linh_word) + insert_space + self.linh_digits + ) + digit_patterns.append(pynutil.add_weight(linh_pattern, -0.1)) + elif remaining_digits == 2: + digit_patterns.append(trailing_prefix + insert_space + self.two_digit) + elif remaining_digits == 3: + digit_patterns.append(trailing_prefix + insert_space + self.hundreds_pattern) + + patterns.append(pynini.closure(NEMO_DIGIT, digits, digits) @ pynini.union(*digit_patterns)) + + return pynini.union(*patterns) + + def _build_all_magnitude_patterns(self): + magnitude_config = [ + ("thousand", 4, 6, 3), + ("million", 7, 9, 6), + ("billion", 10, 12, 9), + ("trillion", 13, 15, 12), + ("quadrillion", 16, 18, 15), + ("quintillion", 19, 21, 18), + ] + patterns = {} + prev_pattern = None + for name, min_digits, max_digits, zero_count in magnitude_config: + if name in self.magnitudes: + patterns[name] = self._build_magnitude_pattern(name, min_digits, max_digits, zero_count, prev_pattern) + prev_pattern = patterns[name] + else: + break + return patterns + + def _get_zero_or_magnitude_pattern(self, digits, magnitude_key): + """Create pattern that handles all-zeros or normal magnitude processing""" + all_zeros = "0" * digits + return pynini.union(pynini.cross(all_zeros, ""), NEMO_DIGIT**digits @ self.magnitude_patterns[magnitude_key]) + + def _build_all_patterns(self): + patterns = [] + delete_dot = pynutil.delete(".") + + # Large number split patterns (>12 digits): front + "tỷ" + back(9 digits) + if "billion" in self.magnitudes: + billion_word = self.magnitudes["billion"] + back_digits = 9 + + for total_digits in range(13, 22): + front_digits = total_digits - back_digits + front_pattern = self._get_pattern_for_digits(front_digits) + if front_pattern: + back_pattern = self._get_zero_or_magnitude_pattern(back_digits, "million") + split_pattern = ( + front_pattern + insert_space + pynutil.insert(billion_word) + insert_space + back_pattern + ) + patterns.append(NEMO_DIGIT**total_digits @ pynutil.add_weight(split_pattern, -0.5)) + + # Dot patterns + dot_configs = [(6, None), (5, None), (4, None), (3, "billion"), (2, "million"), (1, "thousand")] + for dots, magnitude in dot_configs: + pattern = (NEMO_DIGIT - "0") + pynini.closure(NEMO_DIGIT, 0, 2) + for _ in range(dots): + pattern += delete_dot + NEMO_DIGIT**3 + + if magnitude and magnitude in self.magnitude_patterns: + patterns.append(pynini.compose(pynutil.add_weight(pattern, -0.3), self.magnitude_patterns[magnitude])) + elif not magnitude: + if dots == 4: + digit_range = [13, 14, 15] + elif dots == 5: + digit_range = [16, 17, 18] + elif dots == 6: + digit_range = [19, 20, 21] + else: + digit_range = [] + + for digit_count in digit_range: + if 13 <= digit_count <= 21: + front_digits = digit_count - back_digits + front_pattern = self._get_pattern_for_digits(front_digits) + if front_pattern: + back_pattern = self._get_zero_or_magnitude_pattern(back_digits, "million") + split = ( + (NEMO_DIGIT**front_digits @ front_pattern) + + insert_space + + pynutil.insert(self.magnitudes["billion"]) + + insert_space + + back_pattern + ) + patterns.append( + pynini.compose(pattern, NEMO_DIGIT**digit_count @ pynutil.add_weight(split, -1.0)) + ) + + return patterns + + def _get_pattern_for_digits(self, digit_count): + if digit_count <= 0: + return None + elif digit_count == 1: + return self.single_digit + elif digit_count == 2: + return self.two_digit + elif digit_count == 3: + return self.hundreds_pattern + elif digit_count <= 6: + return self.magnitude_patterns.get("thousand") + elif digit_count <= 9: + return self.magnitude_patterns.get("million") + elif digit_count <= 12: + return self.magnitude_patterns.get("billion") + else: + return None diff --git a/nemo_text_processing/text_normalization/vi/taggers/date.py b/nemo_text_processing/text_normalization/vi/taggers/date.py new file mode 100644 index 000000000..2b9031241 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/taggers/date.py @@ -0,0 +1,160 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import NEMO_DIGIT, NEMO_SPACE, GraphFst +from nemo_text_processing.text_normalization.vi.utils import get_abs_path, load_labels + + +class DateFst(GraphFst): + """ + Finite state transducer for classifying Vietnamese dates, e.g. + 15/01/2024 -> date { day: "mười lăm" month: "một" year: "hai nghìn hai mươi tư" } + tháng 4 2024 -> date { month: "tư" year: "hai nghìn hai mươi tư" } + ngày 15/01/2024 -> date { day: "mười lăm" month: "một" year: "hai nghìn hai mươi tư" } + ngày 12 tháng 5 năm 2025 -> date { day: "mười hai" month: "năm" year: "hai nghìn hai mươi lăm" } + năm 20 SCN -> date { year: "hai mươi" era: "sau công nguyên" } + """ + + def __init__(self, cardinal, deterministic: bool = True): + super().__init__(name="date", kind="classify", deterministic=deterministic) + + # Vietnamese date keywords + DAY_WORD = "ngày" + MONTH_WORD = "tháng" + YEAR_WORD = "năm" + ORDINAL_YEAR_WORD = "năm thứ" + + # Prebuilt patterns for common usage + day_prefix = pynini.accep(DAY_WORD + NEMO_SPACE) + month_prefix = pynini.accep(MONTH_WORD + NEMO_SPACE) + year_prefix = pynini.accep(YEAR_WORD + NEMO_SPACE) + ordinal_year_prefix = pynini.accep(ORDINAL_YEAR_WORD + NEMO_SPACE) + + delete_day_prefix = pynutil.delete(DAY_WORD + NEMO_SPACE) + delete_month_prefix = pynutil.delete(MONTH_WORD + NEMO_SPACE) + delete_year_prefix = pynutil.delete(YEAR_WORD + NEMO_SPACE) + delete_ordinal_year_prefix = pynutil.delete(ORDINAL_YEAR_WORD + NEMO_SPACE) + + day_mappings = load_labels(get_abs_path("data/date/days.tsv")) + month_mappings = load_labels(get_abs_path("data/date/months.tsv")) + era_mappings = load_labels(get_abs_path("data/date/year_suffix.tsv")) + + day_digit = pynini.closure(NEMO_DIGIT, 1, 2) + month_digit = pynini.closure(NEMO_DIGIT, 1, 2) + year_digit = pynini.closure(NEMO_DIGIT, 1, 4) + separator = pynini.union("/", "-", ".") + + day_convert = pynini.string_map([(k, v) for k, v in day_mappings]) + month_convert = pynini.string_map([(k, v) for k, v in month_mappings]) + year_convert = pynini.compose(year_digit, cardinal.graph) + + era_to_full = {} + for abbr, full_form in era_mappings: + era_to_full[abbr.lower()] = full_form + era_to_full[abbr.upper()] = full_form + + era_convert = pynini.string_map([(k, v) for k, v in era_to_full.items()]) + + day_part = pynutil.insert("day: \"") + day_convert + pynutil.insert("\" ") + month_part = pynutil.insert("month: \"") + month_convert + pynutil.insert("\" ") + year_part = pynutil.insert("year: \"") + year_convert + pynutil.insert("\"") + month_final = pynutil.insert("month: \"") + month_convert + pynutil.insert("\"") + era_part = pynutil.insert("era: \"") + era_convert + pynutil.insert("\"") + + patterns = [] + + # DD/MM/YYYY format (Vietnamese standard) + date_sep = day_part + pynutil.delete(separator) + month_part + pynutil.delete(separator) + year_part + patterns.append(pynini.compose(day_digit + separator + month_digit + separator + year_digit, date_sep)) + patterns.append( + pynini.compose( + day_prefix + day_digit + separator + month_digit + separator + year_digit, + delete_day_prefix + date_sep, + ) + ) + + # YYYY/MM/DD format (ISO standard) - output in Vietnamese order + iso_year_part = pynutil.insert("year: \"") + year_convert + pynutil.insert("\" ") + iso_month_part = pynutil.insert("month: \"") + month_convert + pynutil.insert("\" ") + iso_day_part = pynutil.insert("day: \"") + day_convert + pynutil.insert("\"") + + iso_date_sep = ( + iso_year_part + pynutil.delete(separator) + iso_month_part + pynutil.delete(separator) + iso_day_part + ) + patterns.append(pynini.compose(year_digit + separator + month_digit + separator + day_digit, iso_date_sep)) + + for sep in [separator, pynini.accep(NEMO_SPACE)]: + patterns.append( + pynini.compose( + month_prefix + month_digit + sep + year_digit, + delete_month_prefix + month_part + pynutil.delete(sep) + year_part, + ) + ) + + day_month_sep = day_part + pynutil.delete(separator) + month_final + patterns.append( + pynini.compose(day_prefix + day_digit + separator + month_digit, delete_day_prefix + day_month_sep) + ) + + patterns.append( + pynini.compose( + day_prefix + day_digit + pynini.accep(NEMO_SPACE + MONTH_WORD + NEMO_SPACE) + month_digit, + delete_day_prefix + day_part + pynutil.delete(NEMO_SPACE + MONTH_WORD + NEMO_SPACE) + month_final, + ) + ) + + patterns.append( + pynini.compose( + day_prefix + + day_digit + + pynini.accep(NEMO_SPACE + MONTH_WORD + NEMO_SPACE) + + month_digit + + pynini.accep(NEMO_SPACE + YEAR_WORD + NEMO_SPACE) + + year_digit, + delete_day_prefix + + day_part + + pynutil.delete(NEMO_SPACE + MONTH_WORD + NEMO_SPACE) + + month_part + + pynutil.delete(NEMO_SPACE + YEAR_WORD + NEMO_SPACE) + + year_part, + ) + ) + + patterns.append(pynini.compose(year_prefix + year_digit, delete_year_prefix + year_part)) + + era_abbrs = list(era_to_full.keys()) + for era_abbr in era_abbrs: + patterns.append( + pynini.compose( + year_prefix + year_digit + pynini.accep(NEMO_SPACE) + pynini.accep(era_abbr), + delete_year_prefix + year_part + pynutil.delete(NEMO_SPACE) + era_part, + ) + ) + + patterns.append( + pynini.compose( + ordinal_year_prefix + year_digit + pynini.accep(NEMO_SPACE) + pynini.accep(era_abbr), + delete_ordinal_year_prefix + + pynutil.insert("ordinal: \"") + + year_convert + + pynutil.insert("\" ") + + pynutil.delete(NEMO_SPACE) + + era_part, + ) + ) + + self.fst = self.add_tokens(pynini.union(*patterns)) diff --git a/nemo_text_processing/text_normalization/vi/taggers/decimal.py b/nemo_text_processing/text_normalization/vi/taggers/decimal.py new file mode 100644 index 000000000..ba07e7601 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/taggers/decimal.py @@ -0,0 +1,165 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import NEMO_COMMA, NEMO_DIGIT, NEMO_SPACE, GraphFst +from nemo_text_processing.text_normalization.vi.utils import get_abs_path, load_labels + + +class DecimalFst(GraphFst): + """ + Finite state transducer for classifying Vietnamese decimal numbers, e.g. + -12,5 tỷ -> decimal { negative: "true" integer_part: "mười hai" fractional_part: "năm" quantity: "tỷ" } + 12.345,67 -> decimal { integer_part: "mười hai nghìn ba trăm bốn mươi lăm" fractional_part: "sáu bảy" } + 1tr2 -> decimal { integer_part: "một triệu hai trăm nghìn" } + 818,303 -> decimal { integer_part: "tám trăm mười tám" fractional_part: "ba không ba" } + 0,2 triệu -> decimal { integer_part: "không" fractional_part: "hai" quantity: "triệu" } + Args: + cardinal: CardinalFst instance for processing integer parts + """ + + def __init__(self, cardinal: GraphFst, deterministic: bool = True): + super().__init__(name="decimal", kind="classify", deterministic=deterministic) + + cardinal_graph = cardinal.graph_with_and + self.graph = cardinal.single_digits_graph.optimize() + if not deterministic: + self.graph = self.graph | cardinal_graph + + # Load data + digit_labels = load_labels(get_abs_path("data/numbers/digit.tsv")) + zero_labels = load_labels(get_abs_path("data/numbers/zero.tsv")) + magnitude_labels = load_labels(get_abs_path("data/numbers/magnitudes.tsv")) + quantity_abbr_labels = load_labels(get_abs_path("data/numbers/quantity_abbr.tsv")) + + # Common components + single_digit_map = pynini.union(*[pynini.cross(k, v) for k, v in digit_labels + zero_labels]) + quantity_units = pynini.union(*[v for _, v in magnitude_labels]) + one_to_three_digits = NEMO_DIGIT + pynini.closure(NEMO_DIGIT, 0, 2) + + # Building blocks + integer_part = pynutil.insert("integer_part: \"") + cardinal_graph + pynutil.insert("\"") + fractional_part = ( + pynutil.insert("fractional_part: \"") + + single_digit_map + + pynini.closure(pynutil.insert(NEMO_SPACE) + single_digit_map) + + pynutil.insert("\"") + ) + optional_quantity = ( + pynutil.delete(NEMO_SPACE).ques + pynutil.insert(" quantity: \"") + quantity_units + pynutil.insert("\"") + ).ques + + patterns = [] + + # 1. Basic decimal patterns: 12,5 and 12,5 tỷ + basic_decimal = ( + (integer_part + pynutil.insert(NEMO_SPACE)).ques + + pynutil.delete(NEMO_COMMA) + + pynutil.insert(NEMO_SPACE) + + fractional_part + ) + patterns.append(basic_decimal) + patterns.append(basic_decimal + optional_quantity) + + # 2. Thousand-separated decimals: 12.345,67 and 12.345,67 tỷ + integer_with_dots = ( + NEMO_DIGIT + pynini.closure(NEMO_DIGIT, 0, 2) + pynini.closure(pynutil.delete(".") + NEMO_DIGIT**3, 1) + ) + separated_integer_part = ( + pynutil.insert("integer_part: \"") + + pynini.compose(integer_with_dots, cardinal_graph) + + pynutil.insert("\"") + ) + separated_decimal = ( + separated_integer_part + + pynutil.insert(NEMO_SPACE) + + pynutil.delete(NEMO_COMMA) + + pynutil.insert(NEMO_SPACE) + + fractional_part + ) + patterns.append(separated_decimal) + patterns.append(separated_decimal + optional_quantity) + + # 3. Integer with quantity: 100 triệu + integer_with_quantity = ( + integer_part + + pynutil.delete(NEMO_SPACE).ques + + pynutil.insert(" quantity: \"") + + quantity_units + + pynutil.insert("\"") + ) + patterns.append(integer_with_quantity) + + # 4. Standard abbreviations: 1k, 100tr, etc. + for abbr, full_name in quantity_abbr_labels: + abbr_pattern = pynini.compose( + one_to_three_digits + pynutil.delete(abbr), + pynutil.insert("integer_part: \"") + + pynini.compose(one_to_three_digits, cardinal_graph) + + pynutil.insert(f"\" quantity: \"{full_name}\""), + ) + patterns.append(abbr_pattern) + + # 5. Decimal with abbreviations: 2,5tr, but avoid measure conflicts + measure_prefix_labels = load_labels(get_abs_path("data/measure/prefixes.tsv")) + measure_prefixes = {prefix.lower() for prefix, _ in measure_prefix_labels} + + # Filter quantity abbreviations to avoid measure conflicts + safe_quantity_abbrs = [ + (abbr, full) for abbr, full in quantity_abbr_labels if abbr.lower() not in measure_prefixes + ] + + for abbr, full_name in safe_quantity_abbrs: + decimal_abbr_pattern = ( + (integer_part + pynutil.insert(NEMO_SPACE)).ques + + pynutil.delete(NEMO_COMMA) + + pynutil.insert(NEMO_SPACE) + + fractional_part + + pynutil.insert(f" quantity: \"{full_name}\"") + + pynutil.delete(abbr) + ) + patterns.append(decimal_abbr_pattern) + + # 6. Compound abbreviations: 1tr2 -> một triệu hai trăm nghìn, 2t3 -> hai tỷ ba trăm triệu + compound_expansions = { + "tr": ("triệu", "trăm nghìn"), # 1tr2 -> một triệu hai trăm nghìn + "t": ("tỷ", "trăm triệu"), # 2t3 -> hai tỷ ba trăm triệu + } + + for abbr, (major_unit, minor_suffix) in compound_expansions.items(): + pattern = one_to_three_digits + pynini.cross(abbr, "") + NEMO_DIGIT + expansion = ( + pynutil.insert("integer_part: \"") + + pynini.compose(one_to_three_digits, cardinal_graph) + + pynutil.insert(f" {major_unit} ") + + pynini.compose(NEMO_DIGIT, cardinal_graph) + + pynutil.insert(f" {minor_suffix}\"") + ) + patterns.append(pynini.compose(pattern, expansion)) + + # Combine all patterns + self._final_graph_wo_negative = pynini.union(*patterns).optimize() + + # Add optional negative prefix + negative = (pynutil.insert("negative: ") + pynini.cross("-", "\"true\" ")).ques + final_graph = negative + self._final_graph_wo_negative + + self.fst = self.add_tokens(final_graph).optimize() + + @property + def final_graph_wo_negative(self): + """Graph without negative prefix, used by MoneyFst""" + return self._final_graph_wo_negative diff --git a/nemo_text_processing/text_normalization/vi/taggers/fraction.py b/nemo_text_processing/text_normalization/vi/taggers/fraction.py new file mode 100644 index 000000000..ca1d11ebf --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/taggers/fraction.py @@ -0,0 +1,72 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import GraphFst +from nemo_text_processing.text_normalization.vi.taggers.cardinal import CardinalFst +from nemo_text_processing.text_normalization.vi.utils import get_abs_path, load_labels + + +class FractionFst(GraphFst): + """ + Finite state transducer for classifying Vietnamese fraction numbers, e.g. + 23 1/5 -> fraction { integer_part: "hai mươi ba" numerator: "một" denominator: "năm" } + 3/9 -> fraction { numerator: "ba" denominator: "chín" } + 1/4 -> fraction { numerator: "một" denominator: "tư" } + + Args: + cardinal: CardinalFst for converting numbers to Vietnamese words + deterministic: if True will provide a single transduction option, + for False multiple options (used for audio-based normalization) + """ + + def __init__(self, cardinal: CardinalFst, deterministic: bool = True): + super().__init__(name="fraction", kind="classify", deterministic=deterministic) + + cardinal_graph = cardinal.graph + digit = pynini.union(*[str(i) for i in range(10)]) + number = pynini.closure(digit, 1) + + denominator_exceptions = { + row[0]: row[1] for row in load_labels(get_abs_path("data/fraction/denominator_exceptions.tsv")) + } + + denominator_exception_patterns = [pynini.cross(k, v) for k, v in denominator_exceptions.items()] + denominator_exception_graph = ( + pynini.union(*denominator_exception_patterns) if denominator_exception_patterns else None + ) + denominator_graph = ( + pynini.union(denominator_exception_graph, cardinal_graph) + if denominator_exception_graph + else cardinal_graph + ) + + numerator = ( + pynutil.insert("numerator: \"") + (number @ cardinal_graph) + pynutil.insert("\" ") + pynutil.delete("/") + ) + denominator = pynutil.insert("denominator: \"") + (number @ denominator_graph) + pynutil.insert("\"") + integer_part = pynutil.insert("integer_part: \"") + (number @ cardinal_graph) + pynutil.insert("\" ") + + simple_fraction = numerator + denominator + mixed_fraction = integer_part + pynutil.delete(" ") + numerator + denominator + + # Create graph without negative for reuse in other FSTs (like measure) + fraction_wo_negative = simple_fraction | mixed_fraction + self.final_graph_wo_negative = fraction_wo_negative.optimize() + + optional_graph_negative = (pynutil.insert("negative: ") + pynini.cross("-", "\"true\" ")).ques + + self.fst = self.add_tokens(optional_graph_negative + (simple_fraction | mixed_fraction)).optimize() diff --git a/nemo_text_processing/text_normalization/vi/taggers/measure.py b/nemo_text_processing/text_normalization/vi/taggers/measure.py new file mode 100644 index 000000000..b236d84a7 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/taggers/measure.py @@ -0,0 +1,153 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import ( + NEMO_COMMA, + NEMO_DIGIT, + NEMO_SPACE, + GraphFst, + delete_space, +) +from nemo_text_processing.text_normalization.vi.utils import get_abs_path + + +class MeasureFst(GraphFst): + """ + Finite state transducer for classifying measure for Vietnamese, e.g. + 12kg -> measure { cardinal { integer: "mười hai" } units: "ki lô gam" } + 1kg -> measure { cardinal { integer: "một" } units: "ki lô gam" } + 0.5kg -> measure { decimal { fractional_part: "năm" } units: "ki lô gam" } + -12kg -> measure { negative: "true" cardinal { integer: "mười hai" } units: "ki lô gam" } + + Args: + cardinal: CardinalFst + decimal: DecimalFst + fraction: FractionFst + deterministic: if True will provide a single transduction option, + for False multiple options (used for audio-based normalization) + """ + + def _create_measure_subgraph(self, measure_type: str, number_graph, optional_negative, graph_unit): + """Helper to create measure subgraph pattern - reduces duplication""" + return ( + optional_negative + + pynutil.insert(f"{measure_type} {{ ") + + number_graph + + pynutil.insert(" }") + + delete_space + + pynutil.insert(" units: \"") + + graph_unit + + pynutil.insert('"') + ) + + def __init__( + self, + cardinal: GraphFst, + decimal: GraphFst, + fraction: GraphFst, + deterministic: bool = True, + ): + super().__init__(name="measure", kind="classify", deterministic=deterministic) + + cardinal_graph = cardinal.graph + + # Load minimal measurement files (massive redundancy removed via subfst) + measurements_path = get_abs_path("data/measure/measurements_minimal.tsv") + prefixes_path = get_abs_path("data/measure/prefixes.tsv") + base_units_path = get_abs_path("data/measure/base_units.tsv") + + # Create subfst for metric units: prefix + space + base_unit + graph_prefixes = pynini.string_file(prefixes_path) + graph_base_units = pynini.string_file(base_units_path) + space = pynutil.insert(NEMO_SPACE) + graph_metric_units = graph_prefixes + space + graph_base_units + + # Load non-metric and special units + graph_special_units = pynini.string_file(measurements_path) + + # Also allow base units without prefixes (e.g., 'g' not just 'kg') + graph_standalone_units = graph_base_units + + # Combine all unit mappings + graph_unit = graph_metric_units | graph_special_units | graph_standalone_units + + # Add compound unit support (unit/unit patterns like km/h) + graph_unit_compound = pynini.cross("/", " trên ") + pynutil.insert(NEMO_SPACE) + graph_unit + + optional_graph_unit_compound = pynini.closure( + pynutil.insert(NEMO_SPACE) + graph_unit_compound, + 0, + 1, + ) + + # Update unit graph to include compound units + graph_unit = graph_unit + optional_graph_unit_compound | graph_unit_compound + + # Create unit symbol pattern using FST operations (no loops needed) + prefix_symbols = pynini.project(graph_prefixes, "input") # Extract prefix symbols + base_symbols = pynini.project(graph_base_units, "input") # Extract base symbols + special_symbols = pynini.project(graph_special_units, "input") # Extract special symbols + + # Build unit pattern: metric combinations | standalone bases | special units + metric_pattern = prefix_symbols + base_symbols # All prefix+base combinations + simple_unit_pattern = metric_pattern | base_symbols | special_symbols + + # Add compound unit patterns to recognition + compound_pattern = simple_unit_pattern + "/" + simple_unit_pattern + unit_pattern = simple_unit_pattern | compound_pattern + + number = pynini.closure(NEMO_DIGIT, 1) + decimal_number = number + NEMO_COMMA + pynini.closure(NEMO_DIGIT, 1) + + # Optional negative sign handling for Vietnamese + optional_graph_negative = pynini.closure( + pynini.cross("-", "negative: \"true\" "), + 0, + 1, + ) + + # Domain restriction patterns - only match core number+unit patterns + # Remove punctuation handling to let punctuation tagger handle it separately + optional_space = pynini.closure(NEMO_SPACE, 0, 1) + optional_negative_sign = pynini.closure("-" + optional_space, 0, 1) + + integer_measure_domain = optional_negative_sign + number + optional_space + unit_pattern + decimal_measure_domain = optional_negative_sign + decimal_number + optional_space + unit_pattern + fraction_measure_domain = optional_negative_sign + number + "/" + number + optional_space + unit_pattern + + cardinal_number_graph = pynutil.insert('integer: "') + (number @ cardinal_graph) + pynutil.insert('"') + + subgraph_cardinal = self._create_measure_subgraph( + "cardinal", cardinal_number_graph, optional_graph_negative, graph_unit + ) + subgraph_decimal = self._create_measure_subgraph( + "decimal", decimal.final_graph_wo_negative, optional_graph_negative, graph_unit + ) + subgraph_fraction = self._create_measure_subgraph( + "fraction", fraction.final_graph_wo_negative, optional_graph_negative, graph_unit + ) + + # Apply domain restrictions to ensure we only match complete number+unit patterns + subgraph_cardinal = pynini.compose(integer_measure_domain, subgraph_cardinal) + subgraph_decimal = pynini.compose(decimal_measure_domain, subgraph_decimal) + subgraph_fraction = pynini.compose(fraction_measure_domain, subgraph_fraction) + + # Final graph combining main patterns + final_graph = subgraph_cardinal | subgraph_decimal | subgraph_fraction + + final_graph = self.add_tokens(final_graph) + self.fst = final_graph.optimize() diff --git a/nemo_text_processing/text_normalization/vi/taggers/money.py b/nemo_text_processing/text_normalization/vi/taggers/money.py new file mode 100644 index 000000000..540094591 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/taggers/money.py @@ -0,0 +1,198 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import ( + NEMO_COMMA, + NEMO_DIGIT, + NEMO_SPACE, + GraphFst, + convert_space, + delete_space, + insert_space, +) +from nemo_text_processing.text_normalization.vi.utils import get_abs_path, load_labels + + +class MoneyFst(GraphFst): + """ + Finite state transducer for classifying money, e.g. + "10,5$" -> money { integer_part: "mười" currency_maj: "đô la" fractional_part: "năm mươi" currency_min: "xu" preserve_order: true } + "10đ" -> money { integer_part: "mười" currency_maj: "đồng" } + "10 triệu đồng" -> money { integer_part: "mười" quantity: "triệu" currency_maj: "đồng" } + + Args: + cardinal: CardinalFst instance for processing integer parts + decimal: DecimalFst instance for processing fractional parts + deterministic: if True will provide a single transduction option, for False multiple transduction are generated. + """ + + def __init__(self, cardinal: GraphFst, decimal: GraphFst, deterministic: bool = True): + super().__init__(name="money", kind="classify", deterministic=deterministic) + + # Load data + currency_major_labels = load_labels(get_abs_path("data/money/currency.tsv")) + currency_minor_labels = load_labels(get_abs_path("data/money/currency_minor.tsv")) + quantity_graph = pynini.string_file(get_abs_path("data/numbers/quantity_abbr.tsv")) + + # Load optimized per_unit files using subfst approach + per_unit_non_metric_path = get_abs_path("data/money/per_unit_non_metric.tsv") + per_unit_prefixes_path = get_abs_path("data/money/per_unit_prefixes.tsv") + per_unit_bases_path = get_abs_path("data/money/per_unit_bases.tsv") + + # Create subfst for metric per_unit patterns + graph_prefixes = pynini.string_file(per_unit_prefixes_path) + graph_bases = pynini.string_file(per_unit_bases_path) + + # Build metric combinations: "/kg" -> "một ki lô gam" + slash = pynutil.delete("/") + one_space = pynutil.insert("một ") + space = pynutil.insert(NEMO_SPACE) + + graph_metric_per_units = slash + one_space + graph_prefixes + space + graph_bases + graph_standalone_per_units = slash + one_space + graph_bases + + # Load non-metric per_unit entries + graph_non_metric_per_units = pynini.string_file(per_unit_non_metric_path) + + # Combine all per_unit mappings + per_unit_graph = graph_metric_per_units | graph_standalone_per_units | graph_non_metric_per_units + + # Basic components + cardinal_graph = cardinal.graph + currency_major_graph = pynini.string_map(currency_major_labels) + currency_minor_map = dict(currency_minor_labels) + decimal_graph = decimal.final_graph_wo_negative + + # Common patterns + integer_part = pynutil.insert('integer_part: "') + cardinal_graph + pynutil.insert('"') + preserve_order = pynutil.insert(" preserve_order: true") + optional_space = pynini.closure(delete_space, 0, 1) + + # Fractional part conversion for cents + two_digits_fractional_part = ( + pynini.closure(NEMO_DIGIT) + (NEMO_DIGIT - "0") + pynini.closure(pynutil.delete("0")) + ) @ ( + (pynutil.delete("0") + (NEMO_DIGIT - "0")) + | ((NEMO_DIGIT - "0") + pynutil.insert("0")) + | ((NEMO_DIGIT - "0") + NEMO_DIGIT) + ) + fractional_conversion = two_digits_fractional_part @ cardinal_graph + fractional_part = pynutil.insert('fractional_part: "') + fractional_conversion + pynutil.insert('"') + + all_patterns = [] + + # 1. Symbol-based patterns + symbol_patterns = [] + minor_only_patterns = [] + + for symbol, major_name in currency_major_labels: + maj_tag = pynutil.insert(f' currency_maj: "{major_name}"') + + # Simple integer pattern: 10$ -> mười đô la + simple_pattern = integer_part + pynutil.delete(symbol) + insert_space + maj_tag + symbol_patterns.append(simple_pattern) + + # Patterns with minor currency (cents/xu) + if symbol in currency_minor_map: + minor_name = currency_minor_map[symbol] + min_tag = pynutil.insert(f' currency_min: "{minor_name}"') + + # Minor-only pattern: 0,5$ -> năm mươi xu (highest priority) + minor_only = ( + pynutil.delete("0") + + pynutil.delete(NEMO_COMMA) + + fractional_part + + insert_space + + min_tag + + pynutil.delete(symbol) + + preserve_order + ) + minor_only_patterns.append(minor_only) + + # Major + minor pattern: 10,5$ -> mười đô la năm mươi xu + major_minor = ( + integer_part + + insert_space + + maj_tag + + pynini.cross(NEMO_COMMA, NEMO_SPACE) + + fractional_part + + insert_space + + min_tag + + pynutil.delete(symbol) + + preserve_order + ) + symbol_patterns.append(major_minor) + + # 2. Word-based patterns + word_patterns = [] + + # Complex decimal + currency: 1tr5 vnd -> một triệu năm trăm nghìn đồng + decimal_with_currency = ( + decimal_graph + + optional_space + + insert_space + + pynutil.insert(' currency_maj: "') + + convert_space(currency_major_graph) + + pynutil.insert('"') + ) + word_patterns.append(decimal_with_currency) + + # Quantity + currency: 10tr đồng -> mười triệu đồng + quantity_tag = pynutil.insert(' quantity: "') + convert_space(quantity_graph) + pynutil.insert('"') + quantity_pattern = ( + integer_part + + optional_space + + insert_space + + quantity_tag + + optional_space + + insert_space + + pynutil.insert(' currency_maj: "') + + convert_space(currency_major_graph) + + pynutil.insert('"') + ) + word_patterns.append(quantity_pattern) + + # Simple word pattern: 10 đồng -> mười đồng + simple_word_pattern = ( + integer_part + + optional_space + + insert_space + + pynutil.insert(' currency_maj: "') + + convert_space(currency_major_graph) + + pynutil.insert('"') + ) + word_patterns.append(simple_word_pattern) + + # Combine patterns with priorities + # Minor-only patterns get highest priority (negative weight) + if minor_only_patterns: + all_patterns.append(pynutil.add_weight(pynini.union(*minor_only_patterns), -0.0001)) + + # Symbol patterns get normal priority + if symbol_patterns: + all_patterns.append(pynini.union(*symbol_patterns)) + + # Word patterns get lowest priority + if word_patterns: + all_patterns.append(pynutil.add_weight(pynini.union(*word_patterns), 0.1)) + + # Final graph with optional per-unit support + final_graph = pynini.union(*all_patterns) + per_unit_tag = pynutil.insert(' morphosyntactic_features: "') + per_unit_graph + pynutil.insert('"') + final_graph += per_unit_tag.ques + + self.fst = self.add_tokens(final_graph.optimize()) diff --git a/nemo_text_processing/text_normalization/vi/taggers/ordinal.py b/nemo_text_processing/text_normalization/vi/taggers/ordinal.py new file mode 100644 index 000000000..acacf63f7 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/taggers/ordinal.py @@ -0,0 +1,62 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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 os +import pynini +from pynini.lib import pynutil + +from nemo_text_processing.text_normalization.vi.graph_utils import NEMO_DIGIT, GraphFst +from nemo_text_processing.text_normalization.vi.utils import get_abs_path, load_labels + + +class OrdinalFst(GraphFst): + """ + Finite state transducer for classifying Vietnamese ordinals, e.g. + thứ 1 -> ordinal { integer: "nhất" } + thứ 4 -> ordinal { integer: "tư" } + thứ 15 -> ordinal { integer: "mười lăm" } + Args: + cardinal: CardinalFst for number conversion + deterministic: if True will provide a single transduction option, + for False multiple options (used for audio-based normalization) + """ + + def __init__(self, cardinal, deterministic: bool = True): + super().__init__(name="ordinal", kind="classify", deterministic=deterministic) + + prefix = "thứ " + number_pattern = pynini.closure(NEMO_DIGIT, 1) + + ordinal_exceptions = { + row[0]: row[1] for row in load_labels(get_abs_path("data/ordinal/ordinal_exceptions.tsv")) + } + + exception_patterns = [] + for digit, word in ordinal_exceptions.items(): + exception_patterns.append(pynini.cross(digit, word)) + + exception_graph = pynini.union(*exception_patterns) if exception_patterns else None + + combined_graph = cardinal.graph + if exception_graph: + combined_graph = pynini.union(exception_graph, cardinal.graph) + + self.graph = ( + pynutil.delete(prefix) + + pynutil.insert("integer: \"") + + pynini.compose(number_pattern, combined_graph) + + pynutil.insert("\"") + ) + + self.fst = self.add_tokens(self.graph).optimize() diff --git a/nemo_text_processing/text_normalization/vi/taggers/punctuation.py b/nemo_text_processing/text_normalization/vi/taggers/punctuation.py new file mode 100644 index 000000000..1244f236a --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/taggers/punctuation.py @@ -0,0 +1,35 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import GraphFst + + +class PunctuationFst(GraphFst): + """ + Finite state transducer for classifying punctuation for Vietnamese + """ + + def __init__(self, deterministic: bool = True): + super().__init__(name="punctuation", kind="classify", deterministic=deterministic) + + s = "!#%&'()*+,-./:;<=>?@^_`{|}~′″°" + + punct = pynini.union(*s) + self.punct_marks = punct + self.graph = punct + + self.fst = (pynutil.insert("name: \"") + self.graph + pynutil.insert("\"")).optimize() diff --git a/nemo_text_processing/text_normalization/vi/taggers/range.py b/nemo_text_processing/text_normalization/vi/taggers/range.py new file mode 100644 index 000000000..f52341d9d --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/taggers/range.py @@ -0,0 +1,66 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import GraphFst + + +class RangeFst(GraphFst): + """ + Finite state transducer for classifying Vietnamese ranges with dash "-" + Examples: + 10k-20k -> tokens { name: "mười nghìn đến hai mười nghìn" } + 10h-8h -> tokens { name: "mười giờ đến tám giờ" } + 10$-20$ -> tokens { name: "mười đô la đến hai mười đô la" } + + Args: + time: composed time tagger and verbalizer + date: composed date tagger and verbalizer + decimal: composed decimal tagger and verbalizer + money: composed money tagger and verbalizer + deterministic: if True will provide a single transduction option, + for False multiple transduction are generated (used for audio-based normalization) + """ + + def __init__( + self, + time: GraphFst, + date: GraphFst, + decimal: GraphFst, + money: GraphFst, + measure: GraphFst, + deterministic: bool = True, + ): + super().__init__(name="range", kind="classify", deterministic=deterministic) + + delete_space = pynini.closure(pynutil.delete(" "), 0, 1) + + # Pattern: X-Y -> X đến Y + # This will handle time ranges, date ranges, decimal ranges, and money ranges with dash + range_pattern = ( + (time | date | decimal | money | measure) + + delete_space + + pynini.cross("-", " đến ") + + delete_space + + (time | date | decimal | money | measure) + ) + + self.graph = range_pattern + + # Convert to final FST format + self.graph = self.graph.optimize() + graph = pynutil.insert("name: \"") + self.graph + pynutil.insert("\"") + self.fst = graph.optimize() diff --git a/nemo_text_processing/text_normalization/vi/taggers/roman.py b/nemo_text_processing/text_normalization/vi/taggers/roman.py new file mode 100644 index 000000000..1c68c7875 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/taggers/roman.py @@ -0,0 +1,91 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import GraphFst +from nemo_text_processing.text_normalization.vi.utils import get_abs_path, load_labels + + +class RomanFst(GraphFst): + """ + Finite state transducer for classifying roman numbers in Vietnamese context: + e.g. "thế kỉ XV" -> tokens { roman { key_cardinal: "thế kỉ" integer: "mười lăm" } } + e.g. "thế kỷ IV" -> tokens { roman { key_cardinal: "thế kỷ" integer: "bốn" } } + e.g. "thứ IV" -> tokens { roman { key_cardinal: "thứ" integer: "bốn" } } + e.g. "chương III" -> tokens { roman { key_cardinal: "chương" integer: "ba" } } + e.g. "phần ix" -> tokens { roman { key_cardinal: "phần" integer: "chín" } } + + Args: + cardinal: CardinalFst + deterministic: if True will provide a single transduction option, + for False multiple transduction are generated (used for audio-based normalization) + """ + + def __init__(self, cardinal: GraphFst, deterministic: bool = True): + super().__init__(name="roman", kind="classify", deterministic=deterministic) + + key_words = [] + key_word_path = get_abs_path("data/roman/key_word.tsv") + for k_word in load_labels(key_word_path): + key_words.append(k_word[0]) + + key_words_fst = pynini.union(*[pynini.accep(word) for word in key_words]).optimize() + + roman_numeral_path = get_abs_path("data/roman/roman_numerals.tsv") + roman_numeral_pairs = load_labels(roman_numeral_path) + + roman_to_arabic = {} + for roman, value in roman_numeral_pairs: + roman_to_arabic[roman] = value + roman_to_arabic[roman.lower()] = value + + self.arabic_to_roman = {} + for roman, value in roman_numeral_pairs: + self.arabic_to_roman[int(value)] = roman + + valid_roman_pairs = [] + for i in range(1, 4000): + roman_upper = self._int_to_roman(i) + roman_lower = roman_upper.lower() + valid_roman_pairs.append((roman_upper, str(i))) + valid_roman_pairs.append((roman_lower, str(i))) + + roman_to_arabic_fst = pynini.string_map(valid_roman_pairs).optimize() + + cardinal_graph = cardinal.graph + + graph = ( + pynutil.insert("key_cardinal: \"") + + key_words_fst + + pynutil.insert("\"") + + pynini.accep(" ") + + pynutil.insert("integer: \"") + + pynini.compose(roman_to_arabic_fst, cardinal_graph) + + pynutil.insert("\"") + ).optimize() + + self.fst = self.add_tokens(graph).optimize() + + def _int_to_roman(self, num): + values = sorted(self.arabic_to_roman.keys(), reverse=True) + + roman_num = '' + for value in values: + while num >= value: + roman_num += self.arabic_to_roman[value] + num -= value + + return roman_num diff --git a/nemo_text_processing/text_normalization/vi/taggers/time.py b/nemo_text_processing/text_normalization/vi/taggers/time.py new file mode 100644 index 000000000..ecbbdb9c7 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/taggers/time.py @@ -0,0 +1,141 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import ( + NEMO_DIGIT, + NEMO_SPACE, + GraphFst, + convert_space, + insert_space, +) +from nemo_text_processing.text_normalization.vi.utils import get_abs_path + + +class TimeFst(GraphFst): + """ + Finite state transducer for classifying time in Vietnamese. + + Supports various formats including: + - Digital formats: "8:30", "14:45", "5:20:35" + - Vietnamese formats: "14 giờ 30 phút", "2 giờ 15 phút 10 giây" + - Abbreviated formats: "9h", "9g", "14h30", "14g30", "3p20s" + - With time zones: "8:23 gmt", "15h cst" + + Args: + cardinal: CardinalFst for number conversion + deterministic: if True will provide a single transduction option, + for False multiple transduction are generated (used for audio-based normalization) + """ + + def __init__(self, cardinal: GraphFst, deterministic: bool = True): + super().__init__(name="time", kind="classify", deterministic=deterministic) + + time_zone = pynini.string_file(get_abs_path("data/time/time_zones.tsv")) + digit = NEMO_DIGIT + delete_leading_zero = (pynutil.delete("0").ques | (digit - "0")) + digit + cardinal_graph = cardinal.graph + + hours = pynini.union(*[str(x) for x in range(0, 25)]) + minutes_seconds = pynini.union(*[str(x) for x in range(0, 60)]) + + def label(name, graph): + return pynutil.insert(f'{name}: "') + graph + pynutil.insert('"') + + hour = label('hours', delete_leading_zero @ hours @ cardinal_graph) + minute = label('minutes', delete_leading_zero @ minutes_seconds @ cardinal_graph) + second = label('seconds', delete_leading_zero @ minutes_seconds @ cardinal_graph) + zone = label('zone', convert_space(time_zone)) + + h_suffix = pynini.union(pynutil.delete("h"), pynutil.delete("g")) + h_word = pynutil.delete(" giờ") + m_word = pynutil.delete(" phút") + s_word = pynutil.delete(" giây") + + opt_zone_space = pynini.closure(pynini.accep(NEMO_SPACE) + zone, 0, 1) + opt_zone = pynini.closure(zone, 0, 1) + preserve = pynutil.insert(" preserve_order: true") + + # Define sub-patterns for better readability + # Digital formats + pattern_hour_minute = hour + pynutil.delete(":") + insert_space + minute + opt_zone_space + + pattern_hour_minute_second = ( + hour + + pynutil.delete(":") + + insert_space + + minute + + pynutil.delete(":") + + insert_space + + second + + opt_zone_space + + preserve + ) + + # Abbreviated formats + pattern_hour_suffix = hour + h_suffix + opt_zone_space + pattern_hour_suffix_minute = hour + h_suffix + minute + opt_zone + pattern_minute_p = minute + pynutil.delete("p") + pattern_second_s = second + pynutil.delete("s") + pattern_minute_p_second_s = minute + pynutil.delete("p") + insert_space + second + pynutil.delete("s") + + # Vietnamese word formats + pattern_hour_word = hour + h_word + opt_zone_space + + pattern_hour_word_minute = hour + h_word + pynutil.delete(NEMO_SPACE) + minute + m_word + opt_zone_space + + pattern_hour_word_minute_second = ( + hour + + h_word + + pynutil.delete(NEMO_SPACE) + + minute + + m_word + + pynutil.delete(NEMO_SPACE) + + second + + s_word + + opt_zone_space + + preserve + ) + + pattern_minute_word = minute + m_word + pattern_minute_word_second = minute + m_word + pynutil.delete(NEMO_SPACE) + second + s_word + pattern_second_word = second + s_word + + # Time zone specific patterns + pattern_hour_suffix_space_zone = hour + h_suffix + pynini.accep(NEMO_SPACE) + zone + pattern_hour_suffix_zone = hour + h_suffix + zone + + patterns = [ + pattern_hour_minute, + pattern_hour_minute_second, + pattern_hour_suffix, + pattern_hour_suffix_minute, + pattern_minute_p, + pattern_second_s, + pattern_minute_p_second_s, + pattern_hour_word, + pattern_hour_word_minute, + pattern_hour_word_minute_second, + pattern_minute_word, + pattern_minute_word_second, + pattern_second_word, + pattern_hour_suffix_space_zone, + pattern_hour_suffix_zone, + ] + + final_graph = pynini.union(*patterns).optimize() + + self.fst = self.add_tokens(final_graph).optimize() diff --git a/nemo_text_processing/text_normalization/vi/taggers/tokenize_and_classify.py b/nemo_text_processing/text_normalization/vi/taggers/tokenize_and_classify.py new file mode 100644 index 000000000..51e645895 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/taggers/tokenize_and_classify.py @@ -0,0 +1,194 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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 os +import time + +import pynini +from pynini.lib import pynutil + +from nemo_text_processing.text_normalization.vi.graph_utils import ( + GraphFst, + delete_extra_space, + delete_space, + generator_main, +) +from nemo_text_processing.text_normalization.vi.taggers.cardinal import CardinalFst +from nemo_text_processing.text_normalization.vi.taggers.date import DateFst +from nemo_text_processing.text_normalization.vi.taggers.decimal import DecimalFst +from nemo_text_processing.text_normalization.vi.taggers.fraction import FractionFst +from nemo_text_processing.text_normalization.vi.taggers.measure import MeasureFst +from nemo_text_processing.text_normalization.vi.taggers.money import MoneyFst +from nemo_text_processing.text_normalization.vi.taggers.ordinal import OrdinalFst +from nemo_text_processing.text_normalization.vi.taggers.punctuation import PunctuationFst +from nemo_text_processing.text_normalization.vi.taggers.range import RangeFst +from nemo_text_processing.text_normalization.vi.taggers.roman import RomanFst +from nemo_text_processing.text_normalization.vi.taggers.time import TimeFst +from nemo_text_processing.text_normalization.vi.taggers.whitelist import WhiteListFst +from nemo_text_processing.text_normalization.vi.taggers.word import WordFst +from nemo_text_processing.text_normalization.vi.verbalizers.cardinal import CardinalFst as VCardinalFst +from nemo_text_processing.text_normalization.vi.verbalizers.date import DateFst as VDateFst +from nemo_text_processing.text_normalization.vi.verbalizers.decimal import DecimalFst as VDecimalFst +from nemo_text_processing.text_normalization.vi.verbalizers.fraction import FractionFst as VFractionFst +from nemo_text_processing.text_normalization.vi.verbalizers.measure import MeasureFst as VMeasureFst +from nemo_text_processing.text_normalization.vi.verbalizers.money import MoneyFst as VMoneyFst +from nemo_text_processing.text_normalization.vi.verbalizers.time import TimeFst as VTimeFst +from nemo_text_processing.utils.logging import logger + + +class ClassifyFst(GraphFst): + 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) + far_file = os.path.join( + cache_dir, + f"vi_tn_{deterministic}_deterministic_{input_case}_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"] + logger.info(f"ClassifyFst.fst was restored from {far_file}.") + else: + logger.info(f"Creating Vietnamese ClassifyFst grammars.") + + start_time = time.time() + cardinal = CardinalFst(deterministic=deterministic) + cardinal_graph = cardinal.fst + logger.debug(f"cardinal: {time.time() - start_time: .2f}s -- {cardinal_graph.num_states()} nodes") + + start_time = time.time() + punctuation = PunctuationFst(deterministic=deterministic) + punct_graph = punctuation.fst + logger.debug(f"punct: {time.time() - start_time: .2f}s -- {punct_graph.num_states()} nodes") + + start_time = time.time() + whitelist = WhiteListFst(input_case=input_case, deterministic=deterministic) + whitelist_graph = whitelist.fst + logger.debug(f"whitelist: {time.time() - start_time: .2f}s -- {whitelist_graph.num_states()} nodes") + + start_time = time.time() + word_graph = WordFst(deterministic=deterministic).fst + logger.debug(f"word: {time.time() - start_time: .2f}s -- {word_graph.num_states()} nodes") + + start_time = time.time() + ordinal = OrdinalFst(cardinal=cardinal, deterministic=deterministic) + ordinal_graph = ordinal.fst + logger.debug(f"ordinal: {time.time() - start_time: .2f}s -- {ordinal_graph.num_states()} nodes") + + start_time = time.time() + decimal = DecimalFst(cardinal=cardinal, deterministic=deterministic) + decimal_graph = decimal.fst + logger.debug(f"decimal: {time.time() - start_time: .2f}s -- {decimal_graph.num_states()} nodes") + + start_time = time.time() + fraction = FractionFst(cardinal=cardinal, deterministic=deterministic) + fraction_graph = fraction.fst + logger.debug(f"fraction: {time.time() - start_time: .2f}s -- {fraction_graph.num_states()} nodes") + + start_time = time.time() + date = DateFst(cardinal=cardinal, deterministic=deterministic) + date_graph = date.fst + logger.debug(f"date: {time.time() - start_time: .2f}s -- {date_graph.num_states()} nodes") + + start_time = time.time() + roman = RomanFst(cardinal=cardinal, deterministic=deterministic) + roman_graph = roman.fst + logger.debug(f"roman: {time.time() - start_time: .2f}s -- {roman_graph.num_states()} nodes") + + start_time = time.time() + time_fst = TimeFst(cardinal=cardinal, deterministic=deterministic) + time_graph = time_fst.fst + logger.debug(f"time: {time.time() - start_time: .2f}s -- {time_graph.num_states()} nodes") + + start_time = time.time() + money = MoneyFst(cardinal=cardinal, decimal=decimal, deterministic=deterministic) + money_graph = money.fst + logger.debug(f"money: {time.time() - start_time: .2f}s -- {money_graph.num_states()} nodes") + + start_time = time.time() + measure = MeasureFst(cardinal=cardinal, decimal=decimal, fraction=fraction, deterministic=deterministic) + measure_graph = measure.fst + logger.debug(f"measure: {time.time() - start_time: .2f}s -- {measure_graph.num_states()} nodes") + + # Create composed verbalizers for range processing + start_time = time.time() + v_cardinal = VCardinalFst(deterministic=deterministic) + v_date = VDateFst(deterministic=deterministic) + date_final = pynini.compose(date_graph, v_date.fst) + + v_decimal = VDecimalFst(v_cardinal, deterministic=deterministic) + decimal_final = pynini.compose(decimal_graph, v_decimal.fst) + + v_time = VTimeFst(deterministic=deterministic) + time_final = pynini.compose(time_graph, v_time.fst) + + v_money = VMoneyFst(deterministic=deterministic) + money_final = pynini.compose(money_graph, v_money.fst) + + v_fraction = VFractionFst(deterministic=deterministic) + v_measure = VMeasureFst( + decimal=v_decimal, cardinal=v_cardinal, fraction=v_fraction, deterministic=deterministic + ) + measure_final = pynini.compose(measure_graph, v_measure.fst) + + # Create range graph + range_fst = RangeFst( + time=time_final, + date=date_final, + decimal=decimal_final, + money=money_final, + measure=measure_final, + deterministic=deterministic, + ) + range_graph = range_fst.fst + logger.debug(f"range: {time.time() - start_time: .2f}s -- {range_graph.num_states()} nodes") + + classify = ( + pynutil.add_weight(whitelist_graph, 1.01) + | pynutil.add_weight(money_graph, 1.1) + | pynutil.add_weight(range_graph, 1.1) + | pynutil.add_weight(decimal_graph, 1.1) + | pynutil.add_weight(date_graph, 1.1) + | pynutil.add_weight(cardinal_graph, 1.1) + | pynutil.add_weight(ordinal_graph, 1.1) + | pynutil.add_weight(fraction_graph, 1.1) + | pynutil.add_weight(time_graph, 1.1) + | pynutil.add_weight(measure_graph, 1.1) + | pynutil.add_weight(word_graph, 100) + | pynutil.add_weight(roman_graph, 101) + ) + punct = ( + pynutil.insert("tokens { ") + pynutil.add_weight(punct_graph, 2.1) + pynutil.insert(" }") + ) # Lower priority than semantic classes + token = pynutil.insert("tokens { ") + classify + pynutil.insert(" }") + token_plus_punct = ( + pynini.closure(punct + pynutil.insert(" ")) + token + pynini.closure(pynutil.insert(" ") + punct) + ) + + graph = token_plus_punct + pynini.closure((delete_extra_space).ques + token_plus_punct) + graph = delete_space + graph + delete_space + + self.fst = graph.optimize() + + if far_file: + generator_main(far_file, {"tokenize_and_classify": self.fst}) diff --git a/nemo_text_processing/text_normalization/vi/taggers/whitelist.py b/nemo_text_processing/text_normalization/vi/taggers/whitelist.py new file mode 100644 index 000000000..d2775f205 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/taggers/whitelist.py @@ -0,0 +1,70 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import GraphFst, convert_space +from nemo_text_processing.text_normalization.vi.utils import get_abs_path, load_labels + + +class WhiteListFst(GraphFst): + """ + Finite state transducer for classifying whitelist for Vietnamese, e.g. + "h" -> tokens { name: "giờ" } + "p" -> tokens { name: "phút" } + "s" -> tokens { name: "giây" } + This class has highest priority among all classifier grammars. Whitelisted tokens are defined and loaded from "data/whitelist.tsv". + + 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) + input_file: path to a file with whitelist replacements + """ + + def __init__(self, input_case: str, deterministic: bool = True, input_file: str = None): + super().__init__(name="whitelist", kind="classify", deterministic=deterministic) + + def _get_whitelist_graph(input_case, file): + whitelist = load_labels(file) + if input_case == "lower_cased": + whitelist = [[x[0].lower()] + x[1:] for x in whitelist] + graph = pynini.string_map(whitelist) + return graph + + graph = _get_whitelist_graph(input_case, get_abs_path("data/whitelist.tsv")) + if not deterministic and input_case != "lower_cased": + graph |= pynutil.add_weight( + _get_whitelist_graph("lower_cased", get_abs_path("data/whitelist.tsv")), weight=0.0001 + ) + + if input_file: + whitelist_provided = _get_whitelist_graph(input_case, input_file) + if not deterministic: + graph |= whitelist_provided + else: + graph = whitelist_provided + + # Add time units from time_units.tsv for better time handling + if not deterministic: + time_units_graph = _get_whitelist_graph(input_case, file=get_abs_path("data/time/time_units.tsv")) + graph |= time_units_graph + + self.graph = graph + self.final_graph = convert_space(self.graph).optimize() + self.fst = (pynutil.insert("name: \"") + self.final_graph + pynutil.insert("\"")).optimize() + + # Add tokens wrapper + self.fst = self.add_tokens(self.fst) diff --git a/nemo_text_processing/text_normalization/vi/taggers/word.py b/nemo_text_processing/text_normalization/vi/taggers/word.py new file mode 100644 index 000000000..96d203467 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/taggers/word.py @@ -0,0 +1,46 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import NEMO_ALPHA, NEMO_DIGIT, NEMO_NOT_SPACE, GraphFst + + +class WordFst(GraphFst): + """ + Finite state transducer for classifying Vietnamese words. + e.g. ngày -> name: "ngày" + + 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="classify", deterministic=deterministic) + + # Symbols that should cause token breaks + # Include measure symbols, currency symbols, and digits + symbols_to_exclude = pynini.union("°", "′", "″", "$", "€", "₩", "£", "¥", "#", "%", "₫", NEMO_DIGIT).optimize() + + word_chars = pynini.closure(pynini.difference(NEMO_NOT_SPACE, symbols_to_exclude), 1) + default_word_graph = word_chars + + alpha_word_graph = pynini.closure(NEMO_ALPHA, 1) + + graph = pynutil.add_weight(alpha_word_graph, -1.0) | default_word_graph + + word = pynutil.insert("name: \"") + graph + pynutil.insert("\"") + self.fst = word.optimize() diff --git a/nemo_text_processing/text_normalization/vi/utils.py b/nemo_text_processing/text_normalization/vi/utils.py new file mode 100644 index 000000000..6b0871d9d --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/utils.py @@ -0,0 +1,42 @@ +# Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES. 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: + labels = list(csv.reader(label_tsv, delimiter="\t")) + return labels diff --git a/nemo_text_processing/text_normalization/vi/verbalizers/__init__.py b/nemo_text_processing/text_normalization/vi/verbalizers/__init__.py new file mode 100644 index 000000000..b2de1dca7 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/verbalizers/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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/vi/verbalizers/cardinal.py b/nemo_text_processing/text_normalization/vi/verbalizers/cardinal.py new file mode 100644 index 000000000..b096e759d --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/verbalizers/cardinal.py @@ -0,0 +1,55 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import NEMO_NOT_QUOTE, GraphFst, delete_space + + +class CardinalFst(GraphFst): + """ + Finite state transducer for verbalizing Vietnamese cardinal numbers, e.g. + cardinal { negative: "true" integer: "hai mươi ba" } -> âm hai mươi ba + cardinal { integer: "một trăm" } -> một trăm + + 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="cardinal", kind="verbalize", deterministic=deterministic) + + # Handle negative sign - Vietnamese uses "âm" for negative numbers + self.optional_sign = pynini.cross("negative: \"true\"", "âm ") + if not deterministic: + # Alternative ways to say negative in Vietnamese + self.optional_sign |= pynini.cross("negative: \"true\"", "trừ ") + self.optional_sign |= pynini.cross("negative: \"true\"", "âm ") + + self.optional_sign = pynini.closure(self.optional_sign + delete_space, 0, 1) + + # Handle the integer part + integer = pynini.closure(NEMO_NOT_QUOTE) + + self.integer = delete_space + pynutil.delete("\"") + integer + pynutil.delete("\"") + integer = pynutil.delete("integer:") + self.integer + + # Combine negative sign with integer + self.numbers = self.optional_sign + integer + + # Delete the token structure and create final FST + delete_tokens = self.delete_tokens(self.numbers) + self.fst = delete_tokens.optimize() diff --git a/nemo_text_processing/text_normalization/vi/verbalizers/date.py b/nemo_text_processing/text_normalization/vi/verbalizers/date.py new file mode 100644 index 000000000..4e918e3d4 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/verbalizers/date.py @@ -0,0 +1,71 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import NEMO_NOT_QUOTE, GraphFst, delete_space, insert_space + + +class DateFst(GraphFst): + """ + Finite state transducer for verbalizing Vietnamese dates, e.g. + date { day: "mười lăm" month: "một" year: "hai nghìn hai mươi tư" } + -> ngày mười lăm tháng một năm hai nghìn hai mươi tư + + date { month: "tư" year: "hai nghìn hai mươi tư" } + -> tháng tư năm hai nghìn hai mươi tư + + date { year: "hai mươi" era: "sau công nguyên" } + -> năm hai mươi sau công nguyên + + date { ordinal: "mười" era: "trước công nguyên" } + -> năm thứ mười trước công nguyên + """ + + def __init__(self, deterministic: bool = True): + super().__init__(name="date", kind="verbalize", deterministic=deterministic) + + quoted_content = pynini.closure(NEMO_NOT_QUOTE) + + day_expr = pynutil.delete("day: \"") + quoted_content + pynutil.delete("\"") + day_with_prefix = pynutil.insert("ngày ") + day_expr + + month_expr = pynutil.delete("month: \"") + quoted_content + pynutil.delete("\"") + month_with_prefix = pynutil.insert("tháng ") + month_expr + + year_expr = pynutil.delete("year: \"") + quoted_content + pynutil.delete("\"") + year_with_prefix = pynutil.insert("năm ") + year_expr + + era_expr = pynutil.delete("era: \"") + quoted_content + pynutil.delete("\"") + + ordinal_expr = pynutil.delete("ordinal: \"") + quoted_content + pynutil.delete("\"") + ordinal_with_prefix = pynutil.insert("năm thứ ") + ordinal_expr + + date_graph = pynini.union( + day_with_prefix + + delete_space + + insert_space + + month_with_prefix + + delete_space + + insert_space + + year_with_prefix, + month_with_prefix + delete_space + insert_space + year_with_prefix, + day_with_prefix + delete_space + insert_space + month_with_prefix, + year_with_prefix, + year_with_prefix + delete_space + insert_space + era_expr, + ordinal_with_prefix + delete_space + insert_space + era_expr, + ) + + self.fst = self.delete_tokens(date_graph).optimize() diff --git a/nemo_text_processing/text_normalization/vi/verbalizers/decimal.py b/nemo_text_processing/text_normalization/vi/verbalizers/decimal.py new file mode 100644 index 000000000..c94dd0653 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/verbalizers/decimal.py @@ -0,0 +1,103 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import ( + NEMO_COMMA_VI, + NEMO_NOT_QUOTE, + GraphFst, + delete_space, + insert_space, +) + + +class DecimalFst(GraphFst): + """ + Finite state transducer for verbalizing Vietnamese decimal numbers, e.g. + decimal { negative: "true" integer_part: "mười hai" fractional_part: "năm" quantity: "tỷ" } -> âm mười hai phẩy năm tỷ + decimal { integer_part: "tám trăm mười tám" fractional_part: "ba không ba" } -> tám trăm mười tám phẩy ba không ba + decimal { integer_part: "không" fractional_part: "hai" quantity: "triệu" } -> không phẩy hai triệu + + Args: + cardinal: CardinalFst instance for handling integer verbalization + deterministic: if True will provide a single transduction option, + for False multiple transduction are generated (used for audio-based normalization) + """ + + def __init__(self, cardinal, deterministic: bool = True): + super().__init__(name="decimal", kind="verbalize", deterministic=deterministic) + + # Basic components + integer = pynutil.delete("integer_part:") + cardinal.integer + fractional = ( + pynutil.delete("fractional_part:") + + delete_space + + pynutil.delete("\"") + + pynini.closure(NEMO_NOT_QUOTE, 1) + + pynutil.delete("\"") + ) + quantity = ( + pynutil.delete("quantity:") + + delete_space + + pynutil.delete("\"") + + pynini.closure(NEMO_NOT_QUOTE, 1) + + pynutil.delete("\"") + ) + + # Negative handling + negative = pynini.cross("negative: \"true\"", "âm ") + if not deterministic: + negative |= pynini.cross("negative: \"true\"", "trừ ") + optional_negative = pynini.closure(negative + delete_space, 0, 1) + + # Simple patterns + simple_integer = integer + + integer_with_quantity = integer + delete_space + insert_space + quantity + + decimal_with_comma = ( + integer + delete_space + insert_space + pynutil.insert(NEMO_COMMA_VI) + insert_space + fractional + ) + + decimal_with_quantity = ( + integer + + delete_space + + insert_space + + pynutil.insert(NEMO_COMMA_VI) + + insert_space + + fractional + + delete_space + + insert_space + + quantity + ) + + fractional_only = ( + pynini.closure(integer + delete_space + insert_space, 0, 1) + + pynutil.insert(NEMO_COMMA_VI) + + insert_space + + fractional + ) + + # Group all patterns + all_patterns = pynini.union( + simple_integer, integer_with_quantity, decimal_with_comma, decimal_with_quantity, fractional_only + ) + + # Combine with negative handling + graph = optional_negative + all_patterns + + self.numbers = graph + self.fst = self.delete_tokens(graph).optimize() diff --git a/nemo_text_processing/text_normalization/vi/verbalizers/fraction.py b/nemo_text_processing/text_normalization/vi/verbalizers/fraction.py new file mode 100644 index 000000000..675d959df --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/verbalizers/fraction.py @@ -0,0 +1,55 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import NEMO_NOT_QUOTE, GraphFst, delete_space + + +class FractionFst(GraphFst): + """ + Finite state transducer for verbalizing Vietnamese fraction numbers, e.g. + fraction { negative: "true" integer_part: "hai mươi ba" numerator: "một" denominator: "năm" } -> âm hai mươi ba và một phần năm + fraction { numerator: "ba" denominator: "chín" } -> ba phần chín + fraction { integer_part: "một trăm" numerator: "hai" denominator: "ba" } -> một trăm và hai phần ba + + 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="fraction", kind="verbalize", deterministic=deterministic) + + optional_sign = pynini.cross("negative: \"true\"", "âm ") + if not deterministic: + optional_sign |= pynini.cross("negative: \"true\"", "trừ ") + optional_sign = pynini.closure(optional_sign + delete_space, 0, 1) + + part = pynini.closure(NEMO_NOT_QUOTE) + delete_quotes = delete_space + pynutil.delete("\"") + part + pynutil.delete("\"") + + integer_tagged = pynutil.delete("integer_part:") + delete_quotes + numerator_tagged = pynutil.delete("numerator:") + delete_quotes + denominator_tagged = pynutil.delete("denominator:") + delete_quotes + + fraction_part = numerator_tagged + delete_space + pynutil.insert(" phần ") + denominator_tagged + + simple_fraction = fraction_part + mixed_fraction = integer_tagged + delete_space + pynutil.insert(" và ") + fraction_part + + self.numbers = optional_sign + (simple_fraction | mixed_fraction) + + self.fst = self.delete_tokens(self.numbers).optimize() diff --git a/nemo_text_processing/text_normalization/vi/verbalizers/measure.py b/nemo_text_processing/text_normalization/vi/verbalizers/measure.py new file mode 100644 index 000000000..49283eb6f --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/verbalizers/measure.py @@ -0,0 +1,67 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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 nemo_text_processing.text_normalization.vi.graph_utils import ( + GraphFst, + delete_preserve_order, + delete_space, + extract_field, + extract_wrapper_content, + insert_space, +) + + +class MeasureFst(GraphFst): + """ + Finite state transducer for verbalizing measure for Vietnamese, e.g. + measure { negative: "true" cardinal { integer: "mười hai" } units: "ki lô gam" } -> âm mười hai ki lô gam + measure { decimal { integer_part: "mười hai" fractional_part: "năm" } units: "ki lô gam" } -> mười hai phẩy năm ki lô gam + measure { cardinal { integer: "một" } units: "ki lô gam" } -> một ki lô gam + + Args: + decimal: DecimalFst verbalizer + cardinal: CardinalFst verbalizer + fraction: FractionFst verbalizer + deterministic: if True will provide a single transduction option, + for False multiple transduction are generated (used for audio-based normalization) + """ + + def __init__(self, decimal: GraphFst, cardinal: GraphFst, fraction: GraphFst, deterministic: bool = True): + super().__init__(name="measure", kind="verbalize", deterministic=deterministic) + + # Extract components + unit = extract_field("units") + + # Handle negative sign - Vietnamese uses "âm" for negative numbers + optional_negative = pynini.closure(pynini.cross("negative: \"true\"", "âm ") + delete_space, 0, 1) + if not deterministic: + # Alternative ways to say negative in Vietnamese + optional_negative |= pynini.closure(pynini.cross("negative: \"true\"", "trừ ") + delete_space, 0, 1) + + # Combine all number types into single graph + number_graph = ( + extract_wrapper_content("decimal", decimal.numbers) + | extract_wrapper_content("cardinal", cardinal.numbers) + | extract_wrapper_content("fraction", fraction.numbers) + ) + + # Main pattern: [negative] number + space + unit (most common case) + graph = optional_negative + number_graph + delete_space + insert_space + unit + + # Handle preserve_order: [negative] unit + space + number + graph |= optional_negative + unit + delete_space + insert_space + number_graph + delete_preserve_order + + self.fst = self.delete_tokens(graph).optimize() diff --git a/nemo_text_processing/text_normalization/vi/verbalizers/money.py b/nemo_text_processing/text_normalization/vi/verbalizers/money.py new file mode 100644 index 000000000..3680c7f0c --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/verbalizers/money.py @@ -0,0 +1,144 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import ( + NEMO_COMMA_VI, + NEMO_NOT_QUOTE, + NEMO_SPACE, + GraphFst, + delete_preserve_order, + delete_space, + insert_space, +) +from nemo_text_processing.text_normalization.vi.utils import get_abs_path + + +class MoneyFst(GraphFst): + """ + Finite state transducer for verbalizing money, e.g. + money { integer_part: "mười" currency_maj: "đồng" } -> "mười đồng" + money { integer_part: "mười" quantity: "triệu" currency_maj: "đồng" } -> "mười triệu đồng" + money { integer_part: "mười" currency_maj: "đô la" fractional_part: "năm mươi" currency_min: "xu" preserve_order: true } -> "mười đô la năm mươi xu" + money { fractional_part: "năm mươi" currency_min: "xu" preserve_order: true } -> "năm mươi xu" + """ + + def __init__(self, deterministic: bool = True): + super().__init__(name="money", kind="verbalize", deterministic=deterministic) + + integer_part = pynutil.delete('integer_part: "') + pynini.closure(NEMO_NOT_QUOTE, 1) + pynutil.delete('"') + fractional_part = ( + pynutil.delete('fractional_part: "') + pynini.closure(NEMO_NOT_QUOTE, 1) + pynutil.delete('"') + ) + quantity = pynutil.delete('quantity: "') + pynini.closure(NEMO_NOT_QUOTE, 1) + pynutil.delete('"') + currency_maj = pynutil.delete('currency_maj: "') + pynini.closure(NEMO_NOT_QUOTE, 1) + pynutil.delete('"') + currency_min = pynutil.delete('currency_min: "') + pynini.closure(NEMO_NOT_QUOTE, 1) + pynutil.delete('"') + + # Following English prioritization pattern for better determinism + + # 1. Minor only: fractional + minor (highest priority for fractional-only cases) + graph_minor = fractional_part + delete_space + insert_space + currency_min + delete_preserve_order + + # 2. Major + minor: integer + major + fractional + minor (for complete cases like 10,5$) + graph_integer_with_minor = ( + integer_part + + delete_space + + insert_space + + currency_maj + + delete_space + + insert_space + + fractional_part + + delete_space + + insert_space + + currency_min + + delete_preserve_order + ) + + # 3. Simple integer + currency (most common case) + graph_integer = integer_part + delete_space + insert_space + currency_maj + + # 4. With quantity: integer + quantity + currency + graph_with_quantity = ( + integer_part + delete_space + insert_space + quantity + delete_space + insert_space + currency_maj + ) + + # 5. Decimal format (using "phẩy" for comma) - for cases like 10,5 đồng + graph_decimal = ( + integer_part + + delete_space + + insert_space + + pynutil.insert(NEMO_COMMA_VI) + + insert_space + + fractional_part + + delete_space + + insert_space + + currency_maj + ) + + # 6. Decimal with quantity: integer + fractional + quantity + currency - for cases like 2,5 triệu đồng + graph_decimal_with_quantity = ( + integer_part + + delete_space + + insert_space + + pynutil.insert(NEMO_COMMA_VI) + + insert_space + + fractional_part + + delete_space + + insert_space + + quantity + + delete_space + + insert_space + + currency_maj + ) + + # Create main graph with proper priority order (similar to English) + graph = ( + graph_minor # Handle minor-only cases first + | graph_integer_with_minor # Handle major+minor cases + | graph_decimal_with_quantity # Handle decimal with quantity cases (before simpler decimal) + | graph_with_quantity # Handle quantity cases + | graph_decimal # Handle decimal cases + | graph_integer # Handle simple cases (most common, lowest priority) + ) + + per_units_non_metric = pynini.string_file(get_abs_path("data/money/per_unit_non_metric.tsv")) + + per_unit_prefixes = pynini.string_file(get_abs_path("data/money/per_unit_prefixes.tsv")) + per_unit_bases = pynini.string_file(get_abs_path("data/money/per_unit_bases.tsv")) + + prefixes_vn = pynini.project(per_unit_prefixes, "output") + bases_vn = pynini.project(per_unit_bases, "output") + + one = pynini.accep("một") + + # Accept metric combinations: "một ki lô gam" + metric_per_units = one + insert_space + prefixes_vn + insert_space + bases_vn + standalone_per_units = one + insert_space + bases_vn + + # Combine all per_unit recognitions + per_units = per_units_non_metric | metric_per_units | standalone_per_units + per_units_normalized = pynini.project(per_units, "output") + per_unit_pattern = ( + pynutil.delete(' morphosyntactic_features: "') + insert_space + per_units_normalized + pynutil.delete('"') + ) + + # Optional per-unit suffix + graph += per_unit_pattern.ques + + # Handle preserve_order deletion (should be last) + graph += (delete_space + pynutil.delete("preserve_order: true")).ques + + self.fst = self.delete_tokens(graph).optimize() diff --git a/nemo_text_processing/text_normalization/vi/verbalizers/ordinal.py b/nemo_text_processing/text_normalization/vi/verbalizers/ordinal.py new file mode 100644 index 000000000..0a0bf3ac0 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/verbalizers/ordinal.py @@ -0,0 +1,48 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import NEMO_NOT_QUOTE, GraphFst, delete_space + + +class OrdinalFst(GraphFst): + """ + Finite state transducer for verbalizing Vietnamese ordinals, e.g. + ordinal { integer: "nhất" } -> thứ nhất + ordinal { integer: "tư" } -> thứ tư + ordinal { integer: "mười lăm" } -> thứ mười lăm + ordinal { integer: "một trăm" } -> thứ một trăm + + 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="ordinal", kind="verbalize", deterministic=deterministic) + + quoted_content = pynini.closure(NEMO_NOT_QUOTE) + + integer = ( + pynutil.delete("integer:") + delete_space + pynutil.delete("\"") + quoted_content + pynutil.delete("\"") + ) + + ordinal_pattern = pynutil.insert("thứ ") + integer + + self.ordinal_graph = ordinal_pattern + + delete_tokens = self.delete_tokens(self.ordinal_graph) + self.fst = delete_tokens.optimize() diff --git a/nemo_text_processing/text_normalization/vi/verbalizers/post_processing.py b/nemo_text_processing/text_normalization/vi/verbalizers/post_processing.py new file mode 100644 index 000000000..1131b3c91 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/verbalizers/post_processing.py @@ -0,0 +1,139 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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 os +from typing import Dict, List + +import pynini +from pynini.lib import pynutil + +from nemo_text_processing.text_normalization.vi.graph_utils import NEMO_SIGMA, generator_main +from nemo_text_processing.utils.logging import logger + + +class PostProcessingFst: + """ + Finite state transducer that post-processes an entire Vietnamese sentence after verbalization is complete, e.g. + removes extra spaces around punctuation marks " ( một trăm hai mươi ba ) " -> "(một trăm hai mươi ba)" + + Args: + 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, cache_dir: str = None, overwrite_cache: bool = False): + + 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, "vi_tn_post_processing.far") + if not overwrite_cache and far_file and os.path.exists(far_file): + self.fst = pynini.Far(far_file, mode="r")["post_process_graph"] + logger.info(f'Post processing graph was restored from {far_file}.') + else: + self.set_punct_dict() + self.fst = self.get_punct_postprocess_graph() + + if far_file: + generator_main(far_file, {"post_process_graph": self.fst}) + + def get_vietnamese_punct_config(self) -> Dict[str, List[str]]: + """ + Returns Vietnamese-specific punctuation configuration. + This method can be easily modified or extended for different Vietnamese punctuation rules. + """ + return { + # Punctuation that should not have space before them + 'no_space_before': [",", ".", "!", "?", ":", ";", ")", r"\]", "}"], + # Punctuation that should not have space after them + 'no_space_after': ["(", r"\[", "{"], + # Punctuation that can have space before them (exceptions) + 'allow_space_before': ["&", "-", "—", "–", "(", r"\[", "{", "\"", "'", "«", "»"], + # Special Vietnamese punctuation handling + 'vietnamese_special': { + # Vietnamese quotation marks + 'quotes': ["\"", "'", "«", "»", """, """, "'", "'"], + # Vietnamese dashes and separators + 'dashes': ["-", "—", "–"], + # Vietnamese brackets + 'brackets': ["(", ")", r"\[", r"\]", "{", "}"], + }, + } + + def set_punct_dict(self): + # Vietnamese punctuation marks that might need special handling + self.punct_marks = { + "'": [ + "'", + '´', + 'ʹ', + 'ʻ', + 'ʼ', + 'ʽ', + 'ʾ', + 'ˈ', + 'ˊ', + 'ˋ', + '˴', + 'ʹ', + '΄', + '`', + '´', + '’', + '‛', + '′', + '‵', + 'ꞌ', + ''', + '`', + ], + } + + def get_punct_postprocess_graph(self): + """ + Returns graph to post process punctuation marks for Vietnamese. + + Uses dynamic configuration for flexible punctuation handling. + Vietnamese punctuation spacing rules are defined in get_vietnamese_punct_config(). + """ + # Get dynamic punctuation configuration + punct_config = self.get_vietnamese_punct_config() + + # Extract configuration + no_space_before_punct = punct_config['no_space_before'] + no_space_after_punct = punct_config['no_space_after'] + + # Create FSTs for punctuation rules + no_space_before_punct_fst = pynini.union(*no_space_before_punct) + no_space_after_punct_fst = pynini.union(*no_space_after_punct) + + delete_space = pynutil.delete(" ") + + # Rule 1: Remove space before punctuation (primary rule) + remove_space_before = pynini.cdrewrite( + delete_space + no_space_before_punct_fst, # " ," -> "," + "", # any context before + "", # any context after + NEMO_SIGMA, + ).optimize() + + # Rule 2: Remove space after opening brackets + remove_space_after = pynini.cdrewrite( + no_space_after_punct_fst + delete_space, "", "", NEMO_SIGMA # "( " -> "(" + ).optimize() + + # Combine the two main rules + graph = pynini.compose(remove_space_before, remove_space_after) + + return graph.optimize() diff --git a/nemo_text_processing/text_normalization/vi/verbalizers/range.py b/nemo_text_processing/text_normalization/vi/verbalizers/range.py new file mode 100644 index 000000000..614c9bb95 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/verbalizers/range.py @@ -0,0 +1,40 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import NEMO_CHAR, NEMO_SIGMA, GraphFst, delete_space + + +class RangeFst(GraphFst): + """ + Finite state transducer for verbalizing Vietnamese ranges. + Range tokens are already verbalized by the tagger, so this just extracts the content. + e.g. tokens { name: "mười nghìn đến hai mười nghìn" } -> mười nghìn đến hai mười nghìn + + 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="range", kind="verbalize", deterministic=deterministic) + + # Range content is already verbalized by the tagger, just extract it + chars = pynini.closure(NEMO_CHAR - " ", 1) + char = pynutil.delete("name:") + delete_space + pynutil.delete("\"") + chars + pynutil.delete("\"") + graph = char @ pynini.cdrewrite(pynini.cross(u"\u00a0", " "), "", "", NEMO_SIGMA) + + self.fst = graph.optimize() diff --git a/nemo_text_processing/text_normalization/vi/verbalizers/roman.py b/nemo_text_processing/text_normalization/vi/verbalizers/roman.py new file mode 100644 index 000000000..977f7e313 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/verbalizers/roman.py @@ -0,0 +1,45 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import NEMO_NOT_QUOTE, NEMO_SIGMA, GraphFst, delete_space + + +class RomanFst(GraphFst): + """ + Finite state transducer for verbalizing Roman numerals in Vietnamese + e.g. tokens { roman { key_cardinal: "thế kỉ" integer: "mười lăm" } } -> thế kỉ mười lăm + e.g. tokens { roman { key_cardinal: "thế kỷ" integer: "bốn" } } -> thế kỷ bốn + e.g. tokens { roman { key_cardinal: "thứ" integer: "bốn" } } -> thứ bốn + e.g. tokens { roman { integer: "mười lăm" } } -> mười lăm + + 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="roman", kind="verbalize", deterministic=deterministic) + + key_cardinal = pynutil.delete("key_cardinal: \"") + pynini.closure(NEMO_NOT_QUOTE) + pynutil.delete("\"") + integer = pynutil.delete("integer: \"") + pynini.closure(NEMO_NOT_QUOTE) + pynutil.delete("\"") + + graph_with_key = key_cardinal + delete_space + pynutil.insert(" ") + integer + graph_without_key = integer + graph = pynini.union(graph_with_key, graph_without_key) + delete_tokens = self.delete_tokens(graph) + + self.fst = delete_tokens.optimize() diff --git a/nemo_text_processing/text_normalization/vi/verbalizers/time.py b/nemo_text_processing/text_normalization/vi/verbalizers/time.py new file mode 100644 index 000000000..966b8bad1 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/verbalizers/time.py @@ -0,0 +1,174 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import ( + NEMO_NOT_QUOTE, + NEMO_SPACE, + GraphFst, + convert_space, + delete_preserve_order, + delete_space, + extract_field, +) +from nemo_text_processing.text_normalization.vi.utils import get_abs_path + + +class TimeFst(GraphFst): + """ + Finite state transducer for verbalizing Vietnamese time. + + Converts tagged time entities into spoken form, e.g.: + - time { hours: "tám" minutes: "ba mươi" } -> tám giờ ba mươi phút + - time { hours: "mười bốn" minutes: "mười lăm" } -> mười bốn giờ mười lăm phút + - time { hours: "chín" } -> chín giờ + - time { minutes: "ba" seconds: "hai mươi" } -> ba phút hai mươi giây + - time { hours: "tám" minutes: "hai mươi ba" zone: "g m t" } -> tám giờ hai mươi ba phút GMT + + 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="time", kind="verbalize", deterministic=deterministic) + + time_zone = convert_space(pynini.string_file(get_abs_path("data/time/time_zones.tsv"))) + + # Extract components + hour_component = extract_field("hours") + timezone_component = extract_field("zone") @ time_zone + + # Handle zero and non-zero components + zero_minute_component = pynutil.delete("minutes:") + delete_space + pynutil.delete("\"không\"") + zero_second_component = pynutil.delete("seconds:") + delete_space + pynutil.delete("\"không\"") + + non_zero_minute_component = ( + pynutil.delete("minutes:") + + delete_space + + pynutil.delete("\"") + + pynini.closure(NEMO_NOT_QUOTE - pynini.accep("không")) + + pynutil.delete("\"") + ) + non_zero_second_component = ( + pynutil.delete("seconds:") + + delete_space + + pynutil.delete("\"") + + pynini.closure(NEMO_NOT_QUOTE - pynini.accep("không")) + + pynutil.delete("\"") + ) + + # Components with units + hour_with_unit = hour_component + pynutil.insert(" giờ") + minute_with_unit = non_zero_minute_component + pynutil.insert(" phút") + second_with_unit = non_zero_second_component + pynutil.insert(" giây") + + # Optional components + optional_timezone = pynini.closure(delete_space + pynutil.insert(NEMO_SPACE) + timezone_component, 0, 1) + optional_preserve_order = pynini.closure(delete_space + delete_preserve_order, 0, 1) + + # Pattern 1: hours + optional zero minutes/seconds + optional timezone + pattern_hours_only = ( + hour_with_unit + + pynini.closure(delete_space + zero_minute_component, 0, 1) + + pynini.closure(delete_space + zero_second_component, 0, 1) + + optional_timezone + + optional_preserve_order + ) + + # Pattern 2: hours + minutes + optional zero seconds + optional timezone + pattern_hours_minutes = ( + hour_with_unit + + delete_space + + pynutil.insert(NEMO_SPACE) + + minute_with_unit + + pynini.closure(delete_space + zero_second_component, 0, 1) + + optional_timezone + + optional_preserve_order + ) + + # Pattern 3: hours + zero minutes + seconds + optional timezone + pattern_hours_seconds = ( + hour_with_unit + + delete_space + + zero_minute_component + + delete_space + + pynutil.insert(NEMO_SPACE) + + second_with_unit + + optional_timezone + + optional_preserve_order + ) + + # Pattern 4: hours + minutes + seconds + optional timezone + pattern_hours_minutes_seconds = ( + hour_with_unit + + delete_space + + pynutil.insert(NEMO_SPACE) + + minute_with_unit + + delete_space + + pynutil.insert(NEMO_SPACE) + + second_with_unit + + optional_timezone + + optional_preserve_order + ) + + # Pattern 5: minutes only + optional zero seconds + pattern_minutes_only = minute_with_unit + pynini.closure(delete_space + zero_second_component, 0, 1) + + # Pattern 6: minutes + seconds + pattern_minutes_seconds = minute_with_unit + delete_space + pynutil.insert(NEMO_SPACE) + second_with_unit + + # Pattern 7: seconds only + pattern_seconds_only = second_with_unit + + patterns = [ + pattern_hours_only, + pattern_hours_minutes, + pattern_hours_seconds, + pattern_hours_minutes_seconds, + pattern_minutes_only, + pattern_minutes_seconds, + pattern_seconds_only, + ] + + final_graph = pynini.union(*patterns) + + if not deterministic: + # Add special case for half hour ("rưỡi") + half_hour = ( + pynutil.delete("minutes:") + delete_space + pynutil.delete("\"ba mươi\"") + pynutil.insert("rưỡi") + ) + half_hour_pattern = ( + hour_with_unit + + delete_space + + pynutil.insert(NEMO_SPACE) + + half_hour + + optional_timezone + + optional_preserve_order + ) + self.graph = pynini.union(final_graph, half_hour_pattern) + else: + self.graph = final_graph + + # Remove zero minutes and seconds from output + remove_zero_minutes = pynini.cdrewrite(pynutil.delete(" không phút"), "", "", pynini.closure(NEMO_NOT_QUOTE)) + remove_zero_seconds = pynini.cdrewrite(pynutil.delete(" không giây"), "", "", pynini.closure(NEMO_NOT_QUOTE)) + + self.fst = ( + self.delete_tokens(self.graph + optional_preserve_order).optimize() + @ remove_zero_minutes + @ remove_zero_seconds + ) diff --git a/nemo_text_processing/text_normalization/vi/verbalizers/verbalize.py b/nemo_text_processing/text_normalization/vi/verbalizers/verbalize.py new file mode 100644 index 000000000..30008275e --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/verbalizers/verbalize.py @@ -0,0 +1,85 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import GraphFst +from nemo_text_processing.text_normalization.vi.verbalizers.cardinal import CardinalFst +from nemo_text_processing.text_normalization.vi.verbalizers.date import DateFst +from nemo_text_processing.text_normalization.vi.verbalizers.decimal import DecimalFst +from nemo_text_processing.text_normalization.vi.verbalizers.fraction import FractionFst +from nemo_text_processing.text_normalization.vi.verbalizers.measure import MeasureFst +from nemo_text_processing.text_normalization.vi.verbalizers.money import MoneyFst +from nemo_text_processing.text_normalization.vi.verbalizers.ordinal import OrdinalFst +from nemo_text_processing.text_normalization.vi.verbalizers.range import RangeFst +from nemo_text_processing.text_normalization.vi.verbalizers.roman import RomanFst +from nemo_text_processing.text_normalization.vi.verbalizers.time import TimeFst +from nemo_text_processing.text_normalization.vi.verbalizers.whitelist import WhiteListFst +from nemo_text_processing.text_normalization.vi.verbalizers.word import WordFst + + +class VerbalizeFst(GraphFst): + def __init__(self, deterministic: bool = True): + super().__init__(name="verbalize", kind="verbalize", deterministic=deterministic) + + cardinal = CardinalFst(deterministic=deterministic) + cardinal_graph = cardinal.fst + + whitelist = WhiteListFst(deterministic=deterministic) + whitelist_graph = whitelist.fst + + word = WordFst(deterministic=deterministic) + word_graph = word.fst + + ordinal = OrdinalFst(deterministic=deterministic) + ordinal_graph = ordinal.fst + + decimal = DecimalFst(cardinal=cardinal, deterministic=deterministic) + decimal_graph = decimal.fst + + fraction = FractionFst(deterministic=deterministic) + fraction_graph = fraction.fst + + date = DateFst(deterministic=deterministic) + date_graph = date.fst + + roman = RomanFst(deterministic=deterministic) + roman_graph = roman.fst + + time_fst = TimeFst(deterministic=deterministic) + time_graph = time_fst.fst + + money = MoneyFst(deterministic=deterministic) + money_graph = money.fst + + measure = MeasureFst(decimal=decimal, cardinal=cardinal, fraction=fraction, deterministic=deterministic) + measure_graph = measure.fst + + range_fst = RangeFst(deterministic=deterministic) + range_graph = range_fst.fst + + graph = ( + cardinal_graph + | whitelist_graph + | word_graph + | ordinal_graph + | decimal_graph + | fraction_graph + | date_graph + | roman_graph + | time_graph + | money_graph + | measure_graph + | range_graph + ) + + self.fst = graph diff --git a/nemo_text_processing/text_normalization/vi/verbalizers/verbalize_final.py b/nemo_text_processing/text_normalization/vi/verbalizers/verbalize_final.py new file mode 100644 index 000000000..aa8344459 --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/verbalizers/verbalize_final.py @@ -0,0 +1,72 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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 os + +import pynini +from pynini.lib import pynutil + +from nemo_text_processing.text_normalization.vi.graph_utils import ( + GraphFst, + delete_extra_space, + delete_space, + generator_main, +) +from nemo_text_processing.text_normalization.vi.verbalizers.verbalize import VerbalizeFst +from nemo_text_processing.text_normalization.vi.verbalizers.word import WordFst +from nemo_text_processing.utils.logging import logger + + +class VerbalizeFinalFst(GraphFst): + """ + Finite state transducer that verbalizes an entire Vietnamese sentence, e.g. + tokens { name: "xin" } tokens { cardinal { integer: "một trăm" } } tokens { name: "chào" } + -> xin một trăm chào + + 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"vi_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"] + logger.info(f'VerbalizeFinalFst graph was restored from {far_file}.') + else: + verbalize = VerbalizeFst(deterministic=deterministic).fst + word = WordFst(deterministic=deterministic).fst + + types = verbalize | word + graph = ( + pynutil.delete("tokens") + + delete_space + + pynutil.delete("{") + + delete_space + + types + + delete_space + + pynutil.delete("}") + ) + 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}) diff --git a/nemo_text_processing/text_normalization/vi/verbalizers/whitelist.py b/nemo_text_processing/text_normalization/vi/verbalizers/whitelist.py new file mode 100644 index 000000000..7afda862e --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/verbalizers/whitelist.py @@ -0,0 +1,42 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import NEMO_NOT_QUOTE, NEMO_SIGMA, GraphFst, delete_space + + +class WhiteListFst(GraphFst): + """ + Finite state transducer for verbalizing whitelist for Vietnamese + e.g. tokens { name: "giờ" } -> giờ + + 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="whitelist", kind="verbalize", deterministic=deterministic) + graph = ( + pynutil.delete("name:") + + delete_space + + pynutil.delete("\"") + + pynini.closure(NEMO_NOT_QUOTE, 1) + + pynutil.delete("\"") + ) + graph = graph @ pynini.cdrewrite(pynini.cross(u"\u00a0", " "), "", "", NEMO_SIGMA) + delete_tokens = self.delete_tokens(graph) + self.fst = delete_tokens.optimize() diff --git a/nemo_text_processing/text_normalization/vi/verbalizers/word.py b/nemo_text_processing/text_normalization/vi/verbalizers/word.py new file mode 100644 index 000000000..f141f7f5a --- /dev/null +++ b/nemo_text_processing/text_normalization/vi/verbalizers/word.py @@ -0,0 +1,37 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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.vi.graph_utils import NEMO_NOT_QUOTE, NEMO_SIGMA, GraphFst, delete_space + + +class WordFst(GraphFst): + """ + Finite state transducer for verbalizing Vietnamese words. + e.g. tokens { name: "ngày" } -> ngày + + 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_NOT_QUOTE, 1) + char = pynutil.delete("name:") + delete_space + pynutil.delete("\"") + chars + pynutil.delete("\"") + graph = char @ pynini.cdrewrite(pynini.cross(u"\u00a0", " "), "", "", NEMO_SIGMA) + + self.fst = graph.optimize() diff --git a/requirements/requirements_test.txt b/requirements/requirements_test.txt index a3e90e5dc..aacfde319 100644 --- a/requirements/requirements_test.txt +++ b/requirements/requirements_test.txt @@ -1,6 +1,6 @@ -black==19.10b0 -click==8.0.2 -isort[requirements]>5.1.0,<6.0.0 +black==25.1.0 +click>=8.0.2 +isort[requirements]>5.1.0,<=6.0.1 parameterized pynini==2.1.6.post1 pytest diff --git a/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_cardinal.txt b/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_cardinal.txt index 53853297c..ca1f1a0aa 100644 --- a/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_cardinal.txt +++ b/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_cardinal.txt @@ -90,4 +90,53 @@ một trăm một~100 một một trăm linh một~101 một trăm mốt~110 một trăm mười~110 -hai triệu ba nghìn~2003000 \ No newline at end of file +hai triệu ba nghìn~2003000 +mười một~11 +mười hai~12 +mười lăm~15 +mười bảy~17 +hai mươi~20 +hai mươi mốt~21 +hai mốt~21 +ba mươi lăm~35 +bốn mươi tư~44 +năm mươi lăm~55 +bảy mươi mốt~71 +chín mươi chín~99 +một trăm~100 +một trăm linh một~101 +một trăm lẻ năm~105 +hai trăm mười hai~212 +ba trăm linh tư~304 +năm trăm linh lăm~505 +sáu trăm lẻ tư~604 +bảy trăm lẻ bảy~707 +chín trăm linh chín~909 +chín trăm chín mươi chín~999 +một nghìn~1000 +một ngàn~1000 +một nghìn không trăm linh năm~1005 +một ngàn hai trăm ba mươi bốn~1234 +mười hai nghìn ba trăm bốn mươi lăm~12345 +một trăm hai mươi ba nghìn không trăm linh bốn~123004 +một triệu~1 triệu +hai triệu ba trăm nghìn bốn trăm lẻ hai~2300402 +hai triệu ba trăm nghìn không trăm linh bảy~2300007 +một tỷ~1 tỷ +ba tỷ không trăm linh ba~3000000003 +ba tỷ bốn trăm năm mươi sáu triệu bảy trăm tám mươi chín nghìn không trăm linh một~3456789001 +một nghìn tỷ~1000 tỷ +một triệu tỷ~1 triệu tỷ +một tỷ tỷ~1 tỷ tỷ +hai mươi ba tỷ bốn trăm năm mươi sáu triệu bảy trăm tám mươi chín nghìn không trăm mười hai~23456789012 +chín trăm chín mươi chín tỷ chín trăm chín mươi chín triệu chín trăm chín mươi chín nghìn chín trăm chín mươi chín~999999999999 +một trăm hai mươi ba tỷ bốn trăm năm mươi sáu triệu bảy trăm tám mươi chín nghìn không trăm mười hai~123456789012 +âm bốn mươi hai~-42 +âm một trăm lẻ tám~-108 +âm ba trăm nghìn không trăm linh năm~-300005 +âm ba triệu không trăm linh chín~-3000009 + ba mươi mốt ~31 +hai mươi mốt~21 +bốn ngàn ba trăm lẻ năm~4305 +iPhone mười lăm~iPhone 15 +đường số mười hai~đường số 12 \ No newline at end of file diff --git a/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_date.txt b/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_date.txt index 182c710e2..21fb26138 100644 --- a/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_date.txt +++ b/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_date.txt @@ -13,4 +13,57 @@ năm chín trăm ba tám~năm 938 năm ba trăm lẻ tám~năm 308 năm bẩy trăm bốn mươi tư~năm 744 học kỳ này sẽ kết thúc vào tháng tư ngày mười tháng năm là tổng kết~học kỳ này sẽ kết thúc vào tháng 4 ngày 10 tháng 5 là tổng kết -học kỳ này sẽ kết thúc vào tháng năm ngày một tháng sáu là tổng kết~học kỳ này sẽ kết thúc vào tháng năm ngày 1 tháng 6 là tổng kết \ No newline at end of file +học kỳ này sẽ kết thúc vào tháng năm ngày một tháng sáu là tổng kết~học kỳ này sẽ kết thúc vào tháng năm ngày 1 tháng 6 là tổng kết +mùng một tháng một năm một chín chín chín~mùng 1 tháng 1 năm 1999 +ngày mùng năm tháng tám~ngày mùng 5 tháng 8 +mồng hai tháng ba~mồng 2 tháng 3 +ngày mồng mười tháng chín~ngày mồng 10 tháng 9 +hai mươi tư tháng bảy~24 tháng 7 +ngày ba mươi tháng sáu~ngày 30 tháng 6 +tháng một~tháng 1 +tháng hai~tháng 2 +tháng ba~tháng 3 +tháng tư~tháng 4 +tháng năm~tháng năm +tháng sáu~tháng 6 +tháng bảy~tháng 7 +tháng tám~tháng 8 +tháng chín~tháng 9 +tháng mười~tháng 10 +tháng mười một~tháng 11 +tháng mười hai~tháng 12 +tháng mười năm hai ngàn linh chín~tháng 10 năm 2009 +năm chín trăm ba tám~năm 938 +năm ba trăm lẻ tám~năm 308 +năm bẩy trăm bốn mươi tư~năm 744 +năm một chín tám tư~năm 1984 +năm hai nghìn linh chín~năm 2009 +năm hai không linh chín~năm 2009 +năm hai không hai ba~năm 2023 +năm hai nghìn không trăm hai mươi bốn~năm 2024 +tháng ba năm một trăm linh năm~tháng 3 năm 105 +tháng tư năm ba trăm lẻ tám~tháng 4 năm 308 +tháng tám năm chín trăm ba tám~tháng 8 năm 938 +tháng năm năm một nghìn không trăm linh ba~tháng 5 năm 1003 +tháng mười một năm một nghìn tám trăm năm hai~tháng 11 năm 1852 +tháng bảy năm một nghìn chín trăm tám tư~tháng 7 năm 1984 +tháng tư năm một chín bảy lăm~tháng 4 năm 1975 +tháng mười hai năm hai nghìn linh hai~tháng 12 năm 2002 +tháng sáu năm hai nghìn không trăm lẻ năm~tháng 6 năm 2005 +tháng một năm hai nghìn mười~tháng 1 năm 2010 +tháng mười năm hai nghìn mười chín~tháng 10 năm 2019 +tháng hai năm hai ngàn mười ba~tháng 2 năm 2013 +tháng chín năm hai không không tám~tháng 9 năm 2008 +tháng mười năm hai không một sáu~tháng 10 năm 2016 +tháng ba năm hai không hai ba~tháng 3 năm 2023 +tháng bảy năm hai không hai bốn~tháng 7 năm 2024 +tháng tư năm hai nghìn không trăm linh chín~tháng 4 năm 2009 +tháng tám năm hai ngàn không trăm mười bảy~tháng 8 năm 2017 +tháng mười một năm hai nghìn không trăm hai mốt~tháng 11 năm 2021 +tháng năm năm hai ngàn hai mươi hai~tháng 5 năm 2022 +tháng tư năm hai không mười tám~tháng 4 năm 2018 +tháng bảy năm hai không mười chín~tháng 7 năm 2019 +tháng ba năm hai không linh sáu~tháng 3 năm 2006 +tháng mười một năm hai không linh tám~tháng 11 năm 2008 +tháng sáu năm hai không hai mốt~tháng 6 năm 2021 +tháng mười hai năm hai không hai hai~tháng 12 năm 2022 \ No newline at end of file diff --git a/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_decimal.txt b/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_decimal.txt index 9888ff64e..654c66e61 100644 --- a/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_decimal.txt +++ b/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_decimal.txt @@ -22,4 +22,53 @@ trừ chín chín tỷ chín~-99.9 tỷ âm chín chín chấm chín lăm tỷ~-99.95 tỷ hai mươi chấm tư~20.4 mười hai chấm mốt~12 chấm mốt -chín trăm chín ba chấm lăm~993 chấm lăm \ No newline at end of file +chín trăm chín ba chấm lăm~993 chấm lăm +không phẩy năm~0.5 +không phẩy không năm~0.05 +không phẩy không không một~0.001 +một phẩy năm~1.5 +một phẩy không năm~1.05 +hai phẩy ba~2.3 +hai phẩy không ba~2.03 +mười hai phẩy ba bốn~12.34 +một trăm phẩy không không một~100.001 +một nghìn phẩy không năm~1000.05 +một triệu phẩy không không không một~1000000.0001 +một phẩy hai ba không năm~1.2305 +hai mươi ba phẩy không không bốn~23.004 +ba tỷ phẩy không không không không một~3000000000.00001 +một chấm năm~1.5 +không chấm không không một~0.001 +âm không phẩy năm~-0.5 +âm mười hai phẩy không hai~-12.02 +âm một trăm lẻ ba phẩy bốn năm sáu~-103.456 +một tỷ rưỡi~1.5 tỷ +một trăm hai mươi ba tỷ bốn trăm năm mươi sáu triệu phẩy bảy tám chín~123456000000.789 +một nghìn không trăm linh năm phẩy không hai~1005.02 + ba phẩy không bảy ~3.07 +một nghìn linh một chấm không năm~1001.05 +hai nghìn lẻ ba phẩy bảy tám~2003.78 +ba triệu linh một phẩy không không chín~3000001.009 +bốn tỷ lẻ năm chấm hai ba bốn~4000000005.234 +năm triệu không trăm mười phẩy năm sáu~5000010.56 +một tỷ hai trăm ba mươi bốn triệu năm trăm sáu mươi bảy nghìn tám trăm chín mươi chấm một hai ba~1234567890.123 +chín tỷ tám trăm bảy mươi sáu triệu năm trăm bốn mươi ba nghìn hai trăm mười phẩy chín tám bảy sáu~9876543210.9876 +hai mươi ba tỷ bốn mươi lăm triệu sáu trăm bảy mươi tám nghìn chín trăm phẩy một hai ba bốn năm~23045678900.12345 +hai mươi mốt chấm tư~21.4 +ba mươi tư phẩy năm~34.5 +năm mươi mốt phẩy bảy~51.7 +sáu mươi tư chấm tám chín~64.89 +không phẩy không không không một~0.0001 +không chấm không không không không một~0.00001 +một phẩy một hai ba bốn năm sáu bảy tám chín~1.123456789 +hai chấm không một hai ba bốn năm sáu bảy~2.01234567 +âm mười hai tỷ ba trăm bốn mươi lăm triệu sáu trăm bảy mươi tám nghìn chín trăm phẩy một hai~-12345678900.12 +trừ bảy trăm tám mươi chín triệu một trăm hai mươi ba nghìn bốn trăm năm mươi sáu chấm bảy tám~-789123456.78 +âm chín trăm chín mươi chín triệu chín trăm chín mươi chín nghìn chín trăm chín mươi chín phẩy chín chín chín~-999999999.999 +một tỷ rưỡi~1.5 tỷ +ba triệu mốt~3.1 triệu +chín mươi chín phẩy chín chín~99.99 +một trăm phẩy không~100.0 +một chấm năm triệu~1.5 triệu +ba phẩy bốn nghìn~3.4 nghìn +một trăm hai mươi ba phẩy bốn năm sáu bảy tám chín~123.456789 \ No newline at end of file diff --git a/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_electronic.txt b/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_electronic.txt index 04168797e..3d0767cdf 100644 --- a/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_electronic.txt +++ b/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_electronic.txt @@ -7,4 +7,12 @@ a b c a móc a b c chấm com~abc@abc.com a s d f một hai ba a móc a b c chấm com~asdf123@abc.com a một b hai a vòng a b c chấm com~a1b2@abc.com a b ba chấm s d d chấm ba a móc g mail chấm com~ab3.sdd.3@gmail.com -a b ba chấm s d d chấm ba a còng g mail chấm com~ab3.sdd.3@gmail.com \ No newline at end of file +a b ba chấm s d d chấm ba a còng g mail chấm com~ab3.sdd.3@gmail.com +a b ba gạch s d d gạch ba a móc g mail chấm com~ab3-sdd-3@gmail.com +w w w chấm nvidia chấm com~www.nvidia.com +nvidia chấm com~nvidia.com +h t t p hai chấm sẹc sẹc w w w chấm nvidia chấm com~http://www.nvidia.com +h t t p s hai chấm sẹc sẹc w w w chấm nvidia chấm com~https://www.nvidia.com +google chấm com chấm v n~google.com.vn +w w w chấm google chấm com chấm v n~www.google.com.vn +nvidia chấm a i~nvidia.ai \ No newline at end of file diff --git a/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_measure.txt b/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_measure.txt index 8b8c97b96..48d0577b6 100644 --- a/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_measure.txt +++ b/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_measure.txt @@ -14,9 +14,22 @@ bốn trăm bốn mươi mi li lít~440 ml ba trăm muy crô mét~300 μm sáu lăm inch~65 inch hai vôn~2 v -ba mươi phần trăm~30 % +ba mươi phần trăm~30% sáu mươi nghìn hai trăm bốn mươi mi li ampe~60240 mA sáu mươi sáu phút~66 phút hai phút~2 phút -năm giây~5 s -năm trăm sáu bảy giây~567 s \ No newline at end of file +năm giây~5s +năm trăm sáu bảy giây~567s +hai mươi độ c~20 °c +chín mươi tám độ f~98 °f +ba bảy độ c~37 °c +không phẩy năm phần trăm~0.5% +một trăm phần trăm~100% +không phẩy không năm mét~0.05 m +không phẩy không không một ki lô gram~0.001 kg +hai trăm megabit trên giây~200 mbps +năm mươi kilobit trên giây~50 kbps +một phẩy năm terabyte~1.5 tb +hai phẩy năm gigabyte~2.5 gb +không mét~0 m +không phần trăm~0% \ No newline at end of file diff --git a/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_money.txt b/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_money.txt index 2d99fd4bb..064ebae56 100644 --- a/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_money.txt +++ b/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_money.txt @@ -6,12 +6,10 @@ hai phẩy hai đô la mỹ~2.2$ hai đô rưỡi~2.5$ hai mươi euro~20€ hai mươi chấm một euro~20.1€ -một đồng~1₫ -mười nghìn năm trăm đồng~10500₫ -năm phẩy sáu đồng~5.6₫ -hai mươi đồng rưỡi~20.5₫ tám mươi nghìn một trăm won~80100₩ tám mươi ngàn một trăm uôn~80100₩ ba ringgit~3RM không phẩy ba ringgit~0.3RM -không euro~0€ \ No newline at end of file +không euro~0€ +không phẩy không không một đô~0.001$ +một phẩy hai ba bốn đô la~1.234$ \ No newline at end of file diff --git a/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_telephone.txt b/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_telephone.txt index 0fa73bcaa..7d8e80613 100644 --- a/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_telephone.txt +++ b/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_telephone.txt @@ -1,4 +1,25 @@ -không chín ba sáu năm năm năm bốn bốn chín~0936555449 -không một hai tám bốn hai hai năm~01284225 +không chín ba sáu năm năm năm bốn bốn chín~093-655-5449 +không ba tám bốn hai hai năm bảy tám chín~038-422-5789 +không bảy chín một hai ba bốn năm sáu bảy~079-123-4567 +không hai bốn ba bảy hai một năm sáu tám chín~02437215689 +không hai tám ba tám hai một năm sáu tám chín~02838215689 +không một hai tám bốn hai hai năm bảy tám chín~01284225789 một hai ba bốn năm sáu bảy tám chín~123456789 -chín tám bảy sáu năm bốn ba hai một không~9876543210 \ No newline at end of file +một hai ba bốn năm sáu bảy tám mốt~123456781 +chín tám bảy sáu năm bốn ba hai một không~9876543210 +một một hai~112 +một một ba~113 +một một bốn~114 +một một năm~115 +một chín không năm~1905 +cộng tám mươi bốn không chín ba sáu năm năm năm bốn bốn chín~+84 093-655-5449 +cộng một không chín ba sáu năm năm năm bốn bốn chín~+1 093-655-5449 +cộng chín một không ba tám bốn hai hai năm bảy tám chín~+91 038-422-5789 +cộng bốn bốn không bảy chín một hai ba bốn năm sáu bảy~+44 079-123-4567 +một chín hai chấm một sáu tám chấm không chấm một~192.168.0.1 +một chín hai chấm một sáu tám chấm một chấm một~192.168.1.1 +hai năm năm chấm hai năm năm chấm hai năm năm chấm hai năm năm~255.255.255.255 +một không chấm không chấm không chấm không~10.0.0.0 +một hai ba bốn năm sáu bảy tám chín không một hai ba bốn năm sáu~1234 5678 9012 3456 +bốn năm ba hai một hai ba bốn năm sáu bảy tám chín không một hai~4532 1234 5678 9012 +ba bảy năm ba bốn hai sáu hai bốn hai sáu hai bốn hai sáu~3753 426242 62426 \ No newline at end of file diff --git a/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_time.txt b/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_time.txt index 4718c6d6c..aad0635d6 100644 --- a/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_time.txt +++ b/tests/nemo_text_processing/vi/data_inverse_text_normalization/test_cases_time.txt @@ -19,4 +19,13 @@ mười hai phút ba giây~12p03s năm chín phút năm mươi chín giây~59p59s tám phút bốn lăm giây~8p45s tám giờ hai ba phút gmt~8:23 gmt -mười lăm giờ cst~15h cst \ No newline at end of file +mười lăm giờ cst~15h cst +năm giờ linh năm~5:05 +sáu giờ không tám phút~6:08 +ba giờ mười giây~3h 10s +bốn giờ kém mười~3:50 +không giờ không phút~0:00 +hai giờ không một phút~2:01 +mười một giờ kém năm~10:55 +mười một giờ kém mười~10:50 +một giờ chiều~1h chiều \ No newline at end of file diff --git a/tests/nemo_text_processing/vi/data_text_normalization/test_cases_cardinal.txt b/tests/nemo_text_processing/vi/data_text_normalization/test_cases_cardinal.txt new file mode 100644 index 000000000..74d2b7e98 --- /dev/null +++ b/tests/nemo_text_processing/vi/data_text_normalization/test_cases_cardinal.txt @@ -0,0 +1,109 @@ +1~một +2~hai +3~ba +4~bốn +5~năm +6~sáu +7~bảy +8~tám +9~chín +10~mười +11~mười một +12~mười hai +15~mười lăm +18~mười tám +19~mười chín +20~hai mươi +21~hai mươi mốt +25~hai mươi lăm +30~ba mươi +34~ba mươi tư +44~bốn mươi tư +55~năm mươi lăm +67~sáu mươi bảy +70~bảy mươi +80~tám mươi +95~chín mươi lăm +100~một trăm +101~một trăm linh một +102~một trăm linh hai +104~một trăm linh bốn +105~một trăm linh năm +110~một trăm mười +111~một trăm mười một +120~một trăm hai mươi +123~một trăm hai mươi ba +200~hai trăm +201~hai trăm linh một +500~năm trăm +999~chín trăm chín mươi chín +1000~một nghìn +1001~một nghìn linh một +1020~một nghìn hai mươi +1095~một nghìn chín mươi lăm +1100~một nghìn một trăm +2000~hai nghìn +10000~mười nghìn +100000~một trăm nghìn +1000000~một triệu +2000000~hai triệu +1000000000~một tỷ +-1~âm một +-25~âm hai mươi lăm +-100~âm một trăm +-1000~âm một nghìn +0~không +24~hai mươi tư +35~ba mươi lăm +41~bốn mươi mốt +55~năm mươi lăm +91~chín mươi mốt +14~mười bốn +16~mười sáu +17~mười bảy +37~ba mươi bảy +47~bốn mươi bảy +57~năm mươi bảy +63~sáu mươi ba +79~bảy mươi chín +84~tám mươi tư +98~chín mươi tám +-123~âm một trăm hai mươi ba +-1001~âm một nghìn linh một +-104~âm một trăm linh bốn +1000001~một triệu linh một +1001001~một triệu một nghìn linh một +1050003~một triệu năm mươi nghìn linh ba +1000000001~một tỷ linh một +1001001101~một tỷ một triệu một nghìn một trăm linh một +300~ba trăm +400~bốn trăm +500~năm trăm +6000~sáu nghìn +7000~bảy nghìn +15000~mười lăm nghìn +300000~ba trăm nghìn +450000~bốn trăm năm mươi nghìn +5000000~năm triệu +700000000~bảy trăm triệu +31~ba mươi mốt +41~bốn mươi mốt +51~năm mươi mốt +61~sáu mươi mốt +71~bảy mươi mốt +81~tám mươi mốt +91~chín mươi mốt +5500000~năm triệu năm trăm nghìn +1000010~một triệu mười +1000100~một triệu một trăm +1000101~một triệu một trăm linh một +1010001~một triệu mười nghìn linh một +10000000000~mười tỷ +150~một trăm năm mươi +1000000000000~một nghìn tỷ +1234567890123~một nghìn hai trăm ba mươi tư tỷ năm trăm sáu mươi bảy triệu tám trăm chín mươi nghìn một trăm hai mươi ba +9876543210987~chín nghìn tám trăm bảy mươi sáu tỷ năm trăm bốn mươi ba triệu hai trăm mười nghìn chín trăm tám mươi bảy +1000000000000000~một triệu tỷ +1111111111111111~một triệu một trăm mười một nghìn một trăm mười một tỷ một trăm mười một triệu một trăm mười một nghìn một trăm mười một +5432109876543210~năm triệu bốn trăm ba mươi hai nghìn một trăm linh chín tỷ tám trăm bảy mươi sáu triệu năm trăm bốn mươi ba nghìn hai trăm mười +1000000000000000000~một tỷ tỷ \ No newline at end of file diff --git a/tests/nemo_text_processing/vi/data_text_normalization/test_cases_date.txt b/tests/nemo_text_processing/vi/data_text_normalization/test_cases_date.txt new file mode 100644 index 000000000..c95e00e97 --- /dev/null +++ b/tests/nemo_text_processing/vi/data_text_normalization/test_cases_date.txt @@ -0,0 +1,13 @@ +ngày 15/01/2024~ngày mười lăm tháng một năm hai nghìn hai mươi tư +01/12/2023~ngày một tháng mười hai năm hai nghìn hai mươi ba +25-03-1975~ngày hai mươi lăm tháng ba năm một nghìn chín trăm bảy mươi lăm +10.05.2000~ngày mười tháng năm năm hai nghìn +tháng 1 2024~tháng một năm hai nghìn hai mươi tư +tháng 12 2023~tháng mười hai năm hai nghìn hai mươi ba +ngày 12 tháng 5 năm 2025~ngày mười hai tháng năm năm hai nghìn hai mươi lăm +tháng 5 năm nay~tháng năm năm nay +ngày 4 tháng này~ngày bốn tháng này +hôm nay là ngày 19/05/2025 sinh nhật Bác Hồ~hôm nay là ngày mười chín tháng năm năm hai nghìn hai mươi lăm sinh nhật Bác Hồ +ngày 14/4 hàng năm~ngày mười bốn tháng tư hàng năm +tháng 04/1969~tháng tư năm một nghìn chín trăm sáu mươi chín +ngày 12 tháng mười hai năm 2023~ngày mười hai tháng mười hai năm hai nghìn hai mươi ba \ No newline at end of file diff --git a/tests/nemo_text_processing/vi/data_text_normalization/test_cases_decimal.txt b/tests/nemo_text_processing/vi/data_text_normalization/test_cases_decimal.txt new file mode 100644 index 000000000..6acc3bda4 --- /dev/null +++ b/tests/nemo_text_processing/vi/data_text_normalization/test_cases_decimal.txt @@ -0,0 +1,29 @@ +0,2 triệu~không phẩy hai triệu +18 vạn~mười tám vạn +818,303~tám trăm mười tám phẩy ba không ba +-99,95 tỷ~âm chín mươi chín phẩy chín năm tỷ +60,240~sáu mươi phẩy hai bốn không +-0,007~âm không phẩy không không bảy +123,000~một trăm hai mươi ba phẩy không không không +1,5 triệu~một phẩy năm triệu +3,14 tỷ~ba phẩy một bốn tỷ +10,01 vạn~mười phẩy không một vạn +-12,5~âm mười hai phẩy năm +0,0001~không phẩy không không không một +999,999~chín trăm chín mươi chín phẩy chín chín chín +1,01~một phẩy không một +-1,01~âm một phẩy không một +15,6~mười lăm phẩy sáu +1k~một nghìn +10k~mười nghìn +100k~một trăm nghìn +1tr~một triệu +10tr~mười triệu +100tr~một trăm triệu +1tr2~một triệu hai trăm nghìn +2tr5~hai triệu năm trăm nghìn +1t~một tỷ +10t~mười tỷ +100t~một trăm tỷ +2t3~hai tỷ ba trăm triệu +1 tỉ~một tỉ \ No newline at end of file diff --git a/tests/nemo_text_processing/vi/data_text_normalization/test_cases_fraction.txt b/tests/nemo_text_processing/vi/data_text_normalization/test_cases_fraction.txt new file mode 100644 index 000000000..1ccd7af94 --- /dev/null +++ b/tests/nemo_text_processing/vi/data_text_normalization/test_cases_fraction.txt @@ -0,0 +1,13 @@ +1/2~một phần hai +4/9~bốn phần chín +9/4~chín phần tư +1/4~một phần tư +3/4~ba phần tư +15/5~mười lăm phần năm +1/3~một phần ba +2/10~hai phần mười +23 1/5~hai mươi ba và một phần năm +-3/4~âm ba phần tư +-12 1/4 nha~âm mười hai và một phần tư nha +-5 2/3~âm năm và hai phần ba +5 1/2~năm và một phần hai \ No newline at end of file diff --git a/tests/nemo_text_processing/vi/data_text_normalization/test_cases_measure.txt b/tests/nemo_text_processing/vi/data_text_normalization/test_cases_measure.txt new file mode 100644 index 000000000..d3a7adeaa --- /dev/null +++ b/tests/nemo_text_processing/vi/data_text_normalization/test_cases_measure.txt @@ -0,0 +1,63 @@ +204m~hai trăm linh bốn mét +12kg~mười hai ki lô gam +1kg~một ki lô gam +100g~một trăm gam +500mg~năm trăm mi li gam +175cm~một trăm bảy mươi lăm xăng ti mét +2m~hai mét +100mm~một trăm mi li mét +5km~năm ki lô mét +1inch~một inch +500ml~năm trăm mi li lít +2l~hai lít +1m³~một mét khối +100cm³~một trăm xăng ti mét khối +2gb~hai gi ga bai +1tb~một terabyte +512Mb~năm trăm mười hai mê ga bai +64kb~sáu mươi tư ki lô bai +25°c~hai mươi lăm độ c +100°f~một trăm độ f +273°k~hai trăm bảy mươi ba độ k +50%~năm mươi phần trăm +100%~một trăm phần trăm +25%~hai mươi lăm phần trăm +220v~hai trăm hai mươi vôn +1kw~một ki lô oát +500mV~năm trăm mi li vôn +1000mA~một nghìn mi li am pe +50hz~năm mươi hẹc +2ghz~hai gi ga hẹc +100Mhz~một trăm mê ga hẹc +1000kw~một nghìn ki lô oát +5hp~năm mã lực +1tw~một tê ra oát +100m²~một trăm mét vuông +5km²~năm ki lô mét vuông +1km2~một ki lô mét vuông +8,5m2~tám phẩy năm mét vuông +1ha~một héc ta +1/2kg~một phần hai ki lô gam +3/4m~ba phần tư mét +1/3l~một phần ba lít +Tôi có 12kg gạo~Tôi có mười hai ki lô gam gạo +Chiều cao 175cm~Chiều cao một trăm bảy mươi lăm xăng ti mét +Dung lượng 2gb~Dung lượng hai gi ga bai +Nhiệt độ 25°c~Nhiệt độ hai mươi lăm độ c +Cân nặng 1/2kg~Cân nặng một phần hai ki lô gam +Điện áp 220v~Điện áp hai trăm hai mươi vôn +Tỷ lệ 50%~Tỷ lệ năm mươi phần trăm +Bộ nhớ 1tb~Bộ nhớ một terabyte +Thể tích 500ml~Thể tích năm trăm mi li lít +1234kg~một nghìn hai trăm ba mươi tư ki lô gam +2500m~hai nghìn năm trăm mét +10000gb~mười nghìn gi ga bai +Kích thước 100cm x 50cm~Kích thước một trăm xăng ti mét x năm mươi xăng ti mét +1,5m2~một phẩy năm mét vuông +1,5m~một phẩy năm mét +120km/h~một trăm hai mươi ki lô mét trên giờ +100 km/h~một trăm ki lô mét trên giờ +50m/s~năm mươi mét trên giây +30 m/min~ba mươi mét trên phút +5cm/s~năm xăng ti mét trên giây +200mg/ml~hai trăm mi li gam trên mi li lít \ No newline at end of file diff --git a/tests/nemo_text_processing/vi/data_text_normalization/test_cases_money.txt b/tests/nemo_text_processing/vi/data_text_normalization/test_cases_money.txt new file mode 100644 index 000000000..755a1030a --- /dev/null +++ b/tests/nemo_text_processing/vi/data_text_normalization/test_cases_money.txt @@ -0,0 +1,30 @@ +10₫~mười đồng +10 đồng~mười đồng +10,5 đồng~mười phẩy năm đồng +10$~mười đô la +1000$~một nghìn đô la +10 đô la~mười đô la +10 usd~mười đô la +10,5$~mười đô la năm mươi xu +10€~mười ơ rô +10¥~mười yên nhật +10£~mười bảng anh +10₹~mười rupee +0,5$~năm mươi xu +12.345 đồng~mười hai nghìn ba trăm bốn mươi lăm đồng +1.234.567₫~một triệu hai trăm ba mươi tư nghìn năm trăm sáu mươi bảy đồng +10.000,50 đồng~mười nghìn phẩy năm không đồng +10k VND~mười nghìn đồng +10đ~mười đồng +50.000đ-100.000đ~năm mươi nghìn đồng đến một trăm nghìn đồng +10$-20$~mười đô la đến hai mươi đô la +50.000đ/ngày~năm mươi nghìn đồng trên ngày +10$/giờ~mười đô la trên giờ +5€/h~năm ơ rô trên giờ +100₫/kg~một trăm đồng một ki lô gam +1tr5 vnd~một triệu năm trăm nghìn đồng +0,01$~một xu +2,50€~hai ơ rô năm mươi xu +1000,50 VND~một nghìn phẩy năm không đồng +5,99$~năm đô la chín mươi chín xu +30đ/TB~ba mươi đồng một tê ra bai \ No newline at end of file diff --git a/tests/nemo_text_processing/vi/data_text_normalization/test_cases_ordinal.txt b/tests/nemo_text_processing/vi/data_text_normalization/test_cases_ordinal.txt new file mode 100644 index 000000000..e28b4f97f --- /dev/null +++ b/tests/nemo_text_processing/vi/data_text_normalization/test_cases_ordinal.txt @@ -0,0 +1,30 @@ +thứ 1~thứ nhất +hôm nay là thứ hai~hôm nay là thứ hai +thứ 3 là ngày giữa tuần~thứ ba là ngày giữa tuần +thứ 4 nên làm gì~thứ tư nên làm gì +thứ 7~thứ bảy +con giáp thứ 13~con giáp thứ mười ba +thứ 1~thứ nhất +thứ 4~thứ tư +thứ 2~thứ hai +thứ 3~thứ ba +thứ 5~thứ năm +thứ 6~thứ sáu +thứ 7~thứ bảy +thứ 8~thứ tám +thứ 9~thứ chín +thứ 10~thứ mười +thứ 11~thứ mười một +thứ 12~thứ mười hai +thứ 15~thứ mười lăm +thứ 21~thứ hai mươi mốt +thứ 24~thứ hai mươi tư +thứ 34~thứ ba mươi tư +thứ 100~thứ một trăm +thứ 101~thứ một trăm linh một +thứ 104~thứ một trăm linh bốn +thứ 234~thứ hai trăm ba mươi tư +thứ 1000~thứ một nghìn +thứ 1234~thứ một nghìn hai trăm ba mươi tư +hôm nay thứ 2~hôm nay thứ hai +đứng thứ 15~đứng thứ mười lăm \ No newline at end of file diff --git a/tests/nemo_text_processing/vi/data_text_normalization/test_cases_range.txt b/tests/nemo_text_processing/vi/data_text_normalization/test_cases_range.txt new file mode 100644 index 000000000..ea858dc4f --- /dev/null +++ b/tests/nemo_text_processing/vi/data_text_normalization/test_cases_range.txt @@ -0,0 +1,11 @@ +10k-20k~mười nghìn đến hai mươi nghìn +1k-5k~một nghìn đến năm nghìn +2tr-5tr~hai triệu đến năm triệu +10$-20$~mười đô la đến hai mươi đô la +1t-2t~một tỷ đến hai tỷ +10:00-11:00~mười giờ đến mười một giờ +10$-20$~mười đô la đến hai mươi đô la +50.000đ-100.000đ~năm mươi nghìn đồng đến một trăm nghìn đồng +3kg-6kg~ba ki lô gam đến sáu ki lô gam +15cm-25cm~mười lăm xăng ti mét đến hai mươi lăm xăng ti mét +31Mhz-44Mhz~ba mươi mốt mê ga hẹc đến bốn mươi tư mê ga hẹc \ No newline at end of file diff --git a/tests/nemo_text_processing/vi/data_text_normalization/test_cases_roman.txt b/tests/nemo_text_processing/vi/data_text_normalization/test_cases_roman.txt new file mode 100644 index 000000000..96d57c551 --- /dev/null +++ b/tests/nemo_text_processing/vi/data_text_normalization/test_cases_roman.txt @@ -0,0 +1,57 @@ +thế kỉ XV~thế kỉ mười lăm +thế kỉ XX~thế kỉ hai mươi +thế kỉ XXI~thế kỉ hai mươi mốt +thế kỷ IV~thế kỷ bốn +thế kỷ V~thế kỷ năm +thứ I~thứ một +thứ V~thứ năm +thứ X~thứ mười +thứ XV~thứ mười lăm +chương III~chương ba +phần ix~phần chín +chương C~chương một trăm +mục XCIX~mục chín mươi chín +chương MMMCMXCIX~chương ba nghìn chín trăm chín mươi chín +thế kỉ xix~thế kỉ mười chín +thế kỷ vi~thế kỷ sáu +phần xl~phần bốn mươi +mục xc~mục chín mươi +mục cd~mục bốn trăm +mục cm~mục chín trăm +thứ viii~thứ tám +thứ ix~thứ chín +thứ xi~thứ mười một +chương lxxxviii~chương tám mươi tám +chương cccxlv~chương ba trăm bốn mươi lăm +thế kỉ XV và chương IX~thế kỉ mười lăm và chương chín +trong phần X có mục IV~trong phần mười có mục bốn +chương I~chương một +chương MMMCMXCIX~chương ba nghìn chín trăm chín mươi chín +đoạn II~đoạn hai +đoạn iv~đoạn bốn +đoạn VII~đoạn bảy +đoạn xii~đoạn mười hai +năm MCMXCIX~năm một nghìn chín trăm chín mươi chín +năm mmxx~năm hai nghìn hai mươi +khoản III~khoản ba +khoản vi~khoản sáu +khoản XIV~khoản mười bốn +khoản xxv~khoản hai mươi lăm +phụ lục I~phụ lục một +phụ lục v~phụ lục năm +phụ lục XII~phụ lục mười hai +phụ lục xx~phụ lục hai mươi +khóa VII~khóa bảy +khóa xi~khóa mười một +khóa XV~khóa mười lăm +khóa xxx~khóa ba mươi +số I~số một +số v~số năm +số X~số mười +số l~số năm mươi +đoạn IX mục III~đoạn chín mục ba +khoản II phụ lục IV~khoản hai phụ lục bốn +khóa XII số IX~khóa mười hai số chín +năm MMXXIII khoản V~năm hai nghìn hai mươi ba khoản năm +chương VII đoạn XI~chương bảy đoạn mười một +phần XX mục XV~phần hai mươi mục mười lăm \ No newline at end of file diff --git a/tests/nemo_text_processing/vi/data_text_normalization/test_cases_time.txt b/tests/nemo_text_processing/vi/data_text_normalization/test_cases_time.txt new file mode 100644 index 000000000..4f576e821 --- /dev/null +++ b/tests/nemo_text_processing/vi/data_text_normalization/test_cases_time.txt @@ -0,0 +1,117 @@ +1h~một giờ +2h~hai giờ +3h~ba giờ +4h~bốn giờ +5h~năm giờ +6h~sáu giờ +7h~bảy giờ +8h~tám giờ +9h~chín giờ +10h~mười giờ +11h~mười một giờ +12h~mười hai giờ +13h~mười ba giờ +14h~mười bốn giờ +15h~mười lăm giờ +16h~mười sáu giờ +17h~mười bảy giờ +18h~mười tám giờ +19h~mười chín giờ +20h~hai mươi giờ +21h~hai mươi mốt giờ +22h~hai mươi hai giờ +23h~hai mươi ba giờ +24h~hai mươi tư giờ +0h~không giờ +01h~một giờ +02h~hai giờ +03h~ba giờ +04h~bốn giờ +05h~năm giờ +06h~sáu giờ +07h~bảy giờ +08h~tám giờ +09h~chín giờ +8:00~tám giờ +8:05~tám giờ năm phút +8:10~tám giờ mười phút +8:15~tám giờ mười lăm phút +8:20~tám giờ hai mươi phút +8:25~tám giờ hai mươi lăm phút +8:30~tám giờ ba mươi phút +8:35~tám giờ ba mươi lăm phút +8:40~tám giờ bốn mươi phút +8:45~tám giờ bốn mươi lăm phút +8:50~tám giờ năm mươi phút +8:55~tám giờ năm mươi lăm phút +8:01~tám giờ một phút +8:02~tám giờ hai phút +8:03~tám giờ ba phút +8:07~tám giờ bảy phút +8:09~tám giờ chín phút +01:00~một giờ +01:05~một giờ năm phút +01:30~một giờ ba mươi phút +02:15~hai giờ mười lăm phút +03:45~ba giờ bốn mươi lăm phút +14:30~mười bốn giờ ba mươi phút +15:45~mười lăm giờ bốn mươi lăm phút +23:59~hai mươi ba giờ năm mươi chín phút +00:00~không giờ +00:30~không giờ ba mươi phút +5:20:35~năm giờ hai mươi phút ba mươi lăm giây +6:10:05~sáu giờ mười phút năm giây +1:01:01~một giờ một phút một giây +8:00:00~tám giờ +12:30:45~mười hai giờ ba mươi phút bốn mươi lăm giây +23:59:59~hai mươi ba giờ năm mươi chín phút năm mươi chín giây +01:01:01~một giờ một phút một giây +02:02:02~hai giờ hai phút hai giây +03:00:03~ba giờ ba giây +04:30:00~bốn giờ ba mươi phút +1p~một phút +3p~ba phút +5p~năm phút +10p~mười phút +30p~ba mươi phút +59p~năm mươi chín phút +1s~một giây +30s~ba mươi giây +59s~năm mươi chín giây +3p20s~ba phút hai mươi giây +4p30s~bốn phút ba mươi giây +12p03s~mười hai phút ba giây +59p59s~năm mươi chín phút năm mươi chín giây +8p45s~tám phút bốn mươi lăm giây +01p01s~một phút một giây +02p30s~hai phút ba mươi giây +05p00s~năm phút +8:23 gmt~tám giờ hai mươi ba phút GMT +15h cst~mười lăm giờ CST +9:00 utc~chín giờ UTC +14:30 pst~mười bốn giờ ba mươi phút PST +20:15 est~hai mươi giờ mười lăm phút EST +12h jst~mười hai giờ JST +14g30~mười bốn giờ ba mươi phút +14h30~mười bốn giờ ba mươi phút +09g05~chín giờ năm phút +09h05~chín giờ năm phút +1 giờ~một giờ +2 giờ~hai giờ +10 giờ~mười giờ +14 giờ~mười bốn giờ +1 giờ 30 phút~một giờ ba mươi phút +14 giờ 30 phút~mười bốn giờ ba mươi phút +2 giờ 15 phút~hai giờ mười lăm phút +2 giờ 15 phút 10 giây~hai giờ mười lăm phút mười giây +14 giờ 30 phút 45 giây~mười bốn giờ ba mươi phút bốn mươi lăm giây +5 phút~năm phút +10 phút~mười phút +30 phút~ba mươi phút +5 phút 30 giây~năm phút ba mươi giây +10 phút 15 giây~mười phút mười lăm giây +10 giây~mười giây +30 giây~ba mươi giây +45 giây~bốn mươi lăm giây +14 giờ 30 phút UTC~mười bốn giờ ba mươi phút UTC +2 giờ 15 phút GMT~hai giờ mười lăm phút GMT \ No newline at end of file diff --git a/tests/nemo_text_processing/vi/test_cardinal.py b/tests/nemo_text_processing/vi/test_cardinal.py index 0a888f84b..00bafe3f1 100644 --- a/tests/nemo_text_processing/vi/test_cardinal.py +++ b/tests/nemo_text_processing/vi/test_cardinal.py @@ -15,29 +15,45 @@ import pytest from parameterized import parameterized -from ..utils import CACHE_DIR, parse_test_case_file +from nemo_text_processing.inverse_text_normalization.inverse_normalize import InverseNormalizer +from nemo_text_processing.text_normalization.normalize import Normalizer +from nemo_text_processing.text_normalization.normalize_with_audio import NormalizerWithAudio -try: - from nemo_text_processing.inverse_text_normalization.inverse_normalize import InverseNormalizer - - PYNINI_AVAILABLE = True -except (ImportError, ModuleNotFoundError): - PYNINI_AVAILABLE = False +from ..utils import CACHE_DIR, RUN_AUDIO_BASED_TESTS, parse_test_case_file class TestCardinal: - inverse_normalizer = ( - InverseNormalizer(lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) if PYNINI_AVAILABLE else None - ) + inverse_normalizer = InverseNormalizer(lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) @parameterized.expand(parse_test_case_file('vi/data_inverse_text_normalization/test_cases_cardinal.txt')) - @pytest.mark.skipif( - not PYNINI_AVAILABLE, - reason="`pynini` not installed, please install via nemo_text_processing/pynini_install.sh", - ) @pytest.mark.run_only_on('CPU') @pytest.mark.unit def test_denorm(self, test_input, expected): pred = self.inverse_normalizer.inverse_normalize(test_input, verbose=False) assert pred == expected + + normalizer = Normalizer( + input_case='cased', lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False, post_process=True + ) + + normalizer_with_audio = ( + NormalizerWithAudio(input_case='cased', lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) + if CACHE_DIR and RUN_AUDIO_BASED_TESTS + else None + ) + + @parameterized.expand(parse_test_case_file('vi/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, punct_post_process=False) + assert pred == expected, f"input: {test_input}" + + if self.normalizer_with_audio: + pred_non_deterministic = self.normalizer_with_audio.normalize( + test_input, + n_tagged=30, + punct_post_process=False, + ) + assert expected in pred_non_deterministic, f"input: {test_input}" diff --git a/tests/nemo_text_processing/vi/test_date.py b/tests/nemo_text_processing/vi/test_date.py index 90885b6e4..b3da475db 100644 --- a/tests/nemo_text_processing/vi/test_date.py +++ b/tests/nemo_text_processing/vi/test_date.py @@ -15,28 +15,45 @@ import pytest from parameterized import parameterized -from ..utils import CACHE_DIR, parse_test_case_file +from nemo_text_processing.inverse_text_normalization.inverse_normalize import InverseNormalizer +from nemo_text_processing.text_normalization.normalize import Normalizer +from nemo_text_processing.text_normalization.normalize_with_audio import NormalizerWithAudio -try: - from nemo_text_processing.inverse_text_normalization.inverse_normalize import InverseNormalizer - - PYNINI_AVAILABLE = True -except (ImportError, ModuleNotFoundError): - PYNINI_AVAILABLE = False +from ..utils import CACHE_DIR, RUN_AUDIO_BASED_TESTS, parse_test_case_file class TestDate: - inverse_normalizer = ( - InverseNormalizer(lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) if PYNINI_AVAILABLE else None - ) + + inverse_normalizer = InverseNormalizer(lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) @parameterized.expand(parse_test_case_file('vi/data_inverse_text_normalization/test_cases_date.txt')) - @pytest.mark.skipif( - not PYNINI_AVAILABLE, - reason="`pynini` not installed, please install via nemo_text_processing/pynini_install.sh", - ) @pytest.mark.run_only_on('CPU') @pytest.mark.unit def test_denorm(self, test_input, expected): pred = self.inverse_normalizer.inverse_normalize(test_input, verbose=False) assert pred == expected + + normalizer = Normalizer( + input_case='cased', lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False, post_process=True + ) + + normalizer_with_audio = ( + NormalizerWithAudio(input_case='cased', lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) + if CACHE_DIR and RUN_AUDIO_BASED_TESTS + else None + ) + + @parameterized.expand(parse_test_case_file('vi/data_text_normalization/test_cases_date.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}" + + if self.normalizer_with_audio: + pred_non_deterministic = self.normalizer_with_audio.normalize( + test_input, + n_tagged=30, + punct_post_process=False, + ) + assert expected in pred_non_deterministic, f"input: {test_input}" diff --git a/tests/nemo_text_processing/vi/test_decimal.py b/tests/nemo_text_processing/vi/test_decimal.py index e1b246e1d..73ed99f54 100644 --- a/tests/nemo_text_processing/vi/test_decimal.py +++ b/tests/nemo_text_processing/vi/test_decimal.py @@ -15,28 +15,45 @@ import pytest from parameterized import parameterized -from ..utils import CACHE_DIR, parse_test_case_file +from nemo_text_processing.inverse_text_normalization.inverse_normalize import InverseNormalizer +from nemo_text_processing.text_normalization.normalize import Normalizer +from nemo_text_processing.text_normalization.normalize_with_audio import NormalizerWithAudio -try: - from nemo_text_processing.inverse_text_normalization.inverse_normalize import InverseNormalizer - - PYNINI_AVAILABLE = True -except (ImportError, ModuleNotFoundError): - PYNINI_AVAILABLE = False +from ..utils import CACHE_DIR, RUN_AUDIO_BASED_TESTS, parse_test_case_file class TestDecimal: - inverse_normalizer = ( - InverseNormalizer(lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) if PYNINI_AVAILABLE else None - ) + + inverse_normalizer = InverseNormalizer(lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) @parameterized.expand(parse_test_case_file('vi/data_inverse_text_normalization/test_cases_decimal.txt')) - @pytest.mark.skipif( - not PYNINI_AVAILABLE, - reason="`pynini` not installed, please install via nemo_text_processing/pynini_install.sh", - ) @pytest.mark.run_only_on('CPU') @pytest.mark.unit def test_denorm(self, test_input, expected): pred = self.inverse_normalizer.inverse_normalize(test_input, verbose=False) assert pred == expected + + normalizer = Normalizer( + input_case='cased', lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False, post_process=True + ) + + normalizer_with_audio = ( + NormalizerWithAudio(input_case='cased', lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) + if CACHE_DIR and RUN_AUDIO_BASED_TESTS + else None + ) + + @parameterized.expand(parse_test_case_file('vi/data_text_normalization/test_cases_decimal.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}" + + if self.normalizer_with_audio: + pred_non_deterministic = self.normalizer_with_audio.normalize( + test_input, + n_tagged=30, + punct_post_process=False, + ) + assert expected in pred_non_deterministic, f"input: {test_input}" diff --git a/tests/nemo_text_processing/vi/test_fraction.py b/tests/nemo_text_processing/vi/test_fraction.py index acd465cfd..efa35fcce 100644 --- a/tests/nemo_text_processing/vi/test_fraction.py +++ b/tests/nemo_text_processing/vi/test_fraction.py @@ -12,32 +12,48 @@ # See the License for the specific language governing permissions and # limitations under the License. - import pytest from parameterized import parameterized -from ..utils import CACHE_DIR, parse_test_case_file - -try: - from nemo_text_processing.inverse_text_normalization.inverse_normalize import InverseNormalizer +from nemo_text_processing.inverse_text_normalization.inverse_normalize import InverseNormalizer +from nemo_text_processing.text_normalization.normalize import Normalizer +from nemo_text_processing.text_normalization.normalize_with_audio import NormalizerWithAudio - PYNINI_AVAILABLE = True -except (ImportError, ModuleNotFoundError): - PYNINI_AVAILABLE = False +from ..utils import CACHE_DIR, RUN_AUDIO_BASED_TESTS, parse_test_case_file class TestFraction: - inverse_normalizer = ( - InverseNormalizer(lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) if PYNINI_AVAILABLE else None - ) + + inverse_normalizer = InverseNormalizer(lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) @parameterized.expand(parse_test_case_file('vi/data_inverse_text_normalization/test_cases_fraction.txt')) - @pytest.mark.skipif( - not PYNINI_AVAILABLE, - reason="`pynini` not installed, please install via nemo_text_processing/pynini_install.sh", - ) @pytest.mark.run_only_on('CPU') @pytest.mark.unit def test_denorm(self, test_input, expected): pred = self.inverse_normalizer.inverse_normalize(test_input, verbose=False) assert pred == expected + + normalizer = Normalizer( + input_case='cased', lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False, post_process=True + ) + + normalizer_with_audio = ( + NormalizerWithAudio(input_case='cased', lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) + if CACHE_DIR and RUN_AUDIO_BASED_TESTS + else None + ) + + @parameterized.expand(parse_test_case_file('vi/data_text_normalization/test_cases_fraction.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}" + + if self.normalizer_with_audio: + pred_non_deterministic = self.normalizer_with_audio.normalize( + test_input, + n_tagged=30, + punct_post_process=False, + ) + assert expected in pred_non_deterministic, f"input: {test_input}" diff --git a/tests/nemo_text_processing/vi/test_measure.py b/tests/nemo_text_processing/vi/test_measure.py index 991cbc487..4cb89cf80 100644 --- a/tests/nemo_text_processing/vi/test_measure.py +++ b/tests/nemo_text_processing/vi/test_measure.py @@ -20,6 +20,7 @@ try: from nemo_text_processing.inverse_text_normalization.inverse_normalize import InverseNormalizer + from nemo_text_processing.text_normalization.normalize import Normalizer PYNINI_AVAILABLE = True except (ImportError, ModuleNotFoundError): @@ -41,3 +42,18 @@ class TestMeasure: def test_denorm(self, test_input, expected): pred = self.inverse_normalizer.inverse_normalize(test_input, verbose=False) assert pred == expected + + normalizer = Normalizer( + input_case='cased', lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False, post_process=True + ) + + @parameterized.expand(parse_test_case_file('vi/data_text_normalization/test_cases_measure.txt')) + @pytest.mark.skipif( + not PYNINI_AVAILABLE, + reason="`pynini` not installed, please install via nemo_text_processing/pynini_install.sh", + ) + @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}" diff --git a/tests/nemo_text_processing/vi/test_money.py b/tests/nemo_text_processing/vi/test_money.py index c626eef41..1d1b1c1c6 100644 --- a/tests/nemo_text_processing/vi/test_money.py +++ b/tests/nemo_text_processing/vi/test_money.py @@ -1,4 +1,4 @@ -# Copyright (c) 2021, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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. @@ -12,32 +12,53 @@ # See the License for the specific language governing permissions and # limitations under the License. - +# pytest tests/nemo_text_processing/vi/test_money.py --cpu --cache-clear import pytest from parameterized import parameterized -from ..utils import CACHE_DIR, parse_test_case_file - -try: - from nemo_text_processing.inverse_text_normalization.inverse_normalize import InverseNormalizer +from nemo_text_processing.inverse_text_normalization.inverse_normalize import InverseNormalizer +from nemo_text_processing.text_normalization.normalize import Normalizer +from nemo_text_processing.text_normalization.normalize_with_audio import NormalizerWithAudio - PYNINI_AVAILABLE = True -except (ImportError, ModuleNotFoundError): - PYNINI_AVAILABLE = False +from ..utils import CACHE_DIR, RUN_AUDIO_BASED_TESTS, parse_test_case_file class TestMoney: - inverse_normalizer = ( - InverseNormalizer(lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) if PYNINI_AVAILABLE else None - ) + + inverse_normalizer = InverseNormalizer(lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) @parameterized.expand(parse_test_case_file('vi/data_inverse_text_normalization/test_cases_money.txt')) - @pytest.mark.skipif( - not PYNINI_AVAILABLE, - reason="`pynini` not installed, please install via nemo_text_processing/pynini_install.sh", - ) @pytest.mark.run_only_on('CPU') @pytest.mark.unit def test_denorm(self, test_input, expected): pred = self.inverse_normalizer.inverse_normalize(test_input, verbose=False) assert pred == expected + + normalizer = Normalizer( + input_case='cased', lang='vi', cache_dir=CACHE_DIR, overwrite_cache=True, post_process=True + ) + + normalizer_with_audio = ( + NormalizerWithAudio(input_case='cased', lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) + if CACHE_DIR and RUN_AUDIO_BASED_TESTS + else None + ) + + @parameterized.expand(parse_test_case_file('vi/data_text_normalization/test_cases_money.txt')) + @pytest.mark.run_only_on('CPU') + @pytest.mark.unit + def test_norm(self, test_input, expected): + # Add debug print to see what's happening + print(f"\nTesting: {test_input}") + pred = self.normalizer.normalize(test_input, verbose=True, punct_post_process=False) + print(f"Predicted: {pred}") + print(f"Expected: {expected}") + assert pred == expected, f"input: {test_input}" + + if self.normalizer_with_audio: + pred_non_deterministic = self.normalizer_with_audio.normalize( + test_input, + n_tagged=30, + punct_post_process=False, + ) + assert expected in pred_non_deterministic, f"input: {test_input}" diff --git a/tests/nemo_text_processing/vi/test_ordinal.py b/tests/nemo_text_processing/vi/test_ordinal.py index 239234dda..9b15bd0c4 100644 --- a/tests/nemo_text_processing/vi/test_ordinal.py +++ b/tests/nemo_text_processing/vi/test_ordinal.py @@ -12,32 +12,48 @@ # See the License for the specific language governing permissions and # limitations under the License. - import pytest from parameterized import parameterized -from ..utils import CACHE_DIR, parse_test_case_file - -try: - from nemo_text_processing.inverse_text_normalization.inverse_normalize import InverseNormalizer +from nemo_text_processing.inverse_text_normalization.inverse_normalize import InverseNormalizer +from nemo_text_processing.text_normalization.normalize import Normalizer +from nemo_text_processing.text_normalization.normalize_with_audio import NormalizerWithAudio - PYNINI_AVAILABLE = True -except (ImportError, ModuleNotFoundError): - PYNINI_AVAILABLE = False +from ..utils import CACHE_DIR, RUN_AUDIO_BASED_TESTS, parse_test_case_file class TestOrdinal: - inverse_normalizer = ( - InverseNormalizer(lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) if PYNINI_AVAILABLE else None - ) + + inverse_normalizer = InverseNormalizer(lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) @parameterized.expand(parse_test_case_file('vi/data_inverse_text_normalization/test_cases_ordinal.txt')) - @pytest.mark.skipif( - not PYNINI_AVAILABLE, - reason="`pynini` not installed, please install via nemo_text_processing/pynini_install.sh", - ) @pytest.mark.run_only_on('CPU') @pytest.mark.unit def test_denorm(self, test_input, expected): pred = self.inverse_normalizer.inverse_normalize(test_input, verbose=False) assert pred == expected + + normalizer = Normalizer( + input_case='cased', lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False, post_process=True + ) + + normalizer_with_audio = ( + NormalizerWithAudio(input_case='cased', lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) + if CACHE_DIR and RUN_AUDIO_BASED_TESTS + else None + ) + + @parameterized.expand(parse_test_case_file('vi/data_text_normalization/test_cases_ordinal.txt')) + @pytest.mark.run_only_on('CPU') + @pytest.mark.unit + def test_norm(self, test_input, expected): + pred = self.normalizer.normalize(test_input, verbose=False, punct_post_process=False) + assert pred == expected, f"input: {test_input}" + + if self.normalizer_with_audio: + pred_non_deterministic = self.normalizer_with_audio.normalize( + test_input, + n_tagged=30, + punct_post_process=False, + ) + assert expected in pred_non_deterministic, f"input: {test_input}" diff --git a/tests/nemo_text_processing/vi/test_range.py b/tests/nemo_text_processing/vi/test_range.py new file mode 100644 index 000000000..7df7f9f9c --- /dev/null +++ b/tests/nemo_text_processing/vi/test_range.py @@ -0,0 +1,30 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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 tests.nemo_text_processing.utils import parse_test_case_file + + +class TestRange: + normalizer = Normalizer(input_case='cased', lang='vi', cache_dir=None, overwrite_cache=True) + + @parameterized.expand(parse_test_case_file("vi/data_text_normalization/test_cases_range.txt")) + @pytest.mark.run_only_on('CPU') + def test_norm(self, test_input, expected): + pred = self.normalizer.normalize(test_input) + assert pred == expected, f"input: {test_input} assert {pred} == {expected}" diff --git a/tests/nemo_text_processing/vi/test_roman.py b/tests/nemo_text_processing/vi/test_roman.py new file mode 100644 index 000000000..a942eb140 --- /dev/null +++ b/tests/nemo_text_processing/vi/test_roman.py @@ -0,0 +1,48 @@ +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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 nemo_text_processing.text_normalization.normalize_with_audio import NormalizerWithAudio + +from ..utils import CACHE_DIR, RUN_AUDIO_BASED_TESTS, parse_test_case_file + + +class TestRoman: + normalizer = Normalizer( + input_case='cased', lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False, post_process=True + ) + + normalizer_with_audio = ( + NormalizerWithAudio(input_case='cased', lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) + if CACHE_DIR and RUN_AUDIO_BASED_TESTS + else None + ) + + @parameterized.expand(parse_test_case_file('vi/data_text_normalization/test_cases_roman.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}" + + if self.normalizer_with_audio: + pred_non_deterministic = self.normalizer_with_audio.normalize( + test_input, + n_tagged=30, + punct_post_process=False, + ) + assert expected in pred_non_deterministic, f"input: {test_input}" diff --git a/tests/nemo_text_processing/vi/test_sparrowhawk_inverse_text_normalization.sh b/tests/nemo_text_processing/vi/test_sparrowhawk_inverse_text_normalization.sh index 751351cd4..684eb3b22 100644 --- a/tests/nemo_text_processing/vi/test_sparrowhawk_inverse_text_normalization.sh +++ b/tests/nemo_text_processing/vi/test_sparrowhawk_inverse_text_normalization.sh @@ -1,7 +1,7 @@ #! /bin/sh GRAMMARS_DIR=${1:-"/workspace/sparrowhawk/documentation/grammars"} -PROJECT_DIR=${2:-"/workspace/tests/en"} +PROJECT_DIR=${2:-"/workspace/tests"} runtest () { input=$1 diff --git a/tests/nemo_text_processing/vi/test_sparrowhawk_normalization.sh b/tests/nemo_text_processing/vi/test_sparrowhawk_normalization.sh new file mode 100644 index 000000000..c11d66ef0 --- /dev/null +++ b/tests/nemo_text_processing/vi/test_sparrowhawk_normalization.sh @@ -0,0 +1,87 @@ + +#! /bin/sh + +GRAMMARS_DIR=${1:-"/workspace/sparrowhawk/documentation/grammars"} +PROJECT_DIR=${2:-"/workspace/tests"} + +runtest () { + input=$1 + echo "INPUT is $input" + cd ${GRAMMARS_DIR} + + # read test file + while read testcase; do + IFS='~' read written spoken <<< $testcase + norm_pred=$(echo $written | normalizer_main --config=sparrowhawk_configuration.ascii_proto 2>&1 | tail -n 1) + + # trim white space + spoken="$(echo -e "${spoken}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + norm_pred="$(echo -e "${norm_pred}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + + # input expected actual + assertEquals "$written" "$spoken" "$norm_pred" + done < "$input" +} + +testTNCardinal() { + input=$PROJECT_DIR/vi/data_text_normalization/test_cases_cardinal.txt + runtest $input +} + +testTNDate() { + input=$PROJECT_DIR/vi/data_text_normalization/test_cases_date.txt + runtest $input +} + +testTNDecimal() { + input=$PROJECT_DIR/vi/data_text_normalization/test_cases_decimal.txt + runtest $input +} + +testTNOrdinal() { + input=$PROJECT_DIR/vi/data_text_normalization/test_cases_ordinal.txt + runtest $input +} + +testTNFraction() { + input=$PROJECT_DIR/vi/data_text_normalization/test_cases_fraction.txt + runtest $input +} + +testTNRoman() { + input=$PROJECT_DIR/vi/data_text_normalization/test_cases_roman.txt + runtest $input +} + +testTNTime() { + input=$PROJECT_DIR/vi/data_text_normalization/test_cases_time.txt + runtest $input +} + +testTNMeasure() { + input=$PROJECT_DIR/vi/data_text_normalization/test_cases_measure.txt + runtest $input +} + +testTNMoney() { + input=$PROJECT_DIR/vi/data_text_normalization/test_cases_money.txt + runtest $input +} + +testTNRange() { + input=$PROJECT_DIR/vi/data_text_normalization/test_cases_range.txt + runtest $input +} + +# testTNTelephone() { +# input=$PROJECT_DIR/vi/data_text_normalization/test_cases_telephone.txt +# runtest $input +# } + +# testTNElectronic() { +# input=$PROJECT_DIR/vi/data_text_normalization/test_cases_electronic.txt +# runtest $input +# } + +# Load shUnit2 +. /workspace/shunit2/shunit2 diff --git a/tests/nemo_text_processing/vi/test_time.py b/tests/nemo_text_processing/vi/test_time.py index 9502cad54..44dfdd875 100644 --- a/tests/nemo_text_processing/vi/test_time.py +++ b/tests/nemo_text_processing/vi/test_time.py @@ -1,4 +1,4 @@ -# Copyright (c) 2021, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. 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. @@ -15,28 +15,45 @@ import pytest from parameterized import parameterized -from ..utils import CACHE_DIR, parse_test_case_file +from nemo_text_processing.inverse_text_normalization.inverse_normalize import InverseNormalizer +from nemo_text_processing.text_normalization.normalize import Normalizer +from nemo_text_processing.text_normalization.normalize_with_audio import NormalizerWithAudio -try: - from nemo_text_processing.inverse_text_normalization.inverse_normalize import InverseNormalizer - - PYNINI_AVAILABLE = True -except (ImportError, ModuleNotFoundError): - PYNINI_AVAILABLE = False +from ..utils import CACHE_DIR, RUN_AUDIO_BASED_TESTS, parse_test_case_file class TestTime: - inverse_normalizer = ( - InverseNormalizer(lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) if PYNINI_AVAILABLE else None - ) + + inverse_normalizer = InverseNormalizer(lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) @parameterized.expand(parse_test_case_file('vi/data_inverse_text_normalization/test_cases_time.txt')) - @pytest.mark.skipif( - not PYNINI_AVAILABLE, - reason="`pynini` not installed, please install via nemo_text_processing/pynini_install.sh", - ) @pytest.mark.run_only_on('CPU') @pytest.mark.unit def test_denorm(self, test_input, expected): pred = self.inverse_normalizer.inverse_normalize(test_input, verbose=False) assert pred == expected + + normalizer = Normalizer( + input_case='cased', lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False, post_process=True + ) + + normalizer_with_audio = ( + NormalizerWithAudio(input_case='cased', lang='vi', cache_dir=CACHE_DIR, overwrite_cache=False) + if CACHE_DIR and RUN_AUDIO_BASED_TESTS + else None + ) + + @parameterized.expand(parse_test_case_file('vi/data_text_normalization/test_cases_time.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}" + + if self.normalizer_with_audio: + pred_non_deterministic = self.normalizer_with_audio.normalize( + test_input, + n_tagged=30, + punct_post_process=False, + ) + assert expected in pred_non_deterministic, f"input: {test_input}" diff --git a/tools/text_processing_deployment/pynini_export.py b/tools/text_processing_deployment/pynini_export.py index 6b82dfbec..445c71c98 100644 --- a/tools/text_processing_deployment/pynini_export.py +++ b/tools/text_processing_deployment/pynini_export.py @@ -137,7 +137,7 @@ def parse_args(): if __name__ == '__main__': args = parse_args() - if args.language in ['pt', 'ru', 'vi', 'es_en', 'mr'] and args.grammars == 'tn_grammars': + if args.language in ['pt', 'ru', 'es_en', 'mr'] and args.grammars == 'tn_grammars': raise ValueError('Only ITN grammars could be deployed in Sparrowhawk for the selected languages.') TNPostProcessingFst = None ITNPostProcessingFst = None @@ -240,6 +240,13 @@ def parse_args(): from nemo_text_processing.inverse_text_normalization.vi.verbalizers.verbalize import ( VerbalizeFst as ITNVerbalizeFst, ) + from nemo_text_processing.text_normalization.vi.taggers.tokenize_and_classify import ( + ClassifyFst as TNClassifyFst, + ) + from nemo_text_processing.text_normalization.vi.verbalizers.post_processing import ( + PostProcessingFst as TNPostProcessingFst, + ) + from nemo_text_processing.text_normalization.vi.verbalizers.verbalize import VerbalizeFst as TNVerbalizeFst elif args.language == 'zh': from nemo_text_processing.inverse_text_normalization.zh.taggers.tokenize_and_classify import ( ClassifyFst as ITNClassifyFst,