Skip to content
Announcements11 min read

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.

Potato Team

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:

yaml
keystroke_logging:
  enabled: true

Registrazione dei tasti in Potato: le risposte composte, trascritte e incollate hanno un processo diverso anche quando il testo finito non lo èRegistrazione 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:

text
{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:

SegnalazioneScatta quandoGravità
paste_dominantMetà o più del testo finale è arrivata per incollaggiosospetto
silent_insertion≥30% dei caratteri inseriti non aveva una battuta dietrosospetto
transcription_rhythmRitmo metronomico e nessuna revisione e quasi nessuna pausada rivedere
offscreen_compositionUn grosso inserimento esterno subito dopo ≥10s fuori dalla paginasospetto
implausible_speedCostantemente sopra i ~180 wpm per un'intera rispostada rivedere
synthetic_inputIl browser riporta isTrusted === falsesospetto

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:

yaml
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 | flag

on_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

Aggiornamento

bash
pip install --upgrade potato-annotation==2.7.2

Per 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.