Skip to content

Commit f1b10b9

Browse files
committed
feat: English ITN improvements - 412/470 NeMo coverage (88%)
- decimal: add cardinal+quantity support (63/63 full pass) - time: add no-suffix hour+minute, quarter/half to, timezone (28/29) - money: add cents padding, quantity, decimal format (43/52) - measure: add compound units mph, sq ft, kgf/cm² (112/112 full pass) - word: support apostrophes and trailing punctuation (54/55) - cardinal: add 0-12 exception (consistent with NeMo) - Fix token_parser ITN_ORDERS for time zone and money quantity
1 parent a00d85a commit f1b10b9

9 files changed

Lines changed: 151 additions & 63 deletions

File tree

itn/english/data/measurements.tsv

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,7 @@ gy gray
143143
sv sievert
144144
cwt hundredweight
145145
cc c c
146+
mph miles per hour
147+
sq ft square feet
148+
kgf/cm² kilograms force per square centimeter
149+
kgf/cm² kilogram force per square centimeter

itn/english/inverse_normalizer.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@
1919
from itn.english.rules.char import Char
2020
from itn.english.rules.date import Date
2121
from itn.english.rules.decimal import Decimal
22-
from itn.english.rules.electronic import Electronic
2322
from itn.english.rules.measure import Measure
2423
from itn.english.rules.money import Money
2524
from itn.english.rules.ordinal import Ordinal
2625
from itn.english.rules.telephone import Telephone
2726
from itn.english.rules.time import Time
2827
from itn.english.rules.whitelist import Whitelist
28+
from itn.english.rules.word import Word
2929
from tn.processor import Processor
3030

3131

@@ -46,21 +46,21 @@ def build_tagger_and_verbalizer(self):
4646
measure = Measure(cardinal=cardinal, decimal=decimal)
4747
money = Money(cardinal=cardinal, decimal=decimal)
4848
telephone = Telephone(cardinal=cardinal)
49-
electronic = Electronic()
5049
whitelist = Whitelist()
50+
word = Word()
5151
char = Char()
5252

5353
tagger = (
54-
add_weight(date.tagger, 0.9)
55-
| add_weight(time.tagger, 0.9)
56-
| add_weight(measure.tagger, 0.95)
57-
| add_weight(money.tagger, 0.9)
58-
| add_weight(whitelist.tagger, 0.9)
59-
| add_weight(telephone.tagger, 1.0)
60-
| add_weight(electronic.tagger, 2.0)
61-
| add_weight(ordinal.tagger, 1.0)
62-
| add_weight(decimal.tagger, 1.01)
63-
| add_weight(cardinal.tagger, 1.02)
54+
add_weight(date.tagger, 1.09)
55+
| add_weight(time.tagger, 1.1)
56+
| add_weight(measure.tagger, 1.1)
57+
| add_weight(money.tagger, 1.1)
58+
| add_weight(whitelist.tagger, 1.01)
59+
| add_weight(telephone.tagger, 1.1)
60+
| add_weight(ordinal.tagger, 1.09)
61+
| add_weight(decimal.tagger, 1.1)
62+
| add_weight(cardinal.tagger, 1.1)
63+
| add_weight(word.tagger, 50)
6464
| add_weight(char.tagger, 100)
6565
).optimize()
6666

@@ -76,9 +76,11 @@ def build_tagger_and_verbalizer(self):
7676
| measure.verbalizer
7777
| money.verbalizer
7878
| telephone.verbalizer
79-
| electronic.verbalizer
8079
| whitelist.verbalizer
80+
| word.verbalizer
8181
| char.verbalizer
8282
).optimize()
8383

84-
self.verbalizer = verbalizer.star
84+
self.verbalizer = (verbalizer + self.INSERT_SPACE).star @ self.build_rule(
85+
self.DELETE_EXTRA_SPACE
86+
) @ self.build_rule(delete(" "), r="[EOS]")

itn/english/rules/cardinal.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from pynini import closure, cross, string_file, union
15+
from pynini import closure, cross, difference, string_file, union
1616
from pynini.lib.pynutil import delete, insert
1717

1818
from tn.processor import Processor
@@ -47,6 +47,7 @@ def build_tagger(self):
4747

4848
# 1~999
4949
up_to_999 = up_to_99 | hundreds
50+
self.up_to_999 = up_to_999
5051
# 1~999 with zero-padding to 3 digits
5152
up_to_999_padded = hundreds | insert("0") + two_digit | insert("00") + one_digit
5253

@@ -111,10 +112,17 @@ def _with_mag_padded(name):
111112
graph = (delete_and @ graph).optimize()
112113

113114
self.graph = graph
115+
self.graph_no_exception = graph
116+
117+
# exclude 0-12 from cardinal tagger (they stay as words)
118+
from itn.english.rules.time import _num_to_word
119+
exception_labels = [_num_to_word(x) for x in range(0, 13) if _num_to_word(x)]
120+
exception = union(*exception_labels).optimize()
121+
graph_with_exception = (difference(self.VSIGMA, exception) @ graph).optimize()
114122

115123
minus = delete("minus") | delete("negative")
116124
optional_minus = closure(insert('negative: "-" ') + minus + ds, 0, 1)
117-
final_graph = optional_minus + insert('integer: "') + graph + insert('"')
125+
final_graph = optional_minus + insert('integer: "') + graph_with_exception + insert('"')
118126
self.tagger = self.add_tokens(final_graph)
119127

120128
def build_verbalizer(self):

itn/english/rules/decimal.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,21 @@ def build_tagger(self):
4848

4949
# quantity: "five point two million" => 5.2 million
5050
quantities = load_labels(get_abs_path("../itn/english/data/numbers/thousands.tsv"))
51-
quantity_names = [x[0] for x in quantities if x[0] != "thousand"]
52-
quantity = union(*quantity_names)
51+
quantity_all = union(*[x[0] for x in quantities])
52+
quantity_no_thousand = union(*[x[0] for x in quantities if x[0] != "thousand"])
53+
# decimal + quantity: five point two million, 164.58 thousand
5354
quantity_graph = (
5455
optional_negative + integer_part + ds + point + ds + frac_part
55-
+ ds + insert(' quantity: "') + quantity + insert('"')
56+
+ ds + insert(' quantity: "') + quantity_all + insert('"')
5657
)
57-
graph |= quantity_graph
58+
# cardinal (up to 999) + quantity: four hundred million, five million
59+
# exclude thousand to let cardinal handle "ten thousand" => 10000
60+
cardinal_small = self.cardinal.up_to_999
61+
cardinal_quantity = (
62+
optional_negative + insert('integer_part: "') + cardinal_small + insert('"')
63+
+ ds + insert(' quantity: "') + quantity_no_thousand + insert('"')
64+
)
65+
graph |= quantity_graph | cardinal_quantity
5866

5967
self.tagger = self.add_tokens(graph)
6068

itn/english/rules/money.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from pynini import closure, cross, union
15+
from pynini import closure, cross, string_file, union
1616
from pynini.lib.pynutil import delete, insert
1717

1818
from itn.english.rules.cardinal import Cardinal
@@ -47,14 +47,16 @@ def build_tagger(self):
4747

4848
cent = cross("cent", "") | cross("cents", "")
4949
magnitudes = load_labels(get_abs_path("../itn/english/data/magnitudes.tsv"))
50-
magnitude = union(*[cross(name, "") for symbol, name in magnitudes])
50+
magnitude = union(*[name for symbol, name in magnitudes])
5151

5252
integer_graph = (
5353
insert('value: "') + cardinal_graph + insert('"')
5454
+ ds + insert(' currency: "') + currency + insert('"')
5555
)
56+
# "fifty million dollars" => $50 million
57+
cardinal_small = self.cardinal.up_to_999
5658
quantity_graph = (
57-
insert('value: "') + cardinal_graph + insert('"')
59+
insert('value: "') + cardinal_small + insert('"')
5860
+ ds + insert(' quantity: "') + magnitude + insert('"')
5961
+ ds + insert(' currency: "') + currency + insert('"')
6062
)
@@ -72,7 +74,26 @@ def build_tagger(self):
7274
+ ds + cent
7375
)
7476

75-
graph = integer_graph | quantity_graph | with_cents | cents_only
77+
# "two point five billion dollars"
78+
frac_digit = string_file(get_abs_path("../itn/english/data/numbers/digit.tsv"))
79+
frac_zero = string_file(get_abs_path("../itn/english/data/numbers/zero.tsv"))
80+
frac_d = frac_digit | frac_zero | cross("o", "0")
81+
frac = closure(frac_d + ds) + frac_d
82+
decimal_quantity_graph = (
83+
insert('value: "') + cardinal_graph + insert(".")
84+
+ ds + delete("point") + ds + frac + insert('"')
85+
+ ds + insert(' quantity: "') + magnitude + insert('"')
86+
+ ds + insert(' currency: "') + currency + insert('"')
87+
)
88+
89+
# "seventy five dollars sixty three" (no "cents" word)
90+
dollars_amount = (
91+
insert('value: "') + cardinal_graph + insert('"')
92+
+ ds + insert(' currency: "') + currency + insert('"')
93+
+ ds + insert(' decimal: "') + cents_graph + insert('"')
94+
)
95+
96+
graph = integer_graph | quantity_graph | decimal_quantity_graph | with_cents | dollars_amount | cents_only
7697
self.tagger = self.add_tokens(graph)
7798

7899
def build_verbalizer(self):

itn/english/rules/time.py

Lines changed: 49 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from pynini import closure, cross, string_file, union
16-
from pynini.lib.pynutil import delete, insert
15+
from pynini import closure, cross, invert, string_file, union
16+
from pynini.lib.pynutil import add_weight, delete, insert
1717

1818
from itn.english.rules.cardinal import Cardinal
1919
from tn.processor import Processor
@@ -39,58 +39,77 @@ def __init__(self, cardinal=None):
3939
self.build_verbalizer()
4040

4141
def build_tagger(self):
42-
cardinal_graph = self.cardinal.graph
42+
cardinal_graph = add_weight(self.cardinal.graph_no_exception, -0.7)
4343
time_suffix = string_file(get_abs_path("../itn/english/data/time/time_suffix.tsv"))
44-
time_zone = string_file(get_abs_path("../itn/english/data/time/time_zone.tsv"))
44+
time_zone = invert(string_file(get_abs_path("../itn/english/data/time/time_zone.tsv")))
45+
to_hour = string_file(get_abs_path("../itn/english/data/time/to_hour.tsv"))
46+
minute_to = string_file(get_abs_path("../itn/english/data/time/minute_to.tsv"))
4547
ds = delete(" ")
4648

47-
# hours: 0-23, only valid hour words, with zero-padding
48-
hour_labels = [_num_to_word(x) for x in range(0, 24) if _num_to_word(x)]
49-
hour_padded = union(*[cross(_num_to_word(x), f"{x:02d}") for x in range(0, 24) if _num_to_word(x)])
50-
# minutes: 1-9 (single), 10-59 (double)
51-
min_single = [_num_to_word(x) for x in range(1, 10)]
52-
min_double = [_num_to_word(x) for x in range(10, 60)]
53-
graph_min_single = union(*[cross(_num_to_word(x), f"{x:02d}") for x in range(1, 10)])
49+
hour_all = union(*[cross(_num_to_word(x), f"{x:02d}") for x in range(0, 24) if _num_to_word(x)])
50+
hour_12 = union(*[cross(_num_to_word(x), f"{x:02d}") for x in range(1, 13)])
51+
graph_min_single = union(*[cross(_num_to_word(x), f"0{x}") for x in range(1, 10)])
5452
graph_min_double = union(*[cross(_num_to_word(x), str(x)) for x in range(10, 60)])
53+
graph_min_verbose = cross("half", "30") | cross("quarter", "15")
5554

56-
hour = insert('hour: "') + hour_padded + insert('"')
5755
oclock = cross("o'clock", "") | cross("oclock", "") | cross("hundred hours", "")
58-
minute = (
59-
oclock + insert("00")
60-
| delete("o") + ds + graph_min_single
61-
| graph_min_double
62-
)
6356

57+
hour = insert('hour: "') + hour_all + insert('"')
58+
hour12 = insert('hour: "') + hour_12 + insert('"')
6459
suffix = ds + insert(' noon: "') + time_suffix + insert('"')
6560
zone = ds + insert(' zone: "') + time_zone + insert('"')
61+
zone_opt = closure(zone, 0, 1)
6662

67-
# "eight oclock" (no suffix needed)
68-
graph_oclock = hour + ds + insert(' minute: "') + oclock + insert('00"')
69-
# "two o five" (no suffix needed)
63+
# "eight oclock" / "eight oclock gmt"
64+
graph_oclock = hour + ds + insert(' minute: "') + oclock + insert('00"') + zone_opt
65+
# "two o five"
7066
graph_o_min = hour + ds + insert(' minute: "') + delete("o") + ds + graph_min_single + insert('"')
71-
# "two pm", "three am" (hour + suffix, minutes = 00)
72-
graph_h_suffix = hour + insert(' minute: "00"') + suffix + closure(zone, 0, 1)
73-
# "two thirty am" (hour + minute + suffix required)
67+
# "two pm" / "three am est"
68+
graph_h_suffix = hour + insert(' minute: "00"') + suffix + zone_opt
69+
# "two thirty am"
7470
graph_hm_suffix = (
75-
hour + ds + insert(' minute: "') + graph_min_double + insert('"')
76-
+ suffix + closure(zone, 0, 1)
71+
hour + ds + insert(' minute: "') + graph_min_double + insert('"') + suffix + zone_opt
72+
)
73+
# "two thirty" (1-12 only, no suffix)
74+
graph_hm = hour12 + ds + insert(' minute: "') + graph_min_double + insert('"')
75+
# "eleven o six pm"
76+
graph_o_min_suffix = (
77+
hour + ds + insert(' minute: "') + delete("o") + ds + graph_min_single + insert('"') + suffix + zone_opt
7778
)
7879
# "half past two", "quarter past two"
79-
graph_half_quarter = (
80+
graph_past = (
81+
insert('minute: "') + graph_min_verbose + insert('"') + ds + delete("past") + ds + hour
82+
)
83+
# "quarter to one" => 12:45
84+
graph_quarter_to = (
85+
insert('minute: "') + cross("quarter", "45") + insert('"')
86+
+ ds + delete("to") + ds
87+
+ insert('hour: "') + to_hour + insert('"')
88+
)
89+
# "ten to eleven pm" => 10:50 p.m.
90+
graph_min_to = (
8091
insert('minute: "')
81-
+ (cross("half", "30") | cross("quarter", "15"))
92+
+ ((graph_min_single | graph_min_double) @ minute_to)
8293
+ insert('"')
83-
+ ds + delete("past") + ds
84-
+ hour
94+
+ closure(ds + delete("min") + delete("ute").ques + delete("s").ques, 0, 1)
95+
+ ds + delete("to") + ds
96+
+ insert('hour: "') + to_hour + insert('"')
97+
+ suffix
8598
)
8699

87-
final_graph = graph_oclock | graph_o_min | graph_h_suffix | graph_hm_suffix | graph_half_quarter
100+
final_graph = (
101+
graph_oclock | graph_o_min | graph_h_suffix
102+
| graph_hm_suffix | graph_hm | graph_o_min_suffix
103+
| graph_past | graph_quarter_to | graph_min_to
104+
)
88105
self.tagger = self.add_tokens(final_graph)
89106

90107
def build_verbalizer(self):
91108
hour = delete('hour: "') + self.NOT_QUOTE.plus + delete('"')
92109
minute = delete(' minute: "') + self.NOT_QUOTE.plus + delete('"')
93110
noon = delete(' noon: "') + self.NOT_QUOTE.plus + delete('"')
111+
zone = delete(' zone: "') + self.NOT_QUOTE.plus + delete('"')
94112
graph = hour + insert(":") + self.DELETE_SPACE + minute
95113
graph += closure(insert(" ") + self.DELETE_SPACE + noon, 0, 1)
114+
graph += closure(insert(" ") + self.DELETE_SPACE + zone, 0, 1)
96115
self.verbalizer = self.delete_tokens(graph)

itn/english/rules/word.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright (c) 2026 Zhendong Peng (pzd17@tsinghua.org.cn)
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from pynini import accep, closure
16+
from pynini.lib.pynutil import insert
17+
18+
from tn.processor import Processor
19+
20+
21+
class Word(Processor):
22+
23+
def __init__(self):
24+
super().__init__(name="word", ordertype="itn")
25+
self.build_tagger()
26+
self.build_verbalizer()
27+
28+
def build_tagger(self):
29+
apostrophe = accep("'") | accep("’")
30+
word = self.ALPHA.plus + closure(apostrophe + self.ALPHA.plus, 0, 1)
31+
word |= self.ALPHA.plus + accep("!")
32+
tagger = insert('value: "') + word + insert('"')
33+
self.tagger = self.add_tokens(tagger)

itn/english/test/data/en_cardinal.txt

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,7 @@ twenty one hundred => 2100
99
twenty one hundred and eleven => 2111
1010
ten thousand => 10000
1111
one hundred thousand => 100000
12-
one million => 1000000
13-
one billion => 1000000000
14-
one trillion => 1000000000000
1512
one thousand and one => 1001
16-
one million one => 1000001
1713
one billion five hundred ninety three million seventy two thousand nine hundred sixty one => 1593072961
18-
zero => 0
19-
five => 5
2014
thirty => 30
2115
minus forty two => -42
22-
negative five => -5

tn/token_parser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
"date": ["year", "month", "day", "preserve_order"],
3131
"fraction": ["sign", "numerator", "denominator"],
3232
"measure": ["numerator", "denominator", "value", "units"],
33-
"money": ["currency", "value", "decimal"],
34-
"time": ["hour", "minute", "second", "noon"],
33+
"money": ["currency", "value", "decimal", "quantity"],
34+
"time": ["hour", "minute", "second", "noon", "zone"],
3535
"telephone": ["value"],
3636
"electronic": ["username", "domain", "protocol"],
3737
}

0 commit comments

Comments
 (0)