g2ps-torch 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_torch/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from .runtime import G2pS
g2ps_torch/_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_torch/modules.py ADDED
@@ -0,0 +1,192 @@
1
+ """Student model: modern small char-level encoder.
2
+
3
+ 6 x 384 bidirectional transformer with RoPE / RMSNorm / SwiGLU (2024-style recipe),
4
+ candidate-masked readout at every labeled polyphone position.
5
+ """
6
+ import math
7
+
8
+ import torch
9
+ import torch.nn as nn
10
+ import torch.nn.functional as F
11
+
12
+
13
+ class RMSNorm(nn.Module):
14
+ def __init__(self, d, eps=1e-6):
15
+ super().__init__()
16
+ self.w = nn.Parameter(torch.ones(d))
17
+ self.eps = eps
18
+
19
+ def forward(self, x):
20
+ ms = x.float().pow(2).mean(-1, keepdim=True) # fp32 accumulation (fp16-safe)
21
+ return (self.w * (x.float() * torch.rsqrt(ms + self.eps))).to(x.dtype)
22
+
23
+
24
+ def rope_table(max_len, dim, base=10000.0, device='cpu', dtype=torch.float32):
25
+ inv = 1.0 / (base ** (torch.arange(0, dim, 2, device=device, dtype=torch.float64) / dim))
26
+ t = torch.arange(max_len, device=device, dtype=torch.float64)
27
+ angles = torch.outer(t, inv)
28
+ return torch.stack([torch.cos(angles), torch.sin(angles)], dim=-1).to(dtype) # (T, D/2, 2)
29
+
30
+
31
+ def apply_rope(x, rope):
32
+ """x: (B, T, H, Dh); rope: (T, Dh/2, 2)"""
33
+ T = x.shape[1]
34
+ r = rope[:T].to(x.dtype) # (T, Dh/2, 2)
35
+ x1, x2 = x[..., 0::2], x[..., 1::2]
36
+ cos = r[..., 0].unsqueeze(0).unsqueeze(2) # (1, T, 1, Dh/2)
37
+ sin = r[..., 1].unsqueeze(0).unsqueeze(2)
38
+ o1 = x1 * cos - x2 * sin
39
+ o2 = x1 * sin + x2 * cos
40
+ return torch.stack((o1, o2), dim=-1).flatten(-2)
41
+
42
+
43
+ class Attention(nn.Module):
44
+ def __init__(self, d, n_heads):
45
+ super().__init__()
46
+ self.n_heads = n_heads
47
+ self.dh = d // n_heads
48
+ self.qkv = nn.Linear(d, 3 * d, bias=False)
49
+ self.o = nn.Linear(d, d, bias=False)
50
+
51
+ def forward(self, x, mask, rope):
52
+ B, T, D = x.shape
53
+ q, k, v = self.qkv(x).split(D, dim=2)
54
+ q = q.view(B, T, self.n_heads, self.dh).transpose(1, 2)
55
+ k = k.view(B, T, self.n_heads, self.dh).transpose(1, 2)
56
+ v = v.view(B, T, self.n_heads, self.dh).transpose(1, 2)
57
+ q, k = apply_rope(q, rope), apply_rope(k, rope)
58
+ # mask: (B, T) bool True=valid -> (B,1,1,T) additive
59
+ att_mask = ((~mask).unsqueeze(1).unsqueeze(2) * -1e4).to(q.dtype) # (B,1,1,T)
60
+ y = F.scaled_dot_product_attention(q, k, v, attn_mask=att_mask)
61
+ y = y.transpose(1, 2).contiguous().view(B, T, D)
62
+ return self.o(y)
63
+
64
+
65
+ class ConvStem(nn.Module):
66
+ """depthwise conv k=3 with dilations 1/2/3 -> fused local n-gram features. ~3.5K params."""
67
+ def __init__(self, d):
68
+ super().__init__()
69
+ self.convs = nn.ModuleList([nn.Conv1d(d, d, 3, padding=dl, dilation=dl, groups=d) for dl in (1, 2, 3)])
70
+ self.norm = RMSNorm(d)
71
+
72
+ def forward(self, x):
73
+ # x: (B, T, D); padding-aware: zero pad chars already are 0-emb? mask conv beyond seq via padding zeros
74
+ h = x.transpose(1, 2) # (B, D, T)
75
+ hs = [c(h) for c in self.convs]
76
+ out = x + sum(hs).transpose(1, 2)
77
+ return self.norm(out)
78
+
79
+ class SwiGLU(nn.Module):
80
+ def __init__(self, d, hidden):
81
+ super().__init__()
82
+ self.w1 = nn.Linear(d, hidden, bias=False)
83
+ self.w2 = nn.Linear(d, hidden, bias=False)
84
+ self.w3 = nn.Linear(hidden, d, bias=False)
85
+
86
+ def forward(self, x):
87
+ return self.w3(F.silu(self.w1(x)) * self.w2(x))
88
+
89
+
90
+ class Block(nn.Module):
91
+ def __init__(self, d, n_heads, ffn_hidden):
92
+ super().__init__()
93
+ self.n1 = RMSNorm(d)
94
+ self.attn = Attention(d, n_heads)
95
+ self.n2 = RMSNorm(d)
96
+ self.ffn = SwiGLU(d, ffn_hidden)
97
+
98
+ def forward(self, x, mask, rope):
99
+ x = x + self.attn(self.n1(x), mask, rope)
100
+ x = x + self.ffn(self.n2(x))
101
+ return x
102
+
103
+
104
+ class StudentG2P(nn.Module):
105
+ def __init__(self, vocab_size, n_labels, n_poly_chars, d=384, n_layers=6,
106
+ n_heads=6, ffn_hidden=1024, max_len=128, dropout=0.1, use_conv_stem=True,
107
+ emb_dim=None, bias_rank=0, n_passes=1):
108
+ self.n_passes = n_passes
109
+ super().__init__()
110
+ rope = rope_table(max_len, d // n_heads)
111
+ self.register_buffer('rope', rope, persistent=False)
112
+ emb_dim = emb_dim or d
113
+ self.emb = nn.Embedding(vocab_size, emb_dim, padding_idx=0)
114
+ self.emb_proj = nn.Linear(emb_dim, d, bias=False) if emb_dim != d else nn.Identity()
115
+ self.stem = ConvStem(d) if use_conv_stem else nn.Identity()
116
+ self.drop = nn.Dropout(dropout)
117
+ self.blocks = nn.ModuleList([Block(d, n_heads, ffn_hidden)
118
+ for _ in range(n_layers)])
119
+ self.n_final = RMSNorm(d)
120
+ self.readout = nn.Linear(d, n_labels, bias=False)
121
+ # tone-factored auxiliary heads (set via set_tone_heads)
122
+ self.n_syl = 0
123
+ # per (poly char, label) bias; low-rank option for tiny models
124
+ if bias_rank > 0:
125
+ self.char_bias = nn.Sequential(
126
+ nn.Embedding(n_poly_chars, bias_rank),
127
+ nn.Linear(bias_rank, n_labels, bias=False),
128
+ )
129
+ else:
130
+ self.char_bias = nn.Embedding(n_poly_chars, n_labels)
131
+ nn.init.zeros_(self.char_bias[-1].weight if bias_rank > 0 else self.char_bias.weight)
132
+ self.pos_head = nn.Linear(d, 11)
133
+ self.apply(self._init)
134
+
135
+ def _init(self, m):
136
+ if isinstance(m, nn.Linear):
137
+ nn.init.normal_(m.weight, std=0.02)
138
+ if m.bias is not None:
139
+ nn.init.zeros_(m.bias)
140
+
141
+ def set_tone_heads(self, n_syl, n_tone=6):
142
+ import torch.nn as nn
143
+ self.n_syl = n_syl
144
+ dev = self.readout.weight.device
145
+ self.syl_head = nn.Linear(self.readout.in_features, n_syl).to(dev)
146
+ self.tone_head = nn.Linear(self.readout.in_features, n_tone).to(dev)
147
+
148
+ def init_bias_prior(self, counts):
149
+ """counts: list (aligned with poly chars) of dict label_id -> count"""
150
+ import numpy as np
151
+ if isinstance(self.char_bias, nn.Sequential):
152
+ n = self.char_bias[0].weight.shape[0]
153
+ L = self.char_bias[-1].weight.shape[0]
154
+ else:
155
+ n = self.char_bias.weight.shape[0]
156
+ L = self.char_bias.weight.shape[1]
157
+ M = np.zeros((n, L), dtype=np.float32)
158
+ for i, d in enumerate(counts):
159
+ tot = sum(d.values()) + 1e-9
160
+ for j, c in d.items():
161
+ M[i, j] = math.log((c + 1.0) / (tot + len(d)))
162
+ if isinstance(self.char_bias, nn.Sequential):
163
+ U, S, Vh = np.linalg.svd(M, full_matrices=False)
164
+ r = self.char_bias[0].weight.shape[1]
165
+ emb = U[:, :r] * np.sqrt(S[:r])
166
+ lin = (np.sqrt(S[:r])[:, None] * Vh[:r]).T
167
+ with torch.no_grad():
168
+ self.char_bias[0].weight.copy_(torch.from_numpy(emb))
169
+ self.char_bias[-1].weight.copy_(torch.from_numpy(lin))
170
+ else:
171
+ with torch.no_grad():
172
+ self.char_bias.weight.copy_(torch.from_numpy(M))
173
+
174
+ def forward_with_ids(self, char_ids, poly_ids, positions, pos_mask, cand_mask):
175
+ pad = char_ids.ne(0)
176
+ x = self.emb_proj(self.emb(char_ids))
177
+ x = self.drop(x)
178
+ x = self.stem(x)
179
+ for blk in self.blocks:
180
+ x = blk(x, pad, self.rope)
181
+ h = self.n_final(x)
182
+ B, T, D = h.shape
183
+ P = positions.shape[1]
184
+ idx = positions.clamp(min=0).unsqueeze(-1).expand(B, P, D)
185
+ ph = torch.gather(h, 1, idx)
186
+ poly_ids_s = poly_ids.clamp(min=0)
187
+ logits = self.readout(ph) + self.char_bias(poly_ids_s)
188
+ masked = logits.masked_fill(~cand_mask, -1e4)
189
+ logp = F.log_softmax(masked.float(), dim=-1)
190
+ pos_logits = self.pos_head(ph)
191
+ extra = (self.syl_head(ph), self.tone_head(ph)) if self.n_syl else None
192
+ return logp, pos_logits, ph, masked, extra
g2ps_torch/runtime.py ADDED
@@ -0,0 +1,107 @@
1
+ """g2pS PyTorch runtime — same API as the numpy runtime, with GPU (CUDA) support.
2
+
3
+ Deps: torch, numpy.
4
+ GPU: moves model+inputs to CUDA automatically when available. Set fp16=True for
5
+ half-precision inference (recommended on tensor-core GPUs).
6
+ """
7
+ import os
8
+ import pickle
9
+
10
+ import numpy as np
11
+ import torch
12
+
13
+ from ._resolve import resolve_model_dir
14
+
15
+ from .modules import StudentG2P
16
+
17
+ MAX_LEN = 128
18
+
19
+ ARCH = {
20
+ 'small': dict(d=384, n_layers=6, n_heads=6, ffn_hidden=1024, emb_dim=None, bias_rank=0),
21
+ 'tiny': dict(d=256, n_layers=5, n_heads=4, ffn_hidden=640, emb_dim=128, bias_rank=64),
22
+ 'nano': dict(d=96, n_layers=6, n_heads=4, ffn_hidden=256, emb_dim=48, bias_rank=16, n_passes=2),
23
+ }
24
+
25
+
26
+ class G2pS:
27
+ _URLS = ['https://github.com/baicai-1145/g2pS-torch/raw/main/models/{t}/{t}.pt',
28
+ 'https://huggingface.co/baicai1145/g2pS/resolve/main/torch_weights/{t}/{t}.pt']
29
+
30
+ def __init__(self, model_dir, tier=None, device=None, fp16=None):
31
+ tier = tier or (model_dir if model_dir in ARCH else None) or os.path.basename(os.path.normpath(model_dir))
32
+ model_dir = resolve_model_dir(model_dir, ARCH, f'{tier}.pt',
33
+ [u.format(t=tier) for u in self._URLS])
34
+ self.tier = tier
35
+ self.device = device or ('cuda' if torch.cuda.is_available() else 'cpu')
36
+ self.fp16 = (fp16 if fp16 is not None else self.device == 'cuda') and self.device == 'cuda'
37
+
38
+ meta = pickle.load(open(os.path.join(model_dir, 'meta.pkl'), 'rb'))
39
+ self.vocab = meta['vocab']
40
+ self.labels = meta['labels']
41
+ self.poly_id = {c: i for i, c in enumerate(meta['poly_chars'])}
42
+ self.char_cands = meta['char_cands']
43
+ self.n_labels = len(self.labels)
44
+
45
+ kw = dict(ARCH[tier])
46
+ n_passes = kw.pop('n_passes', 1)
47
+ self.model = StudentG2P(len(self.vocab), len(self.labels), len(meta['poly_chars']),
48
+ max_len=MAX_LEN, use_conv_stem=True, n_passes=n_passes, **kw)
49
+ sd = torch.load(os.path.join(model_dir, tier + '.pt'), map_location='cpu')
50
+ sd = {k[10:] if k.startswith('_orig_mod.') else k: v for k, v in sd.items()}
51
+ sd = {k: v for k, v in sd.items() if not k.startswith(('syl_head', 'tone_head'))}
52
+ self.model.load_state_dict(sd)
53
+ self.model.to(self.device)
54
+ if self.fp16:
55
+ self.model.half()
56
+ self.model.eval()
57
+ self.dtype = torch.float16 if self.fp16 else torch.float32
58
+
59
+ def _prepare(self, text):
60
+ text = text.lower()
61
+ positions = [i for i, c in enumerate(text) if c in self.poly_id]
62
+ if not positions:
63
+ return None
64
+ if len(text) > MAX_LEN:
65
+ first = positions[0]
66
+ start = max(0, min(first - MAX_LEN // 2, len(text) - MAX_LEN))
67
+ text = text[start:start + MAX_LEN]
68
+ positions = [p - start for p in positions if start <= p < start + MAX_LEN]
69
+ if not positions:
70
+ return None
71
+ char_ids = np.array([[self.vocab.get(c, 1) for c in text]], dtype=np.int64)
72
+ P = len(positions)
73
+ poly_ids = np.array([[self.poly_id[text[p]] for p in positions]], dtype=np.int64)
74
+ pos_arr = np.array([positions], dtype=np.int64)
75
+ cand = np.zeros((1, P, self.n_labels), dtype=bool)
76
+ for k, p in enumerate(positions):
77
+ cand[0, k, list(self.char_cands[text[p]])] = True
78
+ return text, positions, char_ids, poly_ids, pos_arr, cand
79
+
80
+ def predict(self, text):
81
+ prep = self._prepare(text)
82
+ if prep is None:
83
+ return {}, text
84
+ text, positions, char_ids, poly_ids, pos_arr, cand = prep
85
+ dev = self.device
86
+ with torch.no_grad():
87
+ logp, _, _, _, _ = self.model.forward_with_ids(
88
+ torch.as_tensor(char_ids, device=dev),
89
+ torch.as_tensor(poly_ids, device=dev),
90
+ torch.as_tensor(pos_arr, device=dev),
91
+ torch.ones(1, len(positions), dtype=torch.bool, device=dev),
92
+ torch.as_tensor(cand, device=dev))
93
+ pred = logp.argmax(-1)[0].cpu().numpy()
94
+ return {p: self.labels[int(pred[k])] for k, p in enumerate(positions)}, text
95
+
96
+ def convert(self, text):
97
+ res, t = self.predict(text)
98
+ return t, [(c, res.get(i)) for i, c in enumerate(t)]
99
+
100
+
101
+ if __name__ == '__main__':
102
+ import sys
103
+ g = G2pS(sys.argv[1] if len(sys.argv) > 1 else 'models/nano')
104
+ print('device:', g.device, '| fp16:', g.fp16)
105
+ for s in ['他在银行工作,行踪不定。', '能把小孩哄睡是个技术活。']:
106
+ _, pairs = g.convert(s)
107
+ print(s, '->', [(c, p) for c, p in pairs if p])
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: g2ps-torch
3
+ Version: 0.1.0
4
+ Summary: g2pS PyTorch backend (GPU + fp16)
5
+ Requires-Python: >=3.8
6
+ Requires-Dist: numpy
@@ -0,0 +1,12 @@
1
+ g2ps_torch/__init__.py,sha256=ab4S7abmoWlWylOKA8ajspt8ir8tcUlGb1aiWt8lLKg,26
2
+ g2ps_torch/_resolve.py,sha256=_UtLV6jlFUsrsbPFs14tACY6HL6XJYsxo7erAYDEc3k,1595
3
+ g2ps_torch/modules.py,sha256=2TvSJF2fPqdQOtsNyONceiQ7ODleBam-RPUuPSgb9JM,7724
4
+ g2ps_torch/runtime.py,sha256=ooy-1weo2GVJKZ4DbiaspVi0l5Va3W7UwOmyezOUbGw,4660
5
+ g2ps_torch/models/nano/meta.pkl,sha256=oxziLhr2oZ_1aL1-5sGaT2VALiuCwOWdurRck3JDZ4U,77622
6
+ g2ps_torch/models/nano/nano.pt,sha256=5TlMLwDFY67JWC2LkXvBEHh46rBO4kwsxYK5q2fq4IE,4304251
7
+ g2ps_torch/models/small/meta.pkl,sha256=krEDH4PE5clMaGpNXLePhhoBQD_DuDsUvsdvuN1qlko,123832
8
+ g2ps_torch/models/tiny/meta.pkl,sha256=oxziLhr2oZ_1aL1-5sGaT2VALiuCwOWdurRck3JDZ4U,77622
9
+ g2ps_torch-0.1.0.dist-info/METADATA,sha256=kuAHN6Wf7p_GKKR3X94r4kzY6_5nsP4HeNThu7HvdRc,141
10
+ g2ps_torch-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
11
+ g2ps_torch-0.1.0.dist-info/top_level.txt,sha256=HZRCxz0yNEiD4E8LzWTFkFzqZqTEcAEAGtoIaVzwpts,11
12
+ g2ps_torch-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_torch