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 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,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: g2ps
3
+ Version: 0.1.0
4
+ Summary: Mandarin polyphone disambiguation, numpy-only inference
5
+ Requires-Python: >=3.8
6
+ Requires-Dist: numpy
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ g2ps