typeling 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.
typeling/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Offline English autocomplete: completes the word being typed, predicts the next word and learns
2
+ the user's own words. Pure Python, no third-party dependencies."""
3
+
4
+ from .predictor import DEFAULT_MODEL, Predictor
5
+
6
+ __all__ = ["DEFAULT_MODEL", "Predictor"]
typeling/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .demo import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
typeling/demo.py ADDED
@@ -0,0 +1,300 @@
1
+ """
2
+ Terminal autocomplete demo: completes the word being typed and predicts the next word after a space, as grey ghost
3
+ text and in a candidate bar below the line.
4
+
5
+ python -m typeling # start typing; the grey text is the suggestion
6
+ python -m typeling --no-learn # do not learn or save your words
7
+ python -m typeling --lm # use Typeling-25M (pip install "typeling[lm]"); --lm DIR for another model
8
+ python -m typeling --bench # measure per-keystroke latency
9
+ echo I wan | python -m typeling # non-interactive: prints "I wan[t]"
10
+
11
+ Keys: Tab / Right accept the highlighted candidate (the first one, shown as ghost text when it completes the typed
12
+ letters; with the language model it may also correct them), Up / Down move the highlight, Esc hide, Backspace delete
13
+ a character, Ctrl+W delete a word, Enter new line, Ctrl+C quit.
14
+ Each line submitted with Enter is learned by the user model, which is stored only on this machine
15
+ (~/.typeling/user.json, shared by both models) and saved on exit.
16
+ """
17
+
18
+ import argparse
19
+ import os
20
+ import re
21
+ import shutil
22
+ import sys
23
+ import time
24
+ from pathlib import Path
25
+
26
+ from .predictor import Predictor
27
+ from .text import normalize, split_input
28
+
29
+ USER_FILE = Path.home() / ".typeling" / "user.json"
30
+
31
+ GREY, INVERT, RESET = "\x1b[90m", "\x1b[7m", "\x1b[0m"
32
+ PROMPT = "> "
33
+ ACCEPT_ADDS_SPACE = False # append a space after an accepted suggestion
34
+ CANDIDATES = 5 # shown in the bar (the first 3 are the ones the benchmark measures)
35
+
36
+
37
+ class LineEditor:
38
+ """Pure state machine: consumes keys and returns the strings to draw; never touches the terminal."""
39
+
40
+ def __init__(self, completer, candidates=CANDIDATES):
41
+ self.c = completer
42
+ self.n = candidates
43
+ self.text = ""
44
+ self.hidden = False # set by Esc; cleared by the next edit
45
+ self.sel = 0 # highlighted candidate
46
+ self._cache = (None, "", []) # (text, ghost, candidates): one inference per distinct text
47
+ self.latencies = []
48
+
49
+ def view(self):
50
+ """-> (ghost text, [candidate words]) for the current text."""
51
+ if self.hidden:
52
+ return "", []
53
+ if self._cache[0] != self.text:
54
+ t0 = time.perf_counter()
55
+ cands = [w for w, _ in self.c.topk(self.text, self.n)] if self.n else []
56
+ g = self.c.suggest(self.text)
57
+ self.latencies.append(time.perf_counter() - t0)
58
+ self._cache = (self.text, g, cands)
59
+ return self._cache[1], self._cache[2]
60
+
61
+ def accept(self):
62
+ """Replace the partly typed word with the highlighted candidate (or append the ghost text)."""
63
+ g, cands = self.view()
64
+ if self.sel == 0 and g:
65
+ self.text += g
66
+ elif cands:
67
+ sp = split_input(normalize(self.text))
68
+ partial = sp[1] if sp else ""
69
+ self.text = self.text[:len(self.text) - len(partial)] + cands[self.sel]
70
+ else:
71
+ return
72
+ self.text += " " if ACCEPT_ADDS_SPACE else ""
73
+
74
+ def feed(self, key):
75
+ """Handle one key; return "enter", "quit" or None."""
76
+ if key is None:
77
+ return None
78
+ if key in ("UP", "DOWN"):
79
+ n = len(self.view()[1])
80
+ if n:
81
+ self.sel = (self.sel + (1 if key == "DOWN" else -1)) % n
82
+ return None
83
+ if key in ("TAB", "RIGHT"):
84
+ self.accept()
85
+ elif key == "BS":
86
+ self.text = self.text[:-1]
87
+ elif key == "DELWORD":
88
+ self.text = re.sub(r"\S*$", "", self.text.rstrip())
89
+ elif key == "ESC":
90
+ self.hidden = True
91
+ return None
92
+ elif key == "ENTER":
93
+ return "enter"
94
+ elif key == "QUIT":
95
+ return "quit"
96
+ elif len(key) == 1 and key.isprintable():
97
+ self.text += key
98
+ else:
99
+ return None
100
+ self.hidden = False
101
+ self.sel = 0
102
+ return None
103
+
104
+ def render(self, width, show=True):
105
+ """Two lines, the text with its ghost and the candidate bar; leaves the cursor after the text."""
106
+ g, cands = self.view() if show else ("", [])
107
+ avail = max(10, width - len(PROMPT) - 1)
108
+ g = g[: avail - 1]
109
+ room = avail - len(g)
110
+ vis = self.text if len(self.text) <= room else self.text[-room:]
111
+ s = "\r" + PROMPT + vis + "\x1b[K" + (f"{GREY}{g}{RESET}" if g else "")
112
+ bar, used = [], len(PROMPT)
113
+ for i, w in enumerate(cands):
114
+ item = f"{i + 1} {w}"
115
+ if used + len(item) + 2 > width - 1:
116
+ break
117
+ bar.append(f"{INVERT}{item}{RESET}" if i == self.sel else f"{GREY}{item}{RESET}")
118
+ used += len(item) + 2
119
+ return s + "\r\n\x1b[K" + " " * len(PROMPT) + " ".join(bar) + f"\x1b[1A\r\x1b[{len(PROMPT) + len(vis)}C"
120
+
121
+
122
+ # Raw keyboard input, per platform.
123
+
124
+ if os.name == "nt":
125
+ import ctypes
126
+ import msvcrt
127
+
128
+ _SPECIAL = {"M": "RIGHT", "K": "LEFT", "H": "UP", "P": "DOWN"}
129
+ _KEYS = {"\x08": "BS", "\x7f": "DELWORD", "\x17": "DELWORD", "\r": "ENTER",
130
+ "\t": "TAB", "\x1b": "ESC", "\x03": "QUIT", "\x04": "QUIT", "\x1a": "QUIT"}
131
+
132
+ def _read_one():
133
+ ch = msvcrt.getwch()
134
+ if ch in ("\x00", "\xe0"):
135
+ return _SPECIAL.get(msvcrt.getwch())
136
+ return _KEYS.get(ch, ch)
137
+
138
+ def read_keys():
139
+ """Block for one key, then drain any queued keys so fast typing triggers a single redraw."""
140
+ keys = [_read_one()]
141
+ while msvcrt.kbhit():
142
+ keys.append(_read_one())
143
+ return keys
144
+
145
+ class raw_mode:
146
+ def __enter__(self):
147
+ # Enable ANSI escape sequences (colors, cursor movement) in this console.
148
+ k32 = ctypes.windll.kernel32
149
+ h = k32.GetStdHandle(-11)
150
+ mode = ctypes.c_uint32()
151
+ if k32.GetConsoleMode(h, ctypes.byref(mode)):
152
+ k32.SetConsoleMode(h, mode.value | 0x0004)
153
+ return self
154
+
155
+ def __exit__(self, *exc):
156
+ return False
157
+
158
+ else:
159
+ import termios
160
+ import tty
161
+
162
+ _ARROWS = {"A": "UP", "B": "DOWN", "C": "RIGHT", "D": "LEFT"}
163
+ _KEYS = {"\x7f": "BS", "\x08": "DELWORD", "\x17": "DELWORD", "\r": "ENTER",
164
+ "\n": "ENTER", "\t": "TAB", "\x03": "QUIT", "\x04": "QUIT"}
165
+
166
+ def read_keys():
167
+ data = os.read(sys.stdin.fileno(), 1024).decode("utf-8", "ignore")
168
+ keys, i = [], 0
169
+ while i < len(data):
170
+ if data[i] == "\x1b":
171
+ if data[i + 1 : i + 2] in ("[", "O") and i + 2 < len(data):
172
+ keys.append(_ARROWS.get(data[i + 2]))
173
+ i += 3
174
+ continue
175
+ keys.append("ESC")
176
+ else:
177
+ keys.append(_KEYS.get(data[i], data[i]))
178
+ i += 1
179
+ return keys
180
+
181
+ class raw_mode:
182
+ def __enter__(self):
183
+ self.fd = sys.stdin.fileno()
184
+ self.old = termios.tcgetattr(self.fd)
185
+ tty.setcbreak(self.fd)
186
+ return self
187
+
188
+ def __exit__(self, *exc):
189
+ termios.tcsetattr(self.fd, termios.TCSADRAIN, self.old)
190
+ return False
191
+
192
+
193
+ def _stats(latencies):
194
+ if not latencies:
195
+ return ""
196
+ us = sorted(x * 1e6 for x in latencies)
197
+ p99 = us[min(len(us) - 1, int(len(us) * 0.99))]
198
+ return f"{len(us)} suggestions: mean {sum(us) / len(us):.0f} µs, p99 {p99:.0f} µs, max {us[-1]:.0f} µs"
199
+
200
+
201
+ def load(args, learn=False):
202
+ """-> (predictor, description) for the command-line options."""
203
+ user = USER_FILE if learn else None
204
+ note = f"learning your words ({USER_FILE})" if learn else "learning off"
205
+ if args.lm:
206
+ from .lm import LMPredictor # needs typeling[lm]
207
+ if args.threads:
208
+ import torch
209
+ torch.set_num_threads(args.threads)
210
+ return LMPredictor(args.lm, user, device=args.device), f"language model {args.lm} · {note}"
211
+ return Predictor(user=user), note
212
+
213
+
214
+ def interactive(args):
215
+ learn = not args.no_learn
216
+ t0 = time.perf_counter()
217
+ p, note = load(args, learn)
218
+ ed = LineEditor(p, args.candidates)
219
+ load_ms = (time.perf_counter() - t0) * 1000
220
+ out = sys.stdout
221
+ width = lambda: shutil.get_terminal_size().columns
222
+ with raw_mode():
223
+ out.write(f"{GREY}Model loaded in {load_ms:.0f} ms · {note} · Tab/→ accept · ↑/↓ choose · Esc hide · "
224
+ f"Ctrl+W delete word · Enter new line · Ctrl+C quit{RESET}\r\n")
225
+ out.write(ed.render(width()))
226
+ out.flush()
227
+ try:
228
+ while True:
229
+ for k in read_keys():
230
+ r = ed.feed(k)
231
+ if r == "quit":
232
+ raise KeyboardInterrupt
233
+ if r == "enter":
234
+ out.write(ed.render(width(), show=False) + "\r\n")
235
+ if learn and ed.text.strip():
236
+ p.learn(ed.text)
237
+ ed.text = ""
238
+ out.write(ed.render(width()))
239
+ out.flush()
240
+ except KeyboardInterrupt:
241
+ out.write(ed.render(width(), show=False) + "\r\n")
242
+ if learn:
243
+ p.save()
244
+ print(_stats(ed.latencies))
245
+
246
+
247
+ def pipe_mode(args):
248
+ c, _ = load(args)
249
+ for line in sys.stdin:
250
+ line = line.rstrip("\r\n")
251
+ g = c.suggest(line)
252
+ print(f"{line}[{g}]" if g else line)
253
+
254
+
255
+ def bench(args):
256
+ t0 = time.perf_counter()
257
+ c, note = load(args)
258
+ print(f"Model loaded in {(time.perf_counter() - t0) * 1000:.0f} ms ({note})")
259
+ text = ("I want to know whether this little model can keep up with really fast "
260
+ "typing because everyone expects instant suggestions while writing ") * 30
261
+ ed = LineEditor(c, args.candidates)
262
+ for ch in text: # type one key at a time
263
+ ed.feed(ch)
264
+ ed.view()
265
+ for _ in range(200): # then backspace one key at a time
266
+ ed.feed("BS")
267
+ ed.view()
268
+ print(_stats(ed.latencies))
269
+ for s in ["I wan", "th", "Hel", "WOR", "encyc", "(beaut", "I don", "xylo", "Thank you so ", "how are ",
270
+ "See you tomm"]:
271
+ g = c.suggest(s)
272
+ cands = ", ".join(w for w, _ in c.topk(s, args.candidates))
273
+ print(f" {s!r:>15} → {s}{GREY}{g}{RESET}" + ("" if g else " (no suggestion)") + f" [{cands}]")
274
+
275
+
276
+ def main():
277
+ try:
278
+ sys.stdout.reconfigure(encoding="utf-8")
279
+ except Exception:
280
+ pass
281
+ ap = argparse.ArgumentParser(prog="typeling", description="Terminal autocomplete demo")
282
+ ap.add_argument("--no-learn", action="store_true", help="do not learn or save your words")
283
+ ap.add_argument("--lm", metavar="MODEL", nargs="?", const="ScarletKc/Typeling-25M",
284
+ help='use the language model (pip install "typeling[lm]"): Typeling-25M from Hugging Face, or '
285
+ "another repo id or directory")
286
+ ap.add_argument("--device", default=None, help="language model device: cpu or cuda (default: cuda if available)")
287
+ ap.add_argument("--threads", type=int, default=None, help="CPU threads for the language model")
288
+ ap.add_argument("--candidates", type=int, default=CANDIDATES, help=f"candidates in the bar (default {CANDIDATES})")
289
+ ap.add_argument("--bench", action="store_true", help="measure per-keystroke latency")
290
+ args = ap.parse_args()
291
+ if args.bench:
292
+ bench(args)
293
+ elif not sys.stdin.isatty():
294
+ pipe_mode(args)
295
+ else:
296
+ interactive(args)
297
+
298
+
299
+ if __name__ == "__main__":
300
+ main()
typeling/en.bin ADDED
Binary file
typeling/keyboard.py ADDED
@@ -0,0 +1,43 @@
1
+ """
2
+ QWERTY keyboard error model: the strings that a single typing error turns into the typed letters.
3
+
4
+ from typeling.keyboard import corrections
5
+ {q for q, kind in corrections("tomm")} # contains "tom" (extra key), "tpmm" (adjacent key), ...
6
+
7
+ Used by the language model decoder's typo repair (typeling.lm) and by the typo benchmark (lm/typos.py).
8
+ """
9
+
10
+ ROWS, OFFSETS = ("qwertyuiop", "asdfghjkl", "zxcvbnm"), (0.0, 0.25, 0.75)
11
+ ALPHABET = "abcdefghijklmnopqrstuvwxyz'"
12
+
13
+
14
+ def _neighbors():
15
+ pos = {c: (r, i + OFFSETS[r]) for r, row in enumerate(ROWS) for i, c in enumerate(row)}
16
+ return {c: "".join(d for d, (r2, x2) in pos.items() if d != c and (
17
+ (r2 == r and abs(x2 - x) <= 1) or (abs(r2 - r) == 1 and abs(x2 - x) < 1))) for c, (r, x) in pos.items()}
18
+
19
+
20
+ NEIGHBORS = _neighbors() # g -> tyfhvb
21
+
22
+
23
+ def corrections(t):
24
+ """Typed letters -> (intended letters, kind) for each single error that turns the intended letters into t:
25
+ sub an adjacent key was hit instead
26
+ del an extra key was hit, doubling or next to a neighboring letter
27
+ ins a letter or apostrophe was left out (not at the end, where it is simply not typed yet)
28
+ swap two neighboring letters were swapped
29
+ case the first letter was not capitalized
30
+ Results may repeat and need not be words."""
31
+ low = t.lower()
32
+ for i, c in enumerate(t):
33
+ nb = NEIGHBORS.get(low[i], "")
34
+ for d in nb:
35
+ yield t[:i] + (d.upper() if c.isupper() else d) + t[i + 1:], "sub"
36
+ if len(t) > 1 and any(x == low[i] or x in nb for x in low[max(0, i - 1):i] + low[i + 1:i + 2]):
37
+ yield t[:i] + t[i + 1:], "del"
38
+ for d in ALPHABET[:-1] if i == 0 else ALPHABET:
39
+ yield t[:i] + d + t[i:], "ins"
40
+ if i + 1 < len(t) and low[i] != low[i + 1]:
41
+ yield t[:i] + t[i + 1] + c + t[i + 2:], "swap"
42
+ if t[:1].islower():
43
+ yield t[0].upper() + t[1:], "case"