Skip to content

Commit e242041

Browse files
committed
feat: 469/470 (99.8%) NeMo coverage
- electronic: exclude "dot" as email username first token - money: reject singular "one" with plural currency ("one dollars") - telephone: add credit card 4-6-4/4-6-5 formats with optional country code - telephone: exclude "a" as serial first char to avoid "a thirty six" -> "a36" - punctuation: add Punctuation class, split punct from words ("twenty!" -> "20 !")
1 parent 6a6b19f commit e242041

9 files changed

Lines changed: 90 additions & 28 deletions

File tree

itn/english/inverse_normalizer.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from itn.english.rules.measure import Measure
2525
from itn.english.rules.money import Money
2626
from itn.english.rules.ordinal import Ordinal
27+
from itn.english.rules.punctuation import Punctuation
2728
from itn.english.rules.telephone import Telephone
2829
from itn.english.rules.time import Time
2930
from itn.english.rules.whitelist import Whitelist
@@ -52,8 +53,9 @@ def build_tagger_and_verbalizer(self):
5253
whitelist = Whitelist()
5354
word = Word()
5455
char = Char()
56+
punctuation = Punctuation()
5557

56-
tagger = (
58+
classify = (
5759
add_weight(date.tagger, 1.09)
5860
| add_weight(time.tagger, 1.1)
5961
| add_weight(measure.tagger, 1.1)
@@ -68,7 +70,8 @@ def build_tagger_and_verbalizer(self):
6870
| add_weight(char.tagger, 100)
6971
).optimize()
7072

71-
token = tagger
73+
punct = add_weight(punctuation.tagger, 1.1)
74+
token = closure(punct + delete(" ").ques) + classify + closure(delete(" ").ques + punct)
7275
graph = token + closure(self.DELETE_EXTRA_SPACE + token)
7376
self.tagger = delete(" ").star + graph + delete(" ").star
7477

@@ -85,6 +88,7 @@ def build_tagger_and_verbalizer(self):
8588
| whitelist.verbalizer
8689
| word.verbalizer
8790
| char.verbalizer
91+
| punctuation.verbalizer
8892
).optimize()
8993

9094
self.verbalizer = (verbalizer + self.INSERT_SPACE).star @ self.build_rule(

itn/english/rules/electronic.py

Lines changed: 3 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, invert, string_file
15+
from pynini import accep, closure, cross, difference, invert, string_file
1616
from pynini.lib.pynutil import add_weight, delete, insert
1717

1818
from tn.processor import Processor
@@ -35,7 +35,8 @@ def build_tagger(self):
3535
char = self.ALPHA | digit | zero
3636
word = add_weight(closure(self.ALPHA, 2), 0.1)
3737
token = char | symbols | word
38-
component = token + closure(ds + token)
38+
first_token = char | difference(word, accep("dot"))
39+
component = first_token + closure(ds + token)
3940

4041
dot = cross("dot", ".")
4142
domain = component + (ds + dot + ds + component).plus

itn/english/rules/money.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,15 @@ def build_tagger(self):
3737
ds = delete(" ")
3838

3939
currency_labels = load_labels(get_abs_path("../itn/english/data/currency.tsv"))
40-
currency_pairs = []
41-
for symbol, name in currency_labels:
42-
currency_pairs.append((name, symbol))
40+
singular_pairs = [(name, symbol) for symbol, name in currency_labels]
41+
plural_pairs = []
42+
for name, symbol in singular_pairs:
4343
if name.endswith("s"):
44-
currency_pairs.append((name + "es", symbol))
44+
plural_pairs.append((name + "es", symbol))
4545
else:
46-
currency_pairs.append((name + "s", symbol))
47-
currency = union(*[cross(name, symbol) for name, symbol in currency_pairs]).optimize()
46+
plural_pairs.append((name + "s", symbol))
47+
currency_singular = union(*[cross(name, symbol) for name, symbol in singular_pairs]).optimize()
48+
currency_plural = union(*[cross(name, symbol) for name, symbol in singular_pairs + plural_pairs]).optimize()
4849

4950
cent = cross("cent", "") | cross("cents", "")
5051
magnitudes = load_labels(get_abs_path("../itn/english/data/magnitudes.tsv"))
@@ -57,15 +58,23 @@ def build_tagger(self):
5758
compose(cardinal_graph, self.DIGIT ** 3),
5859
)
5960
cardinal_with_hundred = cardinal_graph | with_hundred
61+
not_one = self.DIGIT ** (2, ...) | (self.DIGIT - accep("1"))
62+
cardinal_plural = compose(cardinal_with_hundred, not_one)
63+
# "one dollar" (singular) vs "two dollars" (plural)
64+
one = cross("one", "1")
6065
integer_graph = (
61-
insert('value: "') + cardinal_with_hundred + insert('"')
62-
+ ds + insert(' currency: "') + currency + insert('"')
66+
insert('value: "') + cardinal_plural + insert('"')
67+
+ ds + insert(' currency: "') + currency_plural + insert('"')
68+
)
69+
integer_graph |= (
70+
insert('value: "') + one + insert('"')
71+
+ ds + insert(' currency: "') + currency_singular + insert('"')
6372
)
6473
# "fifty million dollars" / "four hundred billion won"
6574
quantity_graph = (
6675
insert('value: "') + cardinal_small + insert('"')
6776
+ ds + insert(' quantity: "') + magnitude + insert('"')
68-
+ ds + insert(' currency: "') + currency + insert('"')
77+
+ ds + insert(' currency: "') + currency_plural + insert('"')
6978
)
7079
# "two point five billion dollars"
7180
digit = string_file(get_abs_path("../itn/english/data/numbers/digit.tsv"))
@@ -76,38 +85,38 @@ def build_tagger(self):
7685
insert('value: "') + cardinal_graph + insert(".")
7786
+ ds + delete("point") + ds + frac + insert('"')
7887
+ ds + insert(' quantity: "') + magnitude + insert('"')
79-
+ ds + insert(' currency: "') + currency + insert('"')
88+
+ ds + insert(' currency: "') + currency_plural + insert('"')
8089
)
8190
# "twenty point five o six dollars" (decimal without quantity)
8291
decimal_graph = (
8392
insert('value: "') + cardinal_graph + insert(".")
8493
+ ds + delete("point") + ds + frac + insert('"')
85-
+ ds + insert(' currency: "') + currency + insert('"')
94+
+ ds + insert(' currency: "') + currency_plural + insert('"')
8695
)
8796
# "point five o six dollars"
8897
decimal_no_int = (
8998
insert('value: ".') + delete("point") + ds + frac + insert('"')
90-
+ ds + insert(' currency: "') + currency + insert('"')
99+
+ ds + insert(' currency: "') + currency_plural + insert('"')
91100
)
92101
# "one fifty five dollars" => $155 (missing "hundred")
93102
with_hundred = (
94103
insert('value: "') + cardinal_small + insert('"')
95-
+ ds + insert(' currency: "') + currency + insert('"')
104+
+ ds + insert(' currency: "') + currency_plural + insert('"')
96105
)
97106

98107
# cents
99108
cents_graph = union(*[cross(_num_to_word(x), f"{x:02d}") for x in range(1, 100) if _num_to_word(x)])
100109
with_cents = (
101110
insert('value: "') + cardinal_graph + insert('"')
102-
+ ds + insert(' currency: "') + currency + insert('"')
111+
+ ds + insert(' currency: "') + currency_plural + insert('"')
103112
+ ds + (delete("and") + ds).ques
104113
+ insert(' decimal: "') + cents_graph + insert('"')
105114
+ ds + cent
106115
)
107116
# "seventy five dollars sixty three" (no "cents" word)
108117
dollars_amount = (
109118
insert('value: "') + cardinal_graph + insert('"')
110-
+ ds + insert(' currency: "') + currency + insert('"')
119+
+ ds + insert(' currency: "') + currency_plural + insert('"')
111120
+ ds + insert(' decimal: "') + cents_graph + insert('"')
112121
)
113122
cents_only = (

itn/english/rules/punctuation.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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 union
16+
from pynini.lib.pynutil import insert
17+
18+
from tn.processor import Processor
19+
20+
21+
class Punctuation(Processor):
22+
23+
def __init__(self):
24+
super().__init__(name="punctuation", ordertype="itn")
25+
self.build_tagger()
26+
self.build_verbalizer()
27+
28+
def build_tagger(self):
29+
punct = union(*"!#$%&'()*+,-./:;<=>?@^_`{|}~")
30+
tagger = insert('value: "') + punct + insert('"')
31+
self.tagger = self.add_tokens(tagger)

itn/english/rules/telephone.py

Lines changed: 20 additions & 9 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 add_weight, delete, insert
1717

1818
from itn.english.rules.cardinal import Cardinal
@@ -82,17 +82,28 @@ def build_tagger(self):
8282
ip = ip_token + (cross(" dot ", ".") + ip_token) ** 3
8383
graph |= insert('number_part: "') + add_weight(ip, -0.001) + insert('"')
8484

85-
# credit card: XXXX XXXX XXXX XXXX or XXXX XXXXXX XXXXX
86-
cc = seq @ (
87-
self.DIGIT ** 4 + insert(" ") + self.DIGIT ** 4
88-
+ insert(" ") + self.DIGIT ** 4 + insert(" ") + self.DIGIT ** 4
85+
# credit card: 4-4-4-4 (16), 4-6-4 (14), 4-6-5 (15)
86+
space = insert(" ")
87+
D = self.DIGIT
88+
cc_format = (
89+
D ** 4 + space + D ** 4 + space + D ** 4 + space + D ** 4
90+
| D ** 4 + space + D ** 6 + space + D ** 4
91+
| D ** 4 + space + D ** 6 + space + D ** 5
8992
)
90-
graph |= insert('number_part: "') + cc + insert('"')
93+
cc = seq @ cc_format
94+
graph |= optional_cc + insert('number_part: "') + cc + insert('"')
9195

9296
# serial: mixed alpha+digits, at least one digit, length >= 3
93-
serial_char = add_weight(single, 0.001) | add_weight(two_digit, -0.001) | self.ALPHA
94-
serial = serial_char + closure(ds + serial_char, 1)
95-
serial = serial @ (closure(self.ALPHA | self.DIGIT) + self.DIGIT + closure(self.ALPHA | self.DIGIT))
97+
# Exclude "a" as first char to avoid "a thirty six" -> "a36"
98+
not_a = difference(self.ALPHA, union("a", "A"))
99+
serial_digit = single | add_weight(two_digit, -0.002)
100+
serial_char = serial_digit | self.ALPHA
101+
seq1 = (not_a | serial_digit) + closure(ds + serial_char, 2)
102+
seq1 |= serial_char + closure(ds + (single | self.ALPHA), 2)
103+
seq2 = self.ALPHA + closure(ds + self.ALPHA, 1) + closure(ds + two_digit, 1)
104+
seq2 |= not_a + closure(ds + two_digit, 1)
105+
seq2 |= two_digit + closure(ds + two_digit, 1) + closure(ds + self.ALPHA, 1)
106+
serial = (seq1 | seq2) @ (closure(self.ALPHA | D) + D + closure(self.ALPHA | D))
96107
graph |= insert('number_part: "') + add_weight(serial, 2.0) + insert('"')
97108

98109
self.tagger = self.add_tokens(graph)

itn/english/test/data/en_electronic.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ n vidia dot com => nvidia.com
2222
abc at gmail dot com => abc@gmail.com
2323
athreed at gmail dot com => athreed@gmail.com
2424
kore dot ai => kore.ai
25+
dot three at g mail dot com => dot 3@gmail.com

itn/english/test/data/en_money.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,4 @@ one fifty five dollars => $155
4949
fifteen hundred dollars => $1500
5050
ninety nine hundred dollars => $9900
5151
ninety nine hundred and fifteen dollars and one cent => $9915.01
52+
one dollars => one dollars

itn/english/test/data/en_telephone.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@ five w k r a three one => 5wkra31
1818
x eighty six => x86
1919
x three eighty six => x386
2020
r t x forty fifty t i => RTX 4050ti
21+
four three two double seven three two one four three two one four three double zero five => 432 7732 143214 3005
22+
a thirty six => a 36
23+
a ten eighty p display => a 1080p display

itn/english/test/data/en_word.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
, one , two , three , four => , one , two , three , four
44
e s three => es3
55
yahoo! => yahoo!
6+
twenty! => 20 !
67
x => x
78
— => —
89
aaa => aaa

0 commit comments

Comments
 (0)