g2ps 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.
- g2ps/__init__.py +1 -0
- g2ps/_resolve.py +35 -0
- g2ps/models/nano/meta.pkl +0 -0
- g2ps/models/nano/nano.npz +0 -0
- g2ps/models/small/meta.pkl +0 -0
- g2ps/models/tiny/meta.pkl +0 -0
- g2ps/runtime.py +169 -0
- g2ps-0.1.0.dist-info/METADATA +6 -0
- g2ps-0.1.0.dist-info/RECORD +11 -0
- g2ps-0.1.0.dist-info/WHEEL +5 -0
- g2ps-0.1.0.dist-info/top_level.txt +1 -0
g2ps/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .runtime import G2pS
|
g2ps/_resolve.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Tier auto-resolution: packaged weights first, then on-demand download."""
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import urllib.request
|
|
5
|
+
|
|
6
|
+
MODELS_DIR = os.path.join(os.path.dirname(__file__), 'models')
|
|
7
|
+
CACHE_DIR = os.environ.get('G2PS_CACHE', os.path.join(os.path.expanduser('~'), '.cache', 'g2ps'))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def resolve_model_dir(model_dir, arch_keys, weights_name, urls):
|
|
11
|
+
"""model_dir may be a path, or a tier name ('nano'/'tiny'/'small')."""
|
|
12
|
+
if os.path.isdir(model_dir) and os.path.exists(os.path.join(model_dir, weights_name)):
|
|
13
|
+
return model_dir
|
|
14
|
+
tier = model_dir if model_dir in arch_keys else os.path.basename(os.path.normpath(model_dir))
|
|
15
|
+
if tier not in arch_keys:
|
|
16
|
+
raise ValueError(f"model_dir must be a path or one of {list(arch_keys)}, got {model_dir!r}")
|
|
17
|
+
pkg = os.path.join(MODELS_DIR, tier)
|
|
18
|
+
if os.path.exists(os.path.join(pkg, weights_name)):
|
|
19
|
+
return pkg
|
|
20
|
+
dst = os.path.join(CACHE_DIR, tier)
|
|
21
|
+
os.makedirs(dst, exist_ok=True)
|
|
22
|
+
if not os.path.exists(os.path.join(dst, 'meta.pkl')):
|
|
23
|
+
shutil.copy(os.path.join(pkg, 'meta.pkl'), os.path.join(dst, 'meta.pkl'))
|
|
24
|
+
wp = os.path.join(dst, weights_name)
|
|
25
|
+
if not os.path.exists(wp):
|
|
26
|
+
err = []
|
|
27
|
+
for url in urls:
|
|
28
|
+
try:
|
|
29
|
+
print(f'g2pS: downloading {url.rsplit("/", 1)[-1]} ({tier}) ...')
|
|
30
|
+
urllib.request.urlretrieve(url, wp)
|
|
31
|
+
return dst
|
|
32
|
+
except Exception as e: # noqa: BLE001
|
|
33
|
+
err.append(str(e)[:80])
|
|
34
|
+
raise RuntimeError('could not download weights from any mirror:\n' + '\n'.join(err))
|
|
35
|
+
return dst
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
g2ps/runtime.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""g2pS numpy-only runtime. Deps: numpy (that's it).
|
|
2
|
+
|
|
3
|
+
Weights: models/<tier>/<tier>.npz (fp16 storage, fp32 compute) + meta.pkl.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import pickle
|
|
7
|
+
|
|
8
|
+
# set default thread count BEFORE numpy loads BLAS (bigger models benefit from threads)
|
|
9
|
+
_tier_hint = os.environ.get('G2PS_THREADS')
|
|
10
|
+
if _tier_hint is None:
|
|
11
|
+
os.environ.setdefault('OMP_NUM_THREADS', '8')
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from ._resolve import resolve_model_dir
|
|
16
|
+
|
|
17
|
+
MAXLEN = 128
|
|
18
|
+
EPS = 1e-6
|
|
19
|
+
|
|
20
|
+
ARCH = {
|
|
21
|
+
'small': dict(D=384, L=6, H=6, FFN=1024, emb=False, rank=0, passes=1),
|
|
22
|
+
'tiny': dict(D=256, L=5, H=4, FFN=640, emb=True, rank=64, passes=1),
|
|
23
|
+
'nano': dict(D=96, L=6, H=4, FFN=256, emb=True, rank=16, passes=2),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _set_blas_threads(n):
|
|
28
|
+
try:
|
|
29
|
+
import glob
|
|
30
|
+
import ctypes
|
|
31
|
+
for lib in glob.glob(os.path.join(os.path.dirname(np.__file__), '..', 'numpy.libs', 'libopenblas*.so*')) \
|
|
32
|
+
+ glob.glob(os.path.join(os.path.dirname(np.__file__), '.libs', 'libopenblas*.so*')) \
|
|
33
|
+
+ glob.glob(os.path.join(os.path.dirname(np.__file__), '..', 'scipy.libs', 'libopenblas*.so*')):
|
|
34
|
+
try:
|
|
35
|
+
ctypes.CDLL(lib).openblas_set_num_threads(n)
|
|
36
|
+
except OSError:
|
|
37
|
+
pass
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class G2pS:
|
|
43
|
+
_EXT = 'npz'
|
|
44
|
+
_URLS = ['https://huggingface.co/baicai1145/g2pS/resolve/main/models/{t}/{t}.{e}',
|
|
45
|
+
'https://modelscope.cn/models/baicai1145/g2pS/resolve/master/models/{t}/{t}.{e}']
|
|
46
|
+
|
|
47
|
+
def __init__(self, model_dir, tier=None):
|
|
48
|
+
tier = tier or (model_dir if model_dir in ARCH else None) or os.path.basename(os.path.normpath(model_dir))
|
|
49
|
+
model_dir = resolve_model_dir(model_dir, ARCH, f'{tier}.{self._EXT}',
|
|
50
|
+
[u.format(t=tier, e=self._EXT) for u in self._URLS])
|
|
51
|
+
a = ARCH[tier]
|
|
52
|
+
self.tier = tier
|
|
53
|
+
self.D, self.L, self.H, self.FFN, self.PASSES = a['D'], a['L'], a['H'], a['FFN'], a['passes']
|
|
54
|
+
self.DH = a['D'] // a['H']
|
|
55
|
+
D, L = self.D, self.L
|
|
56
|
+
_set_blas_threads(1 if D <= 96 else 8)
|
|
57
|
+
|
|
58
|
+
npz = os.path.join(model_dir, tier + '.npz')
|
|
59
|
+
z = np.load(npz)
|
|
60
|
+
w = {k: z[k].astype(np.float32) for k in z.files}
|
|
61
|
+
self.emb = w['emb.weight'] @ w['emb_proj.weight'].T if a['emb'] else w['emb.weight']
|
|
62
|
+
self.w12 = [np.ascontiguousarray(w[f'blocks.{li}.n2.w'][:, None] * np.concatenate(
|
|
63
|
+
[w[f'blocks.{li}.ffn.w1.weight'].T, w[f'blocks.{li}.ffn.w2.weight'].T], 1)) for li in range(L)]
|
|
64
|
+
self.w3 = [np.ascontiguousarray(w[f'blocks.{li}.ffn.w3.weight'].T) for li in range(L)]
|
|
65
|
+
self.qkv = [np.ascontiguousarray(w[f'blocks.{li}.n1.w'][:, None] * w[f'blocks.{li}.attn.qkv.weight'].T) for li in range(L)]
|
|
66
|
+
self.o = [np.ascontiguousarray(w[f'blocks.{li}.attn.o.weight'].T) for li in range(L)]
|
|
67
|
+
self.char_bias = (w['char_bias.0.weight'] @ w['char_bias.1.weight'].T) if a['rank'] else w['char_bias.weight']
|
|
68
|
+
self.stem_w = [w[f'stem.convs.{i}.weight'][:, 0, :] for i in range(3)]
|
|
69
|
+
self.stem_b = [w[f'stem.convs.{i}.bias'] for i in range(3)]
|
|
70
|
+
self.stem_n = w['stem.norm.w']
|
|
71
|
+
self.readout = np.ascontiguousarray(w['n_final.w'][:, None] * w['readout.weight'].T)
|
|
72
|
+
inv = 1.0 / (10000.0 ** (np.arange(0, self.DH, 2, dtype=np.float64) / self.DH))
|
|
73
|
+
ang = np.outer(np.arange(MAXLEN), inv)
|
|
74
|
+
self.rope_cos = np.repeat(np.cos(ang)[:, None, :], self.H, 1).astype(np.float32)
|
|
75
|
+
self.rope_sin = np.repeat(np.sin(ang)[:, None, :], self.H, 1).astype(np.float32)
|
|
76
|
+
|
|
77
|
+
meta = pickle.load(open(os.path.join(model_dir, 'meta.pkl'), 'rb'))
|
|
78
|
+
self.vocab = meta['vocab']
|
|
79
|
+
self.labels = meta['labels']
|
|
80
|
+
self.poly_id = {c: i for i, c in enumerate(meta['poly_chars'])}
|
|
81
|
+
self.char_cands = meta['char_cands']
|
|
82
|
+
|
|
83
|
+
def _rms(self, x):
|
|
84
|
+
ms = (x * x).sum(-1, keepdims=True) * (1.0 / self.D) + EPS
|
|
85
|
+
return x * (1.0 / np.sqrt(ms))
|
|
86
|
+
|
|
87
|
+
def forward(self, char_ids, poly_ids, positions, cand):
|
|
88
|
+
T = len(char_ids)
|
|
89
|
+
D, H, DH = self.D, self.H, self.DH
|
|
90
|
+
x = self.emb[char_ids]
|
|
91
|
+
s = np.zeros_like(x)
|
|
92
|
+
for i, dl in enumerate((1, 2, 3)):
|
|
93
|
+
wk, b = self.stem_w[i], self.stem_b[i]
|
|
94
|
+
xp = np.zeros((T + 2 * dl, D), dtype=np.float32)
|
|
95
|
+
xp[dl:dl + T] = x
|
|
96
|
+
s += b
|
|
97
|
+
for k in range(3):
|
|
98
|
+
s += wk[:, k] * xp[k * dl:k * dl + T]
|
|
99
|
+
x = self._rms(x + s) * self.stem_n
|
|
100
|
+
am = np.where(char_ids != 0, np.float32(0), np.float32(-1e4))[None, None, :]
|
|
101
|
+
scale = DH ** -0.5
|
|
102
|
+
for _ in range(self.PASSES):
|
|
103
|
+
for li in range(self.L):
|
|
104
|
+
qkv = self._rms(x) @ self.qkv[li]
|
|
105
|
+
q = qkv[:, :D]; k = qkv[:, D:2 * D]
|
|
106
|
+
q4 = q.reshape(T, H, DH // 2, 2)
|
|
107
|
+
k4 = k.reshape(T, H, DH // 2, 2)
|
|
108
|
+
cos, sin = self.rope_cos[:T], self.rope_sin[:T]
|
|
109
|
+
qr = np.empty((T, H, DH // 2, 2), dtype=np.float32)
|
|
110
|
+
kr = np.empty((T, H, DH // 2, 2), dtype=np.float32)
|
|
111
|
+
qe, qo = q4[..., 0], q4[..., 1]
|
|
112
|
+
ke, ko = k4[..., 0], k4[..., 1]
|
|
113
|
+
qr[..., 0] = qe * cos - qo * sin
|
|
114
|
+
qr[..., 1] = qe * sin + qo * cos
|
|
115
|
+
kr[..., 0] = ke * cos - ko * sin
|
|
116
|
+
kr[..., 1] = ke * sin + ko * cos
|
|
117
|
+
qh = qr.reshape(T, H, DH).transpose(1, 0, 2)
|
|
118
|
+
kh = kr.reshape(T, H, DH).transpose(1, 0, 2)
|
|
119
|
+
vh = qkv[:, 2 * D:].reshape(T, H, DH).transpose(1, 0, 2)
|
|
120
|
+
a = qh @ kh.transpose(0, 2, 1) * scale + am
|
|
121
|
+
a -= a.max(-1, keepdims=True)
|
|
122
|
+
np.exp(a, out=a)
|
|
123
|
+
a /= a.sum(-1, keepdims=True)
|
|
124
|
+
y = (a @ vh).transpose(1, 0, 2).reshape(T, D)
|
|
125
|
+
x = x + y @ self.o[li]
|
|
126
|
+
ab = self._rms(x) @ self.w12[li]
|
|
127
|
+
A, B_ = ab[:, :self.FFN], ab[:, self.FFN:]
|
|
128
|
+
A /= (1.0 + np.exp(-A))
|
|
129
|
+
x = x + (A * B_) @ self.w3[li]
|
|
130
|
+
ph = self._rms(x)[positions]
|
|
131
|
+
logits = ph @ self.readout + self.char_bias[poly_ids]
|
|
132
|
+
logits = np.where(cand, logits, np.float32(-1e4))
|
|
133
|
+
return logits
|
|
134
|
+
|
|
135
|
+
def predict(self, text):
|
|
136
|
+
"""Returns {position: pinyin} over the (lowercased, maybe truncated) text."""
|
|
137
|
+
text = text.lower()
|
|
138
|
+
positions = [i for i, c in enumerate(text) if c in self.poly_id]
|
|
139
|
+
if not positions:
|
|
140
|
+
return {}, text
|
|
141
|
+
if len(text) > MAXLEN:
|
|
142
|
+
first = positions[0]
|
|
143
|
+
start = max(0, min(first - MAXLEN // 2, len(text) - MAXLEN))
|
|
144
|
+
text = text[start:start + MAXLEN]
|
|
145
|
+
positions = [p - start for p in positions if start <= p < start + MAXLEN]
|
|
146
|
+
if not positions:
|
|
147
|
+
return {}, text
|
|
148
|
+
char_ids = np.array([self.vocab.get(c, 1) for c in text], dtype=np.int64)
|
|
149
|
+
poly_ids = np.array([self.poly_id[text[p]] for p in positions])
|
|
150
|
+
cand = np.zeros((len(positions), len(self.labels)), dtype=bool)
|
|
151
|
+
for k, p in enumerate(positions):
|
|
152
|
+
cand[k, list(self.char_cands[text[p]])] = True
|
|
153
|
+
logits = self.forward(char_ids, poly_ids, np.array(positions), cand)
|
|
154
|
+
pred = logits.argmax(-1)
|
|
155
|
+
return {p: self.labels[int(pred[k])] for k, p in enumerate(positions)}, text
|
|
156
|
+
|
|
157
|
+
def convert(self, text):
|
|
158
|
+
"""Returns (text_used, [(char, pinyin_or_None), ...]) — polyphones only; pair with
|
|
159
|
+
pypinyin or any dictionary for monophonic chars."""
|
|
160
|
+
res, t = self.predict(text)
|
|
161
|
+
return t, [(c, res.get(i)) for i, c in enumerate(t)]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == '__main__':
|
|
165
|
+
import sys
|
|
166
|
+
g = G2pS(sys.argv[1] if len(sys.argv) > 1 else 'models/nano')
|
|
167
|
+
for s in ['他在银行工作,行踪不定。', '恢复汉阳区人民政府', '能把小孩哄睡是个技术活。']:
|
|
168
|
+
_, pairs = g.convert(s)
|
|
169
|
+
print(s, '->', [(c, p) for c, p in pairs if p])
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
g2ps/__init__.py,sha256=ab4S7abmoWlWylOKA8ajspt8ir8tcUlGb1aiWt8lLKg,26
|
|
2
|
+
g2ps/_resolve.py,sha256=_UtLV6jlFUsrsbPFs14tACY6HL6XJYsxo7erAYDEc3k,1595
|
|
3
|
+
g2ps/runtime.py,sha256=5_TbVPvdC-lzDPzUZZaNCWsREggIoyxQ7HQwMbMig44,7899
|
|
4
|
+
g2ps/models/nano/meta.pkl,sha256=oxziLhr2oZ_1aL1-5sGaT2VALiuCwOWdurRck3JDZ4U,77622
|
|
5
|
+
g2ps/models/nano/nano.npz,sha256=iLl-A-q34ef6OGq1KqfdCAEiPt_79myW-O6YEmJ2xms,2160762
|
|
6
|
+
g2ps/models/small/meta.pkl,sha256=krEDH4PE5clMaGpNXLePhhoBQD_DuDsUvsdvuN1qlko,123832
|
|
7
|
+
g2ps/models/tiny/meta.pkl,sha256=oxziLhr2oZ_1aL1-5sGaT2VALiuCwOWdurRck3JDZ4U,77622
|
|
8
|
+
g2ps-0.1.0.dist-info/METADATA,sha256=rFOBhUyMZt-sYEwqRZoRkoj0eEGosMnzVm3gpd1OFYc,157
|
|
9
|
+
g2ps-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
10
|
+
g2ps-0.1.0.dist-info/top_level.txt,sha256=UyEMJaiuoXNKtftldES0LJ8UFwcFjI05ODKXqihZbbk,5
|
|
11
|
+
g2ps-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
g2ps
|