Den Schreibprozess lesen: Tastenanschlag-Logging für Freitext-Annotation
Potato kann jetzt aufzeichnen, wie Annotatoren Freitextantworten erzeugen, ohne aufzuzeichnen, was sie tippen, und macht aus Pausen, Überarbeitungen und Einfügungen prüfbare Markierungen für Antworten, die eingefügt statt geschrieben wurden.
Ein Crowdworker öffnet Ihre Annotationsaufgabe, liest die Passage, wechselt in einen anderen Tab, kommt zwanzig Sekunden später zurück, und im Feld erscheint in einer einzigen Bewegung eine 280 Zeichen lange Begründung.
Die Begründung ist in Ordnung. Sie trifft das Thema, sie ist grammatisch sauber, sie nimmt Bezug auf die Passage. Neben vierzig anderen gelesen fällt nichts auf. Jeder Klassifikator, den Sie darauf ansetzen, sagt Ihnen etwas zwischen „wahrscheinlich menschlich“ und „unklar“, und das sagen diese Klassifikatoren über die meisten Texte.
Die fertige Antwort verrät nicht, woher sie stammt. Die zwanzig Sekunden und die eine Bewegung schon.
Potato zeichnet das jetzt auf. Freitextfelder können einen inhaltsblinden Strom davon erfassen, wie eine Antwort entstanden ist, ihn zu etwa vierzig Merkmalen verdichten und eine kleine Menge benannter Regeln darüber laufen lassen. Standardmäßig ist das aus und lässt sich mit einer Zeile einschalten:
keystroke_logging:
enabled: trueTastenanschlag-Logging
Verfasst, abgetippt, eingefügt
Die Forschung, auf der das beruht, ist ziemlich gefestigt. Crossley und Kollegen sammelten 500 argumentative Essays, ließen sie von einer zweiten Gruppe von Workern abtippen und trennten authentisches Schreiben mit einem Random Forest zu 99 % Genauigkeit vom Abtippen. Deane et al. und Zhang et al. fanden dieselbe Trennung unabhängig davon. Asher et al. haben die Crowdsourcing-Variante auf Prolific gebaut und Teilnehmende markiert, deren Zahl an Tastenanschlägen zu niedrig war für die Länge dessen, was sie einreichten.
Die Signatur ist über alle Arbeiten hinweg dieselbe. Echtes Verfassen zeigt längere Pausen vor Sätzen und Wörtern, mehr Einfügungen und Löschungen und eine hohe Varianz in den Abständen zwischen den Tasten. Abtippen verläuft linear, in Schüben, mit geringer Varianz. Beim Einfügen gibt es kaum noch Abstände.
Nichts davon ist im Text zu sehen. Alles davon ist im Log zu sehen.
Warum beforeinput und nicht keydown
Das ist die eine technische Entscheidung, an der hängt, ob der Rest der Funktion trägt.
Der naheliegende Weg, einen Tastenanschlag-Logger zu bauen, ist, auf keydown zu hören. Es ist auch der Weg, der scheitert, denn Einfügen, Drag-and-drop, IME-Komposition, Diktat, Autofill und Rückgängigmachen verändern den Inhalt eines Feldes alle, ohne keydown auch nur einmal auszulösen. Ein Logger, der nur auf keydown hört, ist blind für genau die Fälle, wegen derer es ihn gibt.
Potatos primäres Signal ist InputEvent.inputType bei beforeinput. Das Ereignis feuert in all diesen Fällen und sagt, welcher davon eingetreten ist. keydown und keyup werden weiterhin abgehört, aber zu einem anderen Zweck: um die Tasten zu zählen, die eine Person körperlich gedrückt hat.
Die Lücke zwischen diesen beiden Zahlen ist das Nützlichste, was erhoben wird. Zeichen, die im Feld auftauchten, ohne dass ein Tastenanschlag dazugehört, werden als silent_insert_chars festgehalten, ihr Anteil an der Antwort als silent_insert_ratio. Ein Einfügen, das die Seite unterdrückt hat, eine Erweiterung, die Text eingeschleust hat, ein Diktatstrom, ein Skript, das das Feld befüllt: Keines davon erzeugt Tastenanschläge, alle erzeugen Zeichen.
Was aufgezeichnet wird und was nicht
Jedes Ereignis trägt einen Zeitstempel, einen Eingabetyp, die Klasse der Taste, eine Cursorposition und eine Längenänderung:
{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"}}
Die Taste selbst wird nie gespeichert, nur die Familie, zu der sie gehört: letter, digit, punct, space, enter, bksp, del, nav, mod, func, unknown. Eingefügter Text wird auf eine Länge, ein Quellenlabel und einen pro Sitzung gesalzenen Hash reduziert. Passwortfelder werden rundweg abgelehnt. Aus dem Strom lässt sich die Antwort nicht rekonstruieren, und darum geht es: Der Strom beschreibt den Prozess und sagt nichts über den Inhalt.
Dass gewöhnliches Verhalten nicht in den Markierungen landet, liegt an der Klassifikation der Einfügequelle. Kommt eine Einfügung an, vergleicht Potato sie mit der annotierten Passage, mit einem etwaigen KI-Vorschlag auf der Seite und mit dem, was schon im Feld stand, behält dann das Label und verwirft den Vergleich. Die Passage zu zitieren erscheint als instance_text. Den eigenen Entwurf umzustellen erscheint als self. Beides zählt nicht als externe Einfügung.
Sechs Regeln, und nichts, was sich als Modell ausgibt
Die Erkennung läuft serverseitig in drei Stufen. Nur die erste ist standardmäßig aktiv.
Die erste Stufe besteht aus sechs benannten Markierungen, jede mit einem expliziten Schwellenwert, jede gibt die Merkmalswerte zurück, die sie ausgelöst haben:
| Markierung | Löst aus, wenn | Schweregrad |
|---|---|---|
paste_dominant | Die Hälfte oder mehr des endgültigen Textes per Einfügen ankam | suspect |
silent_insertion | ≥30 % der eingefügten Zeichen keinen Tastenanschlag hinter sich hatten | suspect |
transcription_rhythm | Metronomischer Rhythmus und keine Überarbeitung und fast keine Pausen | review |
offscreen_composition | Eine große externe Einfügung direkt nach ≥10 s Abwesenheit von der Seite | suspect |
implausible_speed | Dauerhaft über etwa 180 wpm über eine ganze Antwort hinweg | review |
synthetic_input | Der Browser meldet isTrusted === false | suspect |
transcription_rhythm ist mit Absicht konjunktiv. Metronomisches Tippen für sich genommen ist eine schnelle Schreibkraft. Nie etwas zu löschen ist eine sorgfältige. Kaum zu pausieren ist eine kurze Antwort. Erst wenn alle drei zugleich gelten, haben Sie die Signatur des Abtippens, und Antworten unter 80 Zeichen überspringt die Regel ganz, weil es dort keinen Rhythmus zu lesen gibt.
Die zweite Stufe ist die Kalibrierung. Tastenanschlagsmerkmale hängen stark von der Schreibaufgabe ab, ein Schwellenwert für eine einsätzige Begründung ist also falsch für fünf Absätze. python -m potato.typing_detect calibrate config.yaml passt jede Grenze an ein Randperzentil der Sitzungen Ihres eigenen Projekts an. Dafür braucht es mindestens 30 brauchbare Sitzungen, und kalibrierte Werte werden auf höchstens das 3-Fache der eingebauten Vorgaben begrenzt, damit eine homogene Population einen Schwellenwert nicht auf ihren eigenen Median ziehen kann.
Die dritte ist überwacht. Wenn Sie Labels haben, trainiert fit_supervised() einen echten Klassifikator auf der Merkmalsmatrix. scikit-learn wird verzögert importiert und ist keine Abhängigkeit von Potato.
Ein vortrainiertes Modell wird nicht mitgeliefert. Im Repository liegt kein gelabeltes Korpus, und angepasste Koeffizienten auszuliefern, die aus nichts abgeleitet sind, hieße, eine Validierungszahl zu erfinden. Ausgeliefert werden stattdessen sechs Regeln, die Sie lesen, bestreiten und überschreiben können.
Wenn Sie Labels wollen, kann die Trainingsphase sie innerhalb Ihrer eigenen Studie erzeugen. Geben Sie Annotatoren zum Aufwärmen die Aufgabe, eine Passage abzutippen: Diese Sitzungen sind echte Beispiele fürs Abtippen, von Ihren Annotatoren an Ihrer Aufgabe, und ihre gewöhnlichen Antworten sind die verfasste Klasse. So wurde das Crossley-Korpus gebaut, und das Kalibrierungsbeispiel richtet das von Anfang bis Ende ein.
Schwellenwerte werden auf dem Server ausgewertet und nie an den Browser geschickt. Sie zu veröffentlichen würde einem Annotator genau sagen, wie langsam er einfügen muss.
Wohin die Daten gehen
Rohströme gehen nach SQLite, in <task_dir>/project.sqlite, eine Zeile pro Sitzung, über dieselbe Persistenzschicht wie Memos und Codebuch. Ereignisse werden delta-kodiert und mit zlib gepackt, gemessen 1,7 Byte pro Ereignis, eine Antwort mit 500 Wörtern kostet also etwa 5 KB.
Sie landen bewusst nicht in user_state.json. Diese Datei wird bei jedem Speichern einer Annotation vollständig neu geschrieben, und eine lange Antwort sind rund 3.000 Ereignisse. Nur die kompakte Zusammenfassung wird in die Verhaltensdaten gespiegelt, mit dem Schlüssel "{schema}:::{label}", sodass sie mit der Annotation ins Dashboard und in die Exporte wandert.
Freitextantworten in der Trainingsphase und in Vor- oder Nachbefragungen werden ebenfalls erfasst. Diese Seiten haben keine Instanz-ID, ihre Sitzungen werden deshalb unter dem vorhandenen Platzhalter __phase_page__ einsortiert und stattdessen über phase und page identifiziert. Genau deshalb funktioniert der Trick mit dem Abtippen der Passage: Beispiele fürs Abtippen lassen sich allein anhand der Phase von gewöhnlichen Antworten trennen.
Beide Exporte sind Opt-in. export_include_typing_dynamics: true schreibt eine Sidecar-Datei typing_dynamics.csv neben Ihre Annotationen, und python -m potato.export.cli <config.yaml> --format keystrokes schreibt die Rohströme nach Parquet, ohne pyarrow ersatzweise nach JSONL. Verhaltensdaten landen nie versehentlich in einer Datensatzveröffentlichung.
Bevor Sie das auf Menschen richten
Eine Markierung ist ein Hinweis, den ein Mensch prüfen muss. Sie ist kein Beweis, und sie darf nicht an automatische Ablehnung, an das Einbehalten von Bezahlung oder an Sperren gekoppelt werden.
Der Fehlerfall hier ist, einen ehrlichen Annotator zu beschuldigen, und die Fälle, die die Regeln auslösen, sind nicht exotisch. Einige sind abgedeckt: Das Zitieren der Passage und das Umstellen des eigenen Entwurfs werden über die Quellenklassifikation unterdrückt, und Bildschirmtastaturen sowie IME-Komposition unterdrücken silent_insertion, denn keines von beidem sendet zuverlässig keydown, und jede Einfügung würde sonst still aussehen. Andere sind es nicht. Diktat wird markiert. Grammatik-Erweiterungen werden markiert. Manche assistive Technologie erzeugt nicht vertrauenswürdige Ereignisse und löst synthetic_input aus. Deshalb sagt die Dokumentation offen: Wenn Ihre Studie Nutzenden assistiver Technik offensteht, sollten Sie diese Regel abschalten, statt jemandem eine Markierung zu erklären, der die Werkzeuge benutzt hat, die er zum Arbeiten braucht.
Dann ist da die Rechnung. Wenn 5 % Ihrer Antworten eingefügt sind und Ihre Regel 5 % der Sitzungen markiert, kann das meiste, was Sie markieren, trotzdem ehrliche Arbeit sein. Auf einer Plattform, auf der Fehlverhalten wirklich selten ist, erzeugt eine Regel schon mit einer mäßigen Falsch-Positiv-Rate mehr falsche Beschuldigungen als echte Treffer. Ein kalibrierter Schwellenwert macht das deutlich, nicht besser: Ein Randperzentil markiert seinen Randanteil jeder Population, auch einer, in der niemand etwas falsch gemacht hat.
Die Offenlegung ist standardmäßig aktiv, und wer sie abschaltet, bekommt beim Start eine Warnung ins Log. Diese Voreinstellung gibt es, weil Zeitmuster ein Verhaltensbiometrikum sind. Sie können eine Person identifizieren und Konten über Kontexte hinweg verknüpfen, und die Forschungsliteratur hat daraus auf Tippfertigkeit, Zweitsprachlichkeit und kognitive Belastung geschlossen. Niemand, der sich angemeldet hat, um Sätze zu labeln, hat damit gerechnet. Potato berechnet nichts davon und liefert dafür auch keine Werkzeuge mit, aber die Daten, die Sie aufbewahren, würden solche Analysen tragen, und das zu regeln ist Ihre Aufgabe, nicht die des Werkzeugs. typing_store.delete_for_user() entfernt die Ströme einer teilnehmenden Person, und fidelity: summary behält die Merkmale und lässt das biometrische Detail fallen. Die Ethik-Seite enthält Musterformulierungen für die Einwilligung, Hinweise zur Aufbewahrung sowie Anmerkungen zu Artikel 22 DSGVO, zur IRB-Prüfung und zu den Ablehnungsrichtlinien der Plattformen.
writing_process_risk erscheint im Admin-Dashboard im Schreibprozess-Panel als Hilfe beim Priorisieren, getrennt vom vorhandenen suspicion_score. Keine der beiden Zahlen geht in die andere ein, also verändert auch keine stillschweigend die Bedeutung der anderen.
Einschalten
Die Funktion ist in Potato 2.7.2 enthalten. Eine vollständigere Konfiguration als der Einzeiler oben:
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 verhindert das Einfügen in instrumentierte Felder. Es blockiert auch legitimes Zitieren, und wer entschlossen ist, tippt den Text stattdessen ab, deshalb ist flag mit anschließender Prüfung meist die bessere Wahl.
Ein lauffähiges Projekt liegt unter examples/advanced/keystroke-logging/, die Kalibrierungs-Anleitung unter examples/advanced/keystroke-calibration/.
Dokumentation
- Tastenanschlag-Logging — jedes erfasste Feld, die zusammenfassenden Merkmale, Speicherung und Fehlerbehebung
- Schreibprozess-Erkennung — die sechs Regeln, die drei Stufen, die Tabelle der Falsch-Positiven und die Quellenangaben
- Ethik des Tastenanschlag-Loggings — Einwilligung, IRB, Aufbewahrung, Rechte der Teilnehmenden
- Verhaltens-Tracking — das umfassendere Interaktions-Tracking-System, in dem das steckt
- Qualitätskontrolle — Aufmerksamkeitschecks und Gold-Standards
- Admin-Dashboard — wo das Schreibprozess-Panel liegt
- Crowdsourcing auf Prolific und MTurk — Plattformregeln zu Überwachung und Ablehnung
Aktualisieren
pip install --upgrade potato-annotation==2.7.2Für ein bestehendes Projekt ändert sich nichts, bis Sie es verlangen. keystroke_logging.enabled ist standardmäßig false, ein Upgrade beginnt also nie damit, jemanden aufzuzeichnen.