ExecuTorch
swipe-typing
gesture-typing
shape-writing
keyboard
on-device
ctc
mobile
lee-futo commited on
Commit
345ea55
·
verified ·
1 Parent(s): a504628

Add model card: overview, models, ExecuTorch quickstart

Browse files
Files changed (1) hide show
  1. README.md +174 -0
README.md ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: other
3
+ license_name: futo-model-weights-license-1.0
4
+ license_link: LICENSE.md
5
+ library_name: executorch
6
+ pipeline_tag: token-classification
7
+ tags:
8
+ - swipe-typing
9
+ - gesture-typing
10
+ - shape-writing
11
+ - keyboard
12
+ - on-device
13
+ - executorch
14
+ - ctc
15
+ - mobile
16
+ ---
17
+
18
+ # FUTO Swipe
19
+
20
+ Mobile-oriented models for decoding swipe gestures into text.
21
+
22
+ <p align="center">
23
+ <img src="https://huggingface.co/futo-org/futo-swipe/resolve/main/animations/swipe_demo_computer.gif" width="640" alt="Swipe decode of the word 'computer'">
24
+ </p>
25
+
26
+ See the paper, [*coming soon*](https://huggingface.co/futo-org/futo-swipe),
27
+ for details.
28
+
29
+ ## Models
30
+
31
+ This repository contains 3 models that compose together.
32
+ Only the encoder is required; the decoder and language model are
33
+ additional refinements, leveraging specific layout and language information.
34
+ The encoder can decode for **any** keyboard layout,
35
+ while the decoder is English/QWERTY-only and the language model is English-only.
36
+
37
+ | Model | Codename | Role | Params | Size (fp32) |
38
+ |-------|----------|------|-------:|------------:|
39
+ | **Encoder** | `honorable_sturgeon` | Maps a swipe trajectory to per-timestep character emissions. Layout-agnostic — works on any keyboard supplied at runtime. | 635 K | 2.4 MB |
40
+ | **Decoder** | `magic_macaw` | (Optional) per-layout refinement over the encoder's frozen features. Lifts top-k where layout-specific training data exists (English here). | 304 K | 1.2 MB |
41
+ | **Context LM** | `hungry_jellyfish` | (Optional) next-word and beam-rerank language model that blends sentence context into candidate ranking. | — | — |
42
+
43
+ ### Encoder — `honorable_sturgeon`
44
+
45
+ A 1D temporal convolutional network (TCN) reads the raw `(x, y)` touch trajectory
46
+ and emits a 64-coefficient spectral pattern and a scalar
47
+ "intention" gate for each timestep. Per-key character scores are read off by evaluating a
48
+ fixed cosine (DCT) basis at the layout key centers. Switching layouts on
49
+ device requires no retraining, just a different key-coordinate tensor.
50
+
51
+ ### Decoder (English/QWERTY) — `magic_macaw`
52
+
53
+ A small DFSMN decoder over the frozen encoder features. It refines the
54
+ character distribution on specific layouts.
55
+ Currently we only have data for training an English/QWERTY decoder.
56
+
57
+ ### Context LM (English) — `hungry_jellyfish`
58
+
59
+ A causal DFSMN language model with a hash embedding for large vocabularies.
60
+ It can supply a rerank score and perform next-word prediction. This model
61
+ can be used with or without a decoder.
62
+
63
+ ## Getting started
64
+
65
+ The example below demonstrates the encoder on CPU (x86) with the ExecuTorch
66
+ runtime and greedy-decodes a swipe into characters. Note that greedy decoding
67
+ is fairly inaccurate and should generally be improved by constraining to a vocabulary
68
+ lexicon (eg. trie, WFST).
69
+
70
+ ```python
71
+ import numpy as np
72
+ import torch
73
+ from huggingface_hub import hf_hub_download
74
+ from executorch.runtime import Runtime
75
+
76
+ # QWERTY letter centers in the normalized [0,1] keyboard frame. This is the
77
+ # only layout-specific input — swap in another layout's key coordinates to
78
+ # decode a keyboard the encoder never saw at training time.
79
+ QWERTY = {
80
+ "a": (0.10, 0.500), "b": (0.60, 0.833), "c": (0.40, 0.833), "d": (0.30, 0.500),
81
+ "e": (0.25, 0.167), "f": (0.40, 0.500), "g": (0.50, 0.500), "h": (0.60, 0.500),
82
+ "i": (0.75, 0.167), "j": (0.70, 0.500), "k": (0.80, 0.500), "l": (0.90, 0.500),
83
+ "m": (0.80, 0.833), "n": (0.70, 0.833), "o": (0.85, 0.167), "p": (0.95, 0.167),
84
+ "q": (0.05, 0.167), "r": (0.35, 0.167), "s": (0.20, 0.500), "t": (0.45, 0.167),
85
+ "u": (0.65, 0.167), "v": (0.50, 0.833), "w": (0.15, 0.167), "x": (0.30, 0.833),
86
+ "y": (0.55, 0.167), "z": (0.20, 0.833),
87
+ }
88
+ LETTERS = sorted(QWERTY)
89
+ MAX_KEYS = 64 # export-time padding bound
90
+
91
+ # A real swipe for the word "computer": normalized x, y and timestamps (ms).
92
+ PX = [0.4141, 0.4478, 0.5, 0.5741, 0.6599, 0.7256, 0.7744, 0.8098, 0.8485, 0.867,
93
+ 0.8737, 0.8653, 0.8418, 0.8182, 0.8098, 0.7963, 0.7946, 0.8081, 0.8418, 0.8704,
94
+ 0.9057, 0.9259, 0.9545, 0.9697, 0.968, 0.9529, 0.9141, 0.8468, 0.7811, 0.7273,
95
+ 0.6869, 0.6616, 0.6582, 0.6431, 0.6061, 0.5572, 0.5067, 0.4663, 0.4495, 0.4461,
96
+ 0.4411, 0.4192, 0.3872, 0.362, 0.3283, 0.2795, 0.2391, 0.2323, 0.2407, 0.2593,
97
+ 0.2879, 0.3249, 0.3468, 0.3569]
98
+ PY = [0.8991, 0.858, 0.7876, 0.6702, 0.5352, 0.4237, 0.3357, 0.2653, 0.1655, 0.142,
99
+ 0.142, 0.2183, 0.3709, 0.588, 0.7347, 0.8462, 0.8697, 0.811, 0.6115, 0.4707,
100
+ 0.3122, 0.2066, 0.1303, 0.1068, 0.1068, 0.1068, 0.1185, 0.1596, 0.1772, 0.1772,
101
+ 0.1772, 0.189, 0.189, 0.189, 0.1831, 0.189, 0.189, 0.189, 0.189, 0.189,
102
+ 0.1831, 0.1831, 0.1831, 0.1831, 0.1831, 0.1948, 0.189, 0.1948, 0.189, 0.189,
103
+ 0.189, 0.1831, 0.1831, 0.1831]
104
+ PT = [0.0, 100, 149, 197, 246, 297, 348, 399, 449, 498, 548, 598, 648, 698, 749, 799,
105
+ 849, 949, 999, 1047, 1100, 1152, 1197, 1248, 1314, 1364, 1414, 1465, 1515, 1565,
106
+ 1614, 1666, 1715, 1851, 1898, 1951, 1998, 2049, 2097, 2165, 2231, 2279, 2331,
107
+ 2382, 2431, 2481, 2532, 2584, 2649, 2700, 2751, 2798, 2848, 2899]
108
+
109
+
110
+ def resample(px, py, pt, T=64):
111
+ """Resample a variable-length trajectory to T evenly-spaced points -> [2, T]."""
112
+ x, y, t = map(np.asarray, (px, py, pt))
113
+ t = t - t[0]
114
+ if t[-1] > 1e-3: # uniform 60 Hz resample, then to T points
115
+ n60 = max(2, round(t[-1] / (1000.0 / 60.0)) + 1)
116
+ tt = np.linspace(0.0, t[-1], n60)
117
+ x, y = np.interp(tt, t, x), np.interp(tt, t, y)
118
+ idx = np.linspace(0, len(x) - 1, T)
119
+ rx = np.interp(idx, np.arange(len(x)), x)
120
+ ry = np.interp(idx, np.arange(len(y)), y)
121
+ return np.stack([rx, ry], axis=0).astype(np.float32)
122
+
123
+
124
+ def greedy_ctc(log_emissions):
125
+ """Collapse the per-timestep argmax into a string (blank is the last class)."""
126
+ blank = log_emissions.shape[-1] - 1
127
+ out, prev = [], -1
128
+ for c in log_emissions[0].argmax(axis=-1):
129
+ if c != prev and c != blank and c < len(LETTERS):
130
+ out.append(LETTERS[c])
131
+ prev = c
132
+ return "".join(out)
133
+
134
+
135
+ # Load the encoder .pte and run one forward pass.
136
+ pte = hf_hub_download("futo-org/futo-swipe", "honorable_sturgeon/model_fp32.pte")
137
+ encoder = Runtime.get().load_program(pte).load_method("forward")
138
+
139
+ features = torch.from_numpy(resample(PX, PY, PT)[None]) # [1, 2, 64]
140
+ keys = torch.zeros(1, MAX_KEYS, 2) # [1, 64, 2]
141
+ mask = torch.zeros(1, MAX_KEYS, dtype=torch.bool) # [1, 64]
142
+ for i, ch in enumerate(LETTERS):
143
+ keys[0, i] = torch.tensor(QWERTY[ch])
144
+ mask[0, i] = True
145
+
146
+ log_emissions, coefficients, lambda_ = encoder.execute((features, keys, mask))
147
+ print("greedy decode:", greedy_ctc(log_emissions.numpy())) # -> "computer"
148
+ ```
149
+
150
+ Example output:
151
+
152
+ ```
153
+ greedy decode: computer
154
+ ```
155
+
156
+ ### Encoder inputs and outputs
157
+
158
+ | | Tensor | Shape | Meaning |
159
+ |---|--------|-------|---------|
160
+ | **in** | `features` | `[1, 2, 64]` | Swipe trajectory `(x, y)` resampled to 64 points |
161
+ | **in** | `layout_keys` | `[1, 64, 2]` | Per-key `(x, y)` centers, padded to 64 keys |
162
+ | **in** | `layout_mask` | `[1, 64]` | Boolean mask of valid keys |
163
+ | **out** | `log_emissions` | `[1, 32, 65]` | Log-probabilities over 64 keys + blank |
164
+ | **out** | `coefficients` | `[1, 32, 64]` | Spectral coefficients |
165
+ | **out** | `lambda` | `[1, 32, 1]` | *Intention* gate |
166
+
167
+ The output time dimension is **32**, half the 64 input points: the encoder
168
+ applies a 2× temporal downsample (a stride-2 adapter) inside the network, so
169
+ the 64 trajectory steps become 32 emission steps.
170
+
171
+ ## License
172
+
173
+ Released under the [FUTO Model Weights License 1.0](LICENSE.md).
174
+