Skip to content

कीस्ट्रोक लॉगिंग

Potato किसी फ़्री-टेक्स्ट उत्तर के पीछे के ठहराव, बर्स्ट, संशोधन और पेस्ट रिकॉर्ड कर सकता है, बिना उन अक्षरों में से कोई भी रिकॉर्ड किए जो एनोटेटर टाइप करता है।

Potato यह रिकॉर्ड कर सकता है कि कोई फ़्री-टेक्स्ट उत्तर कैसे तैयार हुआ, बिना उत्तर को रिकॉर्ड किए। हर इवेंट के साथ एक timestamp, एक input type, एक कुंजी वर्ग और लंबाई में हुआ बदलाव जुड़ा होता है; इनमें से किसी के साथ वह अक्षर नहीं जाता जो टाइप किया गया था। उसी स्ट्रीम से Potato लगभग चालीस सारांश फ़ीचर निकालता है और उन्हें एनोटेशन के साथ रख देता है।

मक़सद यह है कि लिखे गए उत्तर को, जो किसी सोचते-समझते व्यक्ति के ठहरावों और दोबारा-विचारों के साथ टाइप हुआ हो, ऐसे उत्तर से अलग बताया जा सके जो किसी दूसरी विंडो से दोबारा टाइप किया गया हो या किसी चैटबॉट से पेस्ट कर दिया गया हो। तैयार उत्तर पढ़ें तो वे एक जैसे लगते हैं। लॉग एक जैसे नहीं लगते।

कीस्ट्रोक लॉगिंग के लिए Potato 2.7.2 या उसके बाद का संस्करण चाहिए, और यह डिफ़ॉल्ट रूप से बंद है: जब तक आप इसे सेट न करें, keystroke_logging.enabled false रहता है, इसलिए अपग्रेड करने से किसी की रिकॉर्डिंग शुरू नहीं होती। इस डेटा पर बने नियमों के लिए लेखन-प्रक्रिया पहचान देखें। इसे मानव प्रतिभागियों पर लगाने से पहले कीस्ट्रोक लॉगिंग नैतिकता पढ़ें।

तुरंत शुरुआत

yaml
keystroke_logging:
  enabled: true

पूरा न्यूनतम कॉन्फ़िगरेशन बस इतना ही है। प्रोजेक्ट का हर फ़्री-टेक्स्ट फ़ील्ड एक कंटेंट-ब्लाइंड इवेंट स्ट्रीम, एक सारांश, और पहचान फ़्लैग का एक सेट देने लगता है।

Potato के साथ एक चलाने योग्य उदाहरण आता है:

bash
python potato/flask_server.py start examples/advanced/keystroke-logging/config.yaml -p 8000

चेतावनी: enabled डिफ़ॉल्ट रूप से false है। Potato अपग्रेड करने से आपके एनोटेटर की रिकॉर्डिंग चुपचाप शुरू नहीं होती।

क्या कैप्चर होता है

हर इवेंट एक timestamp, एक input type, कुंजी का वर्ग, caret की स्थिति, और फ़ील्ड की लंबाई में हुआ बदलाव दर्ज करता है:

text
{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 विशेषता को ::: पर तोड़कर।

दायरा किसी भी सूची से सीमित करें:

yaml
keystroke_logging:
  enabled: true
  include_schemas: [rationale]      # allowlist; empty = all fields
  exclude_schemas: [scratch_notes]  # denylist

या कस्टम HTML में किसी एक एलिमेंट को बाहर रखें:

html
<textarea data-keystroke-logging="off" ...></textarea>

कॉन्फ़िगरेशन संदर्भ

yaml
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
कुंजीडिफ़ॉल्टअर्थ
enabledfalseमुख्य स्विच। false होने पर कुछ भी कैप्चर नहीं होता।
fidelityeventsoff इसे बंद करता है; summary फ़ीचर निकालता है पर कोई स्ट्रीम नहीं रखता; events दोनों रखता है।
include_schemas[]स्कीमा नामों की allowlist। ख़ाली का मतलब सभी।
exclude_schemas[]denylist, allowlist के बाद लागू होती है।
store_eventstrueकच्ची स्ट्रीम रखें। fidelity: events के बिना अनदेखा कर दिया जाता है।
classify_paste_sourcetrueपेस्ट की तुलना अंश, AI सुझावों, और फ़ील्ड की अपनी सामग्री से करता है।
idle_session_ms30000सत्र बंद और फ़्लश होने से पहले कितनी निष्क्रियता।
flush_interval_ms5000ब्राउज़र की फ़्लश दर।
pause_thresholds_ms[500,1000,2000,5000,10000]हर सीमा पर ठहरावों की गिनती बताई जाती है।
disclose_to_annotatorstrueरिकॉर्डिंग की सूचना दिखाएँ। इसे बंद करने पर चेतावनी लॉग होती है।

पहचान से जुड़ी कुंजियाँ लेखन-प्रक्रिया पहचान में दर्ज हैं।

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_jumpscaret की ग़ैर-सटी हुई हलचल
revision_ratiochars_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_eventsInputEvent.isTrusted === false, यानी स्क्रिप्ट से या स्वचालित इनपुट
composition_eventsIME कंपोज़िशन
virtual_keyboardमोबाइल या सॉफ़्ट कीबोर्ड मिला

डेटा कहाँ संग्रहीत होता है

दो जगह, दो अलग वजहों से।

कच्ची स्ट्रीम SQLite में जाती हैं

<task_dir>/project.sqlite, तालिका typing_sessions, प्रति सत्र एक पंक्ति, उसी persistence परत से होकर जिससे मेमो और कोडबुक जाते हैं।

क्वेरी करने लायक सारांश कॉलम एक पूरे JSON सारांश और zlib-packed इवेंट blob के साथ denormalized रखे जाते हैं:

bash
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 कॉलम से होती है:

sql
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 होकर परमाणु रूप से लिखी जाती है, और एक लंबे उत्तर में हज़ारों इवेंट होते हैं।

एक्सपोर्ट करना

दोनों एक्सपोर्ट ऑप्ट-इन हैं, ताकि व्यवहार संबंधी डेटा ग़लती से किसी डेटासेट रिलीज़ में शामिल न हो जाए।

एनोटेशन के साथ सारांश फ़ीचर

yaml
export_include_typing_dynamics: true

annotations.csv के बग़ल में typing_dynamics.csv (या .tsv) बनाता है, प्रति (user, instance, field) एक पंक्ति, जिसमें सारांश फ़ीचर और पहचानकर्ता का फ़ैसला होता है।

कच्ची स्ट्रीम

bash
python -m potato.export.cli <config.yaml> --format keystrokes

keystroke_sessions.parquet और keystroke_events.parquet लिखता है, और pyarrow इंस्टॉल न होने पर JSONL पर लौट आता है। पूरे एक्सपोर्टर के लिए Parquet एक्सपोर्ट देखें।

python
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

MethodRouteउद्देश्य
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 है, तो कॉन्फ़िग टेम्प्लेट तक पहुँचा ही नहीं।

ट्रैकर मौजूद है पर कोई सत्र नहीं दिखता

फ़ील्ड की पहचान जाँचें:

js
const el = document.querySelector('textarea');
window.keystrokeTracker.getFieldIdentity(el);   // null means it is not tracked

null का मतलब है कि एलिमेंट पर न schema या label_name विशेषताएँ हैं और न ::: से अलग किया गया name, या फिर उसे कॉन्फ़िग ने बाहर रखा हुआ है।

silent_insertion हर मोबाइल एनोटेटर को फ़्लैग करता है

ऐसा नहीं होना चाहिए, क्योंकि virtual_keyboard सही होने पर यह नियम दबा दिया जाता है। अगर पहचान ग़लत चल रही है, तो जाँचें कि क्लाइंट ने वह फ़्लैग सेट किया या नहीं। फ़ॉल्स पॉज़िटिव तालिका देखें।

project.sqlite बढ़ता जा रहा है

लगभग 2 बाइट प्रति कीस्ट्रोक। फ़ीचर रखते हुए स्ट्रीम हटाने के लिए fidelity: summary सेट करें, या किसी एक प्रतिभागी का डेटा हटाने के लिए typing_store.delete_for_user() इस्तेमाल करें।

स्वचालित टेस्ट में संख्याएँ ग़लत लगती हैं

ब्राउज़र ऑटोमेशन लगभग शून्य अंतराल पर टाइप करती है, जिससे implausible_speed वाक़ई चल जाता है। यह बग नहीं, फ़्लैग का काम करना है।

आगे पढ़ें

कार्यान्वयन विवरण के लिए, स्रोत दस्तावेज़ देखें।