typingnorm 0.1.0__tar.gz

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.
@@ -0,0 +1,117 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["v*", "js-v*"]
7
+ pull_request:
8
+ workflow_dispatch:
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ python:
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ python: ["3.9", "3.11", "3.13"]
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: actions/setup-python@v5
23
+ with:
24
+ python-version: ${{ matrix.python }}
25
+ - run: pip install -e ".[dev]"
26
+ - run: pytest -q
27
+
28
+ # 模型的輸出分佈必須持續對得上 profile.json 裡的實測值。
29
+ # 這條會抓到「改了時序邏輯卻沒重新校準」這類改壞。
30
+ - name: 模型對帳
31
+ run: python -m typingnorm.simulate --validate
32
+
33
+ # 修正邏輯與按鍵序列的合法性:同一個實體鍵不得在放開前又被按下
34
+ - name: 模擬器自檢
35
+ run: python -m typingnorm.simulate --selftest
36
+
37
+ js:
38
+ runs-on: ubuntu-latest
39
+ strategy:
40
+ matrix:
41
+ node: ["18", "22"]
42
+ steps:
43
+ - uses: actions/checkout@v4
44
+ - uses: actions/setup-node@v4
45
+ with:
46
+ node-version: ${{ matrix.node }}
47
+ # 零依賴,不需要安裝任何東西
48
+ - run: node --test
49
+ working-directory: js
50
+
51
+ # 兩份實作必須對同一組向量給出相同判定。向量由 Python 端產生,
52
+ # 這裡確認它是最新的;js 的 vectors.test.js 則確認 JS 跟得上。
53
+ parity:
54
+ runs-on: ubuntu-latest
55
+ steps:
56
+ - uses: actions/checkout@v4
57
+ - uses: actions/setup-python@v5
58
+ with:
59
+ python-version: "3.13"
60
+ - run: pip install -e .
61
+ - name: 向量與 Python 實作同步
62
+ run: python tools/gen_vectors.py --check
63
+
64
+ build-python:
65
+ needs: [python, parity]
66
+ runs-on: ubuntu-latest
67
+ steps:
68
+ - uses: actions/checkout@v4
69
+ - uses: actions/setup-python@v5
70
+ with:
71
+ python-version: "3.13"
72
+ - run: pip install build
73
+ - run: python -m build
74
+ # 資料檔沒被打包進去是很典型的漏,裝起來實際 import 一次才算數
75
+ - name: 驗證輪檔含常模
76
+ run: |
77
+ pip install dist/*.whl
78
+ cd /tmp && python -c "
79
+ import typingnorm, json, importlib.resources as r
80
+ p = json.loads((r.files('typingnorm') / 'profile.json').read_text())
81
+ assert p['source']['license'] == 'CC BY 4.0'
82
+ print('常模來源:', p['source']['name'])
83
+ print('版本', typingnorm.__version__, '判準', list(typingnorm.RULES))"
84
+ - uses: actions/upload-artifact@v4
85
+ with:
86
+ name: dist
87
+ path: dist/
88
+
89
+ publish-pypi:
90
+ if: startsWith(github.ref, 'refs/tags/v')
91
+ needs: build-python
92
+ runs-on: ubuntu-latest
93
+ environment: pypi
94
+ permissions:
95
+ id-token: write # Trusted Publishing 用 OIDC,不需要在 repo 存 token
96
+ steps:
97
+ - uses: actions/download-artifact@v4
98
+ with:
99
+ name: dist
100
+ path: dist/
101
+ - uses: pypa/gh-action-pypi-publish@release/v1
102
+
103
+ publish-npm:
104
+ if: startsWith(github.ref, 'refs/tags/js-v')
105
+ needs: [js, parity]
106
+ runs-on: ubuntu-latest
107
+ environment: npm
108
+ permissions:
109
+ id-token: write # npm Trusted Publishing,同樣不需要存 token
110
+ steps:
111
+ - uses: actions/checkout@v4
112
+ - uses: actions/setup-node@v4
113
+ with:
114
+ node-version: "22"
115
+ registry-url: "https://registry.npmjs.org"
116
+ - run: npm publish --provenance --access public
117
+ working-directory: js
@@ -0,0 +1,8 @@
1
+ dist/
2
+ build/
3
+ *.egg-info/
4
+ __pycache__/
5
+ *.pyc
6
+ .pytest_cache/
7
+ node_modules/
8
+ .venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MangoCore LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,291 @@
1
+ # 從擊鍵動態資料集擬合真人打字時序模型
2
+
3
+ ## 摘要
4
+
5
+ 本文記錄 `human_typist` 時序模型的參數來源與驗證方法。所有時序常數擬合自 KeyRecs [1],一份採 CC BY 4.0 授權、允許商業使用的公開擊鍵動態資料集,樣本為 99 位受試者、562,372 筆 digraph。模型輸出的擊鍵間隔中位數與資料集相差 1% 至 4%,變異係數相差 0.01 至 0.14,退格率相差 12% 以內。
6
+
7
+ 模型的核心結構是把按鍵的「按下」與「放開」分開排程,因此 rollover(下一鍵在上一鍵放開前就按下)不需要另外建模,而是兩條邊際分佈交叉後自然湧現的結果。我們用資料驗證了這個設計的前提:dwell 與擊鍵間隔在對數尺度上幾乎不相關(r = -0.035,n = 404,739)。
8
+
9
+ ## 1. 問題
10
+
11
+ 自動化打字工具普遍以固定或均勻隨機的延遲送出按鍵。這與真人打字有數個結構性差異,其中最顯著的是 Dhakal 等人 [2] 在 16.8 萬人的研究中指出的 rollover 現象:真人打字時下一鍵經常在上一鍵放開之前就按下,且重疊程度與打字速度高度相關。嚴格序列化的模擬器,其按鍵重疊率恆為零。
12
+
13
+ 本專案的目標是建立一個時序模型,其輸出的統計分佈可以與真人資料對帳,且對帳過程隨程式碼一起發布、可重跑。
14
+
15
+ ## 2. 資料集選擇
16
+
17
+ ### 2.1 選用 KeyRecs 的理由
18
+
19
+ 我們評估了數份公開的擊鍵資料集,最終選用 KeyRecs [1],主要考量有二。
20
+
21
+ **授權。** KeyRecs 採 CC BY 4.0,明確允許商業使用,僅要求標註來源。相對地,規模最大的 Aalto 136M Keystrokes [2] 授權為「研究與非商業用途」,其導出參數若進入商業產品會有授權疑慮。既然本專案希望能被商業產品使用,資料源的授權就是硬性條件,而不是偏好。
22
+
23
+ **欄位。** KeyRecs 以 digraph 形式提供 `DU`(同一鍵的按下到放開,即 dwell)、`DD`(前後兩鍵的按下間隔,即擊鍵間隔)、`UD`(前一鍵放開到後一鍵按下,負值即 rollover)。這三個量正好是模型需要的全部輸入,不需要再從原始時間戳推導。
24
+
25
+ 資料集另含自由書寫(free-text)與固定文字(fixed-text)兩部分。我們只使用自由書寫部分,因為固定文字是重複輸入同一組密碼,其節奏不能代表一般打字。
26
+
27
+ ### 2.2 樣本
28
+
29
+ 自由書寫部分共 562,372 筆 digraph。以每人至少 200 筆有效間隔為門檻,得 99 位受試者。受試者速度中位數為 48.5 WPM,與文獻中一般人口的平均值相符 [2]。
30
+
31
+ ## 3. 方法
32
+
33
+ ### 3.1 速度的操作型定義
34
+
35
+ 打字速度以吞吐量定義。打 N 個字元耗時 N × mean(IKI) 秒,字數為 N / 5,故
36
+
37
+ WPM = 60 / (5 × mean(IKI)) = 12 / mean(IKI)
38
+
39
+ 以此指派每位受試者所屬的速度級距,級距寬度 10 WPM。
40
+
41
+ 需要注意的是,模型在產生擊鍵時**並非**由此式反推對數常態的 mu。真實的擊鍵間隔分佈比對數常態更尖峰厚尾:若讓對數常態去對平均數,其中位數會比實測中位數高出兩到三成。因此模型改為讓擊鍵層對上該級距的**實測中位數**,分佈的長尾則交由認知停頓層產生。
42
+
43
+ ### 3.2 過濾條件
44
+
45
+ 擊鍵間隔取 0.02 至 2.0 秒,dwell 取 0.02 至 0.6 秒。下界排除量測雜訊與按鍵彈跳,上界排除離開鍵盤等級的中斷。所有事後的模型對帳統計都採用**相同的窗口**,否則是拿蘋果比橘子。
46
+
47
+ 樣本數少於 20,000 筆的速度級距不納入 profile,因為級距層級的估計會被雜訊主導。最終涵蓋 30 至 80 WPM。
48
+
49
+ ### 3.3 倍率的正規化
50
+
51
+ 轉換類別(換手/同手/同指)、字詞熟悉度、字內位置這三組倍率,各自除以其**頻率加權平均數**後才寫入 profile。如此三組倍率的期望值皆為 1,相乘後不會改變整體速度,速度只由級距參數決定。少了這一步,倍率會與速度參數互相污染。
52
+
53
+ ## 4. 結果
54
+
55
+ ### 4.1 各速度級距
56
+
57
+ | WPM 級距 | ln(IKI) 的 sigma | IKI 中位數 | 字母鍵 dwell | 變異係數 | rollover | n |
58
+ |---|---|---|---|---|---|---|
59
+ | 30 | 0.712 | 246 ms | 83 ms | 0.84 | 2.0% | 68,400 |
60
+ | 40 | 0.705 | 204 ms | 90 ms | 0.88 | 10.7% | 133,730 |
61
+ | 50 | 0.634 | 178 ms | 95 ms | 0.85 | 13.1% | 130,292 |
62
+ | 60 | 0.612 | 155 ms | 96 ms | 0.82 | 22.5% | 112,555 |
63
+ | 70 | 0.577 | 140 ms | 87 ms | 0.80 | 22.2% | 49,268 |
64
+ | 80 | 0.627 | 122 ms | 92 ms | 0.82 | 31.0% | 43,255 |
65
+
66
+ 兩項結構性發現:
67
+
68
+ **打字越快的人,節奏越規律。** sigma 自 0.712 遞減至 0.627,變異係數亦呈下降趨勢。這與「打得快所以抖動大」的直覺相反,也推翻了模型早期版本的假設。
69
+
70
+ **dwell 幾乎不隨速度改變,但擊鍵間隔大幅縮短。** dwell 中位數在 83 至 96 ms 之間波動,無明顯趨勢;同期間 IKI 中位數自 246 ms 降至 122 ms。兩條曲線的交叉點隨速度前移,rollover 因而自 2.0% 升至 31.0%。
71
+
72
+ ### 4.2 按鍵停留時間
73
+
74
+ | 鍵 | 中位數 | 對數標準差 |
75
+ |---|---|---|
76
+ | 字母 | 91 ms | 0.343 |
77
+ | 空白 | 91 ms | 0.326 |
78
+ | Shift | 254 ms | 0.440 |
79
+ | 退格 | 79 ms | 0.339 |
80
+
81
+ Shift 的停留時間是字母鍵的 2.8 倍。任何以「按下、短暫停留、放開」統一處理所有按鍵的模擬器,在 Shift 上都會有數量級的偏差。
82
+
83
+ ### 4.3 前後兩鍵關係的倍率
84
+
85
+ 已依 3.3 正規化,n = 298,920。
86
+
87
+ | 類別 | 倍率 |
88
+ |---|---|
89
+ | 左右手交替 | 0.890 |
90
+ | 同手不同指 | 1.129 |
91
+ | 同指連按 | 1.072 |
92
+
93
+ **手的交替是主要效應,手指層級的區分則否。** 換手比同手快兩成以上,此效應穩定;而同指連按的懲罰僅 7%,甚至低於同手不同指。這與坊間「同指連按慢 50%」的說法相去甚遠。Dhakal 等人 [2] 亦指出,由不同手或不同手指輸入的字母對,比重複輸入更能預測打字速度。
94
+
95
+ ### 4.4 字詞層級效應
96
+
97
+ 以空白切分還原單字,取長度 3 至 14 且不含退格與修飾鍵的乾淨單字,n = 167,252。
98
+
99
+ | 類別 | 倍率 |
100
+ |---|---|
101
+ | 高頻字(前 150 字) | 0.914 |
102
+ | 其他字 | 1.047 |
103
+ | 字的頭尾 | 0.937 |
104
+ | 字的中段 | 1.078 |
105
+
106
+ 高頻字僅快約 13%。模型早期的手調版本假設高頻字快兩倍以上,經此修正。
107
+
108
+ ### 4.5 錯誤修正行為
109
+
110
+ 以每字元的退格次數衡量,中位數 0.0676(p25 = 0.0472,p75 = 0.0906)。模型的 `error_rate` 參數校準至 0.046 以重現此中位數;校準後模型輸出為 0.0686。
111
+
112
+ ### 4.6 dwell 與擊鍵間隔的獨立性
113
+
114
+ 以 ln(dwell) 對 ln(IKI) 迴歸,n = 404,739,得斜率 -0.0185、相關係數 r = -0.0345、殘差標準差 0.3395(無條件標準差為 0.3397)。
115
+
116
+ 兩者實質上互相獨立。這驗證了模型的一項關鍵簡化:dwell 與間隔可以獨立抽樣,rollover 不需要另外的機率參數,它就是 P(dwell > IKI) 這個交叉事件本身。
117
+
118
+ ## 5. 模型
119
+
120
+ ### 5.1 事件流
121
+
122
+ 模型的輸出是一串帶時間戳的按下與放開事件,而不是一連串 sleep。核心不做任何 I/O,給定亂數種子完全可重現。實際送出按鍵、或送進瀏覽器、或原地讀進測試斷言,都是這串事件的消費端。
123
+
124
+ ### 5.2 rollover 的湧現
125
+
126
+ 每個按鍵在按下時即排程其放開時刻。當下一鍵的按下時刻早於前一鍵的放開時刻,重疊自然發生。模型不含任何「rollover 機率」參數。
127
+
128
+ 唯一的例外是**同一個實體鍵不得重疊**。人的手指無法在放開之前再次按下同一鍵,且作業系統收到連續兩個 keydown 而無 keyup 時的行為未定義。模型在按下前檢查該鍵是否仍被按住,若是則先放開。此條件由 `--selftest` 逐一驗證。
129
+
130
+ Shift 的處理亦為例外:其序列不與後續按鍵重疊,否則下一個字母會一併被大寫。這是模型刻意犧牲的一點保真度,換取輸出正確性。
131
+
132
+ ### 5.3 校準常數
133
+
134
+ 模型中有一個非直接擬合、而是反解得到的常數。認知停頓層會把整體擊鍵間隔的中位數推高約 14%,而資料集的中位數本來就包含受試者自己的停頓,因此擊鍵層需先扣除此量,模型的整體輸出才能對上資料集的整體統計。此係數(0.88)由 `--validate` 反解,並在該指令中持續驗證;停頓層若有改動即須重新校準。
135
+
136
+ ## 6. 驗證
137
+
138
+ `human_typist.py --validate` 跨 40 個亂數種子執行模型,將輸出分佈與 profile 中的實測值對帳。統計窗口與擬合時相同。
139
+
140
+ | WPM | IKI 中位(模型/資料) | 變異係數(模型/資料) | rollover(模型/資料) | 退格率(模型/資料) |
141
+ |---|---|---|---|---|
142
+ | 30 | 241 / 246 ms | 0.85 / 0.84 | 9.9% / 2.0% | 0.0703 / 0.0676 |
143
+ | 40 | 205 / 204 ms | 0.87 / 0.88 | 15.4% / 10.7% | 0.0709 / 0.0676 |
144
+ | 50 | 181 / 178 ms | 0.86 / 0.85 | 19.3% / 13.1% | 0.0762 / 0.0676 |
145
+ | 60 | 158 / 155 ms | 0.89 / 0.82 | 24.2% / 22.5% | 0.0727 / 0.0676 |
146
+ | 70 | 144 / 140 ms | 0.91 / 0.80 | 24.4% / 22.2% | 0.0748 / 0.0676 |
147
+ | 80 | 127 / 122 ms | 0.96 / 0.82 | 32.7% / 31.0% | 0.0758 / 0.0676 |
148
+
149
+ 中位數在全速度域內誤差不超過 4%。rollover 在 60 至 80 WPM 區間誤差在 2 個百分點內,此區間亦是資料量最大之處;低速段則系統性高估,見 8.2。
150
+
151
+ 另有兩項不涉及分佈的正確性檢查,由 `--selftest` 執行 60 組種子 × 兩段文字 × 30 至 100 WPM:輸出文字在經過所有打錯與修正之後必須等於原文;按鍵事件序列中不得出現同一鍵未放開即再次按下。
152
+
153
+ ## 7. 與另一資料集的交叉檢核
154
+
155
+ 開發期間我們同時取用 Aalto 136M Keystrokes [2] 作為獨立檢核。該資料集規模大得多(168,000 人),但授權限研究與非商業用途,因此**其導出的參數並未進入本套件**;`profile.json` 中的每一個數值皆來自 KeyRecs。此處僅報告兩者的結構比較,屬該授權允許的研究用途。
156
+
157
+ | 量 | KeyRecs(本套件採用) | Aalto(僅作檢核) |
158
+ |---|---|---|
159
+ | 換手交替倍率 | 0.890 | 0.902 |
160
+ | 同手不同指 | 1.129 | 1.080 |
161
+ | 同指連按 | 1.072 | 1.129 |
162
+ | 字母鍵 dwell | 91 ms | 106 ms |
163
+ | Shift dwell | 254 ms | 239 ms |
164
+ | sigma 隨速度 | 0.712 → 0.627 遞減 | 0.721 → 0.461 遞減 |
165
+ | rollover 隨速度 | 2.0% → 31.0% 遞增 | 6.6% → 41.2% 遞增 |
166
+
167
+ 四項結構性結論在兩份獨立資料集上均成立:換手交替最快、同指懲罰遠小於坊間說法、sigma 隨速度遞減、rollover 隨速度大幅遞增。絕對值有差異,主要來自任務型態不同(KeyRecs 為自由書寫,Aalto 為短句轉錄)與速度指派方式不同。
168
+
169
+ 同指與同手兩類的相對順序在兩份資料中並不一致,顯示手指層級的區分落在雜訊範圍內;穩定的是手的交替。
170
+
171
+ ### 7.1 生成模型的泛化
172
+
173
+ 取樣 2,245 位 Aalto 受試者,逐人計算統計量後取級距中位數,與 KeyRecs 擬合值比較:
174
+
175
+ | 速度級距 | KeyRecs 擬合的 rollover | Aalto 個人中位數 |
176
+ |---|---|---|
177
+ | 40 | 10.7% | 9.9% |
178
+ | 50 | 13.1% | 16.7% |
179
+ | 60 | 22.5% | 22.3% |
180
+ | 70 | 22.2% | 27.0% |
181
+ | 80 | 31.0% | 34.3% |
182
+
183
+ 以 99 人擬合的模型,在 2,245 位完全不同的受試者上仍能重現 rollover 隨速度上升的量級。dwell 則有系統性差異,KeyRecs 為 83 至 96 ms、Aalto 為 96 至 113 ms。此差異**未予修正**:以保留集的結果回頭調整參數會構成測試集污染,且該資料集的授權不允許其導出值進入發布物。
184
+
185
+ ### 7.2 偵測判準的誤判率
186
+
187
+ 同一組 2,245 人用來量測三條偵測判準會誤判多少真人:
188
+
189
+ | 判準 | 誤判 |
190
+ |---|---|
191
+ | dwell 變異係數 < 0.05 | 0 / 2245(0.00%) |
192
+ | rollover 恰為零 | 7 / 2245(0.31%) |
193
+ | 換手比值 >= 1.00 | 347 / 2245(15.46%) |
194
+
195
+ rollover 恰為零的 7 人速度介於 16 至 39 WPM,中位數 28。此判準的代價因此是明確且有偏的:誤判只會落在慢速使用者身上。
196
+
197
+ 換手比值不可作為硬性判準。該效應在慢速端會反轉:最慢 5%(13 至 28 WPM)的比值中位數為 1.103,112 人中有 77 人的換手反而較慢。這是觸控打字的簽名,不是人類的簽名,兩指輸入者本來就沒有手部分工。
198
+
199
+ 另需修正 5.2 的一項陳述。以正規化後的鍵名判定時,2,245 人中有 15 人(0.67%)出現「同鍵重疊」,成因是左右 Shift 被映射為同一個鍵名。該判準要成立,必須以實體 keycode 判定並排除修飾鍵。
200
+
201
+ ## 8. 限制
202
+
203
+ ### 8.1 未經擬合的參數
204
+
205
+ 以下參數無實測依據,為保守估計,於程式碼中均有標註:
206
+
207
+ - **N-gram 額外加成(0.92)。** 高頻字根的加速多半已被換手交替解釋(th、he、an 恰好都是左右手交替),此處僅保留輕微的額外加成。
208
+ - **認知停頓的長度。** 語塊停頓與標點後停頓的秒數範圍為估計值。資料集的 IKI 中已包含受試者的停頓,但在沒有語意標註的情況下無法將「寫作停頓」與「擊鍵節奏」分離。
209
+ - **疲勞曲線。** 資料集的作業時間過短,量不到長時間打字的速度衰減。
210
+
211
+ ### 8.2 低速段的 rollover 高估
212
+
213
+ 模型在 30 至 50 WPM 區間高估 rollover 達 6 至 8 個百分點。由 4.6 可知 dwell 與 IKI 確實獨立,故此偏差不來自獨立性假設。較可能的原因是低速級距的受試者人數偏少:各級距的 dwell 中位數在 83 至 96 ms 間非單調變動,此種不規則性通常反映樣本覆蓋不足。
214
+
215
+ ### 8.3 樣本與適用範圍
216
+
217
+ 99 位受試者,以歐洲籍為主。profile 涵蓋 30 至 80 WPM;超出此範圍時模型按比例外推並沿用最接近級距的形狀參數,該外推未經驗證。
218
+
219
+ 鍵盤佈局固定為 QWERTY。非 ASCII 字元(含中文)目前直接送出,未模擬注音或拼音的組字過程;該行為與英文擊鍵是不同的模型,宜另行處理。
220
+
221
+ ## 9. 中文輸入法的量測
222
+
223
+ 本文其餘部分的模型只涵蓋拉丁字母輸入。中文透過輸入法組字,擊鍵與輸出字元不是一對一,因此必須先確認瀏覽器在組字期間還提供哪些資訊,才能判斷同一套方法能否延伸。
224
+
225
+ ### 9.1 方法
226
+
227
+ 以一頁探針記錄 `keydown`、`keyup`、`compositionstart`、`compositionupdate`、`compositionend`、`beforeinput`、`input` 的完整欄位。受試者先以英文輸入一段基準文字,再切換至注音輸入法輸入一段中文,同一次工作階段、同一副鍵盤。環境為 macOS 上的 Chromium 系瀏覽器搭配系統內建注音。
228
+
229
+ ### 9.2 量測層的結論
230
+
231
+ 組字期間 `event.code` 完整保留(51/51)。`event.keyCode` 在 keydown 為 229,但在 keyup 為真實值,且兩者都帶 `code`,因此 dwell 可以配對計算。
232
+
233
+ `event.key` 直接回傳注音符號本身(`code` 為 KeyR 時 `key` 為 ㄐ)。鍵位對照表因此不需要寫死,可由實際使用者的事件反推,連不同注音鍵盤佈局都能自動辨識。
234
+
235
+ `compositionend` 每句只觸發一次,一次回傳整句已上屏文字;逐字對齊必須改用 `compositionupdate` 的字串長度變化。每個字皆以聲調鍵或空白鍵結束,字的邊界因此有明確訊號,不需要以空白鍵作為推進判定。
236
+
237
+ 事件的派送順序與 `timeStamp` 不一致,曾觀察到時間戳較早的 keyup 在較晚的事件之後才送達。處理時必須依 `timeStamp` 排序。時間戳本身反映原始輸入時刻,未受輸入法處理延遲影響。
238
+
239
+ ### 9.3 注音與英文的時序差異
240
+
241
+ 同一受試者、同一次工作階段:
242
+
243
+ | 量 | 英文 | 注音 |
244
+ |---|---|---|
245
+ | dwell 中位數 | 73.0 ms | 88.3 ms |
246
+ | 擊鍵間隔中位數 | 147.5 ms | 82.6 ms |
247
+ | rollover | 0.0% | 51.1% |
248
+ | 同時按住的最大鍵數 | 1 | 4 |
249
+ | 速度 | 48 WPM | 134 字/分 |
250
+ | KSPC | 1.0 | 4.00 |
251
+
252
+ 注音的擊鍵間隔約為英文的一半,重疊率超過五成。成因是注音的一個字為聲母、韻母、聲調、上屏構成的單一運動單元,四個按鍵以和弦方式一次擊出,手指在其中重疊。英文以字母為決策單位,注音以字為決策單位。
253
+
254
+ 此差異的直接後果是:任何以拉丁文字打字校準的偵測系統,套用於注音輸入者時會在兩個方向上失準。注音的正常重疊率在英文分佈中屬於極端值;反之,以英文判準看注音時,「零重疊」是遠比英文更強的合成訊號。
255
+
256
+ ### 9.4 此節的限制
257
+
258
+ 本節為單一受試者、單一瀏覽器、單一作業系統的量測,樣本量不足以支持任何分佈性結論,僅用於確認量測層可行與指出量級差異。Windows 上的微軟注音、WebKit、以及倉頡與拼音等其他輸入法均未測試。
259
+
260
+ ## 10. 重現
261
+
262
+ ```bash
263
+ # 下載資料集(31 MB,CC BY 4.0)
264
+ curl -L -o free-text.csv "https://zenodo.org/records/7886743/files/free-text.csv?download=1"
265
+
266
+ # 重跑擬合,產生 profile.json
267
+ python3 fit/build_profile.py
268
+
269
+ # 對帳
270
+ python3 human_typist.py --validate
271
+
272
+ # 保留驗證(需自備 Aalto 資料集,僅供研究用途,結果不得回頭調參)
273
+ python3 validate/aalto_holdout.py 2500
274
+
275
+ # 正確性檢查
276
+ python3 human_typist.py --selftest
277
+ ```
278
+
279
+ ## 11. 引用與授權
280
+
281
+ 本套件的時序參數導出自:
282
+
283
+ > [1] Dias, T., Vitorino, J., Maia, E., Sousa, O., & Praça, I. (2023). KeyRecs: A keystroke dynamics and typing pattern recognition dataset. *Data in Brief*, 50, 109509. https://doi.org/10.1016/j.dib.2023.109509
284
+
285
+ 該資料集採 **CC BY 4.0** 授權,允許商業使用,條件為標註來源。本文件與 `profile.json` 的 `source` 欄位即構成該標註。使用本套件的下游產品無須在使用者介面顯示任何標註。
286
+
287
+ 第 7 節的比較資料出自:
288
+
289
+ > [2] Dhakal, V., Feit, A. M., Kristensson, P. O., & Oulasvirta, A. (2018). Observations on Typing from 136 Million Keystrokes. *Proceedings of the 2018 CHI Conference on Human Factors in Computing Systems*. https://userinterfaces.aalto.fi/136Mkeystrokes/
290
+
291
+ 該資料集授權限研究與非商業用途。本專案僅將其用於結構比較,未重新散布其資料,亦未將其導出參數納入發布物。
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.5
2
+ Name: typingnorm
3
+ Version: 0.1.0
4
+ Summary: 人類打字的常模,以及據此判斷輸入是否為真人的判準。每條判準的誤判率都在獨立資料集上量過。
5
+ Project-URL: Homepage, https://github.com/gba3124/typingnorm
6
+ Project-URL: Source, https://github.com/gba3124/typingnorm
7
+ Project-URL: Method, https://github.com/gba3124/typingnorm/blob/main/METHOD.md
8
+ Author: MangoCore LLC
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: bot-detection,human-verification,keystroke-dynamics,liveness,typing
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
17
+ Classifier: Topic :: Security
18
+ Requires-Python: >=3.9
19
+ Provides-Extra: desktop
20
+ Requires-Dist: pynput>=1.7; extra == 'desktop'
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=7; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # typingnorm
26
+
27
+ 人類打字的常模,以及據此判斷一段輸入是否為真人的判準。
28
+
29
+ 每條判準的誤判率都在一份獨立資料集上量過,數字寫在程式碼裡,方法寫在
30
+ [METHOD.md](METHOD.md),擬合與驗證的腳本都在這個 repo 裡,你可以自己重跑。
31
+
32
+ ```python
33
+ from typingnorm import check
34
+
35
+ report = check(events) # events: [(毫秒, 'D'|'U', 實體鍵碼, key), ...]
36
+
37
+ report.verdict # 'human-consistent' | 'synthetic-signals' | 'insufficient-data'
38
+ report.flags # ['zero_rollover']
39
+ report.measures # {'wpm': 61.2, 'rollover_pct': 0.0, 'dwell_cv': 0.31, ...}
40
+ report.false_positive_budget # 0.0031,觸發判準當中最高的誤判率
41
+ ```
42
+
43
+ ## 判準
44
+
45
+ 只有三條,而且都是刻意選的。
46
+
47
+ | 判準 | 內容 | 在 2,245 位真人上的誤判率 |
48
+ |---|---|---|
49
+ | `same_key_overlap` | 同一個實體鍵在放開前又被按下 | 0.00% |
50
+ | `constant_dwell` | 每個鍵按住的時間幾乎完全一樣 | 0.00% |
51
+ | `zero_rollover` | 完全沒有按鍵重疊(僅 35 WPM 以上套用) | 0.31% |
52
+
53
+ 選擇的原則是**只用不受打字速度影響的判準**。打字快與慢的人,重疊率、間隔、按住時間
54
+ 全都差很多,任何「離人類平均多遠」的判準都會系統性誤判速度極端的人,而那恰好是最
55
+ 不該被誤傷的族群:密碼管理器、語音輸入、切換式存取、螢幕小鍵盤的使用者在時序上都
56
+ 長得像機器人。
57
+
58
+ 被排除在外的例子:換手比值(左右手交替是否比同手快)看起來是很好的特徵,實測卻在
59
+ 慢速端反轉,用它當硬判準會誤判 15.46% 的真人。詳見 METHOD.md 第 7.2 節。
60
+
61
+ ## 這個方法擋得住什麼、擋不住什麼
62
+
63
+ 擋得住不用心的自動化。擋不住刻意模仿真人時序的程式,包括本套件自己的模擬器。
64
+
65
+ 所以 `check()` 回傳的是訊號不是判決,也刻意不提供 `is_human` 這種布林值:那會誘導
66
+ 呼叫端拿它當閘門。請把它當成風險分數的其中一個輸入,不要當成身分驗證。
67
+
68
+ 樣本不足時會回 `insufficient-data` 而不是猜。少於 150 次擊鍵的統計不可靠,登入表單
69
+ 那種二三十個按鍵的長度做不了任何判斷。
70
+
71
+ ## 速度指標
72
+
73
+ 同一份原始資料可以用不同國家的慣用指標計分,因為分數不儲存,只在顯示時現算。
74
+
75
+ ```python
76
+ from typingnorm import score, load_session
77
+
78
+ s = load_session("session.json")
79
+ score(s) # 依語言自動選當地慣用指標
80
+ score(s, "kdph_ssc") # 換成印度公職考試的單位
81
+ ```
82
+
83
+ | 指標 | 單位 | 出處 |
84
+ |---|---|---|
85
+ | `cpm_tqc` | 字/分 | TQC 中文輸入,專業級 80 |
86
+ | `cpm_jp` | 字/分 | 全商速度部門,1 級 70 |
87
+ | `tasu` | 타수 | 워드프로세서 1급 300,按字母鍵計數 |
88
+ | `kdph_ssc` | KDPH | SSC 公職考試,10,500 = 35 WPM |
89
+ | `wpm_net_5` | WPM | 英文慣例,五字元一詞 |
90
+ | `wpm_4` | คำ/นาที | 泰文慣例,四字元一詞 |
91
+ | `kpm` | 打鍵/分 | 跨語言比較用的共同分母 |
92
+
93
+ 標為 `authoritative=False` 的指標沒有官方標準,介面上不應出現「合格」「專業級」
94
+ 這類字眼。
95
+
96
+ ## 模擬器
97
+
98
+ `typingnorm.simulate` 從同一份常模抽樣,產生帶時間戳的按鍵事件流。它存在的主要理由
99
+ 是給偵測端當測試對手:本套件的測試就是拿它產生的真人樣本,驗證判準不會誤判。
100
+
101
+ ```bash
102
+ python -m typingnorm.simulate --validate # 輸出分佈與常模對帳
103
+ python -m typingnorm.simulate --selftest # 修正邏輯與按鍵序列合法性
104
+ ```
105
+
106
+ ## 資料來源
107
+
108
+ 常模擬合自 KeyRecs(99 位受試者、562,372 筆 digraph,CC BY 4.0,**允許商業使用**):
109
+
110
+ > Dias, T., Vitorino, J., Maia, E., Sousa, O., & Praça, I. (2023). KeyRecs: A keystroke
111
+ > dynamics and typing pattern recognition dataset. *Data in Brief*, 50, 109509.
112
+
113
+ 誤判率則在 Aalto 136M Keystrokes 的 2,245 位受試者上獨立量測。該資料集授權限研究與
114
+ 非商業用途,**其導出參數未進入本套件**,僅用於驗證,且驗證結果不回頭調整參數。
115
+
116
+ ## 授權
117
+
118
+ MIT。
@@ -0,0 +1,94 @@
1
+ # typingnorm
2
+
3
+ 人類打字的常模,以及據此判斷一段輸入是否為真人的判準。
4
+
5
+ 每條判準的誤判率都在一份獨立資料集上量過,數字寫在程式碼裡,方法寫在
6
+ [METHOD.md](METHOD.md),擬合與驗證的腳本都在這個 repo 裡,你可以自己重跑。
7
+
8
+ ```python
9
+ from typingnorm import check
10
+
11
+ report = check(events) # events: [(毫秒, 'D'|'U', 實體鍵碼, key), ...]
12
+
13
+ report.verdict # 'human-consistent' | 'synthetic-signals' | 'insufficient-data'
14
+ report.flags # ['zero_rollover']
15
+ report.measures # {'wpm': 61.2, 'rollover_pct': 0.0, 'dwell_cv': 0.31, ...}
16
+ report.false_positive_budget # 0.0031,觸發判準當中最高的誤判率
17
+ ```
18
+
19
+ ## 判準
20
+
21
+ 只有三條,而且都是刻意選的。
22
+
23
+ | 判準 | 內容 | 在 2,245 位真人上的誤判率 |
24
+ |---|---|---|
25
+ | `same_key_overlap` | 同一個實體鍵在放開前又被按下 | 0.00% |
26
+ | `constant_dwell` | 每個鍵按住的時間幾乎完全一樣 | 0.00% |
27
+ | `zero_rollover` | 完全沒有按鍵重疊(僅 35 WPM 以上套用) | 0.31% |
28
+
29
+ 選擇的原則是**只用不受打字速度影響的判準**。打字快與慢的人,重疊率、間隔、按住時間
30
+ 全都差很多,任何「離人類平均多遠」的判準都會系統性誤判速度極端的人,而那恰好是最
31
+ 不該被誤傷的族群:密碼管理器、語音輸入、切換式存取、螢幕小鍵盤的使用者在時序上都
32
+ 長得像機器人。
33
+
34
+ 被排除在外的例子:換手比值(左右手交替是否比同手快)看起來是很好的特徵,實測卻在
35
+ 慢速端反轉,用它當硬判準會誤判 15.46% 的真人。詳見 METHOD.md 第 7.2 節。
36
+
37
+ ## 這個方法擋得住什麼、擋不住什麼
38
+
39
+ 擋得住不用心的自動化。擋不住刻意模仿真人時序的程式,包括本套件自己的模擬器。
40
+
41
+ 所以 `check()` 回傳的是訊號不是判決,也刻意不提供 `is_human` 這種布林值:那會誘導
42
+ 呼叫端拿它當閘門。請把它當成風險分數的其中一個輸入,不要當成身分驗證。
43
+
44
+ 樣本不足時會回 `insufficient-data` 而不是猜。少於 150 次擊鍵的統計不可靠,登入表單
45
+ 那種二三十個按鍵的長度做不了任何判斷。
46
+
47
+ ## 速度指標
48
+
49
+ 同一份原始資料可以用不同國家的慣用指標計分,因為分數不儲存,只在顯示時現算。
50
+
51
+ ```python
52
+ from typingnorm import score, load_session
53
+
54
+ s = load_session("session.json")
55
+ score(s) # 依語言自動選當地慣用指標
56
+ score(s, "kdph_ssc") # 換成印度公職考試的單位
57
+ ```
58
+
59
+ | 指標 | 單位 | 出處 |
60
+ |---|---|---|
61
+ | `cpm_tqc` | 字/分 | TQC 中文輸入,專業級 80 |
62
+ | `cpm_jp` | 字/分 | 全商速度部門,1 級 70 |
63
+ | `tasu` | 타수 | 워드프로세서 1급 300,按字母鍵計數 |
64
+ | `kdph_ssc` | KDPH | SSC 公職考試,10,500 = 35 WPM |
65
+ | `wpm_net_5` | WPM | 英文慣例,五字元一詞 |
66
+ | `wpm_4` | คำ/นาที | 泰文慣例,四字元一詞 |
67
+ | `kpm` | 打鍵/分 | 跨語言比較用的共同分母 |
68
+
69
+ 標為 `authoritative=False` 的指標沒有官方標準,介面上不應出現「合格」「專業級」
70
+ 這類字眼。
71
+
72
+ ## 模擬器
73
+
74
+ `typingnorm.simulate` 從同一份常模抽樣,產生帶時間戳的按鍵事件流。它存在的主要理由
75
+ 是給偵測端當測試對手:本套件的測試就是拿它產生的真人樣本,驗證判準不會誤判。
76
+
77
+ ```bash
78
+ python -m typingnorm.simulate --validate # 輸出分佈與常模對帳
79
+ python -m typingnorm.simulate --selftest # 修正邏輯與按鍵序列合法性
80
+ ```
81
+
82
+ ## 資料來源
83
+
84
+ 常模擬合自 KeyRecs(99 位受試者、562,372 筆 digraph,CC BY 4.0,**允許商業使用**):
85
+
86
+ > Dias, T., Vitorino, J., Maia, E., Sousa, O., & Praça, I. (2023). KeyRecs: A keystroke
87
+ > dynamics and typing pattern recognition dataset. *Data in Brief*, 50, 109509.
88
+
89
+ 誤判率則在 Aalto 136M Keystrokes 的 2,245 位受試者上獨立量測。該資料集授權限研究與
90
+ 非商業用途,**其導出參數未進入本套件**,僅用於驗證,且驗證結果不回頭調整參數。
91
+
92
+ ## 授權
93
+
94
+ MIT。