Skip to content
Announcements12 min read

लेखन-प्रक्रिया को पढ़ना: फ़्री-टेक्स्ट एनोटेशन के लिए कीस्ट्रोक लॉगिंग

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

Potato Team

एक क्राउडवर्कर आपका एनोटेशन कार्य खोलता है, अंश पढ़ता है, दूसरे टैब पर चला जाता है, बीस सेकंड बाद लौटता है, और 280 अक्षरों का एक तर्क एक ही झटके में बॉक्स में आ जाता है।

तर्क ठीक है। विषय पर है, व्याकरण सही है, अंश का हवाला देता है। चालीस और तर्कों के साथ रखकर पढ़िए तो कुछ भी अलग नहीं दिखता। आप उस पर जो भी क्लासिफ़ायर चलाएँ, वह “शायद इंसान” और “स्पष्ट नहीं” के बीच कुछ बताएगा, और अधिकांश टेक्स्ट के बारे में वे क्लासिफ़ायर यही कहते हैं।

तैयार उत्तर यह नहीं बताता कि वह कहाँ से आया। वे बीस सेकंड और वह एक झटका बताते हैं।

Potato अब यही रिकॉर्ड करता है। फ़्री-टेक्स्ट फ़ील्ड इस बात की कंटेंट-ब्लाइंड स्ट्रीम कैप्चर कर सकते हैं कि उत्तर कैसे तैयार हुआ, उसे लगभग चालीस फ़ीचर में समेट सकते हैं, और नतीजे पर नामित नियमों का एक छोटा सेट चला सकते हैं। यह डिफ़ॉल्ट रूप से बंद है और एक पंक्ति से चालू होता है:

yaml
keystroke_logging:
  enabled: true

Potato में कीस्ट्रोक लॉगिंग: लिखा गया, नकल किया गया, और पेस्ट किया गया उत्तर प्रक्रिया में अलग दिखते हैं, भले ही तैयार टेक्स्ट अलग न दिखेकीस्ट्रोक लॉगिंग

लिखा गया, नकल किया गया, पेस्ट किया गया

जिस शोध पर यह टिका है वह काफ़ी हद तक तय है। Crossley और सहयोगियों ने 500 तर्कात्मक निबंध इकट्ठा किए, वर्करों के एक दूसरे समूह से उनकी नकल करवाई, और random forest की मदद से असली लेखन को नकल से 99% सटीकता के साथ अलग कर दिखाया। Deane et al. और Zhang et al. ने स्वतंत्र रूप से यही अंतर पाया। Asher et al. ने Prolific पर इसका क्राउडसोर्सिंग संस्करण बनाया, ऐसे प्रतिभागियों को फ़्लैग करते हुए जिनकी कीस्ट्रोक गिनती उनके जमा किए गए उत्तर की लंबाई के हिसाब से बहुत कम थी।

हस्ताक्षर इन सबमें एक-सा है। असली लेखन में वाक्यों और शब्दों से पहले लंबे ठहराव होते हैं, अधिक इंसर्शन और डिलीशन, और कुंजियों के बीच के अंतरालों में ऊँचा प्रसरण। कॉपी-टाइपिंग रैखिक होती है, बर्स्ट-केंद्रित, और कम प्रसरण वाली। पेस्ट करने में अंतराल लगभग होते ही नहीं।

इसमें से कुछ भी टेक्स्ट में नहीं दिखता। यह सब लॉग में दिखता है।

beforeinput क्यों, keydown क्यों नहीं

यही वह एक तकनीकी निर्णय है जो तय करता है कि बाक़ी सुविधा काम करेगी या नहीं।

कीस्ट्रोक लॉगर लिखने का ज़ाहिर तरीक़ा keydown सुनना है। यही तरीक़ा नाकाम भी होता है, क्योंकि पेस्ट, ड्रैग-एंड-ड्रॉप, IME कंपोज़िशन, डिक्टेशन, ऑटोफ़िल, और undo, ये सब किसी फ़ील्ड की सामग्री बदल देते हैं और keydown एक बार भी नहीं चलता। सिर्फ़ keydown पर टिका लॉगर ठीक उन्हीं मामलों में अंधा है जिन्हें पकड़ने के लिए यह मौजूद है।

Potato का प्राथमिक संकेत beforeinput पर आने वाला InputEvent.inputType है, जो इन सबके लिए चलता है और बताता है कि इनमें से कौन-सा हुआ। keydown और keyup अब भी सुने जाते हैं, पर एक अलग मक़सद से: यह गिनने के लिए कि व्यक्ति ने शारीरिक रूप से कितनी कुंजियाँ दबाईं।

इन दो संख्याओं का अंतर वह सबसे उपयोगी चीज़ है जो जुटाई जाती है। फ़ील्ड में आए ऐसे अक्षर जिनके पीछे कोई कीस्ट्रोक नहीं था, silent_insert_chars के रूप में दर्ज होते हैं, और उत्तर में उनका हिस्सा silent_insert_ratio के रूप में। पेज ने जिस पेस्ट को दबा दिया, कोई एक्सटेंशन जिसने टेक्स्ट डाल दिया, कोई डिक्टेशन स्ट्रीम, कोई स्क्रिप्ट जो बॉक्स भर रही हो: इनमें से कोई कीस्ट्रोक नहीं बनाता, और ये सभी अक्षर बनाते हैं।

क्या दर्ज होता है और क्या नहीं

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

text
{t_ms: 1240, input_type: "insertText",            key_class: "letter", pos: 41, 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"}}

कुंजी ख़ुद कभी संग्रहीत नहीं होती, सिर्फ़ यह कि वह किस परिवार की है: letter, digit, punct, space, enter, bksp, del, nav, mod, func, unknown। पेस्ट किया गया टेक्स्ट घटकर एक लंबाई, एक स्रोत लेबल, और प्रति-सत्र salted hash रह जाता है। पासवर्ड फ़ील्ड सीधे अस्वीकार कर दिए जाते हैं। स्ट्रीम से उत्तर दोबारा नहीं बनाया जा सकता, और यही बात मायने रखती है: स्ट्रीम प्रक्रिया का ब्योरा देती है और सामग्री के बारे में कुछ नहीं कहती।

पेस्ट के स्रोत का वर्गीकरण ही सामान्य व्यवहार को फ़्लैग से बाहर रखता है। पेस्ट आने पर Potato उसकी तुलना एनोटेशन वाले अंश से, पेज पर दिखाए गए किसी भी AI सुझाव से, और फ़ील्ड में पहले से मौजूद सामग्री से करता है, फिर लेबल रख लेता है और तुलना हटा देता है। अंश उद्धृत करना instance_text के रूप में पढ़ा जाता है। अपना ही मसौदा इधर-उधर करना self के रूप में। इनमें से कोई बाहरी इंसर्शन नहीं गिना जाता।

छह नियम, और कुछ भी ऐसा नहीं जो मॉडल होने का दिखावा करे

पहचान सर्वर पर तीन स्तरों में चलती है। इनमें से सिर्फ़ पहला डिफ़ॉल्ट रूप से चालू है।

पहला स्तर छह नामित फ़्लैग हैं, हर एक के साथ स्पष्ट थ्रेशोल्ड, और हर एक वे फ़ीचर मान लौटाता है जिनकी वजह से वह चला:

फ़्लैगकब चलता हैगंभीरता
paste_dominantअंतिम टेक्स्ट का आधा या उससे अधिक हिस्सा पेस्ट से आयाsuspect
silent_insertionडाले गए अक्षरों में से ≥30% के पीछे कोई कीस्ट्रोक नहीं थाsuspect
transcription_rhythmमेट्रोनोम जैसी लय और कोई संशोधन नहीं और लगभग कोई ठहराव नहींreview
offscreen_compositionपेज से ≥10s दूर रहने के ठीक बाद एक बड़ा बाहरी इंसर्शनsuspect
implausible_speedपूरे उत्तर भर ~180 wpm से ऊपर टिकी हुई रफ़्तारreview
synthetic_inputब्राउज़र isTrusted === false बताता हैsuspect

transcription_rhythm जानबूझकर संयोजी है। अकेले मेट्रोनोम जैसी टाइपिंग का मतलब है तेज़ टाइपिस्ट। कुछ भी न मिटाना सावधान टाइपिस्ट की निशानी है। मुश्किल से ठहरना छोटे उत्तर की निशानी है। कॉपी-टाइपिंग का हस्ताक्षर तभी बनता है जब तीनों एक साथ सही हों, और यह नियम 80 अक्षरों से छोटे उत्तर पूरी तरह छोड़ देता है, जहाँ पढ़ने लायक कोई लय होती ही नहीं।

दूसरा स्तर कैलिब्रेशन है। कीस्ट्रोक फ़ीचर लेखन कार्य पर बहुत निर्भर करते हैं, इसलिए एक-वाक्य के तर्क के लिए ट्यून किया गया थ्रेशोल्ड पाँच पैराग्राफ़ के लिए ग़लत होता है। python -m potato.typing_detect calibrate config.yaml हर कटऑफ़ को आपके अपने प्रोजेक्ट के सत्रों के एक tail percentile पर दोबारा फ़िट करता है। इसके लिए कम से कम 30 उपयोगी सत्र चाहिए, और कैलिब्रेट किए गए मान अंतर्निर्मित डिफ़ॉल्ट के 3× के भीतर सीमित रहते हैं, ताकि कोई समरूप आबादी थ्रेशोल्ड को खींचकर अपनी ही माध्यिका पर न ले आए।

तीसरा स्तर सुपरवाइज़्ड है। अगर आपके पास लेबल हैं, तो fit_supervised() फ़ीचर मैट्रिक्स पर एक असली क्लासिफ़ायर प्रशिक्षित करता है। scikit-learn आलसी ढंग से import होता है और Potato की निर्भरता नहीं है।

कोई पूर्व-प्रशिक्षित मॉडल साथ नहीं आता। रिपॉज़िटरी में कोई लेबल किया हुआ कॉर्पस नहीं है, और शून्य से निकाले गए फ़िट किए हुए गुणांक भेजना एक validation संख्या गढ़ने जैसा होता। इसके बजाय जो आता है वह छह नियम हैं, जिन्हें आप पढ़ सकते हैं, जिनसे असहमत हो सकते हैं, और जिन्हें बदल सकते हैं।

अगर आपको लेबल चाहिए, तो प्रशिक्षण चरण उन्हें आपके अपने अध्ययन के भीतर से तैयार कर सकता है। एनोटेटर को अंश की नकल करने वाला एक वार्म-अप दीजिए: वे सत्र आपके एनोटेटर से, आपके कार्य पर, नकल के असली नमूने हैं, और उनके सामान्य उत्तर लिखे-गए वर्ग में आते हैं। Crossley कॉर्पस इसी तरह बना था, और कैलिब्रेशन उदाहरण इसे शुरू से अंत तक सेट करता है।

थ्रेशोल्ड सर्वर पर आँके जाते हैं और ब्राउज़र को कभी नहीं भेजे जाते। उन्हें प्रकाशित करना एनोटेटर को ठीक-ठीक बता देगा कि कितनी धीमे पेस्ट करना है।

यह डेटा कहाँ जाता है

कच्ची स्ट्रीम SQLite में जाती हैं, <task_dir>/project.sqlite में, प्रति सत्र एक पंक्ति, उसी persistence परत से होकर जिससे मेमो और कोडबुक जाते हैं। इवेंट delta-encoded और zlib-packed होते हैं, मापी हुई दर 1.7 बाइट प्रति इवेंट, यानी 500 शब्दों का उत्तर लगभग 5 KB लेता है।

वे जानबूझकर user_state.json में नहीं जातीं। वह फ़ाइल हर एनोटेशन सेव पर पूरी दोबारा लिखी जाती है, और एक लंबे उत्तर में क़रीब 3,000 इवेंट होते हैं। सिर्फ़ संक्षिप्त सारांश व्यवहार डेटा में मिरर होता है, कुंजी "{schema}:::{label}" के साथ, ताकि वह एनोटेशन के साथ डैशबोर्ड और एक्सपोर्ट तक जाए।

प्रशिक्षण चरण और prestudy या poststudy सर्वे के फ़्री-टेक्स्ट उत्तर भी कैप्चर होते हैं। उन पेजों पर कोई instance id नहीं होती, इसलिए उनके सत्र मौजूदा __phase_page__ sentinel के नीचे जमा होते हैं और उनकी पहचान phase तथा page से होती है। अंश-की-नकल वाली तरकीब इसी वजह से काम करती है: नकल के नमूने सिर्फ़ phase के आधार पर सामान्य उत्तरों से अलग किए जा सकते हैं।

दोनों एक्सपोर्ट ऑप्ट-इन हैं। export_include_typing_dynamics: true आपके एनोटेशन के बग़ल में एक typing_dynamics.csv साइडकार लिखता है, और python -m potato.export.cli <config.yaml> --format keystrokes कच्ची स्ट्रीम Parquet में लिखता है, pyarrow न होने पर JSONL पर लौट आता है। व्यवहार संबंधी डेटा ग़लती से किसी डेटासेट रिलीज़ में कभी शामिल नहीं होता।

इसे लोगों पर लगाने से पहले

फ़्लैग किसी इंसान की समीक्षा के लिए एक साक्ष्य है। यह प्रमाण नहीं है, और इसे स्वचालित अस्वीकृति, भुगतान रोकने, या प्रतिबंध से नहीं जोड़ा जाना चाहिए।

यहाँ विफलता का रूप यह है कि किसी ईमानदार एनोटेटर पर आरोप लग जाए, और नियमों को छेड़ने वाले मामले असामान्य नहीं हैं। कुछ सँभाल लिए गए हैं: अंश उद्धृत करना और अपना ही मसौदा इधर-उधर करना स्रोत वर्गीकरण से दब जाते हैं, और सॉफ़्ट कीबोर्ड तथा IME कंपोज़िशन silent_insertion को दबा देते हैं, क्योंकि दोनों में keydown भरोसे से नहीं निकलता और वरना हर इंसर्ट मौन दिखता। कुछ नहीं सँभाले गए। डिक्टेशन फ़्लैग होगा। व्याकरण एक्सटेंशन फ़्लैग होंगे। कुछ सहायक तकनीक untrusted इवेंट पैदा करती है और synthetic_input छेड़ देगी, इसीलिए दस्तावेज़ीकरण साफ़ कहता है कि अगर आपका अध्ययन AT उपयोगकर्ताओं के लिए खुला है तो उस नियम को बंद कर दीजिए, बजाय इसके कि किसी ऐसे व्यक्ति को फ़्लैग की सफ़ाई देनी पड़े जो काम करने के लिए ज़रूरी उपकरण ही इस्तेमाल कर रहा था।

फिर गणित है। अगर आपके 5% उत्तर पेस्ट किए गए हैं और आपका नियम 5% सत्र फ़्लैग करता है, तो जो फ़्लैग हुआ उसका अधिकांश हिस्सा फिर भी ईमानदार काम हो सकता है। ऐसे प्लेटफ़ॉर्म पर जहाँ कदाचार वाक़ई दुर्लभ है, मामूली फ़ॉल्स-पॉज़िटिव दर वाला नियम भी सही पकड़ से ज़्यादा झूठे आरोप पैदा करता है। कैलिब्रेट किया हुआ थ्रेशोल्ड इसे बेहतर नहीं करता, बस साफ़ कर देता है: tail percentile किसी भी आबादी का उतना ही tail हिस्सा फ़्लैग करता है, उस आबादी का भी जिसमें किसी ने कुछ ग़लत नहीं किया।

सूचना देना डिफ़ॉल्ट रूप से चालू है, और इसे बंद करने पर स्टार्टअप के समय चेतावनी लॉग होती है। यह डिफ़ॉल्ट इसलिए है क्योंकि टाइमिंग पैटर्न एक व्यवहारगत बायोमेट्रिक हैं। वे किसी व्यक्ति की पहचान कर सकते हैं और अलग-अलग संदर्भों में खातों को जोड़ सकते हैं, और शोध साहित्य ने उनसे टाइपिंग कौशल, दूसरी भाषा की स्थिति, और संज्ञानात्मक भार का अनुमान लगाया है। वाक्यों पर लेबल लगाने के लिए साइन अप करने वाला कोई इसकी उम्मीद नहीं कर रहा था। Potato इनमें से कुछ भी नहीं निकालता और इसके लिए कोई उपकरण नहीं देता, पर आप जो डेटा रखते हैं वह ऐसे विश्लेषण को सहारा देगा, और यह सँभालना टूल की नहीं, आपकी ज़िम्मेदारी है। typing_store.delete_for_user() एक प्रतिभागी की स्ट्रीम हटा देता है, और fidelity: summary फ़ीचर रखते हुए बायोमेट्रिक ब्योरा छोड़ देता है। नैतिकता पृष्ठ पर नमूना सहमति भाषा, प्रतिधारण मार्गदर्शन, और GDPR अनुच्छेद 22, IRB समीक्षा तथा प्लेटफ़ॉर्म की अस्वीकृति नीतियों पर टिप्पणियाँ हैं।

writing_process_risk एडमिन डैशबोर्ड के Writing Process पैनल में एक रैंकिंग सहायक के रूप में दिखता है, मौजूदा suspicion_score से अलग। कोई भी संख्या दूसरी में नहीं मिलती, इसलिए कोई भी चुपचाप दूसरी का अर्थ नहीं बदलती।

इसे चालू करना

यह सुविधा Potato 2.7.2 में आती है। ऊपर दी गई एक-पंक्ति से ज़्यादा भरा-पूरा कॉन्फ़िग:

yaml
keystroke_logging:
  enabled: true
  fidelity: events              # off | summary | events
  include_schemas: [rationale]  # empty means every free-text field
  disclose_to_annotators: true
  detection:
    enabled: true
    on_external_insert: flag    # allow | warn | block | flag

on_external_insert: block मापे जा रहे फ़ील्ड में पेस्ट करना रोक देता है। यह जायज़ उद्धरण भी रोकता है, और ठान लेने वाला कोई भी व्यक्ति उसके बदले दोबारा टाइप कर सकता है, इसलिए flag रखकर समीक्षा करना आमतौर पर बेहतर सौदा है।

चलाने योग्य एक प्रोजेक्ट examples/advanced/keystroke-logging/ में है, और कैलिब्रेशन का वॉकथ्रू examples/advanced/keystroke-calibration/ में।

दस्तावेज़ीकरण

अपग्रेड करना

bash
pip install --upgrade potato-annotation==2.7.2

मौजूदा प्रोजेक्ट में तब तक कुछ नहीं बदलता जब तक आप न कहें। keystroke_logging.enabled डिफ़ॉल्ट रूप से false है, इसलिए अपग्रेड करने से किसी की रिकॉर्डिंग शुरू नहीं होती।