قراءة عملية الكتابة: تسجيل ضغطات المفاتيح في التعليق التوضيحي النصي الحر
صار بإمكان Potato أن يسجّل كيف ينتج المعلّقون إجاباتهم النصية الحرة، دون أن يسجّل ما يكتبونه، وأن يحوّل التوقفات والمراجعات وعمليات اللصق إلى إشارات قابلة للتدقيق على الإجابات التي لُصقت بدل أن تُكتب.
يفتح أحد عمّال الحشد مهمة التعليق التوضيحي عندك، ويقرأ المقطع، وينتقل إلى تبويب آخر، ثم يعود بعد عشرين ثانية، فيظهر في الصندوق تبرير من 280 حرفاً دفعة واحدة.
التبرير جيد. في صلب الموضوع، وسليم نحوياً، ويحيل إلى المقطع. اقرأه إلى جانب أربعين تبريراً آخر ولن يلفت شيء انتباهك. وأي مصنّف قد توجّهه إليه سيقول لك شيئاً بين «بشري على الأرجح» و«غير واضح»، وهو ما تقوله تلك المصنّفات عن معظم النصوص.
الإجابة النهائية لا تخبرك من أين جاءت. أما العشرون ثانية والحركة الواحدة فتخبرانك.
صار Potato يسجّل ذلك. فحقول النص الحر تستطيع التقاط تدفق لكيفية إنتاج الإجابة لا يطّلع على محتواها، وتلخيصه في نحو أربعين سمة، وتشغيل مجموعة صغيرة من القواعد المسمّاة على الناتج. والميزة معطّلة افتراضياً وتُفعَّل بسطر واحد:
keystroke_logging:
enabled: trueتسجيل ضغطات المفاتيح
مؤلَّف، منسوخ، ملصوق
البحث الذي يستند إليه هذا مستقر إلى حد بعيد. جمع Crossley وزملاؤه 500 مقالة حجاجية، وطلبوا من مجموعة ثانية من العاملين نسخها، وفصلوا الكتابة الأصيلة عن النسخ بدقة 99% بغابة عشوائية. ووجد Deane وزملاؤه، وكذلك Zhang وزملاؤه، الفصل نفسه على نحو مستقل. وبنى Asher وزملاؤه النسخة الخاصة بالتعهيد الجماعي على Prolific، فأشاروا إلى المشاركين الذين جاء عدد ضغطاتهم منخفضاً أكثر مما يحتمله طول ما قدّموه.
والبصمة متسقة عندهم جميعاً. فالتأليف الحقيقي فيه توقفات أطول قبل الجمل والكلمات، وإدراجات وحذوفات أكثر، وتباين مرتفع في الفواصل الزمنية بين المفاتيح. أما النسخ بالطباعة فخطّي، قائم على الدفقات، ومنخفض التباين. واللصق يكاد لا تكون فيه فواصل زمنية أصلاً.
لا شيء من ذلك ظاهر في النص. وكله ظاهر في السجل.
لماذا beforeinput وليس keydown
هذا هو القرار التقني الوحيد الذي يحدد إن كانت بقية الميزة تعمل.
الطريقة البديهية لكتابة مسجّل ضغطات مفاتيح هي الإنصات إلى keydown. وهي أيضاً الطريقة التي تفشل، لأن اللصق والسحب والإفلات وتأليف IME والإملاء والملء التلقائي والتراجع تغيّر جميعها محتوى الحقل دون أن تطلق keydown ولو مرة واحدة. والمسجّل القائم على keydown وحده أعمى عن الحالات نفسها التي وُجد ليمسكها.
الإشارة الأساسية في Potato هي InputEvent.inputType على beforeinput، وهي تُطلَق في تلك الحالات كلها وتقول أيّها وقع. وما زال الإنصات إلى keydown وkeyup قائماً، لكن لغرض آخر: عدّ المفاتيح التي ضغطها الشخص فعلياً.
والفجوة بين هذين الرقمين هي أنفع ما يُجمع. فالحروف التي ظهرت في الحقل دون ضغطة مفتاح مقابلة تُسجَّل في silent_insert_chars، ونصيبها من الإجابة في silent_insert_ratio. لصقة كتمتها الصفحة، أو امتداد حقن نصاً، أو تدفق إملاء، أو سكربت يملأ الصندوق: لا شيء منها ينتج ضغطات مفاتيح، وكلها ينتج حروفاً.
ما يُسجَّل وما لا يُسجَّل
يحمل كل حدث طابعاً زمنياً، ونوع إدخال، وفئة مفتاح، وموضع مؤشر الكتابة، وفرق طول:
{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. ويُختزل النص الملصوق إلى طول واسم مصدر وبصمة مملّحة لكل جلسة. أما حقول كلمات المرور فمرفوضة تماماً. ولا يمكنك إعادة بناء الإجابة من التدفق، وهذا هو المقصود: التدفق يصف العملية ولا يقول شيئاً عن المحتوى.
وتصنيف مصدر اللصق هو ما يُبقي السلوك العادي خارج الإشارات. فحين تصل لصقة، يقارنها Potato بالمقطع محل التعليق التوضيحي، وبأي اقتراح ذكاء اصطناعي معروض على الصفحة، وبما كان في الحقل أصلاً، ثم يحتفظ بالتسمية ويتخلص من المقارنة. فاقتباس المقطع يُقرأ instance_text. وتحريك مسودتك أنت يُقرأ self. ولا يُحسب أي منهما إدراجاً خارجياً.
ست قواعد، ولا شيء يتظاهر بأنه نموذج
يجري الكشف على الخادم في ثلاث طبقات. والأولى وحدها مفعّلة افتراضياً.
الطبقة الأولى ست إشارات مسمّاة، لكل منها عتبة صريحة، وكل منها يعيد قيم السمات التي أطلقته:
| الإشارة | تُطلَق حين | الخطورة |
|---|---|---|
paste_dominant | وصل نصف النص النهائي أو أكثر عن طريق اللصق | suspect |
silent_insertion | لم تكن وراء ≥30% من الحروف المُدرَجة أي ضغطة مفتاح | suspect |
transcription_rhythm | إيقاع رتيب و لا مراجعة و لا توقفات تقريباً | review |
offscreen_composition | إدراج خارجي كبير مباشرة بعد غياب ≥10 ثوانٍ عن الصفحة | suspect |
implausible_speed | سرعة متواصلة فوق نحو 180 wpm طوال إجابة كاملة | review |
synthetic_input | يبلّغ المتصفح أن isTrusted === false | suspect |
وtranscription_rhythm اقترانية عن قصد. فالطباعة الرتيبة وحدها تعني كاتباً سريعاً. وعدم حذف أي شيء يعني كاتباً متأنياً. وقلة التوقف تعني إجابة قصيرة. ولا تحصل على بصمة النسخ بالطباعة إلا حين تتحقق الثلاثة معاً، والقاعدة تتخطى تماماً الإجابات دون 80 حرفاً، حيث لا إيقاع يُقرأ.
الطبقة الثانية هي المعايرة. تعتمد سمات ضغطات المفاتيح اعتماداً كبيراً على مهمة الكتابة، فالعتبة المضبوطة لتبرير من جملة واحدة خاطئة لخمس فقرات. ويعيد python -m potato.typing_detect calibrate config.yaml ضبط كل حدّ إلى مئين طرفي من جلسات مشروعك أنت. وهو يحتاج إلى 30 جلسة صالحة على الأقل، وتُقيَّد القيم المعايَرة ضمن 3 أضعاف الافتراضيات المدمجة حتى لا يجرّ مجتمع متجانس عتبةً إلى وسيطه هو.
والثالثة خاضعة للإشراف. فإن كانت لديك تسميات، تدرّب fit_supervised() مصنّفاً حقيقياً على مصفوفة السمات. وscikit-learn يُستورد عند الحاجة وليس اعتمادية في Potato.
ولا يُشحن أي نموذج مدرَّب مسبقاً. فلا متن موسوم في المستودع، وشحن معاملات مضبوطة على لا شيء يعني اختلاق رقم تحقق. والذي يُشحن بدلاً من ذلك ست قواعد يمكنك قراءتها والاعتراض عليها وتجاوزها.
وإن أردت تسميات، فمرحلة التدريب تستطيع إنتاجها من داخل دراستك أنت. أعطِ المعلّقين تمرين إحماء بنسخ المقطع: تلك الجلسات نماذج نسخ حقيقية من معلّقيك على مهمتك، وإجاباتهم العادية هي الصنف المؤلَّف. هكذا بُني متن Crossley، ومثال المعايرة يهيّئه من أوله إلى آخره.
تُقيَّم العتبات على الخادم ولا تُرسل إلى المتصفح أبداً. فنشرها يخبر المعلّق بالضبط بأي بطء يلصق.
أين تذهب البيانات
تذهب التدفقات الخام إلى SQLite، في <task_dir>/project.sqlite، بصف واحد لكل جلسة، عبر طبقة الحفظ نفسها التي تمر بها المذكرات ودليل الترميز. وتُرمَّز الأحداث بالفروق وتُحزَم بـ zlib بمعدل مقيس قدره 1.7 بايت للحدث، فتكلّف إجابة من 500 كلمة نحو 5 كيلوبايت.
وهي لا تذهب إلى user_state.json عن قصد. فذلك الملف يُعاد كتابته بالكامل عند كل حفظ للتعليق التوضيحي، والإجابة الطويلة نحو 3000 حدث. ولا ينعكس في البيانات السلوكية إلا الملخص المضغوط، بمفتاح "{schema}:::{label}"، فيسافر مع التعليق التوضيحي إلى لوحة التحكم وإلى التصديرات.
وتُلتقط أيضاً الإجابات النصية الحرة في مرحلة التدريب وفي استبيانات ما قبل الدراسة وما بعدها. وتلك الصفحات لا معرّف عنصر لها، فتُجمَّع جلساتها تحت العلامة الحارسة __phase_page__ القائمة، وتُعرَّف بـ phase وpage بدلاً من ذلك. وهذا ما يجعل حيلة نسخ المقطع تعمل: نماذج النسخ قابلة للفصل عن الإجابات العادية بالمرحلة وحدها.
والتصديران كلاهما اختياري. يكتب export_include_typing_dynamics: true ملفاً مرافقاً typing_dynamics.csv بجانب تعليقاتك التوضيحية، ويكتب python -m potato.export.cli <config.yaml> --format keystrokes التدفقات الخام إلى Parquet، مع الرجوع إلى JSONL عند غياب pyarrow. فلا تُضمَّن البيانات السلوكية في إصدار مجموعة بيانات عن غير قصد.
قبل أن توجّه هذا إلى أشخاص
الإشارة قرينة يراجعها إنسان. وهي ليست دليلاً قاطعاً، ولا يجوز ربطها برفض تلقائي أو حجب أجر أو حظر.
ووضع الفشل هنا هو اتهام معلّق نزيه، والحالات التي تُعثر القواعد ليست نادرة. بعضها معالَج: اقتباس المقطع وإعادة ترتيب مسودتك أنت يكبتهما تصنيف المصدر، ولوحات المفاتيح اللمسية وتأليف IME يكبتان silent_insertion، إذ لا يصدر أي منهما keydown بشكل موثوق ولولا ذلك لبدا كل إدراج صامتاً. وبعضها غير معالَج. فالإملاء سيرفع إشارة. وامتدادات التدقيق النحوي سترفع إشارة. وبعض التقنيات المساعِدة ينتج أحداثاً غير موثوقة ويُعثر synthetic_input، ولذلك يقول التوثيق صراحةً إنك إن كانت دراستك مفتوحة لمستخدمي التقنيات المساعِدة فالأولى أن تعطّل تلك القاعدة بدل أن تشرح إشارة لشخص كان يستعمل الأدوات التي يحتاجها ليعمل.
ثم هناك الحساب. إذا كان 5% من إجاباتك ملصوقاً وكانت قاعدتك تشير إلى 5% من الجلسات، فقد يظل معظم ما تشير إليه عملاً نزيهاً. وعلى منصة يندر فيها سوء السلوك فعلاً، تنتج قاعدة بمعدل إيجابيات كاذبة متواضع اتهامات باطلة أكثر من الضبطات الصحيحة. والعتبة المعايَرة تجعل هذا صريحاً لا أفضل: فالمئين الطرفي يشير إلى نسبته الطرفية من أي مجتمع، بما في ذلك مجتمع لم يخطئ فيه أحد.
الإفصاح مفعّل افتراضياً، وتعطيله يسجّل تحذيراً عند الإقلاع. وهذا الافتراضي قائم لأن أنماط التوقيت قياس حيوي سلوكي. فهي تستطيع تعريف الشخص وربط حساباته عبر السياقات، وقد استُخدمت في أدبيات البحث لاستنتاج مهارة الطباعة، وكون اللغة ثانية، والحمل المعرفي. ولم يكن أحد ممن سجّلوا لتسمية جمل يتوقع ذلك. لا يحسب Potato شيئاً من هذا ولا يشحن أي أدوات له، لكن البيانات التي تحتفظ بها تحتمل ذلك التحليل، وهذه مشكلتك أنت لا مشكلة الأداة. تحذف typing_store.delete_for_user() تدفقات مشارك واحد، ويُبقي fidelity: summary على السمات ويُسقط التفصيل الحيوي. وفي صفحة الأخلاقيات صياغة موافقة نموذجية، وإرشادات احتفاظ، وملاحظات عن المادة 22 من GDPR ومراجعة IRB وسياسات الرفض على المنصات.
ويظهر writing_process_risk في لوحة «عملية الكتابة» بلوحة تحكم المشرف بوصفه معيناً على الترتيب، منفصلاً عن suspicion_score القائم. ولا يندرج أي من الرقمين في الآخر، فلا يغيّر أحدهما معنى الآخر في صمت.
تفعيلها
تُشحن الميزة في Potato 2.7.2. وإليك تهيئة أوفى من السطر الواحد في الأعلى:
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/.
التوثيق
- تسجيل ضغطات المفاتيح — كل حقل يُلتقط، والسمات الملخّصة، والتخزين، وحل المشكلات
- كشف عملية الكتابة — القواعد الست، والطبقات الثلاث، وجدول الإيجابيات الكاذبة، والمراجع
- أخلاقيات تسجيل ضغطات المفاتيح — الموافقة، ومراجعة IRB، والاحتفاظ، وحقوق المشاركين
- تتبع السلوك — نظام تتبع التفاعل الأوسع الذي تقع هذه الميزة داخله
- مراقبة الجودة — فحوصات الانتباه والمعايير الذهبية
- لوحة تحكم المشرف — حيث تقع لوحة «عملية الكتابة»
- التعهيد الجماعي على Prolific وMTurk — قواعد المنصات في المراقبة والرفض
الترقية
pip install --upgrade potato-annotation==2.7.2لا يتغير شيء في مشروع قائم حتى تطلبه. فقيمة keystroke_logging.enabled الافتراضية هي false، فالترقية لا تبدأ تسجيل أحد أبداً.