e2k 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
e2k/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ from .inference import P2K, C2K
2
+ from .constants import en_phones, kanas, ascii_entries
3
+ import os.path
4
+
5
+ # make internal modules invisible
6
+ __path__ = [os.path.dirname(__file__)]
7
+ # re-export the public API
8
+ __all__ = ["P2K", "C2K", "en_phones", "kanas", "ascii_entries"]
e2k/constants.py ADDED
@@ -0,0 +1,171 @@
1
+ from string import ascii_letters
2
+
3
+
4
+ PAD_IDX = 0
5
+ SOS_IDX = 1
6
+ EOS_IDX = 2
7
+
8
+ kanas = [
9
+ "ァ",
10
+ "ア",
11
+ "ィ",
12
+ "イ",
13
+ "ゥ",
14
+ "ウ",
15
+ "ェ",
16
+ "エ",
17
+ "ォ",
18
+ "オ",
19
+ "カ",
20
+ "ガ",
21
+ "キ",
22
+ "ギ",
23
+ "ク",
24
+ "グ",
25
+ "ケ",
26
+ "ゲ",
27
+ "コ",
28
+ "ゴ",
29
+ "サ",
30
+ "ザ",
31
+ "シ",
32
+ "ジ",
33
+ "ス",
34
+ "ズ",
35
+ "セ",
36
+ "ゼ",
37
+ "ソ",
38
+ "ゾ",
39
+ "タ",
40
+ "ダ",
41
+ "チ",
42
+ "ヂ",
43
+ "ッ",
44
+ "ツ",
45
+ "ヅ",
46
+ "テ",
47
+ "デ",
48
+ "ト",
49
+ "ド",
50
+ "ナ",
51
+ "ニ",
52
+ "ヌ",
53
+ "ネ",
54
+ "ノ",
55
+ "ハ",
56
+ "バ",
57
+ "パ",
58
+ "ヒ",
59
+ "ビ",
60
+ "ピ",
61
+ "フ",
62
+ "ブ",
63
+ "プ",
64
+ "ヘ",
65
+ "ベ",
66
+ "ペ",
67
+ "ホ",
68
+ "ボ",
69
+ "ポ",
70
+ "マ",
71
+ "ミ",
72
+ "ム",
73
+ "メ",
74
+ "モ",
75
+ "ャ",
76
+ "ヤ",
77
+ "ュ",
78
+ "ユ",
79
+ "ョ",
80
+ "ヨ",
81
+ "ラ",
82
+ "リ",
83
+ "ル",
84
+ "レ",
85
+ "ロ",
86
+ "ヮ",
87
+ "ワ",
88
+ "ヰ",
89
+ "ヱ",
90
+ "ヲ",
91
+ "ン",
92
+ "ヴ",
93
+ "ー",
94
+ ]
95
+
96
+ en_phones = [
97
+ "AA0",
98
+ "AA1",
99
+ "AA2",
100
+ "AE0",
101
+ "AE1",
102
+ "AE2",
103
+ "AH0",
104
+ "AH1",
105
+ "AH2",
106
+ "AO0",
107
+ "AO1",
108
+ "AO2",
109
+ "AW0",
110
+ "AW1",
111
+ "AW2",
112
+ "AY0",
113
+ "AY1",
114
+ "AY2",
115
+ "B",
116
+ "CH",
117
+ "D",
118
+ "DH",
119
+ "EH0",
120
+ "EH1",
121
+ "EH2",
122
+ "ER0",
123
+ "ER1",
124
+ "ER2",
125
+ "EY0",
126
+ "EY1",
127
+ "EY2",
128
+ "F",
129
+ "G",
130
+ "HH",
131
+ "IH0",
132
+ "IH1",
133
+ "IH2",
134
+ "IY0",
135
+ "IY1",
136
+ "IY2",
137
+ "JH",
138
+ "K",
139
+ "L",
140
+ "M",
141
+ "N",
142
+ "NG",
143
+ "OW0",
144
+ "OW1",
145
+ "OW2",
146
+ "OY0",
147
+ "OY1",
148
+ "OY2",
149
+ "P",
150
+ "R",
151
+ "S",
152
+ "SH",
153
+ "T",
154
+ "TH",
155
+ "UH0",
156
+ "UH1",
157
+ "UH2",
158
+ "UW0",
159
+ "UW1",
160
+ "UW2",
161
+ "V",
162
+ "W",
163
+ "Y",
164
+ "Z",
165
+ "ZH",
166
+ ]
167
+
168
+ ascii_entries = ["<pad>", "<sos>", "<eos>"] + list(ascii_letters) + [" "]
169
+
170
+ kanas = ["<pad>", "<sos>", "<eos>"] + kanas
171
+ en_phones = ["<pad>", "<sos>", "<eos>"] + en_phones + [" "]
e2k/inference.py ADDED
@@ -0,0 +1,272 @@
1
+ # Description: Inference functions for the E2K model in numpy
2
+ import numpy as np
3
+ from typing import Optional, Dict
4
+ import importlib.resources
5
+ from e2k.constants import SOS_IDX, EOS_IDX, en_phones, kanas, ascii_entries
6
+
7
+
8
+ class Linear:
9
+ def __init__(self, weight, bias):
10
+ self.weight = weight
11
+ self.bias = bias
12
+
13
+ def forward(self, x):
14
+ return np.matmul(x, self.weight.T) + self.bias
15
+
16
+
17
+ class Embedding:
18
+ def __init__(self, weight):
19
+ self.weight = weight
20
+
21
+ def forward(self, x):
22
+ return self.weight[x]
23
+
24
+
25
+ def sigmoid(x):
26
+ return 1 / (1 + np.exp(-x))
27
+
28
+
29
+ def tanh(x):
30
+ return np.tanh(x)
31
+
32
+
33
+ class GRUCell:
34
+ def __init__(self, weight_ih, weight_hh, bias_ih, bias_hh):
35
+ self.ih = Linear(weight_ih, bias_ih)
36
+ self.hh = Linear(weight_hh, bias_hh)
37
+
38
+ def forward(self, x: np.ndarray, h: Optional[np.ndarray] = None):
39
+ """
40
+ x: [D]
41
+ h: [D]
42
+ """
43
+ if h is None:
44
+ h = np.zeros(
45
+ [
46
+ self.hh.weight.shape[-1],
47
+ ]
48
+ )
49
+ rzn_ih = self.ih.forward(x)
50
+ rzn_hh = self.hh.forward(h)
51
+
52
+ rz_ih, n_ih = (
53
+ rzn_ih[: rzn_ih.shape[-1] * 2 // 3],
54
+ rzn_ih[rzn_ih.shape[-1] * 2 // 3 :],
55
+ )
56
+ rz_hh, n_hh = (
57
+ rzn_hh[: rzn_hh.shape[-1] * 2 // 3],
58
+ rzn_hh[rzn_hh.shape[-1] * 2 // 3 :],
59
+ )
60
+
61
+ rz = sigmoid(rz_ih + rz_hh)
62
+ r, z = np.split(rz, 2, axis=-1)
63
+
64
+ n = np.tanh(n_ih + r * n_hh)
65
+ h = (1 - z) * n + z * h
66
+
67
+ return h
68
+
69
+
70
+ class GRU:
71
+ def __init__(self, cell: GRUCell, reverse: bool = False):
72
+ self.cell = cell
73
+ self.reverse = reverse
74
+
75
+ def forward(self, x, h: Optional[np.ndarray] = None):
76
+ """
77
+ x: [T, D], unbatched
78
+ """
79
+ if self.reverse:
80
+ x = np.flip(x, axis=0)
81
+ outputs = []
82
+ for i in range(x.shape[0]):
83
+ h = self.cell.forward(x[i], h)
84
+ outputs.append(h)
85
+ outputs = np.stack(outputs)
86
+ if self.reverse:
87
+ outputs = np.flip(outputs, axis=0)
88
+ return outputs, h
89
+
90
+
91
+ class MHA:
92
+ """
93
+ Multi-head attention
94
+ """
95
+
96
+ def __init__(
97
+ self,
98
+ in_proj_weight,
99
+ in_proj_bias,
100
+ out_proj_weight,
101
+ out_proj_bias,
102
+ num_heads: int,
103
+ ):
104
+ [q_w, k_w, v_w] = np.split(in_proj_weight, 3, axis=0)
105
+ [q_b, k_b, v_b] = np.split(in_proj_bias, 3, axis=0)
106
+ self.dim = q_w.shape[-1]
107
+ self.q_proj = Linear(q_w, q_b)
108
+ self.k_proj = Linear(k_w, k_b)
109
+ self.v_proj = Linear(v_w, v_b)
110
+ self.o_proj = Linear(out_proj_weight, out_proj_bias)
111
+ self.num_heads = num_heads
112
+ self.d_heads = self.dim // num_heads
113
+ self.scale = np.sqrt(self.dim)
114
+
115
+ def forward(self, q: np.ndarray, k: np.ndarray, v: np.ndarray):
116
+ """
117
+ q: [T, D]
118
+ k: [T, D]
119
+ v: [T, D]
120
+ """
121
+ q = self.q_proj.forward(q)
122
+ k = self.k_proj.forward(k)
123
+ v = self.v_proj.forward(v)
124
+ q = np.split(q, self.num_heads, axis=-1)
125
+ q = np.stack(q, axis=0)
126
+ k = np.split(k, self.num_heads, axis=-1)
127
+ k = np.stack(k, axis=0)
128
+ v = np.split(v, self.num_heads, axis=-1)
129
+ v = np.stack(v, axis=0)
130
+ attn = np.matmul(q, np.transpose(k, (0, 2, 1)))
131
+ attn = attn / self.scale
132
+ attn = np.exp(attn)
133
+ attn = attn / attn.sum(axis=-1, keepdims=True)
134
+ o = np.matmul(attn, v)
135
+ o = np.transpose(o, (1, 0, 2))
136
+ o = o.reshape([o.shape[0], -1])
137
+ return self.o_proj.forward(o)
138
+
139
+
140
+ class S2S:
141
+ def __init__(self, weights: Dict[str, np.ndarray], max_len: int = 16):
142
+ self.e_emb = Embedding(weights["e_emb.weight"])
143
+ self.k_emb = Embedding(weights["k_emb.weight"])
144
+ self.encoder = GRU(
145
+ GRUCell(
146
+ weights["encoder.weight_ih_l0"],
147
+ weights["encoder.weight_hh_l0"],
148
+ weights["encoder.bias_ih_l0"],
149
+ weights["encoder.bias_hh_l0"],
150
+ )
151
+ )
152
+ self.encoder_reverse = GRU(
153
+ GRUCell(
154
+ weights["encoder.weight_ih_l0_reverse"],
155
+ weights["encoder.weight_hh_l0_reverse"],
156
+ weights["encoder.bias_ih_l0_reverse"],
157
+ weights["encoder.bias_hh_l0_reverse"],
158
+ ),
159
+ reverse=True,
160
+ )
161
+ self.encoder_fc = Linear(
162
+ weights["encoder_fc.0.weight"], weights["encoder_fc.0.bias"]
163
+ )
164
+ self.pre_decoder = GRU(
165
+ GRUCell(
166
+ weights["pre_decoder.weight_ih_l0"],
167
+ weights["pre_decoder.weight_hh_l0"],
168
+ weights["pre_decoder.bias_ih_l0"],
169
+ weights["pre_decoder.bias_hh_l0"],
170
+ )
171
+ )
172
+ self.post_decoder = GRU(
173
+ GRUCell(
174
+ weights["post_decoder.weight_ih_l0"],
175
+ weights["post_decoder.weight_hh_l0"],
176
+ weights["post_decoder.bias_ih_l0"],
177
+ weights["post_decoder.bias_hh_l0"],
178
+ )
179
+ )
180
+ self.attn = MHA(
181
+ weights["attn.in_proj_weight"],
182
+ weights["attn.in_proj_bias"],
183
+ weights["attn.out_proj.weight"],
184
+ weights["attn.out_proj.bias"],
185
+ 4,
186
+ )
187
+ self.fc = Linear(weights["fc.weight"], weights["fc.bias"])
188
+ self.max_len = max_len
189
+
190
+ def greedy(self, step_dec: np.ndarray):
191
+ """
192
+ step_dec: [N]
193
+ """
194
+ return np.argmax(step_dec, axis=-1)
195
+
196
+ def forward(self, src):
197
+ """
198
+ In numpy, only inference is supported.
199
+ """
200
+ e_emb = self.e_emb.forward(src)
201
+ enc_out, _ = self.encoder.forward(e_emb)
202
+ enc_out_rev, _ = self.encoder_reverse.forward(e_emb)
203
+ enc_out = np.concatenate([enc_out, enc_out_rev], axis=-1)
204
+ enc_out = self.encoder_fc.forward(enc_out)
205
+ enc_out = tanh(enc_out)
206
+ res = [SOS_IDX]
207
+ h1 = None
208
+ h2 = None
209
+ for _ in range(self.max_len):
210
+ dec_emb = self.k_emb.forward(np.array([res[-1]]))
211
+ dec_out, h1 = self.pre_decoder.forward(dec_emb, h1)
212
+ attn_out = self.attn.forward(dec_out, enc_out, enc_out)
213
+ x = np.concatenate([dec_out, attn_out], axis=-1)
214
+ x, h2 = self.post_decoder.forward(x, h2)
215
+ x = self.fc.forward(x)
216
+ x = x[0]
217
+ res.append(self.greedy(x))
218
+ if res[-1] == EOS_IDX:
219
+ break
220
+ return res
221
+
222
+
223
+ class BaseE2K:
224
+ def __init__(self):
225
+ self.s2s = None
226
+ self.in_table = None
227
+ self.out_table = None
228
+ raise NotImplementedError
229
+
230
+ def __call__(self, src: str) -> str:
231
+ src = list(filter(lambda x: x in self.in_table, src))
232
+ src = [self.in_table[c] for c in src]
233
+ src = [SOS_IDX] + src + [EOS_IDX]
234
+ src = np.array(src)
235
+ tgt = self.s2s.forward(src)
236
+ tgt = [int(v) for v in tgt[1:-1]]
237
+ tgt = [self.out_table[c] for c in tgt]
238
+ return "".join(tgt)
239
+
240
+ def get_weight_path(filename) -> str:
241
+ return importlib.resources.files("e2k.models").joinpath(filename)
242
+
243
+ class P2K(BaseE2K):
244
+ def __init__(self, max_len: int = 16):
245
+ weights = np.load(get_weight_path("model-p2k.npz"), allow_pickle=True)
246
+ self.s2s = S2S(weights, max_len)
247
+ self.in_table = {c: i for i, c in enumerate(en_phones)}
248
+ self.out_table = {i: c for i, c in enumerate(kanas)}
249
+
250
+
251
+ class C2K(BaseE2K):
252
+ def __init__(self, max_len: int = 16):
253
+ weights = np.load(get_weight_path("model-c2k.npz"), allow_pickle=True)
254
+ self.s2s = S2S(weights, max_len)
255
+ self.in_table = {c: i for i, c in enumerate(ascii_entries)}
256
+ self.out_table = {i: c for i, c in enumerate(kanas)}
257
+
258
+
259
+ if __name__ == "__main__":
260
+ import argparse
261
+ from g2p_en import G2p
262
+
263
+ g2p = G2p()
264
+ parser = argparse.ArgumentParser()
265
+ args = parser.parse_args()
266
+ p2k = P2K()
267
+ c2k = C2K()
268
+ word = "voicevox"
269
+ phonemes = g2p(word)
270
+ print(word)
271
+ print("P2K: ", p2k(phonemes)) # ザワールド
272
+ print("C2K: ", c2k(word)) # ザワールド
Binary file
Binary file
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.2
2
+ Name: e2k
3
+ Version: 0.1.0
4
+ Summary: A tool for automatic English to Katakana conversion
5
+ Author-email: Patchethium <asguftuikh@gmail.com>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: numpy>=2.2.2
9
+
10
+ # English to Katakana Dictionary
11
+
12
+ It's an extraction of the Wikitionary's Japanese dictionary and `JMdict / EDICT` to create a dictionary of English words and their Katakana representation.
13
+
14
+ We also provide a GRU model to convert English to Katakana without a dictionary.
15
+
16
+ ```json
17
+ {"word": "word", "katakana": "ワード"}
18
+ {"word": "another", "katakana": "アナザー"}
19
+ {"word": "example", "katakana": "エグザンプル"}
20
+ ```
21
+
22
+ It's for the purpose of converting English words to Katakana and further use in Japanese Text-to-Speech applications or Japanese learning.
23
+
24
+ ## Re-creating the dictionary
25
+
26
+ The script has zero dependencies, as long as you have a Python 3 interpreter it should work.
27
+
28
+ ### Download the dictionary
29
+
30
+ Download the raw dump of the Japanese Wikitionary from https://kaikki.org/dictionary/rawdata.html, they kindly provide the parsed data in a JSONL format.
31
+
32
+ Look for the `Japanese ja-extract.jsonl.gz (compressed 37.5MB)` entry and download it. If you prefer command line, use
33
+
34
+ ```bash
35
+ curl -O https://kaikki.org/dictionary/downloads/ja/ja-extract.jsonl.gz
36
+ ```
37
+
38
+ ### Extract it
39
+
40
+ Extract it into `/vendor` folder.
41
+
42
+ On Linux, you can use
43
+
44
+ ```bash
45
+ gzip -d ja-extract.jsonl.gz
46
+ ```
47
+
48
+ ### Run the script
49
+
50
+ ```bash
51
+ python extract.py
52
+ # if you have another name for the file
53
+ python extract.py --path /path/to/your_file.jsonl
54
+ ```
55
+
56
+ And a `katakana_dict.jsonl` file will be created in the root folder.
57
+
58
+ ## E2K model
59
+
60
+ We also provide a seq2seq GRU model to convert English automatically to Katakana. It's trained on the aforementioned dictionary.
61
+
62
+ We provide 2 types of models, one converts phoneme to Katakana and one that converts character to Katakana. Choose the one that fits your use case.
63
+
64
+ ```bash
65
+ pip install e2k
66
+ ```
67
+
68
+ The weights are included in PyPI, you can simply use the model like this:
69
+
70
+ ```python
71
+ from e2k import P2K, C2K
72
+ from g2p_en import G2p # or `phonemizer`, as long as it outputs CMUdict phoneme
73
+
74
+ # cmudict phoneme to katakana
75
+ p2k = P2K()
76
+
77
+ g2p = G2p()
78
+
79
+ katakana = p2k(g2p("word"))
80
+
81
+ print(katakana) # "ワード"
82
+
83
+ # character directly to katakana
84
+ c2k = C2K()
85
+
86
+ # since it's an autoregressive model, you can set the decoding strategy
87
+ # to greedy, top-k or nucleus sampling (top-p)
88
+ # see https://huggingface.co/docs/transformers/en/generation_strategies
89
+ c2k.set_decoding_strategy("top-p", p=0.3)
90
+
91
+ katakana = c2k("word")
92
+
93
+ print(katakana) # "ワード"
94
+ ```
95
+
96
+ We rewrite the inference of GRU model in `numpy`, minimizing the dependencies to `numpy` only.
97
+
98
+ ### Benchmark
99
+
100
+ ```bash
101
+ # --p2k for phoneme to katakana, if not provided, it will be character to katakana
102
+ python eval.py --data ./vendor/katakana_dict.jsonl --model /path/to/your/model.pth --p2k
103
+ ```
104
+
105
+ | Model | BLEU Score |
106
+ | --------------------- | ---------- |
107
+ | Phoneme to Katakana | 0.85 |
108
+ | Character to Katakana | 0.90 |
109
+
110
+ ### Training the model
111
+
112
+ You'll need `torch` and `g2p_en`. After that, you can run the `train.py` script to train the model.
113
+
114
+ ```bash
115
+ python train.py --data ./vendor/kanji_dict.jsonl
116
+ ```
117
+
118
+ It takes around 10 minutes to train the model on a desktop CPU. The model will be saved as `model.pth` in the root folder.
119
+
120
+ Also, you'll need to either download the `kanji_dict.jsonl` from the releases or create it yourself using the `extract.py` script.
121
+
122
+ Be noted that the training script is not included in the PyPI package, you'll need to clone the repository to train the model.
123
+
124
+ ## Development
125
+
126
+ ### Install the dependencies
127
+
128
+ I use [`uv`](https://docs.astral.sh/uv/) to manage the dependencies and publish the package.
129
+
130
+ ```bash
131
+ uv sync
132
+ ```
133
+
134
+ ## License
135
+
136
+ The code is released under WTFPL.
137
+
138
+ The dictionary should follow the [Wikimedia's license](https://dumps.wikimedia.org/legal.html) and the [JMdict / EDICT's Copyright](https://www.edrdg.org/) license.
139
+
140
+ In short, as long as you provide the attribution, you can use it for both commercial and non-commercial purposes.
141
+
142
+ The model weights are released under the same license as the dictionary.
143
+
144
+ ## Credits
145
+
146
+ - [Wikitionary](https://www.wiktionary.org/)
147
+ - [JMdict / EDICT](http://www.edrdg.org/jmdict/edict.html)
@@ -0,0 +1,9 @@
1
+ e2k/__init__.py,sha256=KxTSqeTPYlyYhjozlcruFWtgn9iODsLYin7cY9QKPBI,267
2
+ e2k/constants.py,sha256=Lajx--JL1kduvNN3BIbr9soQ_A9T-jPEk-jdJFthKQw,1935
3
+ e2k/inference.py,sha256=g1OG68x4I26u2uii-NNzISG4_1la_1eLmf4y9UtVMzs,8163
4
+ e2k/models/model-c2k.npz,sha256=6Fi5I4USkgYJ-V4QiEbWX2_b_ZTLn3O7qvOKiZgsBZE,4466282
5
+ e2k/models/model-p2k.npz,sha256=jqUjk_MsGX7kc882Pkwf6GaBm2vdHnhFtu6NKrYPe-o,4474986
6
+ e2k-0.1.0.dist-info/METADATA,sha256=mq0f2ofBy2dZKbtgS-GMdhG_u_uzQUJwJB_5rBY9E20,4330
7
+ e2k-0.1.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
8
+ e2k-0.1.0.dist-info/top_level.txt,sha256=C1ChHRXMXLY8K9ZIwiRtchhn1MW-fW9gY1HlFjRLCYo,4
9
+ e2k-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.8.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ e2k