कीस्ट्रोक लॉगिंग
Potato किसी फ़्री-टेक्स्ट उत्तर के पीछे के ठहराव, बर्स्ट, संशोधन और पेस्ट रिकॉर्ड कर सकता है, बिना उन अक्षरों में से कोई भी रिकॉर्ड किए जो एनोटेटर टाइप करता है।
Potato यह रिकॉर्ड कर सकता है कि कोई फ़्री-टेक्स्ट उत्तर कैसे तैयार हुआ, बिना उत्तर को रिकॉर्ड किए। हर इवेंट के साथ एक timestamp, एक input type, एक कुंजी वर्ग और लंबाई में हुआ बदलाव जुड़ा होता है; इनमें से किसी के साथ वह अक्षर नहीं जाता जो टाइप किया गया था। उसी स्ट्रीम से Potato लगभग चालीस सारांश फ़ीचर निकालता है और उन्हें एनोटेशन के साथ रख देता है।
मक़सद यह है कि लिखे गए उत्तर को, जो किसी सोचते-समझते व्यक्ति के ठहरावों और दोबारा-विचारों के साथ टाइप हुआ हो, ऐसे उत्तर से अलग बताया जा सके जो किसी दूसरी विंडो से दोबारा टाइप किया गया हो या किसी चैटबॉट से पेस्ट कर दिया गया हो। तैयार उत्तर पढ़ें तो वे एक जैसे लगते हैं। लॉग एक जैसे नहीं लगते।
कीस्ट्रोक लॉगिंग के लिए Potato 2.7.2 या उसके बाद का संस्करण चाहिए, और यह डिफ़ॉल्ट रूप से बंद है: जब तक आप इसे सेट न करें, keystroke_logging.enabled false रहता है, इसलिए अपग्रेड करने से किसी की रिकॉर्डिंग शुरू नहीं होती। इस डेटा पर बने नियमों के लिए लेखन-प्रक्रिया पहचान देखें। इसे मानव प्रतिभागियों पर लगाने से पहले कीस्ट्रोक लॉगिंग नैतिकता पढ़ें।
तुरंत शुरुआत
keystroke_logging:
enabled: trueपूरा न्यूनतम कॉन्फ़िगरेशन बस इतना ही है। प्रोजेक्ट का हर फ़्री-टेक्स्ट फ़ील्ड एक कंटेंट-ब्लाइंड इवेंट स्ट्रीम, एक सारांश, और पहचान फ़्लैग का एक सेट देने लगता है।
Potato के साथ एक चलाने योग्य उदाहरण आता है:
python potato/flask_server.py start examples/advanced/keystroke-logging/config.yaml -p 8000चेतावनी:
enabledडिफ़ॉल्ट रूप सेfalseहै। Potato अपग्रेड करने से आपके एनोटेटर की रिकॉर्डिंग चुपचाप शुरू नहीं होती।
क्या कैप्चर होता है
हर इवेंट एक timestamp, एक input type, कुंजी का वर्ग, caret की स्थिति, और फ़ील्ड की लंबाई में हुआ बदलाव दर्ज करता है:
{t_ms: 1240, input_type: "insertText", key_class: "letter", pos: 41, delta: +1}
{t_ms: 1310, input_type: "insertText", key_class: "letter", pos: 42, delta: +1}
{t_ms: 3980, input_type: "deleteContentBackward", key_class: "bksp", pos: 42, delta: -1}
{t_ms: 9120, input_type: "insertFromPaste", key_class: "unknown",pos: 43, delta: +287,
meta: {paste_source: "external", paste_hash: "sekqf3"}}
क्या जानबूझकर कैप्चर नहीं होता
| कैप्चर नहीं होता | क्यों |
|---|---|
| टाइप किए गए अक्षर | स्ट्रीम प्रक्रिया दोबारा बनाती है, टेक्स्ट नहीं |
| पेस्ट किया गया टेक्स्ट | सिर्फ़ एक लंबाई, एक स्रोत लेबल, और एक salted hash |
| बीच के मसौदे | सिर्फ़ लंबाई के बदलावों से दोबारा नहीं बनाए जा सकते |
| पासवर्ड फ़ील्ड की कोई भी सामग्री | getFieldIdentity type="password" को सीधे अस्वीकार कर देता है |
| आम तौर पर क्लिपबोर्ड की सामग्री | पेस्ट के समय वर्गीकरण के लिए पढ़ी जाती है, फिर हटा दी जाती है |
कुंजी वर्ग
कुंजी ख़ुद कभी संग्रहीत नहीं होती, सिर्फ़ यह कि वह किस परिवार की है:
letter, digit, punct, space, enter, bksp, del, nav, mod, func, unknown
इनपुट प्रकार
Potato का प्राथमिक संकेत beforeinput पर आने वाला InputEvent.inputType है, keydown नहीं। यही केंद्रीय तकनीकी चुनाव है। पेस्ट, ड्रैग-एंड-ड्रॉप, IME कंपोज़िशन, डिक्टेशन, ऑटोफ़िल, और undo, ये सब किसी फ़ील्ड को बदल देते हैं और keydown बिल्कुल नहीं चलता, इसलिए सिर्फ़ keydown पर टिका लॉगर ठीक उन्हीं मामलों में अंधा है जिन्हें पकड़ने के लिए यह सुविधा मौजूद है।
कैप्चर किए जाने वाले input type: insertText, insertReplacementText, insertFromPaste, insertFromDrop, insertCompositionText, insertLineBreak, insertParagraph, deleteContentBackward, deleteContentForward, deleteWordBackward, deleteWordForward, deleteByCut, deleteByDrag, historyUndo, historyRedo, और इनके साथ कृत्रिम focus, blur, तथा keydown।
keydown और keyup अब भी सुने जाते हैं, पर सिर्फ़ शारीरिक कीस्ट्रोक गिनने और dwell मापने के लिए। जो अक्षर आए और जो कुंजियाँ वाक़ई दबाई गईं, उनके बीच का अंतर जुटाया गया सबसे मज़बूत अकेला संकेत है। नीचे silent_insert_ratio देखें।
कौन-से फ़ील्ड मापे जाते हैं
डिफ़ॉल्ट रूप से हर फ़्री-टेक्स्ट फ़ील्ड: text स्कीमा, radio और multiselect के भीतर के फ़्री-रिस्पॉन्स बॉक्स, और text_edit, pairwise, trajectory_eval जैसे स्कीमा में तर्क या नोट्स वाले textarea।
फ़ील्ड की पहचान उन्हीं schema और label_name विशेषताओं से होती है जो Potato पहले से हर एनोटेशन इनपुट पर लगाता है, और वे न मिलें तो name विशेषता को ::: पर तोड़कर।
दायरा किसी भी सूची से सीमित करें:
keystroke_logging:
enabled: true
include_schemas: [rationale] # allowlist; empty = all fields
exclude_schemas: [scratch_notes] # denylistया कस्टम HTML में किसी एक एलिमेंट को बाहर रखें:
<textarea data-keystroke-logging="off" ...></textarea>कॉन्फ़िगरेशन संदर्भ
keystroke_logging:
enabled: false # master switch
fidelity: events # off | summary | events
include_schemas: [] # empty = every free-text field
exclude_schemas: []
store_events: true # persist raw streams (needs fidelity: events)
classify_paste_source: true # label pastes self/instance_text/ai_suggestion/external
idle_session_ms: 30000 # close a session after this much inactivity
flush_interval_ms: 5000 # how often the browser posts completed sessions
pause_thresholds_ms: [500, 1000, 2000, 5000, 10000]
disclose_to_annotators: true # show a recording notice
detection:
enabled: true
calibrate: false # use project-fitted thresholds
on_external_insert: flag # allow | warn | block | flag
thresholds: {} # per-rule overrides| कुंजी | डिफ़ॉल्ट | अर्थ |
|---|---|---|
enabled | false | मुख्य स्विच। false होने पर कुछ भी कैप्चर नहीं होता। |
fidelity | events | off इसे बंद करता है; summary फ़ीचर निकालता है पर कोई स्ट्रीम नहीं रखता; events दोनों रखता है। |
include_schemas | [] | स्कीमा नामों की allowlist। ख़ाली का मतलब सभी। |
exclude_schemas | [] | denylist, allowlist के बाद लागू होती है। |
store_events | true | कच्ची स्ट्रीम रखें। fidelity: events के बिना अनदेखा कर दिया जाता है। |
classify_paste_source | true | पेस्ट की तुलना अंश, AI सुझावों, और फ़ील्ड की अपनी सामग्री से करता है। |
idle_session_ms | 30000 | सत्र बंद और फ़्लश होने से पहले कितनी निष्क्रियता। |
flush_interval_ms | 5000 | ब्राउज़र की फ़्लश दर। |
pause_thresholds_ms | [500,1000,2000,5000,10000] | हर सीमा पर ठहरावों की गिनती बताई जाती है। |
disclose_to_annotators | true | रिकॉर्डिंग की सूचना दिखाएँ। इसे बंद करने पर चेतावनी लॉग होती है। |
पहचान से जुड़ी कुंजियाँ लेखन-प्रक्रिया पहचान में दर्ज हैं।
fidelity चुनना
| Fidelity | स्ट्रीम रखी जाती है | बाद में नए मेट्रिक निकाल सकते हैं? | कब इस्तेमाल करें |
|---|---|---|---|
off | — | — | इस प्रोजेक्ट में सुविधा बंद है |
summary | नहीं | नहीं | आप निश्चित हैं कि कौन-से फ़ीचर चाहिए, या आपकी नैतिकता स्वीकृति स्ट्रीम रखने की अनुमति नहीं देती |
events | हाँ | हाँ | डिफ़ॉल्ट। लगभग 2 बाइट प्रति कीस्ट्रोक |
events अनुशंसित सेटिंग है। 500 शब्दों का उत्तर लगभग 5 KB लेता है, और इसका मतलब है कि डेटा जुटाने के बाद सूझा कोई मेट्रिक भी निकाला जा सकता है।
सारांश फ़ीचर
प्रति (user, instance, field) एक सारांश। फ़ीचर परिवार Crossley et al. (2024) के अनुसार हैं; देखें शोध आधार।
मात्रा और product-to-process
| फ़ील्ड | अर्थ |
|---|---|
keystrokes | ऐसे शारीरिक keydown जिनसे टेक्स्ट बना |
final_chars | सत्र के अंत में फ़ील्ड की लंबाई |
chars_typed / chars_inserted | टाइप करके डाले गए / किसी भी तरीक़े से डाले गए अक्षर |
chars_deleted | हटाए गए अक्षर |
chars_per_keystroke | ~1.1 से ऊपर होने का मतलब है टेक्स्ट बिना कीस्ट्रोक के आ रहा है |
active_ms / wall_ms | फ़ील्ड पर बिताया समय, दूर रहे समय को छोड़कर / जोड़कर |
लय
| फ़ील्ड | अर्थ |
|---|---|
iki_median_ms, iki_mean_ms | कुंजियों के बीच के अंतराल की केंद्रीय प्रवृत्ति |
iki_p10/p25/p75/p90_ms | अंतराल वितरण का आकार |
iki_log_sd, iki_log_cv | लघुगणकीय पैमाने पर फैलाव। कम होना यानी मेट्रोनोम जैसा, यानी नकल। |
लघुगणकीय पैमाना इसलिए कि कुंजियों के बीच के अंतरालों का वितरण बहुत दाईं ओर झुका होता है। 30 सेकंड से ऊपर के अंतराल इन आँकड़ों से बाहर रखे जाते हैं, ताकि एक कॉफ़ी ब्रेक उन पर हावी न हो जाए।
ठहराव
| फ़ील्ड | अर्थ |
|---|---|
pause_counts | हर कॉन्फ़िगर की गई सीमा पर गिनती |
pause_total_ms | ठहरावों में बीता कुल समय |
pre_word_pause_mean_ms | शब्द शुरू करने से पहले औसत ठहराव |
pre_sentence_pause_mean_ms | विराम-चिह्न के बाद औसत ठहराव |
intraword_iki_median_ms | शब्दों के भीतर अंतराल की माध्यिका, कीबोर्ड कौशल का एक अनुमान |
बर्स्टिंग
| फ़ील्ड | अर्थ |
|---|---|
bursts, burst_mean_chars, burst_max_chars | लगातार उत्पादन के आँकड़े |
p_bursts | ठहराव पर ख़त्म हुए बर्स्ट |
r_bursts | संशोधन पर ख़त्म हुए बर्स्ट |
संशोधन
| फ़ील्ड | अर्थ |
|---|---|
backspaces, deletes, undo_events | हटाने का व्यवहार |
non_terminal_edits | टेक्स्ट के अंत से पीछे किए गए संपादन, यानी लिखने वाला संशोधन के लिए वापस गया |
caret_jumps | caret की ग़ैर-सटी हुई हलचल |
revision_ratio | chars_deleted / chars_typed |
बाहरी इंसर्शन
| फ़ील्ड | अर्थ |
|---|---|
paste_events, pasted_chars, largest_paste_chars | पेस्ट की मात्रा |
pasted_fraction | अंतिम टेक्स्ट का वह हिस्सा जो पेस्ट किया गया था |
drop_events | ड्रैग-एंड-ड्रॉप से डाली गई सामग्री |
silent_insert_chars / silent_insert_ratio | ऐसे अक्षर जिनके पीछे कोई कीस्ट्रोक नहीं |
external_insert_chars / external_insert_ratio | ऊपर जैसा ही, पर अपने उद्धरण और अंश के उद्धरण छोड़कर |
paste_sources, paste_chars_by_source | प्रति स्रोत लेबल गिनती और अक्षर |
पहचान के लिए external_insert_ratio इस्तेमाल करें। silent_insert_ratio हर तरह का मौन इंसर्शन गिनता है, जायज़ वाला भी।
ध्यान
| फ़ील्ड | अर्थ |
|---|---|
blur_events, blur_total_ms | पेज से दूर बीता समय |
max_blur_before_insert_ms | किसी बड़े इंसर्शन से ठीक पहले की सबसे लंबी अनुपस्थिति |
first_keystroke_latency_ms | पहले अक्षर से पहले सोचने का समय |
अखंडता
| फ़ील्ड | अर्थ |
|---|---|
untrusted_events | InputEvent.isTrusted === false, यानी स्क्रिप्ट से या स्वचालित इनपुट |
composition_events | IME कंपोज़िशन |
virtual_keyboard | मोबाइल या सॉफ़्ट कीबोर्ड मिला |
डेटा कहाँ संग्रहीत होता है
दो जगह, दो अलग वजहों से।
कच्ची स्ट्रीम SQLite में जाती हैं
<task_dir>/project.sqlite, तालिका typing_sessions, प्रति सत्र एक पंक्ति, उसी persistence परत से होकर जिससे मेमो और कोडबुक जाते हैं।
क्वेरी करने लायक सारांश कॉलम एक पूरे JSON सारांश और zlib-packed इवेंट blob के साथ denormalized रखे जाते हैं:
sqlite3 <task_dir>/project.sqlite "
SELECT user_id, schema_name, keystrokes, final_chars,
pasted_fraction, silent_insert_ratio, iki_log_cv,
json_extract(flags,'\$.level') AS level
FROM typing_sessions;"स्ट्रीम प्रति कीस्ट्रोक एक पंक्ति के बजाय प्रति सत्र एक पैक किए हुए blob के रूप में रखी जाती है। उसे हमेशा पूरा ही पढ़ा जाता है, और लगभग 2 बाइट प्रति इवेंट पर प्रति-कीस्ट्रोक-एक-पंक्ति वाला स्कीमा किसी क्वेरी लाभ के बिना प्रोजेक्ट फ़ाइल में करोड़ों पंक्तियाँ डाल देता।
Phase पेज
प्रशिक्षण चरण और prestudy या poststudy सर्वे के फ़्री-टेक्स्ट उत्तर भी कैप्चर होते हैं। उन पेजों पर कोई instance id नहीं होती, इसलिए उनके सत्र उसी __phase_page__ sentinel के नीचे जमा होते हैं जो बाक़ी व्यवहार प्रणाली पहले से इस्तेमाल करती है, और उनकी पहचान उनके phase तथा page कॉलम से होती है:
SELECT phase, page, count(*) FROM typing_sessions GROUP BY phase, page;कैलिब्रेशन उदाहरण इसी वजह से काम करता है। प्रशिक्षण चरण में अंश की नकल कराने वाला कार्य नकल के ऐसे नमूने देता है जिन्हें सिर्फ़ phase के आधार पर सामान्य लिखे गए उत्तरों से अलग किया जा सकता है।
सारांश user_state.json में जाते हैं
संक्षिप्त खाका <output_annotation_dir>/<user>/user_state.json में instance_id_to_behavioral_data.<instance>.typing_summaries के नीचे मिरर होता है, कुंजी "{schema}:::{label}" के साथ, ताकि वह एनोटेशन के साथ एडमिन डैशबोर्ड और एक्सपोर्ट तक जाए।
कच्ची स्ट्रीम जानबूझकर वहाँ नहीं जातीं। वह फ़ाइल हर एनोटेशन सेव पर पूरी दोबारा serialize होकर परमाणु रूप से लिखी जाती है, और एक लंबे उत्तर में हज़ारों इवेंट होते हैं।
एक्सपोर्ट करना
दोनों एक्सपोर्ट ऑप्ट-इन हैं, ताकि व्यवहार संबंधी डेटा ग़लती से किसी डेटासेट रिलीज़ में शामिल न हो जाए।
एनोटेशन के साथ सारांश फ़ीचर
export_include_typing_dynamics: trueannotations.csv के बग़ल में typing_dynamics.csv (या .tsv) बनाता है, प्रति (user, instance, field) एक पंक्ति, जिसमें सारांश फ़ीचर और पहचानकर्ता का फ़ैसला होता है।
कच्ची स्ट्रीम
python -m potato.export.cli <config.yaml> --format keystrokeskeystroke_sessions.parquet और keystroke_events.parquet लिखता है, और pyarrow इंस्टॉल न होने पर JSONL पर लौट आता है। पूरे एक्सपोर्टर के लिए Parquet एक्सपोर्ट देखें।
import pandas as pd
events = pd.read_parquet("keystroke_events.parquet")
# Inter-key intervals for one session
s = events[events.session_id == events.session_id.iloc[0]].sort_values("t_ms")
iki = s.t_ms.diff().dropna()
print(iki.median(), iki.std())
# Every externally-sourced paste in the project
print(events[events.paste_source == "external"])API endpoint
| Method | Route | उद्देश्य |
|---|---|---|
POST | /api/track_typing | ब्राउज़र से पूरे हुए सत्र लेता है |
GET | /api/typing_summary/<instance_id> | मौजूदा उपयोगकर्ता के लिए एक instance के सारांश |
GET | /admin/api/writing_process | प्रति-एनोटेटर रोलअप (admin key ज़रूरी) |
सत्रों का सारांश सर्वर पर बनता है। ब्राउज़र कभी बना-बनाया सारांश नहीं भेजता, इसलिए बदले हुए क्लाइंट से संख्याएँ गढ़ी नहीं जा सकतीं, और बाद में जोड़ा गया कोई मेट्रिक संग्रहीत स्ट्रीम से दोबारा निकाला जा सकता है।
सत्र कैसे काम करते हैं
सत्र तब शुरू होता है जब कोई फ़ील्ड focus पाता है, और इनमें से जो पहले हो उस पर ख़त्म होता है: focus खोना, दूसरे instance पर जाना, idle_session_ms तक निष्क्रियता, या पेज unload। पूरे हुए सत्र हर flush_interval_ms पर भेजे जाते हैं, और unload पर navigator.sendBeacon के ज़रिए, ताकि चल रहा सत्र खोए नहीं।
एक ही फ़ील्ड पर हुए कई सत्र, उपयोगकर्ता स्थिति में सारांश लिखे जाने से पहले जोड़ दिए जाते हैं, ताकि फ़ील्ड छोड़कर वापस आना कई संदिग्ध रूप से छोटे उत्तरों के बजाय एक ही उत्तर के रूप में पढ़ा जाए। गिनतियाँ और अवधियाँ जुड़ जाती हैं। वितरण संबंधी आँकड़े कीस्ट्रोक-भारित अनुमान हैं, इसलिए अगर आपको ठीक-ठीक संयुक्त वितरण चाहिए तो कच्ची स्ट्रीम इस्तेमाल करें।
समस्या निवारण
कोई डेटा रिकॉर्ड नहीं हो रहा
जाँचें कि keystroke_logging.enabled: true है और fidelity off नहीं है। ब्राउज़र कंसोल में window.keystrokeTracker मौजूद होना चाहिए और उसका isInitialized === true होना चाहिए। अगर वह undefined है, तो कॉन्फ़िग टेम्प्लेट तक पहुँचा ही नहीं।
ट्रैकर मौजूद है पर कोई सत्र नहीं दिखता
फ़ील्ड की पहचान जाँचें:
const el = document.querySelector('textarea');
window.keystrokeTracker.getFieldIdentity(el); // null means it is not trackednull का मतलब है कि एलिमेंट पर न schema या label_name विशेषताएँ हैं और न ::: से अलग किया गया name, या फिर उसे कॉन्फ़िग ने बाहर रखा हुआ है।
silent_insertion हर मोबाइल एनोटेटर को फ़्लैग करता है
ऐसा नहीं होना चाहिए, क्योंकि virtual_keyboard सही होने पर यह नियम दबा दिया जाता है। अगर पहचान ग़लत चल रही है, तो जाँचें कि क्लाइंट ने वह फ़्लैग सेट किया या नहीं। फ़ॉल्स पॉज़िटिव तालिका देखें।
project.sqlite बढ़ता जा रहा है
लगभग 2 बाइट प्रति कीस्ट्रोक। फ़ीचर रखते हुए स्ट्रीम हटाने के लिए fidelity: summary सेट करें, या किसी एक प्रतिभागी का डेटा हटाने के लिए typing_store.delete_for_user() इस्तेमाल करें।
स्वचालित टेस्ट में संख्याएँ ग़लत लगती हैं
ब्राउज़र ऑटोमेशन लगभग शून्य अंतराल पर टाइप करती है, जिससे implausible_speed वाक़ई चल जाता है। यह बग नहीं, फ़्लैग का काम करना है।
आगे पढ़ें
- लेखन-प्रक्रिया पहचान - छह नियम और पहचान के तीन स्तर
- कीस्ट्रोक लॉगिंग नैतिकता - सहमति, IRB, प्रतिधारण, प्रतिभागी अधिकार
- व्यवहार ट्रैकिंग - वह व्यापक इंटरैक्शन-ट्रैकिंग प्रणाली जिसके भीतर यह बैठता है
- गुणवत्ता नियंत्रण - attention checks और gold standards
- एडमिन डैशबोर्ड - जहाँ Writing Process पैनल रहता है
कार्यान्वयन विवरण के लिए, स्रोत दस्तावेज़ देखें।