Journalisation des frappes
Potato peut enregistrer les pauses, les rafales, les révisions et les collages derrière une réponse en texte libre sans enregistrer aucun des caractères que tape l'annotateur.
Potato peut enregistrer comment une réponse en texte libre a été produite sans enregistrer la réponse. Chaque événement porte un horodatage, un type d'entrée, une classe de touche et une variation de longueur ; aucun ne porte le caractère qui a été tapé. À partir de ce flux, Potato calcule une quarantaine de caractéristiques de synthèse et les stocke avec l'annotation.
L'objectif est de distinguer une réponse rédigée, tapée avec les pauses et les remords de quelqu'un qui la construit, d'une réponse recopiée depuis une autre fenêtre ou collée depuis un agent conversationnel. Lisez les réponses finies : elles se ressemblent. Les journaux, non.
La journalisation des frappes demande Potato 2.7.2 ou une version ultérieure, et elle est désactivée par défaut : keystroke_logging.enabled vaut false tant que vous ne le changez pas, donc une mise à niveau ne se met jamais à enregistrer qui que ce soit. Pour les règles bâties sur ces données, voir Détection du processus de rédaction. Avant de pointer la fonction vers des participants humains, lisez Éthique de la journalisation des frappes.
Démarrage rapide
keystroke_logging:
enabled: trueC'est toute la configuration minimale. Chaque champ de texte libre du projet se met à produire un flux d'événements aveugle au contenu, une synthèse et un jeu de signalements de détection.
Un exemple exécutable est livré avec Potato :
python potato/flask_server.py start examples/advanced/keystroke-logging/config.yaml -p 8000Avertissement :
enabledvautfalsepar défaut. Mettre Potato à niveau ne démarre jamais silencieusement l'enregistrement de vos annotateurs.
Ce qui est capté
Chaque événement enregistre un horodatage, un type d'entrée, une classe de touche, la position du curseur et la variation de longueur du champ :
{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"}}
Ce qui n'est délibérément pas capté
| Non capté | Pourquoi |
|---|---|
| Les caractères tapés | Le flux reconstitue le processus, pas le texte |
| Le texte collé | Seulement une longueur, une étiquette de source et une empreinte salée |
| Les brouillons intermédiaires | Non reconstituables à partir des seules variations de longueur |
| Tout ce qui se trouve dans un champ de mot de passe | getFieldIdentity refuse type="password" d'emblée |
| Le contenu du presse-papiers en général | Lu au moment du collage pour la classification, puis jeté |
Classes de touches
La touche elle-même n'est jamais conservée, seulement la famille à laquelle elle appartient :
letter, digit, punct, space, enter, bksp, del, nav, mod, func, unknown
Types d'entrée
Le signal principal de Potato est InputEvent.inputType sur beforeinput, pas keydown. C'est le choix technique central. Le collage, le glisser-déposer, la composition IME, la dictée, le remplissage automatique et l'annulation modifient tous un champ sans déclencher keydown du tout, si bien qu'un journaliseur qui n'écoute que keydown est aveugle précisément aux cas que cette fonction existe pour détecter.
Types d'entrée captés : insertText, insertReplacementText, insertFromPaste, insertFromDrop, insertCompositionText, insertLineBreak, insertParagraph, deleteContentBackward, deleteContentForward, deleteWordBackward, deleteWordForward, deleteByCut, deleteByDrag, historyUndo, historyRedo, plus les événements synthétiques focus, blur et keydown.
keydown et keyup restent écoutés, mais uniquement pour compter les frappes physiques et mesurer le temps d'appui. L'écart entre les caractères apparus et les touches réellement enfoncées est le signal isolé le plus fort de toute la collecte. Voir silent_insert_ratio plus bas.
Quels champs sont instrumentés
Par défaut, tous les champs de texte libre : le schéma text, les zones de réponse libre à l'intérieur de radio et multiselect, et les zones de texte de justification ou de notes dans text_edit, pairwise, trajectory_eval et les schémas similaires.
Les champs sont identifiés par les attributs schema et label_name que Potato appose déjà sur chaque champ de saisie d'annotation, avec repli sur la découpe de l'attribut name sur :::.
Restreignez la portée avec l'une ou l'autre liste :
keystroke_logging:
enabled: true
include_schemas: [rationale] # allowlist; empty = all fields
exclude_schemas: [scratch_notes] # denylistOu excluez un seul élément dans du HTML personnalisé :
<textarea data-keystroke-logging="off" ...></textarea>Référence de configuration
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| Clé | Défaut | Signification |
|---|---|---|
enabled | false | Interrupteur principal. Rien n'est capté quand il vaut false. |
fidelity | events | off désactive ; summary calcule les caractéristiques sans stocker de flux ; events stocke les deux. |
include_schemas | [] | Liste blanche de noms de schémas. Vide signifie tous. |
exclude_schemas | [] | Liste noire, appliquée après la liste blanche. |
store_events | true | Conserve les flux bruts. Ignoré sauf avec fidelity: events. |
classify_paste_source | true | Compare les collages au passage, aux suggestions d'IA et au contenu déjà présent dans le champ. |
idle_session_ms | 30000 | Durée d'inactivité avant qu'une session soit close et envoyée. |
flush_interval_ms | 5000 | Cadence d'envoi du navigateur. |
pause_thresholds_ms | [500,1000,2000,5000,10000] | Le nombre de pauses est rapporté pour chacun de ces seuils. |
disclose_to_annotators | true | Affiche un avis d'enregistrement. Le désactiver inscrit un avertissement. |
Les clés de détection sont documentées dans Détection du processus de rédaction.
Choisir une fidélité
| Fidélité | Flux stocké | Recalculer de nouvelles métriques plus tard ? | À utiliser quand |
|---|---|---|---|
off | — | — | La fonction est désactivée pour ce projet |
summary | Non | Non | Vous savez exactement quelles caractéristiques vous voulez, ou votre approbation éthique ne couvre pas la conservation des flux |
events | Oui | Oui | Par défaut. Environ 2 octets par frappe |
events est le réglage recommandé. Une réponse de 500 mots coûte environ 5 Ko, et cela veut dire qu'une métrique à laquelle vous penserez après la collecte pourra encore être calculée.
Caractéristiques de synthèse
Une synthèse par (utilisateur, instance, champ). Les familles de caractéristiques suivent Crossley et al. (2024) ; voir les fondements de recherche.
Volume et rapport produit/processus
| Champ | Signification |
|---|---|
keystrokes | Appuis de touche physiques ayant produit du texte |
final_chars | Longueur du champ à la fin de la session |
chars_typed / chars_inserted | Caractères insérés par la frappe / par n'importe quel moyen |
chars_deleted | Caractères supprimés |
chars_per_keystroke | Au-dessus d'environ 1,1, du texte arrive sans frappes |
active_ms / wall_ms | Temps passé sur le champ, hors / y compris le temps d'absence |
Rythme
| Champ | Signification |
|---|---|
iki_median_ms, iki_mean_ms | Tendance centrale des intervalles entre touches |
iki_p10/p25/p75/p90_ms | Forme de la distribution des intervalles |
iki_log_sd, iki_log_cv | Dispersion en échelle logarithmique. Une valeur basse signifie métronomique, donc transcription. |
Échelle logarithmique parce que les distributions d'intervalles entre touches sont fortement asymétriques à droite. Les intervalles de plus de 30 secondes sont exclus de ces statistiques pour qu'une pause café ne les domine pas.
Pauses
| Champ | Signification |
|---|---|
pause_counts | Nombre de pauses à chaque seuil configuré |
pause_total_ms | Temps total passé en pause |
pre_word_pause_mean_ms | Pause moyenne avant d'entamer un mot |
pre_sentence_pause_mean_ms | Pause moyenne après une ponctuation |
intraword_iki_median_ms | Intervalle médian à l'intérieur des mots, indicateur indirect de l'aisance au clavier |
Rafales
| Champ | Signification |
|---|---|
bursts, burst_mean_chars, burst_max_chars | Statistiques des séquences de production |
p_bursts | Rafales terminées par une pause |
r_bursts | Rafales terminées par une révision |
Révision
| Champ | Signification |
|---|---|
backspaces, deletes, undo_events | Comportement de suppression |
non_terminal_edits | Modifications faites en deçà de la fin du texte, signe que l'auteur est revenu en arrière pour réviser |
caret_jumps | Déplacements non contigus du curseur |
revision_ratio | chars_deleted / chars_typed |
Insertion externe
| Champ | Signification |
|---|---|
paste_events, pasted_chars, largest_paste_chars | Volume de collage |
pasted_fraction | Part du texte final qui a été collée |
drop_events | Insertions par glisser-déposer |
silent_insert_chars / silent_insert_ratio | Caractères sans frappe correspondante |
external_insert_chars / external_insert_ratio | Comme ci-dessus, hors auto-citations et citations du passage |
paste_sources, paste_chars_by_source | Nombre de collages et de caractères par étiquette de source |
Utilisez external_insert_ratio pour la détection. silent_insert_ratio compte toute insertion silencieuse, y compris la légitime.
Attention
| Champ | Signification |
|---|---|
blur_events, blur_total_ms | Temps passé hors de la page |
max_blur_before_insert_ms | Plus longue absence précédant immédiatement une insertion importante |
first_keystroke_latency_ms | Temps de réflexion avant le premier caractère |
Intégrité
| Champ | Signification |
|---|---|
untrusted_events | InputEvent.isTrusted === false, c'est-à-dire une saisie scriptée ou automatisée |
composition_events | Composition IME |
virtual_keyboard | Clavier mobile ou logiciel détecté |
Où les données sont stockées
Deux destinations, pour deux raisons différentes.
Les flux bruts vont dans SQLite
<task_dir>/project.sqlite, table typing_sessions, une ligne par session, par la même couche de persistance que les mémos et le livre de codes.
Des colonnes de synthèse interrogeables sont dénormalisées à côté d'une synthèse JSON complète et d'un blob d'événements compressé par 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;"Le flux est stocké sous forme d'un blob compressé par session plutôt que d'une ligne par frappe. Il n'est jamais relu qu'en entier, et à environ 2 octets par événement, un schéma à une ligne par frappe mettrait des dizaines de millions de lignes dans un fichier de projet sans aucun bénéfice pour les requêtes.
Pages de phase
Les réponses en texte libre de la phase de formation et des enquêtes de pré-étude ou de post-étude sont captées elles aussi. Ces pages n'ont pas d'identifiant d'instance : leurs sessions sont donc regroupées sous la sentinelle __phase_page__ que le reste du système comportemental utilise déjà, et identifiées par leurs colonnes phase et page :
SELECT phase, page, count(*) FROM typing_sessions GROUP BY phase, page;C'est ce qui fait fonctionner l'exemple de calibration. Une tâche de recopie du passage en phase de formation fournit des exemples de transcription que l'on peut distinguer des réponses rédigées ordinaires par la seule colonne phase.
Les synthèses vont dans user_state.json
L'esquisse compacte est recopiée dans <output_annotation_dir>/<user>/user_state.json, sous instance_id_to_behavioral_data.<instance>.typing_summaries, avec la clé "{schema}:::{label}", si bien qu'elle voyage avec l'annotation jusqu'au tableau de bord d'administration et aux exports.
Les flux bruts n'y vont délibérément pas. Ce fichier est entièrement re-sérialisé et réécrit de façon atomique à chaque enregistrement d'annotation, et une réponse longue représente des milliers d'événements.
Exporter
Les deux exports sont optionnels, pour que les données comportementales ne se retrouvent jamais par accident dans une publication de jeu de données.
Caractéristiques de synthèse à côté des annotations
export_include_typing_dynamics: trueProduit typing_dynamics.csv (ou .tsv) à côté d'annotations.csv, une ligne par (utilisateur, instance, champ), avec les caractéristiques de synthèse et le verdict du détecteur.
Flux bruts
python -m potato.export.cli <config.yaml> --format keystrokesÉcrit keystroke_sessions.parquet et keystroke_events.parquet, avec repli sur JSONL quand pyarrow n'est pas installé. Voir Export Parquet pour l'exportateur plus général.
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"])Points de terminaison de l'API
| Méthode | Route | Objet |
|---|---|---|
POST | /api/track_typing | Reçoit les sessions terminées envoyées par le navigateur |
GET | /api/typing_summary/<instance_id> | Synthèses pour une instance, utilisateur courant |
GET | /admin/api/writing_process | Récapitulatif par annotateur (clé d'administration requise) |
Les sessions sont résumées côté serveur. Le navigateur n'envoie jamais de synthèse déjà calculée, donc les chiffres ne peuvent pas être falsifiés par un client modifié, et une métrique ajoutée plus tard peut être recalculée à partir des flux stockés.
Comment fonctionnent les sessions
Une session commence quand un champ prend le focus et se termine au premier de ces événements : perte du focus, navigation vers une autre instance, idle_session_ms d'inactivité, ou déchargement de la page. Les sessions terminées sont envoyées toutes les flush_interval_ms, et via navigator.sendBeacon au déchargement pour qu'une session en cours ne soit pas perdue.
Les sessions multiples sur un même champ sont fusionnées avant l'écriture de la synthèse dans l'état utilisateur, si bien que quitter un champ puis y revenir se lit comme une seule réponse plutôt que comme plusieurs réponses suspectement courtes. Les comptes et les durées s'additionnent. Les statistiques de distribution sont des approximations pondérées par les frappes : utilisez les flux bruts si vous avez besoin de la distribution agrégée exacte.
Dépannage
Aucune donnée n'est enregistrée
Vérifiez keystroke_logging.enabled: true et que fidelity ne vaut pas off. Dans la console du navigateur, window.keystrokeTracker doit exister avec isInitialized === true. S'il vaut undefined, la configuration n'est jamais parvenue au gabarit.
Le traceur existe mais aucune session n'apparaît
Vérifiez l'identification des champs :
const el = document.querySelector('textarea');
window.keystrokeTracker.getFieldIdentity(el); // null means it is not trackednull signifie que l'élément n'a ni attribut schema ni label_name, ni name séparé par :::, ou qu'il est exclu par la configuration.
silent_insertion signale tous les annotateurs sur mobile
Cela ne devrait pas arriver, car la règle est neutralisée quand virtual_keyboard est vrai. Si la détection se trompe, vérifiez que le client a bien positionné ce drapeau. Voir le tableau des faux positifs.
project.sqlite grossit
Environ 2 octets par frappe. Passez à fidelity: summary pour garder les caractéristiques et abandonner les flux, ou utilisez typing_store.delete_for_user() pour supprimer les données d'un participant.
Les chiffres semblent faux pour les tests automatisés
L'automatisation de navigateur tape avec des intervalles quasi nuls, ce qui déclenche véritablement implausible_speed. C'est la règle qui fonctionne, pas un bogue.
Pour aller plus loin
- Détection du processus de rédaction - les six règles et les trois niveaux de détection
- Éthique de la journalisation des frappes - consentement, IRB, conservation, droits des participants
- Suivi comportemental - le système de suivi des interactions plus large dans lequel cela s'inscrit
- Contrôle de la qualité - contrôles d'attention et étalons
- Tableau de bord d'administration - l'endroit où vit le panneau Processus de rédaction
Pour les détails d'implémentation, consultez la documentation source.