Skip to content

Commit 60425ed

Browse files
committed
feat: English ITN enhancements from NeMo reference
Time: - Support numeric minutes with "past" (e.g. "ten past four" -> 04:10) - Add "till" as alias for "to" (e.g. "quarter till two" -> 01:45) - Add "o' clock" and "o clock" variants - Zero-pad hours in to_hour.tsv Telephone: - Add "triple X" support (e.g. "triple five" -> 555) Date: - Add BCE, CE and long-form year suffixes - Add H1/H2 financial half-year periods - Add century ("nineteen hundreds" -> 1900s) and millennium ranges - Add "X hundred" year form (e.g. "nineteen hundred" -> 1900) Measure: - Replace cdrewrite with finite string_map for pluralization (fixes OOM) - Add -ies/-es rules and irregular plurals (feet, inches, ounces) - Fix "per" unit priority to prefer direct TSV matches (e.g. mph)
1 parent 0ca3110 commit 60425ed

9 files changed

Lines changed: 108 additions & 35 deletions

File tree

itn/english/data/time/to_hour.tsv

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
one 12
2-
two 1
3-
three 2
4-
four 3
5-
five 4
6-
six 5
7-
seven 6
8-
eight 7
9-
nine 8
10-
ten 9
2+
two 01
3+
three 02
4+
four 03
5+
five 04
6+
six 05
7+
seven 06
8+
eight 07
9+
nine 08
10+
ten 09
1111
eleven 10
1212
twelve 11

itn/english/rules/date.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ def build_tagger(self):
7272

7373
# Year as hundreds: "nineteen oh five" => 1905
7474
year_hundreds = (teen | two_digit) + ds + oh_digit
75+
# Year as "X hundred": "nineteen hundred" => 1900
76+
year_xx_hundred = (teen | two_digit) + ds + delete("hundred") + insert("00")
7577

76-
year_graph = year_two_parts | year_thousands | year_hundreds
78+
year_graph = year_two_parts | year_thousands | year_hundreds | year_xx_hundred
7779

7880
# Delete optional "and" within year
7981
delete_and = self.build_rule(delete("and "), " ", self.ALPHA)
@@ -138,12 +140,38 @@ def build_tagger(self):
138140
+ insert(' year: "') + year_graph + insert('"') + po
139141
)
140142

141-
# BC/AD suffix
142-
bc_ad = ds + (cross("b c", "BC") | cross("a d", "AD"))
143+
# BC/AD/BCE/CE suffix
144+
bc_ad = ds + (
145+
cross("b c e", "BCE") | cross("before common era", "BCE")
146+
| cross("b c", "BC")
147+
| cross("c e", "CE") | cross("common era", "CE")
148+
| cross("a d", "AD")
149+
)
143150
year_graph_with_3digit = year_graph | year_three_digit
144151
graph_y_bc = insert('year: "') + year_graph_with_3digit + bc_ad + insert('"') + po
145152

146-
final_graph = graph_mdy | graph_md | graph_my | graph_dmy | graph_dm | graph_y | graph_decade | graph_quarter | graph_y_bc
153+
# Half: "first half of twenty twenty two" => H1 2022
154+
half_num = cross("first", "1") | cross("second", "2")
155+
graph_half = (
156+
insert('day: "H') + half_num + insert('"')
157+
+ ds + delete("half") + ds + delete("of") + ds
158+
+ insert(' year: "') + year_graph + insert('"') + po
159+
)
160+
161+
# Century: "nineteen hundreds" => 1900s
162+
graph_century = (
163+
insert('year: "') + (teen | two_digit) + ds + cross("hundreds", "00s") + insert('"') + po
164+
)
165+
# Millennium: "two thousands" => 2000s
166+
graph_millennium = (
167+
insert('year: "') + cross("two", "2") + ds + cross("thousands", "000s") + insert('"') + po
168+
)
169+
170+
final_graph = (
171+
graph_mdy | graph_md | graph_my | graph_dmy | graph_dm | graph_y
172+
| graph_decade | graph_quarter | graph_half | graph_y_bc
173+
| graph_century | graph_millennium
174+
)
147175
self.tagger = self.add_tokens(final_graph)
148176

149177
def build_verbalizer(self):

itn/english/rules/measure.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import pynini
1616
from pynini import closure, cross, invert, string_file
17-
from pynini.lib.pynutil import delete, insert
17+
from pynini.lib.pynutil import add_weight, delete, insert
1818

1919
from itn.english.rules.cardinal import Cardinal
2020
from itn.english.rules.decimal import Decimal
@@ -35,24 +35,40 @@ def build_tagger(self):
3535
ds = delete(" ")
3636

3737
# Load measurements: symbol\tname, invert to get name -> symbol
38-
units_graph = invert(
39-
string_file(get_abs_path("../itn/english/data/measurements.tsv"))
40-
)
38+
tsv_path = get_abs_path("../itn/english/data/measurements.tsv")
39+
units_graph = invert(string_file(tsv_path))
40+
41+
# Handle plurals: generate plural->symbol mappings from the singular TSV entries
42+
# Uses finite string_map instead of cdrewrite to avoid slow runtime compose
43+
singular_names = {}
44+
with open(tsv_path, encoding="utf-8") as f:
45+
for line in f:
46+
parts = line.strip().split("\t", 1)
47+
if len(parts) == 2:
48+
singular_names.setdefault(parts[1], parts[0])
49+
50+
plural_pairs = []
51+
irregular_plurals = {
52+
"foot": "feet", "inch": "inches",
53+
"ounce": "ounces",
54+
}
55+
for name, symbol in singular_names.items():
56+
if name in irregular_plurals:
57+
plural_pairs.append((irregular_plurals[name], symbol))
58+
elif name.endswith(("s", "z", "sh", "ch", "x")):
59+
plural_pairs.append((name + "es", symbol))
60+
elif name.endswith("y") and len(name) > 1 and name[-2] not in "aeiou":
61+
plural_pairs.append((name[:-1] + "ies", symbol))
62+
else:
63+
plural_pairs.append((name + "s", symbol))
4164

42-
# Handle plurals: strip trailing "s" to match singular form
43-
# e.g. "meters" -> "meter" -> "m", "kilograms" -> "kilogram" -> "kg"
44-
depluralize = pynini.cdrewrite(
45-
cross("s", ""), "", "[EOS]", self.VSIGMA
46-
)
47-
# Handle irregular plurals: "feet" -> "foot"
48-
irregular = pynini.string_map([("feet", "foot")])
4965
unit_singular = units_graph
50-
unit_plural = (depluralize | irregular) @ units_graph
66+
unit_plural = pynini.string_map(plural_pairs)
5167

5268
unit = unit_singular | unit_plural
5369

5470
# Handle "per" units: "per hour" -> "/h"
55-
per_unit = insert("/") + delete("per") + ds + unit_singular
71+
per_unit = add_weight(insert("/") + delete("per") + ds + unit_singular, 1)
5672
full_unit = unit + closure(ds + per_unit, 0, 1) | per_unit
5773

5874
# Cardinal value

itn/english/rules/telephone.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,17 @@ def build_tagger(self):
4040
("five","5"),("six","6"),("seven","7"),("eight","8"),
4141
("nine","9"),("zero","0"),("oh","0"),("o","0")]])
4242

43+
# "triple X" => XXX
44+
triple = union(*[cross(f"triple {w}", f"{d}{d}{d}")
45+
for w, d in [("one","1"),("two","2"),("three","3"),("four","4"),
46+
("five","5"),("six","6"),("seven","7"),("eight","8"),
47+
("nine","9"),("zero","0"),("oh","0"),("o","0")]])
48+
4349
# two-digit cardinal: twenty three => 23 (uses graph_two_digit for proper space handling)
4450
two_digit = self.cardinal.graph_two_digit
4551

46-
# a token is 1 or 2 digits
47-
token = single | double | add_weight(two_digit, 0.002)
52+
# a token is 1, 2, or 3 digits
53+
token = single | double | triple | add_weight(two_digit, 0.002)
4854

4955
# sequence of tokens separated by spaces
5056
seq = token + closure(ds + token)
@@ -75,6 +81,7 @@ def build_tagger(self):
7581
ip_token = (
7682
single + closure(ds + single, 0, 2)
7783
| double
84+
| triple
7885
| add_weight(two_digit, 0.002)
7986
| single + ds + two_digit
8087
| two_digit + ds + single

itn/english/rules/time.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from pynini import closure, cross, invert, string_file, union
1616
from pynini.lib.pynutil import add_weight, delete, insert
1717

18+
TO_OR_TILL = union("to", "till")
19+
1820
from itn.english.rules.cardinal import Cardinal
1921
from tn.processor import Processor
2022
from tn.utils import get_abs_path
@@ -56,7 +58,7 @@ def build_tagger(self):
5658
min_single_raw = union(*[cross(_num_to_word(x), str(x)) for x in range(1, 10)])
5759
min_double_raw = graph_min_double # already no padding
5860

59-
oclock = cross("o'clock", "") | cross("oclock", "") | cross("hundred hours", "")
61+
oclock = cross("o'clock", "") | cross("o' clock", "") | cross("o clock", "") | cross("oclock", "") | cross("hundred hours", "")
6062

6163
hour = insert('hour: "') + hour_all + insert('"')
6264
hour12 = insert('hour: "') + hour_12 + insert('"')
@@ -80,23 +82,25 @@ def build_tagger(self):
8082
graph_o_min_suffix = (
8183
hour + ds + insert(' minute: "') + delete("o") + ds + graph_min_single + insert('"') + suffix + zone_opt
8284
)
83-
# "half past two", "quarter past two"
85+
# "half past two", "quarter past two", "ten past four"
8486
graph_past = (
85-
insert('minute: "') + graph_min_verbose + insert('"') + ds + delete("past") + ds + hour
87+
insert('minute: "')
88+
+ (graph_min_single | graph_min_double | graph_min_verbose)
89+
+ insert('"') + ds + delete("past") + ds + hour
8690
)
87-
# "quarter to one" => 12:45
91+
# "quarter to one" / "quarter till one" => 12:45
8892
graph_quarter_to = (
8993
insert('minute: "') + cross("quarter", "45") + insert('"')
90-
+ ds + delete("to") + ds
94+
+ ds + delete(TO_OR_TILL) + ds
9195
+ insert('hour: "') + to_hour + insert('"')
9296
)
93-
# "ten to eleven pm" => 10:50 p.m.
97+
# "ten to eleven pm" / "ten till eleven pm" => 10:50 p.m.
9498
graph_min_to = (
9599
insert('minute: "')
96100
+ ((min_single_raw | min_double_raw) @ minute_to)
97101
+ insert('"')
98102
+ closure(ds + delete("min") + delete("ute").ques + delete("s").ques, 0, 1)
99-
+ ds + delete("to") + ds
103+
+ ds + delete(TO_OR_TILL) + ds
100104
+ insert('hour: "') + to_hour + insert('"')
101105
+ suffix
102106
)

itn/english/test/data/en_date.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,11 @@ nineteen seventy five => 1975
3434
eleven fifty five => 1155
3535
second quarter of twenty twenty two => Q2 2022
3636
seven fifty b c => 750BC
37+
seven fifty b c e => 750BCE
38+
nineteen hundred c e => 1900CE
39+
nineteen hundred a d => 1900AD
40+
first half of twenty twenty two => H1 2022
41+
second half of twenty twenty => H2 2020
42+
nineteen hundreds => 1900s
43+
twenty hundreds => 2000s
44+
two thousands => 2000s

itn/english/test/data/en_measure.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,5 @@ eight point five megawatts => 8.5 mW
110110
eight point five meters => 8.5 m
111111
eight point five two percent => 8.52 %
112112
eight point four four percent => 8.44 %
113+
five ounces => 5 oz
114+
ten kilo calories => 10 kcal

itn/english/test/data/en_telephone.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ r t x forty fifty t i => RTX 4050ti
2121
four three two double seven three two one four three two one four three double zero five => 432 7732 143214 3005
2222
a thirty six => a 36
2323
a ten eighty p display => a 1080p display
24+
triple five one two three one two three four => 555-123-1234

itn/english/test/data/en_time.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,10 @@ quarter to one => 12:45
2727
quarter to twelve => 11:45
2828
set alarm at ten to eleven pm => set alarm at 10:50 p.m.
2929
one min to one am => 12:59 a.m.
30+
ten past four => 04:10
31+
twenty five past three => 03:25
32+
five past twelve => 12:05
33+
quarter till two => 01:45
34+
ten till four pm => 03:50 p.m.
35+
three o' clock => 03:00
36+
three o clock => 03:00

0 commit comments

Comments
 (0)