تسجيل ضغطات المفاتيح
يستطيع Potato تسجيل التوقفات والدفقات والمراجعات وعمليات اللصق التي وراء إجابة نصية حرة دون تسجيل أي من الحروف التي يكتبها المعلّق.
يستطيع Potato تسجيل كيفية إنتاج إجابة نصية حرة دون تسجيل الإجابة نفسها. يحمل كل حدث طابعاً زمنياً ونوع إدخال وفئة مفتاح وتغيّراً في الطول؛ ولا يحمل أي منها الحرف الذي كُتب. ومن ذلك التدفق يحسب 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 لا تبدأ تسجيل معلّقيك في صمت أبداً.
ما يُلتقط
يسجّل كل حدث طابعاً زمنياً، ونوع إدخال، وفئة مفتاح، وموضع مؤشر الكتابة، والتغيّر في طول الحقل:
{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 عند :::.
قيّد النطاق بأي من القائمتين:
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 | [] | قائمة سماح بأسماء المخططات. والفراغ يعني الكل. |
exclude_schemas | [] | قائمة منع، تُطبَّق بعد قائمة السماح. |
store_events | true | حفظ التدفقات الخام. يُتجاهل ما لم يكن fidelity: events. |
classify_paste_source | true | مقارنة عمليات اللصق بالمقطع وباقتراحات الذكاء الاصطناعي وبمحتوى الحقل نفسه. |
idle_session_ms | 30000 | مدة الخمول قبل إغلاق الجلسة وإرسالها. |
flush_interval_ms | 5000 | وتيرة الإرسال من المتصفح. |
pause_thresholds_ms | [500,1000,2000,5000,10000] | تُبلَّغ أعداد التوقفات عند كل عتبة منها. |
disclose_to_annotators | true | عرض إشعار بالتسجيل. وتعطيله يسجّل تحذيراً. |
مفاتيح الكشف موثّقة في كشف عملية الكتابة.
اختيار مستوى الدقة
| مستوى الدقة | تخزين التدفق | إعادة حساب مقاييس جديدة لاحقاً؟ | استخدمه حين |
|---|---|---|---|
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_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، بصف واحد لكل جلسة، عبر طبقة الحفظ نفسها التي تمر بها المذكرات ودليل الترميز.
وتُفكَّك أعمدة ملخّصة قابلة للاستعلام إلى جانب ملخص JSON كامل وكتلة أحداث محزومة بـ zlib:
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 بدلاً من ذلك:
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}"، فيسافر مع التعليق التوضيحي إلى لوحة تحكم المشرف وإلى التصديرات.
ولا تذهب التدفقات الخام إلى هناك عن قصد. فذلك الملف يُعاد تسلسله بالكامل ويُكتب كتابةً ذرّية عند كل حفظ للتعليق التوضيحي، والإجابة الطويلة آلاف الأحداث.
التصدير
التصديران كلاهما اختياري، فلا تُضمَّن البيانات السلوكية في إصدار مجموعة بيانات عن غير قصد.
السمات الملخّصة إلى جانب التعليقات التوضيحية
export_include_typing_dynamics: trueينتج typing_dynamics.csv (أو .tsv) بجانب annotations.csv، بصف واحد لكل (مستخدم، عنصر، حقل)، يضم السمات الملخّصة وحكم الكاشف.
التدفقات الخام
python -m potato.export.cli <config.yaml> --format keystrokesيكتب keystroke_sessions.parquet وkeystroke_events.parquet، مع الرجوع إلى JSONL عند عدم تثبيت pyarrow. انظر تصدير 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
| الطريقة | المسار | الغرض |
|---|---|---|
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 فالتهيئة لم تصل إلى القالب.
المتتبع موجود لكن لا تظهر جلسات
تحقق من تعريف الحقل:
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 فعلاً. وذلك عمل الإشارة لا خلل فيها.
قراءات إضافية
- كشف عملية الكتابة - القواعد الست وطبقات الكشف الثلاث
- أخلاقيات تسجيل ضغطات المفاتيح - الموافقة، وIRB، والاحتفاظ، وحقوق المشاركين
- تتبع السلوك - نظام تتبع التفاعل الأوسع الذي تقع هذه الميزة داخله
- مراقبة الجودة - فحوصات الانتباه والمعايير الذهبية
- لوحة تحكم المشرف - حيث تقع لوحة «عملية الكتابة»
لمعرفة تفاصيل التنفيذ، انظر توثيق المصدر.