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
keystroke_logging:
enabled: trueQuesta è 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:
python potato/flask_server.py start examples/advanced/keystroke-logging/config.yaml -p 8000Attenzione:
enabledvalefalseper 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:
{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 catturato | Perché |
|---|---|
| I caratteri digitati | Il flusso ricostruisce il processo, non il testo |
| Il testo incollato | Solo una lunghezza, un'etichetta di provenienza e un hash con sale |
| Le bozze intermedie | Non ricostruibili dalle sole variazioni di lunghezza |
| Qualsiasi cosa in un campo password | getFieldIdentity rifiuta senza eccezioni type="password" |
| I contenuti degli appunti in generale | Letti 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:
keystroke_logging:
enabled: true
include_schemas: [rationale] # allowlist; empty = all fields
exclude_schemas: [scratch_notes] # denylistOppure escludi un singolo elemento in HTML personalizzato:
<textarea data-keystroke-logging="off" ...></textarea>Riferimento di configurazione
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| Chiave | Predefinito | Significato |
|---|---|---|
enabled | false | Interruttore generale. Con false non viene catturato nulla. |
fidelity | events | off 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_events | true | Conserva i flussi grezzi. Ignorato se non c'è fidelity: events. |
classify_paste_source | true | Confronta gli incollaggi con il passaggio, i suggerimenti AI e il contenuto del campo stesso. |
idle_session_ms | 30000 | Inattività prima che una sessione venga chiusa e inviata. |
flush_interval_ms | 5000 | Cadenza di invio dal browser. |
pause_thresholds_ms | [500,1000,2000,5000,10000] | I conteggi delle pause vengono riportati per ciascuna soglia. |
disclose_to_annotators | true | Mostra 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 archiviato | Ricalcolare nuove metriche in seguito? | Da usare quando |
|---|---|---|---|
off | — | — | La funzionalità è disattivata per questo progetto |
summary | No | No | Sai con certezza quali caratteristiche ti servono, oppure la tua approvazione etica non copre la conservazione dei flussi |
events | Sì | Sì | Predefinito. 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
| Campo | Significato |
|---|---|
keystrokes | Battute fisiche che hanno prodotto testo |
final_chars | Lunghezza del campo alla fine della sessione |
chars_typed / chars_inserted | Caratteri inseriti digitando / con qualsiasi mezzo |
chars_deleted | Caratteri rimossi |
chars_per_keystroke | Sopra ~1,1 implica testo che arriva senza battute |
active_ms / wall_ms | Tempo sul campo, escludendo / includendo il tempo trascorso altrove |
Ritmo
| Campo | Significato |
|---|---|
iki_median_ms, iki_mean_ms | Tendenza centrale dell'intervallo fra i tasti |
iki_p10/p25/p75/p90_ms | Forma della distribuzione degli intervalli |
iki_log_sd, iki_log_cv | Dispersione 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
| Campo | Significato |
|---|---|
pause_counts | Conteggi per ciascuna soglia configurata |
pause_total_ms | Tempo totale in pausa |
pre_word_pause_mean_ms | Pausa media prima di iniziare una parola |
pre_sentence_pause_mean_ms | Pausa media dopo la punteggiatura |
intraword_iki_median_ms | Intervallo mediano all'interno delle parole, indicatore indiretto dell'abilità dattilografica |
Raffiche
| Campo | Significato |
|---|---|
bursts, burst_mean_chars, burst_max_chars | Statistiche sulle sequenze di produzione |
p_bursts | Raffiche terminate da una pausa |
r_bursts | Raffiche terminate da una revisione |
Revisione
| Campo | Significato |
|---|---|
backspaces, deletes, undo_events | Comportamento di cancellazione |
non_terminal_edits | Modifiche fatte prima della fine del testo, cioè chi scrive è tornato indietro a rivedere |
caret_jumps | Spostamenti non adiacenti del cursore |
revision_ratio | chars_deleted / chars_typed |
Inserimento esterno
| Campo | Significato |
|---|---|
paste_events, pasted_chars, largest_paste_chars | Volume degli incollaggi |
pasted_fraction | Quota del testo finale arrivata per incollaggio |
drop_events | Inserimenti per drag-and-drop |
silent_insert_chars / silent_insert_ratio | Caratteri senza una battuta corrispondente |
external_insert_chars / external_insert_ratio | Come sopra, escludendo autocitazioni e citazioni del passaggio |
paste_sources, paste_chars_by_source | Conteggi e caratteri per etichetta di provenienza |
Per il rilevamento usa external_insert_ratio. silent_insert_ratio conta ogni inserimento silenzioso, compreso quello legittimo.
Attenzione
| Campo | Significato |
|---|---|
blur_events, blur_total_ms | Tempo trascorso fuori dalla pagina |
max_blur_before_insert_ms | Assenza più lunga immediatamente precedente a un inserimento consistente |
first_keystroke_latency_ms | Tempo di riflessione prima del primo carattere |
Integrità
| Campo | Significato |
|---|---|
untrusted_events | InputEvent.isTrusted === false, cioè input da script o automatizzato |
composition_events | Composizione IME |
virtual_keyboard | Rilevata 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:
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:
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
export_include_typing_dynamics: trueProduce 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
python -m potato.export.cli <config.yaml> --format keystrokesScrive keystroke_sessions.parquet e keystroke_events.parquet, ripiegando su JSONL quando pyarrow non è installato. Vedi Esportazione Parquet per l'esportatore nel suo complesso.
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
| Metodo | Route | Scopo |
|---|---|---|
POST | /api/track_typing | Riceve dal browser le sessioni completate |
GET | /api/typing_summary/<instance_id> | Sintesi per una istanza, utente corrente |
GET | /admin/api/writing_process | Riepilogo 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:
const el = document.querySelector('textarea');
window.keystrokeTracker.getFieldIdentity(el); // null means it is not trackednull 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
- Rilevamento del processo di scrittura - le sei regole e i tre livelli di rilevamento
- Etica della registrazione dei tasti - consenso, IRB, conservazione, diritti dei partecipanti
- Tracciamento Comportamentale - il sistema più ampio di tracciamento delle interazioni in cui questo si inserisce
- Controllo della Qualità - verifiche di attenzione e standard gold
- Dashboard di Amministrazione - dove si trova il pannello Processo di scrittura
Per i dettagli di implementazione, consulta la documentazione sorgente.