chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:25:10 +08:00
commit c397331b1e
3684 changed files with 990993 additions and 0 deletions
@@ -0,0 +1,47 @@
import pynini
from fun_text_processing.text_normalization.en.graph_utils import DAMO_NOT_QUOTE, GraphFst
from fun_text_processing.text_normalization.es.graph_utils import (
add_cardinal_apocope_fem,
shift_cardinal_gender,
strip_cardinal_apocope,
)
from pynini.lib import pynutil
class CardinalFst(GraphFst):
"""
Finite state transducer for verbalizing cardinals
e.g. cardinal { integer: "dos" } -> "dos"
Args:
deterministic: if True will provide a single transduction option,
for False multiple transduction are generated (used for audio-based normalization)
"""
def __init__(self, deterministic: bool = True):
super().__init__(name="cardinal", kind="verbalize", deterministic=deterministic)
optional_sign = pynini.closure(pynini.cross('negative: "true" ', "menos "), 0, 1)
self.optional_sign = optional_sign
integer = pynini.closure(DAMO_NOT_QUOTE, 1)
self.integer = pynutil.delete(' "') + integer + pynutil.delete('"')
integer = pynutil.delete("integer:") + self.integer
graph_masc = optional_sign + integer
graph_fem = shift_cardinal_gender(graph_masc)
self.graph_masc = pynini.optimize(graph_masc)
self.graph_fem = pynini.optimize(graph_fem)
# Adding adjustment for fem gender (choice of gender will be random)
graph = graph_masc | graph_fem
if not deterministic:
# For alternate renderings when apocope is omitted (i.e. cardinal stands alone)
graph |= strip_cardinal_apocope(graph_masc)
# "una" will drop to "un" in unique contexts
graph |= add_cardinal_apocope_fem(graph_fem)
delete_tokens = self.delete_tokens(graph)
self.fst = delete_tokens.optimize()
@@ -0,0 +1,75 @@
import pynini
from fun_text_processing.text_normalization.en.graph_utils import (
DAMO_NOT_QUOTE,
DAMO_SIGMA,
DAMO_SPACE,
GraphFst,
delete_preserve_order,
)
from fun_text_processing.text_normalization.es.graph_utils import strip_cardinal_apocope
from fun_text_processing.text_normalization.es.taggers.date import articles
from pynini.lib import pynutil
class DateFst(GraphFst):
"""
Finite state transducer for verbalizing date, e.g.
date { day: "treinta y uno" month: "marzo" year: "dos mil" } -> "treinta y uno de marzo de dos mil"
date { day: "uno" month: "mayo" year: "del mil novecientos noventa" } -> "primero de mayo del mil novecientos noventa"
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="date", kind="verbalize", deterministic=deterministic)
day_cardinal = (
pynutil.delete('day: "') + pynini.closure(DAMO_NOT_QUOTE, 1) + pynutil.delete('"')
)
day = strip_cardinal_apocope(day_cardinal)
primero = pynini.cdrewrite(pynini.cross("uno", "primero"), "[BOS]", "[EOS]", DAMO_SIGMA)
day = (
(day @ primero) if deterministic else pynini.union(day, day @ primero)
) # Primero for first day is traditional, but will vary depending on region
month = pynutil.delete('month: "') + pynini.closure(DAMO_NOT_QUOTE, 1) + pynutil.delete('"')
year = (
pynutil.delete('year: "')
+ articles
+ DAMO_SPACE
+ pynini.closure(DAMO_NOT_QUOTE, 1)
+ pynutil.delete('"')
)
# Insert preposition if wasn't originally with the year. This would mean a space was present
year = pynutil.add_weight(year, -0.001)
year |= (
pynutil.delete('year: "')
+ pynutil.insert("de ")
+ pynini.closure(DAMO_NOT_QUOTE, 1)
+ pynutil.delete('"')
)
# day month year
graph_dmy = (
day
+ pynini.cross(DAMO_SPACE, " de ")
+ month
+ pynini.closure(pynini.accep(" ") + year, 0, 1)
)
graph_mdy = month + DAMO_SPACE + day + pynini.closure(DAMO_SPACE + year, 0, 1)
if deterministic:
graph_mdy += pynutil.delete(
" preserve_order: true"
) # Only accepts this if was explicitly passed
self.graph = graph_dmy | graph_mdy
final_graph = self.graph + delete_preserve_order
delete_tokens = self.delete_tokens(final_graph)
self.fst = delete_tokens.optimize()
@@ -0,0 +1,89 @@
import pynini
from fun_text_processing.text_normalization.en.graph_utils import (
DAMO_NOT_QUOTE,
GraphFst,
delete_preserve_order,
delete_space,
insert_space,
)
from fun_text_processing.text_normalization.es import LOCALIZATION
from fun_text_processing.text_normalization.es.graph_utils import (
add_cardinal_apocope_fem,
shift_cardinal_gender,
shift_number_gender,
strip_cardinal_apocope,
)
from pynini.lib import pynutil
class DecimalFst(GraphFst):
"""
Finite state transducer for classifying decimal, e.g.
decimal { negative: "true" integer_part: "dos" fractional_part: "cuatro cero" quantity: "billones" } -> menos dos coma quatro cero billones
decimal { integer_part: "un" quantity: "billón" } -> un billó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="decimal", kind="classify", deterministic=deterministic)
optional_sign = pynini.closure(
pynini.cross('negative: "true"', "menos ") + delete_space, 0, 1
)
integer = (
pynutil.delete('integer_part: "')
+ pynini.closure(DAMO_NOT_QUOTE, 1)
+ pynutil.delete('"')
)
fractional_default = (
pynutil.delete('fractional_part: "')
+ pynini.closure(DAMO_NOT_QUOTE, 1)
+ pynutil.delete('"')
)
conjunction = (
pynutil.insert(" punto ") if LOCALIZATION == "am" else pynutil.insert(" coma ")
)
if not deterministic:
conjunction |= pynutil.insert(pynini.union(" con ", " y "))
fractional_default |= strip_cardinal_apocope(fractional_default)
fractional = conjunction + fractional_default
quantity = (
delete_space
+ insert_space
+ pynutil.delete('quantity: "')
+ pynini.closure(DAMO_NOT_QUOTE, 1)
+ pynutil.delete('"')
)
optional_quantity = pynini.closure(quantity, 0, 1)
graph_masc = optional_sign + pynini.union(
(integer + quantity), (integer + delete_space + fractional + optional_quantity)
)
# Allowing permutation for fem gender, don't include quantity since "million","billion", etc.. are masculine
graph_fem = optional_sign + (
shift_cardinal_gender(integer) + delete_space + shift_number_gender(fractional)
)
if not deterministic: # "una" will drop to "un" in certain cases
graph_fem |= add_cardinal_apocope_fem(graph_fem)
self.numbers_only_quantity = (
optional_sign
+ pynini.union(
(integer + quantity), (integer + delete_space + fractional + quantity)
).optimize()
)
self.graph_masc = (graph_masc + delete_preserve_order).optimize()
self.graph_fem = (graph_fem + delete_preserve_order).optimize()
graph = graph_masc | graph_fem
graph += delete_preserve_order
delete_tokens = self.delete_tokens(graph)
self.fst = delete_tokens.optimize()
@@ -0,0 +1,67 @@
import pynini
from fun_text_processing.text_normalization.en.graph_utils import (
DAMO_NOT_QUOTE,
DAMO_SIGMA,
GraphFst,
delete_preserve_order,
insert_space,
)
from fun_text_processing.text_normalization.es.utils import get_abs_path
from pynini.lib import pynutil
digit_no_zero = pynini.invert(pynini.string_file(get_abs_path("data/numbers/digit.tsv")))
zero = pynini.invert(pynini.string_file(get_abs_path("data/numbers/zero.tsv")))
graph_symbols = pynini.string_file(get_abs_path("data/electronic/symbols.tsv"))
server_common = pynini.string_file(get_abs_path("data/electronic/server_name.tsv"))
domain_common = pynini.string_file(get_abs_path("data/electronic/domain.tsv"))
class ElectronicFst(GraphFst):
"""
Finite state transducer for verbalizing electronic
e.g. electronic { username: "abc" domain: "hotmail.com" } -> "a b c arroba hotmail punto com"
-> "a b c arroba h o t m a i l punto c o m"
-> "a b c arroba hotmail punto c o m"
-> "a b c at h o t m a i l punto com"
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="electronic", kind="verbalize", deterministic=deterministic)
graph_digit_no_zero = (
digit_no_zero
@ pynini.cdrewrite(pynini.cross("un", "uno"), "", "", DAMO_SIGMA).optimize()
)
graph_digit = graph_digit_no_zero | zero
def add_space_after_char():
return pynini.closure(DAMO_NOT_QUOTE - pynini.accep(" ") + insert_space) + (
DAMO_NOT_QUOTE - pynini.accep(" ")
)
verbalize_characters = pynini.cdrewrite(graph_symbols | graph_digit, "", "", DAMO_SIGMA)
user_name = pynutil.delete('username: "') + add_space_after_char() + pynutil.delete('"')
user_name @= verbalize_characters
convert_defaults = (
pynutil.add_weight(DAMO_NOT_QUOTE, weight=0.0001) | domain_common | server_common
)
domain = convert_defaults + pynini.closure(insert_space + convert_defaults)
domain @= verbalize_characters
domain = pynutil.delete('domain: "') + domain + pynutil.delete('"')
protocol = (
pynutil.delete('protocol: "')
+ add_space_after_char() @ pynini.cdrewrite(graph_symbols, "", "", DAMO_SIGMA)
+ pynutil.delete('"')
)
self.graph = (pynini.closure(protocol + pynini.accep(" "), 0, 1) + domain) | (
user_name + pynini.accep(" ") + pynutil.insert("arroba ") + domain
)
delete_tokens = self.delete_tokens(self.graph + delete_preserve_order)
self.fst = delete_tokens.optimize()
@@ -0,0 +1,195 @@
import pynini
from fun_text_processing.text_normalization.en.graph_utils import (
DAMO_CHAR,
DAMO_NOT_QUOTE,
DAMO_NOT_SPACE,
DAMO_SIGMA,
DAMO_SPACE,
GraphFst,
delete_space,
insert_space,
)
from fun_text_processing.text_normalization.es.graph_utils import (
accents,
shift_cardinal_gender,
strip_cardinal_apocope,
)
from pynini.lib import pynutil
class FractionFst(GraphFst):
"""
Finite state transducer for verbalizing fraction
e.g. tokens { fraction { integer: "treinta y tres" numerator: "cuatro" denominator: "quinto" } } ->
treinta y tres y cuatro quintos
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="fraction", kind="verbalize", deterministic=deterministic)
# Derivational strings append 'avo' as a suffix. Adding space for processing aid
fraction_stem = pynutil.insert(" avo")
plural = pynutil.insert("s")
conjunction = pynutil.insert(" y ")
integer = (
pynutil.delete('integer_part: "')
+ strip_cardinal_apocope(pynini.closure(DAMO_NOT_QUOTE))
+ pynutil.delete('"')
)
numerator_one = pynutil.delete('numerator: "') + pynini.accep("un") + pynutil.delete('" ')
numerator = (
pynutil.delete('numerator: "')
+ pynini.difference(pynini.closure(DAMO_NOT_QUOTE), "un")
+ pynutil.delete('" ')
)
denominator_add_stem = pynutil.delete('denominator: "') + (
pynini.closure(DAMO_NOT_QUOTE)
+ fraction_stem
+ pynutil.delete('" morphosyntactic_features: "add_root"')
)
denominator_ordinal = pynutil.delete('denominator: "') + (
pynini.closure(DAMO_NOT_QUOTE) + pynutil.delete('" morphosyntactic_features: "ordinal"')
)
denominator_cardinal = pynutil.delete('denominator: "') + (
pynini.closure(DAMO_NOT_QUOTE) + pynutil.delete('"')
)
denominator_singular = pynini.union(denominator_add_stem, denominator_ordinal)
if not deterministic:
# Occasional exceptions
denominator_singular |= denominator_add_stem @ pynini.string_map(
[("once avo", "undécimo"), ("doce avo", "duodécimo")]
)
denominator_plural = denominator_singular + plural
# Merging operations
merge = pynini.cdrewrite(
pynini.cross(" y ", "i"), "", "", DAMO_SIGMA
) # The denominator must be a single word, with the conjunction "y" replaced by i
merge @= pynini.cdrewrite(
delete_space, "", pynini.difference(DAMO_CHAR, "parte"), DAMO_SIGMA
)
# The merger can produce duplicate vowels. This is not allowed in orthography
delete_duplicates = pynini.string_map([("aa", "a"), ("oo", "o")]) # Removes vowels
delete_duplicates = pynini.cdrewrite(delete_duplicates, "", "", DAMO_SIGMA)
remove_accents = pynini.cdrewrite(
accents,
pynini.union(DAMO_SPACE, pynini.accep("[BOS]")) + pynini.closure(DAMO_NOT_SPACE),
pynini.closure(DAMO_NOT_SPACE) + pynini.union("avo", "ava", "ésimo", "ésima"),
DAMO_SIGMA,
)
merge_into_single_word = merge @ remove_accents @ delete_duplicates
fraction_default = (
numerator + delete_space + insert_space + (denominator_plural @ merge_into_single_word)
)
fraction_with_one = (
numerator_one
+ delete_space
+ insert_space
+ (denominator_singular @ merge_into_single_word)
)
fraction_with_cardinal = strip_cardinal_apocope(numerator | numerator_one)
fraction_with_cardinal += (
delete_space + pynutil.insert(" sobre ") + strip_cardinal_apocope(denominator_cardinal)
)
if not deterministic:
# There is an alternative rendering where ordinals act as adjectives for 'parte'. This requires use of the feminine
# Other rules will manage use of "un" at end, so just worry about endings
exceptions = pynini.string_map([("tercia", "tercera")])
apply_exceptions = pynini.cdrewrite(exceptions, "", "", DAMO_SIGMA)
vowel_change = pynini.cdrewrite(
pynini.cross("o", "a"), "", pynini.accep("[EOS]"), DAMO_SIGMA
)
denominator_singular_fem = (
shift_cardinal_gender(denominator_singular) @ vowel_change @ apply_exceptions
)
denominator_plural_fem = denominator_singular_fem + plural
numerator_one_fem = shift_cardinal_gender(numerator_one)
numerator_fem = shift_cardinal_gender(numerator)
fraction_with_cardinal |= (
(numerator_one_fem | numerator_fem)
+ delete_space
+ pynutil.insert(" sobre ")
+ shift_cardinal_gender(denominator_cardinal)
)
# Still need to manage stems
merge_stem = pynini.cdrewrite(
delete_space, "", pynini.union("avo", "ava", "avos", "avas"), DAMO_SIGMA
) # For managing alternative spacing
merge_stem @= remove_accents @ delete_duplicates
fraction_with_one_fem = numerator_one_fem + delete_space + insert_space
fraction_with_one_fem += pynini.union(
denominator_singular_fem @ merge_stem,
denominator_singular_fem @ merge_into_single_word,
) # Both forms exists
fraction_with_one_fem += pynutil.insert(" parte")
fraction_with_one_fem @= pynini.cdrewrite(
pynini.cross("una media", "media"), "", "", DAMO_SIGMA
) # "media" not "una media"
fraction_default_fem = numerator_fem + delete_space + insert_space
fraction_default_fem += pynini.union(
denominator_plural_fem @ merge_stem, denominator_plural_fem @ merge_into_single_word
)
fraction_default_fem += pynutil.insert(" partes")
fraction_default |= (
numerator + delete_space + insert_space + denominator_plural @ merge_stem
) # Case of no merger
fraction_default |= fraction_default_fem
fraction_with_one |= (
numerator_one + delete_space + insert_space + denominator_singular @ merge_stem
)
fraction_with_one |= fraction_with_one_fem
fraction_with_one @= pynini.cdrewrite(
pynini.cross("un medio", "medio"), "", "", DAMO_SIGMA
) # "medio" not "un medio"
fraction = fraction_with_one | fraction_default | fraction_with_cardinal
graph_masc = pynini.closure(integer + delete_space + conjunction, 0, 1) + fraction
# Manage cases of fem gender (only shows on integer except for "medio")
integer_fem = shift_cardinal_gender(integer)
fraction_default |= (
shift_cardinal_gender(numerator)
+ delete_space
+ insert_space
+ (denominator_plural @ pynini.cross("medios", "medias"))
)
fraction_with_one |= (
pynutil.delete(numerator_one)
+ delete_space
+ (denominator_singular @ pynini.cross("medio", "media"))
)
fraction_fem = fraction_with_one | fraction_default | fraction_with_cardinal
graph_fem = pynini.closure(integer_fem + delete_space + conjunction, 0, 1) + fraction_fem
self.graph_masc = pynini.optimize(graph_masc)
self.graph_fem = pynini.optimize(graph_fem)
self.graph = graph_masc | graph_fem
delete_tokens = self.delete_tokens(self.graph)
self.fst = delete_tokens.optimize()
@@ -0,0 +1,118 @@
import pynini
from fun_text_processing.text_normalization.en.graph_utils import (
DAMO_NOT_QUOTE,
DAMO_SIGMA,
DAMO_SPACE,
DAMO_WHITE_SPACE,
GraphFst,
delete_extra_space,
delete_preserve_order,
)
from fun_text_processing.text_normalization.es.graph_utils import ones
from fun_text_processing.text_normalization.es.utils import get_abs_path
from pynini.lib import pynutil
unit_plural_fem = pynini.string_file(get_abs_path("data/measures/measurements_plural_fem.tsv"))
unit_plural_masc = pynini.string_file(get_abs_path("data/measures/measurements_plural_masc.tsv"))
unit_singular_fem = pynini.project(unit_plural_fem, "input")
unit_singular_masc = pynini.project(unit_plural_masc, "input")
unit_plural_fem = pynini.project(unit_plural_fem, "output")
unit_plural_masc = pynini.project(unit_plural_masc, "output")
class MeasureFst(GraphFst):
"""
Finite state transducer for verbalizing measure, e.g.
measure { cardinal { integer: "dos" units: "gramos" } } -> "dos gramos"
measure { decimal { integer_part: "dos" quantity: "millones" units: "gramos" } } -> "dos millones de gramos"
Args:
decimal: DecimalFst
cardinal: CardinalFst
fraction: FractionFst
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
):
super().__init__(name="measure", kind="verbalize", deterministic=deterministic)
graph_decimal_masc = decimal.delete_tokens(decimal.graph_masc)
graph_decimal_fem = decimal.delete_tokens(decimal.graph_fem)
graph_cardinal_masc = cardinal.delete_tokens(cardinal.graph_masc)
graph_cardinal_fem = cardinal.delete_tokens(cardinal.graph_fem)
graph_fraction_fem = fraction.delete_tokens(fraction.graph_fem)
graph_fraction_masc = fraction.delete_tokens(fraction.graph_masc)
unit_masc = (unit_plural_masc | unit_singular_masc) + pynini.closure(
DAMO_WHITE_SPACE + "por" + pynini.closure(DAMO_NOT_QUOTE, 1), 0, 1
)
unit_masc |= "por" + pynini.closure(DAMO_NOT_QUOTE, 1)
unit_masc = (
pynutil.delete('units: "')
+ (pynini.closure(DAMO_NOT_QUOTE) @ unit_masc)
+ pynutil.delete('"')
)
unit_fem = (unit_plural_fem | unit_singular_fem) + pynini.closure(
DAMO_WHITE_SPACE + "por" + pynini.closure(DAMO_NOT_QUOTE, 1), 0, 1
)
unit_fem = (
pynutil.delete('units: "')
+ (pynini.closure(DAMO_NOT_QUOTE) @ unit_fem)
+ pynutil.delete('"')
)
graph_masc = (graph_cardinal_masc | graph_decimal_masc) + DAMO_WHITE_SPACE + unit_masc
graph_masc |= graph_fraction_masc + DAMO_WHITE_SPACE + pynutil.insert("de ") + unit_masc
graph_masc |= pynutil.add_weight(
graph_fraction_masc @ (DAMO_SIGMA + pynini.union("medio", "medios"))
+ DAMO_WHITE_SPACE
+ unit_masc,
-0.001,
) # "medio litro" not "medio de litro"
graph_fem = (graph_cardinal_fem | graph_decimal_fem) + DAMO_WHITE_SPACE + unit_fem
graph_fem |= graph_fraction_fem + DAMO_WHITE_SPACE + pynutil.insert("de ") + unit_fem
graph_fem |= pynutil.add_weight(
graph_fraction_fem @ (DAMO_SIGMA + pynini.union("media", "medias"))
+ DAMO_WHITE_SPACE
+ unit_fem,
-0.001,
)
graph = graph_masc | graph_fem
graph = (
pynini.cdrewrite(
pynutil.insert(" de"),
'quantity: "' + pynini.closure(DAMO_NOT_QUOTE, 1),
'"',
DAMO_SIGMA,
)
@ graph
) # billones de xyz
graph @= pynini.cdrewrite(
pynini.cross(ones, "uno"), "", DAMO_WHITE_SPACE + "por", DAMO_SIGMA
)
# To manage alphanumeric combonations ("a-8, 5x"), we let them use a weighted default path.
alpha_num_unit = (
pynutil.delete('units: "') + pynini.closure(DAMO_NOT_QUOTE) + pynutil.delete('"')
)
graph_alpha_num = pynini.union(
(graph_cardinal_masc | graph_decimal_masc) + DAMO_SPACE + alpha_num_unit,
alpha_num_unit + delete_extra_space + (graph_cardinal_masc | graph_decimal_masc),
)
graph |= pynutil.add_weight(graph_alpha_num, 0.01)
graph += delete_preserve_order
delete_tokens = self.delete_tokens(graph)
self.fst = delete_tokens.optimize()
@@ -0,0 +1,169 @@
import pynini
from fun_text_processing.text_normalization.en.graph_utils import (
DAMO_NOT_QUOTE,
DAMO_SIGMA,
DAMO_SPACE,
GraphFst,
delete_preserve_order,
)
from fun_text_processing.text_normalization.es.graph_utils import (
shift_cardinal_gender,
strip_cardinal_apocope,
)
from fun_text_processing.text_normalization.es.utils import get_abs_path
from pynini.lib import pynutil
fem = pynini.string_file((get_abs_path("data/money/currency_plural_fem.tsv")))
masc = pynini.string_file((get_abs_path("data/money/currency_plural_masc.tsv")))
fem_singular = pynini.project(fem, "input")
masc_singular = pynini.project(masc, "input")
fem_plural = pynini.project(fem, "output")
masc_plural = pynini.project(masc, "output")
class MoneyFst(GraphFst):
"""
Finite state transducer for verbalizing money, e.g.
money { currency_maj: "euro" integer_part: "un"} -> "un euro"
money { currency_maj: "euro" integer_part: "un" fractional_part: "cero cero un"} -> "uno coma cero cero uno euros"
money { integer_part: "un" currency_maj: "libra" fractional_part: "cuarenta" preserve_order: true} -> "una libra cuarenta"
money { integer_part: "un" currency_maj: "libra" fractional_part: "cuarenta" currency_min: "peniques" preserve_order: true} -> "una libra con cuarenta peniques"
money { fractional_part: "un" currency_min: "penique" preserve_order: true} -> "un penique"
Args:
decimal: GraphFst
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, deterministic: bool = True):
super().__init__(name="money", kind="verbalize", deterministic=deterministic)
maj_singular_masc = (
pynutil.delete('currency_maj: "')
+ (pynini.closure(DAMO_NOT_QUOTE, 1) @ masc_singular)
+ pynutil.delete('"')
)
maj_singular_fem = (
pynutil.delete('currency_maj: "')
+ (pynini.closure(DAMO_NOT_QUOTE, 1) @ fem_singular)
+ pynutil.delete('"')
)
maj_plural_masc = (
pynutil.delete('currency_maj: "')
+ (pynini.closure(DAMO_NOT_QUOTE, 1) @ masc_plural)
+ pynutil.delete('"')
)
maj_plural_fem = (
pynutil.delete('currency_maj: "')
+ (pynini.closure(DAMO_NOT_QUOTE, 1) @ fem_plural)
+ pynutil.delete('"')
)
maj_masc = maj_plural_masc | maj_singular_masc # Tagger kept quantity resolution stable
maj_fem = maj_plural_fem | maj_singular_fem
min_singular_masc = (
pynutil.delete('currency_min: "')
+ (pynini.closure(DAMO_NOT_QUOTE, 1) @ masc_singular)
+ pynutil.delete('"')
)
min_singular_fem = (
pynutil.delete('currency_min: "')
+ (pynini.closure(DAMO_NOT_QUOTE, 1) @ fem_singular)
+ pynutil.delete('"')
)
min_plural_masc = (
pynutil.delete('currency_min: "')
+ (pynini.closure(DAMO_NOT_QUOTE, 1) @ masc_plural)
+ pynutil.delete('"')
)
min_plural_fem = (
pynutil.delete('currency_min: "')
+ (pynini.closure(DAMO_NOT_QUOTE, 1) @ fem_plural)
+ pynutil.delete('"')
)
min_masc = min_plural_masc | min_singular_masc
min_fem = min_plural_fem | min_singular_fem
fractional_part = (
pynutil.delete('fractional_part: "')
+ pynini.closure(DAMO_NOT_QUOTE, 1)
+ pynutil.delete('"')
)
integer_part = (
pynutil.delete('integer_part: "')
+ pynini.closure(DAMO_NOT_QUOTE, 1)
+ pynutil.delete('"')
)
optional_add_and = pynini.closure(pynutil.insert(pynini.union("con ", "y ")), 0, 1)
# *** currency_maj
graph_integer_masc = integer_part + DAMO_SPACE + maj_masc
graph_integer_fem = shift_cardinal_gender(integer_part) + DAMO_SPACE + maj_fem
graph_integer = graph_integer_fem | graph_integer_masc
# *** currency_maj + (***) | ((con) *** current_min)
graph_integer_with_minor_masc = (
graph_integer_masc
+ DAMO_SPACE
+ pynini.union(
optional_add_and + strip_cardinal_apocope(fractional_part),
(optional_add_and + fractional_part + DAMO_SPACE + min_masc),
(optional_add_and + shift_cardinal_gender(fractional_part) + DAMO_SPACE + min_fem),
) # Could be minor currency that is different gender
+ delete_preserve_order
)
graph_integer_with_minor_fem = (
graph_integer_fem
+ DAMO_SPACE
+ pynini.union(
optional_add_and + shift_cardinal_gender(fractional_part),
(optional_add_and + fractional_part + DAMO_SPACE + min_masc),
(optional_add_and + shift_cardinal_gender(fractional_part) + DAMO_SPACE + min_fem),
) # Could be minor currency that is different gender
+ delete_preserve_order
)
graph_integer_with_minor = graph_integer_with_minor_fem | graph_integer_with_minor_masc
## *** coma *** currency_maj
graph_decimal_masc = decimal.graph_masc + DAMO_SPACE + maj_masc
graph_decimal_fem = decimal.graph_fem
graph_decimal_fem |= (
decimal.numbers_only_quantity
) # can still have "x billions" with fem currency
graph_decimal_fem += DAMO_SPACE + maj_fem
graph_decimal = graph_decimal_fem | graph_decimal_masc
graph_decimal = (
pynini.cdrewrite(
pynutil.insert(" de"),
'quantity: "' + pynini.closure(DAMO_NOT_QUOTE, 1),
'"',
DAMO_SIGMA,
)
@ graph_decimal
) # formally it's millones/billones de ***
# *** current_min
graph_minor_masc = fractional_part + DAMO_SPACE + min_masc + delete_preserve_order
graph_minor_fem = (
shift_cardinal_gender(fractional_part) + DAMO_SPACE + min_fem + delete_preserve_order
)
graph_minor = graph_minor_fem | graph_minor_masc
graph = graph_integer | graph_integer_with_minor | graph_decimal | graph_minor
delete_tokens = self.delete_tokens(graph)
self.fst = delete_tokens.optimize()
@@ -0,0 +1,68 @@
import pynini
from fun_text_processing.text_normalization.en.graph_utils import (
DAMO_NOT_QUOTE,
DAMO_SIGMA,
DAMO_SPACE,
GraphFst,
)
from fun_text_processing.text_normalization.es.graph_utils import shift_number_gender
from pynini.lib import pynutil
class OrdinalFst(GraphFst):
"""
Finite state transducer for verbalizing ordinals
e.g. ordinal { integer: "tercer" } } -> "tercero"
-> "tercera"
-> "tercer"
Args:
deterministic: if True will provide a single transduction option,
for False multiple transduction are generated (used for audio-based normalization)
"""
def __init__(self, deterministic: bool = True):
super().__init__(name="ordinal", kind="verbalize", deterministic=deterministic)
graph = (
pynutil.delete('integer: "') + pynini.closure(DAMO_NOT_QUOTE, 1) + pynutil.delete('"')
)
# masculne gender we leave as is
graph_masc = graph + pynutil.delete(' morphosyntactic_features: "gender_masc')
# shift gender
graph_fem_ending = graph @ pynini.cdrewrite(
pynini.cross("o", "a"), "", DAMO_SPACE | pynini.accep("[EOS]"), DAMO_SIGMA
)
graph_fem = shift_number_gender(graph_fem_ending) + pynutil.delete(
' morphosyntactic_features: "gender_fem'
)
# Apocope just changes tercero and primero. May occur if someone wrote 11.er (uncommon)
graph_apocope = (
pynini.cross("tercero", "tercer")
| pynini.cross("primero", "primer")
| pynini.cross("undécimo", "decimoprimer")
) # In case someone wrote 11.er with deterministic
graph_apocope = (
graph @ pynini.cdrewrite(graph_apocope, "", "", DAMO_SIGMA)
) + pynutil.delete(' morphosyntactic_features: "apocope')
graph = graph_apocope | graph_masc | graph_fem
if not deterministic:
# Plural graph
graph_plural = pynini.cdrewrite(
pynutil.insert("s"),
pynini.union("o", "a"),
DAMO_SPACE | pynini.accep("[EOS]"),
DAMO_SIGMA,
)
graph |= (graph @ graph_plural) + pynutil.delete("/plural")
self.graph = graph + pynutil.delete('"')
delete_tokens = self.delete_tokens(self.graph)
self.fst = delete_tokens.optimize()
@@ -0,0 +1,26 @@
import pynini
from fun_text_processing.text_normalization.en.graph_utils import DAMO_NOT_QUOTE, GraphFst
from pynini.lib import pynutil
class TelephoneFst(GraphFst):
"""
Finite state transducer for verbalizing telephone, e.g.
telephone { number_part: "uno dos tres uno dos tres cinco seis siete ocho" }
-> uno dos tres uno dos tres cinco seis siete ocho
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="telephone", kind="verbalize")
number_part = (
pynutil.delete('number_part: "')
+ pynini.closure(DAMO_NOT_QUOTE, 1)
+ pynutil.delete('"')
)
delete_tokens = self.delete_tokens(number_part)
self.fst = delete_tokens.optimize()
@@ -0,0 +1,249 @@
import pynini
from fun_text_processing.text_normalization.en.graph_utils import (
DAMO_NOT_QUOTE,
DAMO_SIGMA,
GraphFst,
delete_preserve_order,
delete_space,
insert_space,
)
from fun_text_processing.text_normalization.es.utils import get_abs_path
from pynini.lib import pynutil
alt_minutes = pynini.string_file(get_abs_path("data/time/alt_minutes.tsv"))
morning_times = pynini.string_file(get_abs_path("data/time/morning_times.tsv"))
afternoon_times = pynini.string_file(get_abs_path("data/time/afternoon_times.tsv"))
evening_times = pynini.string_file(get_abs_path("data/time/evening_times.tsv"))
class TimeFst(GraphFst):
"""
Finite state transducer for verbalizing time, e.g.
time { hours: "doce" minutes: "media" suffix: "a m" } -> doce y media de la noche
time { hours: "doce" } -> twelve o'clock
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)
change_minutes = pynini.cdrewrite(
alt_minutes, pynini.accep("[BOS]"), pynini.accep("[EOS]"), DAMO_SIGMA
)
morning_phrases = pynini.cross("am", "de la mañana")
afternoon_phrases = pynini.cross("pm", "de la tarde")
evening_phrases = pynini.cross("pm", "de la noche")
# For the 12's
mid_times = pynini.accep("doce")
mid_phrases = (
pynini.string_map([("pm", "del mediodía"), ("am", "de la noche")])
if deterministic
else pynini.string_map(
[
("pm", "de la mañana"),
("pm", "del día"),
("pm", "del mediodía"),
("am", "de la noche"),
("am", "de la medianoche"),
]
)
)
hour = (
pynutil.delete("hours:")
+ delete_space
+ pynutil.delete('"')
+ pynini.closure(DAMO_NOT_QUOTE, 1)
+ pynutil.delete('"')
)
minute = (
pynutil.delete("minutes:")
+ delete_space
+ pynutil.delete('"')
+ pynini.closure(DAMO_NOT_QUOTE, 1)
+ pynutil.delete('"')
)
minute = (
(minute @ change_minutes)
if deterministic
else pynini.union(minute, minute @ change_minutes)
)
suffix = (
pynutil.delete("suffix:")
+ delete_space
+ pynutil.delete('"')
+ pynini.closure(DAMO_NOT_QUOTE, 1)
+ pynutil.delete('"')
)
zone = (
pynutil.delete("zone:")
+ delete_space
+ pynutil.delete('"')
+ pynini.closure(DAMO_NOT_QUOTE, 1)
+ pynutil.delete('"')
)
optional_zone = pynini.closure(delete_space + insert_space + zone, 0, 1)
second = (
pynutil.delete("seconds:")
+ delete_space
+ pynutil.delete('"')
+ pynini.closure(DAMO_NOT_QUOTE, 1)
+ pynutil.delete('"')
)
graph_hms = (
hour
+ pynutil.insert(" horas ")
+ delete_space
+ minute
+ pynutil.insert(" minutos y ")
+ delete_space
+ second
+ pynutil.insert(" segundos")
)
graph_hm = hour + delete_space + pynutil.insert(" y ") + minute
graph_hm |= pynini.union(
(hour @ morning_times)
+ delete_space
+ pynutil.insert(" y ")
+ minute
+ delete_space
+ insert_space
+ (suffix @ morning_phrases),
(hour @ afternoon_times)
+ delete_space
+ pynutil.insert(" y ")
+ minute
+ delete_space
+ insert_space
+ (suffix @ afternoon_phrases),
(hour @ evening_times)
+ delete_space
+ pynutil.insert(" y ")
+ minute
+ delete_space
+ insert_space
+ (suffix @ evening_phrases),
(hour @ mid_times)
+ delete_space
+ pynutil.insert(" y ")
+ minute
+ delete_space
+ insert_space
+ (suffix @ mid_phrases),
)
graph_h = pynini.union(
hour,
(hour @ morning_times) + delete_space + insert_space + (suffix @ morning_phrases),
(hour @ afternoon_times) + delete_space + insert_space + (suffix @ afternoon_phrases),
(hour @ evening_times) + delete_space + insert_space + (suffix @ evening_phrases),
(hour @ mid_times) + delete_space + insert_space + (suffix @ mid_phrases),
)
graph = (graph_hms | graph_hm | graph_h) + optional_zone
if not deterministic:
graph_style_1 = pynutil.delete(' style: "1"')
graph_style_2 = pynutil.delete(' style: "2"')
graph_menos = hour + delete_space + pynutil.insert(" menos ") + minute + graph_style_1
graph_menos |= (
(hour @ morning_times)
+ delete_space
+ pynutil.insert(" menos ")
+ minute
+ delete_space
+ insert_space
+ (suffix @ morning_phrases)
+ graph_style_1
)
graph_menos |= (
(hour @ afternoon_times)
+ delete_space
+ pynutil.insert(" menos ")
+ minute
+ delete_space
+ insert_space
+ (suffix @ afternoon_phrases)
+ graph_style_1
)
graph_menos |= (
(hour @ evening_times)
+ delete_space
+ pynutil.insert(" menos ")
+ minute
+ delete_space
+ insert_space
+ (suffix @ evening_phrases)
+ graph_style_1
)
graph_menos |= (
(hour @ mid_times)
+ delete_space
+ pynutil.insert(" menos ")
+ minute
+ delete_space
+ insert_space
+ (suffix @ mid_phrases)
+ graph_style_1
)
graph_menos += optional_zone
graph_para = minute + pynutil.insert(" para las ") + delete_space + hour + graph_style_2
graph_para |= (
minute
+ pynutil.insert(" para las ")
+ delete_space
+ (hour @ morning_times)
+ delete_space
+ insert_space
+ (suffix @ morning_phrases)
+ graph_style_2
)
graph_para |= (
minute
+ pynutil.insert(" para las ")
+ delete_space
+ (hour @ afternoon_times)
+ delete_space
+ insert_space
+ (suffix @ afternoon_phrases)
+ graph_style_2
)
graph_para |= (
minute
+ pynutil.insert(" para las ")
+ delete_space
+ (hour @ evening_times)
+ delete_space
+ insert_space
+ (suffix @ evening_phrases)
+ graph_style_2
)
graph_para |= (
minute
+ pynutil.insert(" para las ")
+ delete_space
+ (hour @ mid_times)
+ delete_space
+ insert_space
+ (suffix @ mid_phrases)
+ graph_style_2
)
graph_para += optional_zone
graph_para @= pynini.cdrewrite(
pynini.cross(" las ", " la "), "para", "una", DAMO_SIGMA
) # Need agreement with one
graph |= graph_menos | graph_para
delete_tokens = self.delete_tokens(graph + delete_preserve_order)
self.fst = delete_tokens.optimize()
@@ -0,0 +1,62 @@
from fun_text_processing.text_normalization.en.graph_utils import GraphFst
from fun_text_processing.text_normalization.en.verbalizers.whitelist import WhiteListFst
from fun_text_processing.text_normalization.es.verbalizers.cardinal import CardinalFst
from fun_text_processing.text_normalization.es.verbalizers.date import DateFst
from fun_text_processing.text_normalization.es.verbalizers.decimals import DecimalFst
from fun_text_processing.text_normalization.es.verbalizers.electronic import ElectronicFst
from fun_text_processing.text_normalization.es.verbalizers.fraction import FractionFst
from fun_text_processing.text_normalization.es.verbalizers.measure import MeasureFst
from fun_text_processing.text_normalization.es.verbalizers.money import MoneyFst
from fun_text_processing.text_normalization.es.verbalizers.ordinal import OrdinalFst
from fun_text_processing.text_normalization.es.verbalizers.telephone import TelephoneFst
from fun_text_processing.text_normalization.es.verbalizers.time import TimeFst
class VerbalizeFst(GraphFst):
"""
Composes other verbalizer grammars.
For deployment, this grammar will be compiled and exported to OpenFst Finate State Archiv (FAR) File.
More details to deployment at NeMo/tools/text_processing_deployment.
Args:
deterministic: if True will provide a single transduction option,
for False multiple options (used for audio-based normalization)
"""
def __init__(self, deterministic: bool = True):
super().__init__(name="verbalize", kind="verbalize", deterministic=deterministic)
cardinal = CardinalFst(deterministic=deterministic)
cardinal_graph = cardinal.fst
ordinal = OrdinalFst(deterministic=deterministic)
ordinal_graph = ordinal.fst
decimal = DecimalFst(deterministic=deterministic)
decimal_graph = decimal.fst
fraction = FractionFst(deterministic=deterministic)
fraction_graph = fraction.fst
date = DateFst(deterministic=deterministic)
date_graph = date.fst
measure = MeasureFst(
cardinal=cardinal, decimal=decimal, fraction=fraction, deterministic=deterministic
)
measure_graph = measure.fst
electronic = ElectronicFst(deterministic=deterministic)
electronic_graph = electronic.fst
whitelist_graph = WhiteListFst(deterministic=deterministic).fst
money_graph = MoneyFst(decimal=decimal, deterministic=deterministic).fst
telephone_graph = TelephoneFst(deterministic=deterministic).fst
time_graph = TimeFst(deterministic=deterministic).fst
graph = (
cardinal_graph
| measure_graph
| decimal_graph
| ordinal_graph
| date_graph
| electronic_graph
| money_graph
| fraction_graph
| whitelist_graph
| telephone_graph
| time_graph
)
self.fst = graph
@@ -0,0 +1,61 @@
import os
import pynini
from fun_text_processing.text_normalization.en.graph_utils import (
GraphFst,
delete_extra_space,
delete_space,
generator_main,
)
from fun_text_processing.text_normalization.en.verbalizers.word import WordFst
from fun_text_processing.text_normalization.es.verbalizers.verbalize import VerbalizeFst
from pynini.lib import pynutil
import logging
class VerbalizeFinalFst(GraphFst):
"""
Finite state transducer that verbalizes an entire sentence
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"es_tn_{deterministic}_deterministic_verbalizer.far"
)
if not overwrite_cache and far_file and os.path.exists(far_file):
self.fst = pynini.Far(far_file, mode="r")["verbalize"]
logging.info(f"VerbalizeFinalFst graph was restored from {far_file}.")
else:
verbalize = VerbalizeFst(deterministic=deterministic).fst
word = WordFst(deterministic=deterministic).fst
types = verbalize | word
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})
logging.info(f"VerbalizeFinalFst grammars are saved to {far_file}.")