Skip to content

تسجيل ضغطات المفاتيح

يستطيع Potato تسجيل التوقفات والدفقات والمراجعات وعمليات اللصق التي وراء إجابة نصية حرة دون تسجيل أي من الحروف التي يكتبها المعلّق.

يستطيع Potato تسجيل كيفية إنتاج إجابة نصية حرة دون تسجيل الإجابة نفسها. يحمل كل حدث طابعاً زمنياً ونوع إدخال وفئة مفتاح وتغيّراً في الطول؛ ولا يحمل أي منها الحرف الذي كُتب. ومن ذلك التدفق يحسب 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 لا تبدأ تسجيل معلّقيك في صمت أبداً.

ما يُلتقط

يسجّل كل حدث طابعاً زمنياً، ونوع إدخال، وفئة مفتاح، وموضع مؤشر الكتابة، والتغيّر في طول الحقل:

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"}}

ما لا يُلتقط عن قصد

ما لا يُلتقطلماذا
الحروف المكتوبةالتدفق يعيد بناء العملية لا النص
النص الملصوقطول واسم مصدر وبصمة مملّحة فقط
المسودات الوسيطةلا يمكن إعادة بنائها من فروق الطول وحدها
أي شيء في حقل كلمة مروريرفض getFieldIdentity القيمة type="password" رفضاً تاماً
محتويات الحافظة عموماًتُقرأ عند اللصق للتصنيف ثم يُتخلص منها

فئات المفاتيح

لا يُخزَّن المفتاح نفسه قط، بل العائلة التي ينتمي إليها فقط:

letter, digit, punct, space, enter, bksp, del, nav, mod, func, unknown

أنواع الإدخال

الإشارة الأساسية في Potato هي InputEvent.inputType على beforeinput، لا keydown. وهذا هو الخيار التقني المحوري. فاللصق والسحب والإفلات وتأليف IME والإملاء والملء التلقائي والتراجع تغيّر جميعها الحقل دون أن تطلق keydown إطلاقاً، فالمسجّل القائم على keydown وحده أعمى عن الحالات نفسها التي وُجدت هذه الميزة لكشفها.

أنواع الإدخال الملتقَطة: insertText وinsertReplacementText وinsertFromPaste وinsertFromDrop وinsertCompositionText وinsertLineBreak وinsertParagraph وdeleteContentBackward وdeleteContentForward وdeleteWordBackward وdeleteWordForward وdeleteByCut وdeleteByDrag وhistoryUndo وhistoryRedo، إضافة إلى الأحداث التركيبية focus وblur وkeydown.

وما زال الإنصات إلى keydown وkeyup قائماً، لكن فقط لعدّ ضغطات المفاتيح الفعلية وقياس مدة الضغط. والفجوة بين الحروف التي ظهرت والمفاتيح التي ضُغطت فعلاً هي أقوى إشارة مفردة تُجمع. انظر silent_insert_ratio أدناه.

أي الحقول تُرصد

افتراضياً، كل حقل نص حر: مخطط text، وصناديق الإجابة الحرة داخل radio وmultiselect، ومربعات التبرير أو الملاحظات في text_edit وpairwise وtrajectory_eval والمخططات المشابهة.

وتُعرَّف الحقول بالسمتين 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[]قائمة سماح بأسماء المخططات. والفراغ يعني الكل.
exclude_schemas[]قائمة منع، تُطبَّق بعد قائمة السماح.
store_eventstrueحفظ التدفقات الخام. يُتجاهل ما لم يكن fidelity: events.
classify_paste_sourcetrueمقارنة عمليات اللصق بالمقطع وباقتراحات الذكاء الاصطناعي وبمحتوى الحقل نفسه.
idle_session_ms30000مدة الخمول قبل إغلاق الجلسة وإرسالها.
flush_interval_ms5000وتيرة الإرسال من المتصفح.
pause_thresholds_ms[500,1000,2000,5000,10000]تُبلَّغ أعداد التوقفات عند كل عتبة منها.
disclose_to_annotatorstrueعرض إشعار بالتسجيل. وتعطيله يسجّل تحذيراً.

مفاتيح الكشف موثّقة في كشف عملية الكتابة.

اختيار مستوى الدقة

مستوى الدقةتخزين التدفقإعادة حساب مقاييس جديدة لاحقاً؟استخدمه حين
offتكون الميزة معطّلة في هذا المشروع
summaryلالاتكون متيقناً من السمات التي تحتاجها، أو لا تغطي موافقتك الأخلاقية الاحتفاظ بالتدفقات
eventsنعمنعمالافتراضي. نحو 2 بايت لكل ضغطة مفتاح

وevents هو الإعداد المستحسن. فإجابة من 500 كلمة تكلّف نحو 5 كيلوبايت، ويعني ذلك أن مقياساً يخطر لك بعد جمع البيانات يظل قابلاً للحساب.

السمات الملخّصة

ملخص واحد لكل (مستخدم، عنصر، حقل). وتتبع عائلات السمات ما جاء عند Crossley et al. (2024)؛ انظر الأساس البحثي.

الحجم ونسبة الناتج إلى العملية

الحقلالمعنى
keystrokesضغطات مفاتيح فعلية أنتجت نصاً
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حركات غير متجاورة لمؤشر الكتابة
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_eventsتأليف IME
virtual_keyboardاكتُشفت لوحة مفاتيح هاتف أو لوحة لمسية

أين تُخزَّن البيانات

وجهتان، لسببين مختلفين.

التدفقات الخام تذهب إلى SQLite

<task_dir>/project.sqlite، الجدول typing_sessions، بصف واحد لكل جلسة، عبر طبقة الحفظ نفسها التي تمر بها المذكرات ودليل الترميز.

وتُفكَّك أعمدة ملخّصة قابلة للاستعلام إلى جانب ملخص JSON كامل وكتلة أحداث محزومة بـ zlib:

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;"

ويُخزَّن التدفق ككتلة محزومة واحدة لكل جلسة لا كصف لكل ضغطة مفتاح. فهو لا يُقرأ إلا دفعة واحدة، وبنحو 2 بايت للحدث فإن مخططاً بصف لكل ضغطة سيضع عشرات الملايين من الصفوف في ملف المشروع دون أي فائدة استعلامية.

صفحات المراحل

تُلتقط أيضاً الإجابات النصية الحرة في مرحلة التدريب وفي استبيانات ما قبل الدراسة وما بعدها. وتلك الصفحات لا معرّف عنصر لها، فتُجمَّع جلساتها تحت العلامة الحارسة __phase_page__ التي يستخدمها بقية النظام السلوكي أصلاً، وتُعرَّف بعمودَي 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}"، فيسافر مع التعليق التوضيحي إلى لوحة تحكم المشرف وإلى التصديرات.

ولا تذهب التدفقات الخام إلى هناك عن قصد. فذلك الملف يُعاد تسلسله بالكامل ويُكتب كتابةً ذرّية عند كل حفظ للتعليق التوضيحي، والإجابة الطويلة آلاف الأحداث.

التصدير

التصديران كلاهما اختياري، فلا تُضمَّن البيانات السلوكية في إصدار مجموعة بيانات عن غير قصد.

السمات الملخّصة إلى جانب التعليقات التوضيحية

yaml
export_include_typing_dynamics: true

ينتج typing_dynamics.csv (أو .tsv) بجانب annotations.csv، بصف واحد لكل (مستخدم، عنصر، حقل)، يضم السمات الملخّصة وحكم الكاشف.

التدفقات الخام

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

يكتب keystroke_sessions.parquet وkeystroke_events.parquet، مع الرجوع إلى JSONL عند عدم تثبيت pyarrow. انظر تصدير 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

الطريقةالمسارالغرض
POST/api/track_typingاستقبال الجلسات المكتملة من المتصفح
GET/api/typing_summary/<instance_id>ملخصات عنصر واحد للمستخدم الحالي
GET/admin/api/writing_processتجميع لكل معلّق (يتطلب مفتاح المشرف)

تُلخَّص الجلسات على الخادم. فالمتصفح لا يرسل ملخصاً محسوباً، وبذلك لا يمكن تزوير الأرقام من عميل معدَّل، ويمكن إعادة حساب أي مقياس يُضاف لاحقاً من التدفقات المخزَّنة.

كيف تعمل الجلسات

تبدأ الجلسة حين يتلقى الحقل التركيز، وتنتهي بأول ما يقع: فقدان التركيز، أو الانتقال إلى عنصر آخر، أو خمول بمقدار idle_session_ms، أو إغلاق الصفحة. وتُرسَل الجلسات المكتملة كل flush_interval_ms، وعبر 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 فعلاً. وذلك عمل الإشارة لا خلل فيها.

قراءات إضافية

لمعرفة تفاصيل التنفيذ، انظر توثيق المصدر.