Leggere il processo di scrittura: registrazione dei tasti per l'annotazione a testo libero
Potato può ora registrare come gli annotatori producono le risposte a testo libero, senza registrare che cosa scrivono, e trasformare pause, revisioni e incollaggi in segnalazioni verificabili per le risposte incollate anziché composte.
Un crowdworker apre il tuo task di annotazione, legge il passaggio, passa a un'altra scheda, torna venti secondi dopo e nella casella compare in un solo movimento una motivazione di 280 caratteri.
La motivazione va bene. È in tema, è grammaticale, fa riferimento al passaggio. Leggila accanto ad altre quaranta e non salta fuori nulla. Qualsiasi classificatore le si punti contro dirà qualcosa fra «probabilmente umano» e «non chiaro», che è quello che quei classificatori dicono della maggior parte dei testi.
La risposta finita non dice da dove viene. I venti secondi e il movimento unico sì.
Potato ora lo registra. I campi di testo libero possono catturare un flusso cieco rispetto al contenuto di come è stata prodotta una risposta, riassumerlo in circa quaranta caratteristiche ed eseguire sul risultato una piccola serie di regole con nome. È disattivato per impostazione predefinita e si attiva con una riga:
keystroke_logging:
enabled: trueRegistrazione dei tasti
Composto, trascritto, incollato
La ricerca su cui tutto questo si appoggia è piuttosto assestata. Crossley e colleghi hanno raccolto 500 saggi argomentativi, li hanno fatti trascrivere da un secondo gruppo di lavoratori e hanno separato la scrittura autentica dalla trascrizione con un'accuratezza del 99% usando una random forest. Deane et al. e Zhang et al. hanno trovato la stessa separazione in modo indipendente. Asher et al. ne hanno costruito la versione crowdsourcing su Prolific, segnalando i partecipanti il cui conteggio di tasti era troppo basso rispetto alla lunghezza di quanto avevano consegnato.
La firma è coerente in tutti questi lavori. La composizione reale ha pause più lunghe prima delle frasi e delle parole, più inserimenti e cancellazioni e una varianza alta negli intervalli fra i tasti. La copiatura a macchina è lineare, procede a raffiche e ha varianza bassa. L'incollaggio quasi non ha intervalli.
Niente di tutto questo si vede nel testo. Tutto si vede nel log.
Perché beforeinput e non keydown
Questa è l'unica decisione tecnica che determina se il resto della funzionalità funziona.
Il modo ovvio di scrivere un registratore di tasti è mettersi in ascolto su keydown. È anche il modo che fallisce, perché incollaggio, drag-and-drop, composizione IME, dettatura, riempimento automatico e annullamento cambiano tutti il contenuto di un campo senza far scattare keydown nemmeno una volta. Un registratore basato solo su keydown è cieco esattamente sui casi per cui esiste.
Il segnale primario di Potato è InputEvent.inputType su beforeinput, che scatta per tutti questi casi e dice quale si è verificato. keydown e keyup restano sotto ascolto, ma per uno scopo diverso: contare i tasti che una persona ha premuto fisicamente.
Lo scarto fra questi due numeri è la cosa più utile che viene raccolta. I caratteri comparsi nel campo senza una battuta corrispondente vengono registrati come silent_insert_chars, e la loro quota sulla risposta come silent_insert_ratio. Un incollaggio soppresso dalla pagina, un'estensione che inietta testo, un flusso di dettatura, uno script che riempie la casella: nessuno di questi produce battute, tutti producono caratteri.
Cosa viene registrato e cosa no
Ogni evento porta con sé un timestamp, un tipo di input, una classe di tasto, una posizione del cursore e una variazione di lunghezza:
{t_ms: 1240, input_type: "insertText", key_class: "letter", pos: 41, 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"}}
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. Il testo incollato viene ridotto a una lunghezza, un'etichetta di provenienza e un hash con sale per sessione. I campi password vengono rifiutati del tutto. Non puoi ricostruire la risposta dal flusso, ed è questo il punto: il flusso descrive il processo e non dice nulla sul contenuto.
La classificazione della provenienza degli incollaggi è ciò che tiene il comportamento ordinario fuori dalle segnalazioni. Quando arriva un incollaggio, Potato lo confronta con il passaggio in annotazione, con l'eventuale suggerimento AI mostrato nella pagina e con quanto era già nel campo, poi conserva l'etichetta e scarta il confronto. Citare il passaggio risulta instance_text. Spostare la propria bozza risulta self. Nessuno dei due conta come inserimento esterno.
Sei regole, e niente che finga di essere un modello
Il rilevamento gira lato server su tre livelli. Solo il primo è attivo per impostazione predefinita.
Il primo livello è costituito da sei segnalazioni con nome, ciascuna con una soglia esplicita, ciascuna con in ritorno i valori delle caratteristiche che l'hanno fatta scattare:
| Segnalazione | Scatta quando | Gravità |
|---|---|---|
paste_dominant | Metà o più del testo finale è arrivata per incollaggio | sospetto |
silent_insertion | ≥30% dei caratteri inseriti non aveva una battuta dietro | sospetto |
transcription_rhythm | Ritmo metronomico e nessuna revisione e quasi nessuna pausa | da rivedere |
offscreen_composition | Un grosso inserimento esterno subito dopo ≥10s fuori dalla pagina | sospetto |
implausible_speed | Costantemente sopra i ~180 wpm per un'intera risposta | da rivedere |
synthetic_input | Il browser riporta isTrusted === false | sospetto |
transcription_rhythm è congiuntiva di proposito. Una digitazione metronomica da sola è un dattilografo veloce. Non cancellare mai nulla è una persona attenta. Fare appena qualche pausa è una risposta breve. Solo quando le tre cose valgono insieme si ha la firma della copiatura a macchina, e la regola salta del tutto le risposte sotto gli 80 caratteri, dove non c'è ritmo da leggere.
Il secondo livello è la calibrazione. Le caratteristiche dei tasti dipendono molto dal compito di scrittura, quindi una soglia tarata su una motivazione di una frase è sbagliata per cinque paragrafi. python -m potato.typing_detect calibrate config.yaml ritara ogni soglia su un percentile di coda delle sessioni del tuo progetto. Servono almeno 30 sessioni utilizzabili, e i valori calibrati vengono limitati entro 3× i valori predefiniti, così una popolazione omogenea non può trascinare una soglia sulla propria mediana.
Il terzo è supervisionato. Se hai delle etichette, fit_supervised() addestra un vero classificatore sulla matrice delle caratteristiche. scikit-learn viene importato in modo lazy e non è una dipendenza di Potato.
Non viene distribuito alcun modello pre-addestrato. Nel repository non c'è un corpus etichettato, e distribuire coefficienti stimati sul nulla vorrebbe dire inventarsi un numero di validazione. Quello che viene distribuito sono invece sei regole che puoi leggere, contestare e scavalcare.
Se vuoi delle etichette, la fase di addestramento può produrle dall'interno del tuo studio. Dai agli annotatori un riscaldamento in cui copiano il passaggio: quelle sessioni sono esempi genuini di trascrizione, prodotti dai tuoi annotatori sul tuo compito, e le loro risposte ordinarie sono la classe composta. È così che è stato costruito il corpus di Crossley, e l'esempio di calibrazione lo imposta dall'inizio alla fine.
Le soglie vengono valutate sul server e non vengono mai inviate al browser. Pubblicarle direbbe a un annotatore con quanta lentezza esattamente incollare.
Dove finisce
I flussi grezzi finiscono in SQLite, in <task_dir>/project.sqlite, una riga per sessione, attraverso lo stesso livello di persistenza dei memo e del codebook. Gli eventi sono codificati come delta e compressi con zlib a 1,7 byte per evento misurati, quindi una risposta di 500 parole costa circa 5 KB.
Deliberatamente non finiscono in user_state.json. Quel file viene riscritto per intero a ogni salvataggio di annotazione, e una risposta lunga è attorno ai 3.000 eventi. Solo la sintesi compatta viene rispecchiata nei dati comportamentali, con chiave "{schema}:::{label}", così viaggia insieme all'annotazione fino alla dashboard e alle esportazioni.
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__ già esistente e sono identificate da phase e page. È questo che fa funzionare il trucco della copiatura del passaggio: gli esempi di trascrizione sono separabili dalle risposte ordinarie in base alla sola fase.
Entrambe le esportazioni sono opt-in. export_include_typing_dynamics: true scrive un file sidecar typing_dynamics.csv accanto alle tue annotazioni, e python -m potato.export.cli <config.yaml> --format keystrokes scrive i flussi grezzi in Parquet, ripiegando su JSONL in assenza di pyarrow. I dati comportamentali non finiscono mai per errore in una pubblicazione di dataset.
Prima di puntarlo sulle persone
Una segnalazione è un indizio da far verificare a una persona. Non è una prova, e non deve essere collegata a rifiuti automatici, trattenute di pagamento o blocchi.
Il modo in cui questa cosa fallisce è accusare un annotatore onesto, e i casi che fanno scattare le regole non sono esotici. Alcuni sono gestiti: citare il passaggio e riordinare la propria bozza vengono soppressi dalla classificazione della provenienza, e le tastiere software e la composizione IME sopprimono silent_insertion, dato che nessuna delle due emette keydown in modo affidabile e altrimenti ogni inserimento sembrerebbe silenzioso. Altri non lo sono. La dettatura farà scattare una segnalazione. Le estensioni per la grammatica pure. Alcune tecnologie assistive producono eventi non attendibili e faranno scattare synthetic_input: per questo la documentazione dice chiaramente che se il tuo studio è aperto a chi usa tecnologie assistive è meglio disattivare quella regola, piuttosto che spiegare una segnalazione a qualcuno che stava usando gli strumenti di cui ha bisogno per lavorare.
Poi c'è l'aritmetica. Se il 5% delle tue risposte è incollato e la tua regola segnala il 5% delle sessioni, gran parte di ciò che segnali può comunque essere lavoro onesto. Su una piattaforma dove la scorrettezza è davvero rara, una regola anche con un tasso di falsi positivi modesto produce più false accuse che veri ritrovamenti. Una soglia calibrata rende la cosa esplicita, non migliore: un percentile di coda segnala la sua frazione di coda in qualsiasi popolazione, compresa una in cui nessuno ha fatto niente di male.
La comunicazione agli annotatori è attiva per impostazione predefinita e disattivarla registra un avviso all'avvio. Quella scelta predefinita esiste perché i pattern temporali sono un dato biometrico comportamentale. Possono identificare una persona e collegare account fra contesti diversi, e la letteratura di ricerca li ha usati per inferire l'abilità dattilografica, lo stato di parlante di seconda lingua e il carico cognitivo. Nessuno che si è iscritto per etichettare frasi se lo aspettava. Potato non calcola niente di tutto ciò e non distribuisce strumenti per farlo, ma i dati che conservi sosterrebbero quell'analisi, e questo è un problema tuo da gestire più che dello strumento. typing_store.delete_for_user() rimuove i flussi di un singolo partecipante, e fidelity: summary conserva le caratteristiche eliminando il dettaglio biometrico. La pagina sull'etica contiene testo di consenso di esempio, indicazioni sulla conservazione e note sull'articolo 22 del GDPR, sulla revisione IRB e sulle politiche di rifiuto delle piattaforme.
writing_process_risk compare nel pannello Processo di scrittura della dashboard di amministrazione come ausilio all'ordinamento, separato dal suspicion_score esistente. Nessuno dei due numeri confluisce nell'altro, quindi nessuno dei due cambia in silenzio il significato dell'altro.
Come attivarlo
La funzionalità arriva in Potato 2.7.2. Una configurazione più completa di quella a una riga qui sopra:
keystroke_logging:
enabled: true
fidelity: events # off | summary | events
include_schemas: [rationale] # empty means every free-text field
disclose_to_annotators: true
detection:
enabled: true
on_external_insert: flag # allow | warn | block | flagon_external_insert: block impedisce di incollare nei campi strumentati. Blocca però anche le citazioni legittime, e chi è determinato può sempre ridigitare, quindi flag più revisione è di solito il compromesso migliore.
Un progetto eseguibile si trova in examples/advanced/keystroke-logging/, e la guida alla calibrazione in examples/advanced/keystroke-calibration/.
Documentazione
- Registrazione dei tasti — ogni campo catturato, le caratteristiche di sintesi, l'archiviazione e la risoluzione dei problemi
- Rilevamento del processo di scrittura — le sei regole, i tre livelli, la tabella dei falsi positivi e le citazioni
- 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
- Crowdsourcing su Prolific e MTurk — le regole delle piattaforme su monitoraggio e rifiuti
Aggiornamento
pip install --upgrade potato-annotation==2.7.2Per un progetto esistente non cambia nulla finché non lo chiedi. keystroke_logging.enabled è false per impostazione predefinita, quindi aggiornare non fa mai partire la registrazione di nessuno.