Skip to content

Registrazione dei tasti

Potato può registrare le pause, le raffiche, le revisioni e gli incollaggi dietro una risposta a testo libero senza registrare nessuno dei caratteri che l'annotatore digita.

Potato può registrare come è stata prodotta una risposta a testo libero senza registrare la risposta. Ogni evento porta con sé un timestamp, un tipo di input, una classe di tasto e una variazione di lunghezza; nessuno porta il carattere digitato. Da quel flusso Potato calcola circa quaranta caratteristiche di sintesi e le archivia insieme all'annotazione.

Lo scopo è distinguere una risposta composta, digitata con le pause e i ripensamenti di chi la sta elaborando, da una trascritta, ridigitata da un'altra finestra, o da una incollata, arrivata da un chatbot. A leggere le risposte finite si somigliano. I log no.

La registrazione dei tasti richiede Potato 2.7.2 o successivo ed è disattivata per impostazione predefinita: keystroke_logging.enabled è false finché non lo imposti, quindi un aggiornamento non fa mai partire la registrazione di nessuno. Per le regole costruite su questi dati, vedi Rilevamento del processo di scrittura. Prima di puntarla su partecipanti umani, leggi Etica della registrazione dei tasti.

Avvio rapido

yaml
keystroke_logging:
  enabled: true

Questa è tutta la configurazione minima. Ogni campo di testo libero del progetto inizia a produrre un flusso di eventi cieco rispetto al contenuto, una sintesi e un insieme di segnalazioni di rilevamento.

Con Potato viene distribuito un esempio eseguibile:

bash
python potato/flask_server.py start examples/advanced/keystroke-logging/config.yaml -p 8000

Attenzione: enabled vale false per impostazione predefinita. Aggiornare Potato non fa mai partire in silenzio la registrazione dei tuoi annotatori.

Che cosa viene catturato

Ogni evento registra un timestamp, un tipo di input, una classe di tasto, la posizione del cursore e la variazione di lunghezza del campo:

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

Che cosa non viene catturato, deliberatamente

Non catturatoPerché
I caratteri digitatiIl flusso ricostruisce il processo, non il testo
Il testo incollatoSolo una lunghezza, un'etichetta di provenienza e un hash con sale
Le bozze intermedieNon ricostruibili dalle sole variazioni di lunghezza
Qualsiasi cosa in un campo passwordgetFieldIdentity rifiuta senza eccezioni type="password"
I contenuti degli appunti in generaleLetti al momento dell'incollaggio per classificarli, poi scartati

Classi di tasto

Il tasto in sé non viene mai memorizzato, solo la famiglia a cui appartiene:

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

Tipi di input

Il segnale primario di Potato è InputEvent.inputType su beforeinput, non keydown. È questa la scelta tecnica centrale. Incollaggio, drag-and-drop, composizione IME, dettatura, riempimento automatico e annullamento modificano tutti un campo senza far scattare keydown, quindi un registratore basato solo su keydown è cieco proprio sui casi che questa funzionalità esiste per rilevare.

Tipi di input catturati: insertText, insertReplacementText, insertFromPaste, insertFromDrop, insertCompositionText, insertLineBreak, insertParagraph, deleteContentBackward, deleteContentForward, deleteWordBackward, deleteWordForward, deleteByCut, deleteByDrag, historyUndo, historyRedo, più i sintetici focus, blur e keydown.

keydown e keyup restano sotto ascolto, ma solo per contare le battute fisiche e misurare il tempo di pressione. Lo scarto fra i caratteri comparsi e i tasti realmente premuti è il singolo segnale più forte fra quelli raccolti. Vedi silent_insert_ratio più sotto.

Quali campi vengono strumentati

Per impostazione predefinita, ogni campo di testo libero: lo schema text, le caselle a risposta libera dentro radio e multiselect, e le textarea di motivazione o di note in text_edit, pairwise, trajectory_eval e schemi simili.

I campi vengono identificati dagli attributi schema e label_name che Potato già applica a ogni input di annotazione, con ripiego sulla suddivisione dell'attributo name su :::.

Restringi l'ambito con una delle due liste:

yaml
keystroke_logging:
  enabled: true
  include_schemas: [rationale]      # allowlist; empty = all fields
  exclude_schemas: [scratch_notes]  # denylist

Oppure escludi un singolo elemento in HTML personalizzato:

html
<textarea data-keystroke-logging="off" ...></textarea>

Riferimento di configurazione

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
ChiavePredefinitoSignificato
enabledfalseInterruttore generale. Con false non viene catturato nulla.
fidelityeventsoff disattiva; summary calcola le caratteristiche ma non archivia il flusso; events archivia entrambi.
include_schemas[]Lista di schemi ammessi. Vuota significa tutti.
exclude_schemas[]Lista di esclusione, applicata dopo quella di ammissione.
store_eventstrueConserva i flussi grezzi. Ignorato se non c'è fidelity: events.
classify_paste_sourcetrueConfronta gli incollaggi con il passaggio, i suggerimenti AI e il contenuto del campo stesso.
idle_session_ms30000Inattività prima che una sessione venga chiusa e inviata.
flush_interval_ms5000Cadenza di invio dal browser.
pause_thresholds_ms[500,1000,2000,5000,10000]I conteggi delle pause vengono riportati per ciascuna soglia.
disclose_to_annotatorstrueMostra un avviso di registrazione. Disattivarlo registra un avvertimento.

Le chiavi di rilevamento sono documentate in Rilevamento del processo di scrittura.

Scegliere una fedeltà

FedeltàFlusso archiviatoRicalcolare nuove metriche in seguito?Da usare quando
offLa funzionalità è disattivata per questo progetto
summaryNoNoSai con certezza quali caratteristiche ti servono, oppure la tua approvazione etica non copre la conservazione dei flussi
eventsPredefinito. Circa 2 byte per battuta

events è l'impostazione consigliata. Una risposta di 500 parole costa circa 5 KB, e significa che una metrica che ti viene in mente dopo la raccolta dati si può ancora calcolare.

Caratteristiche di sintesi

Una sintesi per ogni (utente, istanza, campo). Le famiglie di caratteristiche seguono Crossley et al. (2024); vedi le basi di ricerca.

Volume e rapporto prodotto/processo

CampoSignificato
keystrokesBattute fisiche che hanno prodotto testo
final_charsLunghezza del campo alla fine della sessione
chars_typed / chars_insertedCaratteri inseriti digitando / con qualsiasi mezzo
chars_deletedCaratteri rimossi
chars_per_keystrokeSopra ~1,1 implica testo che arriva senza battute
active_ms / wall_msTempo sul campo, escludendo / includendo il tempo trascorso altrove

Ritmo

CampoSignificato
iki_median_ms, iki_mean_msTendenza centrale dell'intervallo fra i tasti
iki_p10/p25/p75/p90_msForma della distribuzione degli intervalli
iki_log_sd, iki_log_cvDispersione su scala logaritmica. Bassa significa metronomica, cioè trascrizione.

Scala logaritmica perché le distribuzioni degli intervalli fra i tasti sono fortemente asimmetriche a destra. Gli intervalli sopra i 30 secondi sono esclusi da queste statistiche, così una pausa caffè non può dominarle.

Pause

CampoSignificato
pause_countsConteggi per ciascuna soglia configurata
pause_total_msTempo totale in pausa
pre_word_pause_mean_msPausa media prima di iniziare una parola
pre_sentence_pause_mean_msPausa media dopo la punteggiatura
intraword_iki_median_msIntervallo mediano all'interno delle parole, indicatore indiretto dell'abilità dattilografica

Raffiche

CampoSignificato
bursts, burst_mean_chars, burst_max_charsStatistiche sulle sequenze di produzione
p_burstsRaffiche terminate da una pausa
r_burstsRaffiche terminate da una revisione

Revisione

CampoSignificato
backspaces, deletes, undo_eventsComportamento di cancellazione
non_terminal_editsModifiche fatte prima della fine del testo, cioè chi scrive è tornato indietro a rivedere
caret_jumpsSpostamenti non adiacenti del cursore
revision_ratiochars_deleted / chars_typed

Inserimento esterno

CampoSignificato
paste_events, pasted_chars, largest_paste_charsVolume degli incollaggi
pasted_fractionQuota del testo finale arrivata per incollaggio
drop_eventsInserimenti per drag-and-drop
silent_insert_chars / silent_insert_ratioCaratteri senza una battuta corrispondente
external_insert_chars / external_insert_ratioCome sopra, escludendo autocitazioni e citazioni del passaggio
paste_sources, paste_chars_by_sourceConteggi e caratteri per etichetta di provenienza

Per il rilevamento usa external_insert_ratio. silent_insert_ratio conta ogni inserimento silenzioso, compreso quello legittimo.

Attenzione

CampoSignificato
blur_events, blur_total_msTempo trascorso fuori dalla pagina
max_blur_before_insert_msAssenza più lunga immediatamente precedente a un inserimento consistente
first_keystroke_latency_msTempo di riflessione prima del primo carattere

Integrità

CampoSignificato
untrusted_eventsInputEvent.isTrusted === false, cioè input da script o automatizzato
composition_eventsComposizione IME
virtual_keyboardRilevata tastiera mobile o software

Dove vengono archiviati i dati

Due destinazioni, per due ragioni diverse.

I flussi grezzi vanno in SQLite

<task_dir>/project.sqlite, tabella typing_sessions, una riga per sessione, attraverso lo stesso livello di persistenza dei memo e del codebook.

Le colonne di sintesi interrogabili sono denormalizzate accanto a una sintesi JSON completa e a un blob di eventi compresso con 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;"

Il flusso è archiviato come un unico blob compresso per sessione anziché come una riga per battuta. Viene riletto solo per intero, e a circa 2 byte per evento uno schema con una riga per battuta metterebbe decine di milioni di righe in un file di progetto senza alcun vantaggio nelle query.

Pagine di fase

Vengono catturate anche le risposte a testo libero nella fase di addestramento e nei questionari pre-studio o post-studio. Quelle pagine non hanno un id di istanza, quindi le loro sessioni finiscono sotto il sentinella __phase_page__ che il resto del sistema comportamentale già usa, e sono identificate dalle colonne phase e page:

sql
SELECT phase, page, count(*) FROM typing_sessions GROUP BY phase, page;

È questo che fa funzionare l'esempio di calibrazione. Un compito di copiatura del passaggio nella fase di addestramento produce esempi di trascrizione distinguibili dalle normali risposte composte in base al solo phase.

Le sintesi vanno in user_state.json

Il quadro compatto viene rispecchiato in <output_annotation_dir>/<user>/user_state.json sotto instance_id_to_behavioral_data.<instance>.typing_summaries, con chiave "{schema}:::{label}", così viaggia insieme all'annotazione fino alla dashboard di amministrazione e alle esportazioni.

I flussi grezzi deliberatamente non finiscono lì. Quel file viene riserializzato per intero e riscritto in modo atomico a ogni salvataggio di annotazione, e una risposta lunga è fatta di migliaia di eventi.

Esportazione

Entrambe le esportazioni sono opt-in, così i dati comportamentali non finiscono mai per errore in una pubblicazione di dataset.

Caratteristiche di sintesi accanto alle annotazioni

yaml
export_include_typing_dynamics: true

Produce typing_dynamics.csv (o .tsv) accanto ad annotations.csv, una riga per ogni (utente, istanza, campo), con le caratteristiche di sintesi e il verdetto del rilevatore.

Flussi grezzi

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

Scrive keystroke_sessions.parquet e keystroke_events.parquet, ripiegando su JSONL quando pyarrow non è installato. Vedi Esportazione Parquet per l'esportatore nel suo complesso.

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"])

Endpoint API

MetodoRouteScopo
POST/api/track_typingRiceve dal browser le sessioni completate
GET/api/typing_summary/<instance_id>Sintesi per una istanza, utente corrente
GET/admin/api/writing_processRiepilogo per annotatore (richiede la chiave di amministrazione)

Le sessioni vengono riassunte lato server. Il browser non invia mai una sintesi già calcolata, quindi i numeri non possono essere falsificati da un client modificato, e una metrica aggiunta in seguito può essere ricalcolata dai flussi archiviati.

Come funzionano le sessioni

Una sessione inizia quando un campo riceve il focus e termina alla prima di queste condizioni: perdita del focus, navigazione verso un'altra istanza, idle_session_ms di inattività, o scaricamento della pagina. Le sessioni completate vengono inviate ogni flush_interval_ms, e tramite navigator.sendBeacon allo scaricamento, così una sessione in corso non va persa.

Più sessioni sullo stesso campo vengono unite prima che la sintesi sia scritta nello stato utente, così lasciare un campo e tornarci risulta come una sola risposta anziché diverse risposte sospettosamente brevi. I conteggi e le durate si sommano. Le statistiche di distribuzione sono approssimazioni pesate sulle battute, quindi usa i flussi grezzi se ti serve una distribuzione aggregata esatta.

Risoluzione dei problemi

Non viene registrato alcun dato

Verifica keystroke_logging.enabled: true e che fidelity non sia off. Nella console del browser, window.keystrokeTracker dovrebbe esistere con isInitialized === true. Se è undefined, la configurazione non è mai arrivata al template.

Il tracker c'è ma non compare nessuna sessione

Controlla l'identificazione dei campi:

js
const el = document.querySelector('textarea');
window.keystrokeTracker.getFieldIdentity(el);   // null means it is not tracked

null significa che l'elemento non ha attributi schema o label_name né un name separato da :::, oppure che è escluso dalla configurazione.

silent_insertion segnala ogni annotatore da mobile

Non dovrebbe, perché la regola è soppressa quando virtual_keyboard è true. Se il rilevamento parte a vuoto, verifica che il client abbia impostato quel flag. Vedi la tabella dei falsi positivi.

project.sqlite sta crescendo

Circa 2 byte per battuta. Imposta fidelity: summary per conservare le caratteristiche e lasciar cadere i flussi, oppure usa typing_store.delete_for_user() per rimuovere i dati di un singolo partecipante.

I numeri sembrano sbagliati per i test automatici

L'automazione del browser digita a intervalli quasi nulli, il che fa scattare davvero implausible_speed. È la segnalazione che funziona, non un bug.

Ulteriori Letture

Per i dettagli di implementazione, consulta la documentazione sorgente.