Leer el proceso de escritura: registro de pulsaciones para la anotación de texto libre
Potato ya puede registrar cómo producen los anotadores sus respuestas de texto libre, sin registrar lo que escriben, y convertir las pausas, las revisiones y los pegados en marcas auditables para respuestas que se pegaron en lugar de escribirse.
Un trabajador de crowdsourcing abre tu tarea de anotación, lee el pasaje, se cambia a otra pestaña, vuelve veinte segundos después y una justificación de 280 caracteres aparece en la casilla de una sola vez.
La justificación está bien. Va al tema, está bien escrita, hace referencia al pasaje. Léela junto a otras cuarenta y no destaca en nada. Cualquier clasificador que le apuntes te dirá algo entre «probablemente humano» y «no está claro», que es lo que esos clasificadores dicen de casi cualquier texto.
La respuesta terminada no dice de dónde salió. Los veinte segundos y el movimiento único sí.
Potato ahora registra eso. Los campos de texto libre pueden capturar un flujo ciego al contenido sobre cómo se produjo una respuesta, resumirlo en unas cuarenta características y aplicar sobre el resultado un pequeño conjunto de reglas con nombre. Viene desactivado por defecto y se activa con una línea:
keystroke_logging:
enabled: trueRegistro de pulsaciones
Redactado, transcrito, pegado
La investigación en la que se apoya está bastante asentada. Crossley y sus colegas recogieron 500 ensayos argumentativos, pidieron a un segundo grupo de trabajadores que los transcribiera y separaron la escritura auténtica de la transcripción con un 99 % de exactitud usando un random forest. Deane et al. y Zhang et al. encontraron la misma separación de forma independiente. Asher et al. construyeron la versión para crowdsourcing en Prolific, marcando a los participantes cuyo recuento de pulsaciones era demasiado bajo para la longitud de lo que enviaban.
La firma es la misma en todos ellos. La redacción real tiene pausas más largas antes de las frases y de las palabras, más inserciones y borrados, y mucha varianza en los intervalos entre teclas. Copiar al teclado es lineal, va por ráfagas y tiene poca varianza. Pegar apenas tiene intervalos.
Nada de eso se ve en el texto. Todo se ve en el registro.
Por qué beforeinput y no keydown
Esta es la decisión técnica de la que depende que el resto de la funcionalidad sirva de algo.
La forma obvia de escribir un registrador de pulsaciones es escuchar keydown. También es la que falla, porque el pegado, arrastrar y soltar, la composición IME, el dictado, el autorrelleno y deshacer cambian el contenido de un campo sin disparar keydown ni una sola vez. Un registrador que solo mire keydown es ciego justo a los casos que esto existe para detectar.
La señal principal de Potato es InputEvent.inputType en beforeinput, que se dispara con todos ellos y dice cuál ocurrió. keydown y keyup se siguen escuchando, pero con otro fin: contar las teclas que una persona pulsó físicamente.
La diferencia entre esos dos números es lo más útil que se recoge. Los caracteres que aparecieron en el campo sin una pulsación que les corresponda se registran como silent_insert_chars, y su proporción sobre la respuesta como silent_insert_ratio. Un pegado que la página suprimió, una extensión que inyectó texto, un flujo de dictado, un script que rellena la casilla: ninguno produce pulsaciones, todos producen caracteres.
Qué se registra y qué no
Cada evento lleva una marca de tiempo, un tipo de entrada, una clase de tecla, una posición del cursor y una variación de longitud:
{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"}}
La tecla en sí nunca se almacena, solo a qué familia pertenece: letter, digit, punct, space, enter, bksp, del, nav, mod, func, unknown. El texto pegado se reduce a una longitud, una etiqueta de origen y un hash con sal por sesión. Los campos de contraseña se rechazan sin más. No se puede reconstruir la respuesta a partir del flujo, y esa es la idea: el flujo describe el proceso y no dice nada sobre el contenido.
La clasificación del origen del pegado es lo que mantiene el comportamiento normal fuera de las marcas. Cuando llega un pegado, Potato lo compara con el pasaje que se está anotando, con cualquier sugerencia de IA visible en la página y con lo que ya había en el campo, y después se queda con la etiqueta y descarta la comparación. Citar el pasaje se lee como instance_text. Mover de sitio tu propio borrador se lee como self. Ninguno de los dos cuenta como inserción externa.
Seis reglas, y nada que finja ser un modelo
La detección se ejecuta en el servidor en tres niveles. Solo el primero está activado por defecto.
El primer nivel son seis marcas con nombre, cada una con un umbral explícito y cada una devuelve los valores de las características que la dispararon:
| Marca | Se dispara cuando | Gravedad |
|---|---|---|
paste_dominant | La mitad o más del texto final llegó pegada | suspect |
silent_insertion | El 30 % o más de los caracteres insertados no tenía una pulsación detrás | suspect |
transcription_rhythm | Ritmo metronómico y sin revisiones y casi sin pausas | review |
offscreen_composition | Una inserción externa grande justo después de 10 s o más fuera de la página | suspect |
implausible_speed | Sostenida por encima de ~180 wpm a lo largo de toda una respuesta | review |
synthetic_input | El navegador informa de isTrusted === false | suspect |
transcription_rhythm es conjuntiva a propósito. Teclear de forma metronómica, por sí solo, es alguien que escribe rápido. No borrar nunca nada es alguien cuidadoso. Apenas hacer pausas es una respuesta corta. Solo cuando se dan las tres a la vez tienes la firma de copiar al teclado, y la regla se salta por completo las respuestas de menos de 80 caracteres, donde no hay ritmo que leer.
El segundo nivel es la calibración. Las características de pulsaciones dependen mucho de la tarea de escritura, así que un umbral ajustado para una justificación de una frase está mal para cinco párrafos. python -m potato.typing_detect calibrate config.yaml reajusta cada corte a un percentil de cola de las sesiones de tu propio proyecto. Necesita al menos 30 sesiones utilizables, y los valores calibrados se acotan a un máximo de 3× los valores por defecto, para que una población homogénea no pueda arrastrar un umbral hasta su propia mediana.
El tercero es supervisado. Si tienes etiquetas, fit_supervised() entrena un clasificador de verdad sobre la matriz de características. scikit-learn se importa de forma diferida y no es una dependencia de Potato.
No se distribuye ningún modelo preentrenado. No hay un corpus etiquetado en el repositorio, y publicar coeficientes ajustados a partir de nada sería inventarse una cifra de validación. Lo que sí se distribuye son seis reglas que puedes leer, discutir y sobrescribir.
Si quieres etiquetas, la fase de entrenamiento puede generarlas dentro de tu propio estudio. Dale a los anotadores un calentamiento de copiar el pasaje: esas sesiones son ejemplos genuinos de transcripción, de tus anotadores y sobre tu tarea, y sus respuestas normales son la clase redactada. Así se construyó el corpus de Crossley, y el ejemplo de calibración lo monta de principio a fin.
Los umbrales se evalúan en el servidor y nunca se envían al navegador. Publicarlos le diría a un anotador con qué lentitud exacta tiene que pegar.
Dónde acaba
Los flujos en bruto van a SQLite, en <task_dir>/project.sqlite, una fila por sesión, por la misma capa de persistencia que los memos y el libro de códigos. Los eventos se codifican por diferencias y se empaquetan con zlib a 1,7 bytes por evento medidos, así que una respuesta de 500 palabras cuesta unos 5 KB.
A propósito no van a user_state.json. Ese archivo se reescribe entero en cada guardado de anotación, y una respuesta larga son unos 3000 eventos. Solo el resumen compacto se replica en los datos de comportamiento, con la clave "{schema}:::{label}", de modo que viaja con la anotación hasta el panel y las exportaciones.
Las respuestas de texto libre de la fase de entrenamiento y de las encuestas previas o posteriores al estudio también se capturan. Esas páginas no tienen id de instancia, así que sus sesiones se agrupan bajo el centinela __phase_page__ que ya existía y se identifican por phase y page. Eso es lo que hace que funcione el truco de copiar el pasaje: los ejemplos de transcripción se pueden separar de las respuestas normales solo por la fase.
Ambas exportaciones son opcionales. export_include_typing_dynamics: true escribe un archivo adjunto typing_dynamics.csv junto a tus anotaciones, y python -m potato.export.cli <config.yaml> --format keystrokes escribe los flujos en bruto en Parquet, con JSONL como alternativa si falta pyarrow. Los datos de comportamiento nunca se incluyen en una publicación de datos por accidente.
Antes de apuntar esto a personas
Una marca es un indicio para que lo revise una persona. No es una prueba, y no debe conectarse a rechazos automáticos, retención de pagos ni expulsiones.
El modo de fallo aquí es acusar a un anotador honesto, y los casos que disparan las reglas no son raros. Algunos están contemplados: citar el pasaje y reordenar tu propio borrador quedan suprimidos por la clasificación de origen, y los teclados en pantalla y la composición IME suprimen silent_insertion, porque ninguno emite keydown de forma fiable y, si no, toda inserción parecería silenciosa. Otros no lo están. El dictado va a marcar. Las extensiones de gramática van a marcar. Algunas tecnologías de apoyo producen eventos no confiables y disparan synthetic_input, y por eso la documentación dice sin rodeos que, si tu estudio está abierto a personas que usan tecnología de apoyo, conviene desactivar esa regla en lugar de tener que explicarle una marca a alguien que estaba usando las herramientas que necesita para trabajar.
Luego está la aritmética. Si el 5 % de tus respuestas están pegadas y tu regla marca el 5 % de las sesiones, la mayor parte de lo que marques puede seguir siendo trabajo honesto. En una plataforma donde la mala conducta es de verdad poco frecuente, una regla con una tasa de falsos positivos incluso modesta produce más acusaciones falsas que capturas reales. Un umbral calibrado hace esto explícito, no lo mejora: un percentil de cola marca su fracción de cola en cualquier población, incluida una en la que nadie hizo nada malo.
La divulgación viene activada por defecto, y desactivarla registra una advertencia al arrancar. Ese valor por defecto existe porque los patrones temporales son un dato biométrico de comportamiento. Pueden identificar a una persona y enlazar cuentas entre contextos, y la literatura de investigación los ha usado para inferir destreza al teclado, condición de hablante de segunda lengua y carga cognitiva. Nadie que se apuntó a etiquetar frases contaba con eso. Potato no calcula nada de eso y no incluye herramientas para hacerlo, pero los datos que conserves permitirían ese análisis, y eso es un problema tuyo y no de la herramienta. typing_store.delete_for_user() elimina los flujos de un participante, y fidelity: summary conserva las características y descarta el detalle biométrico. La página de ética tiene texto de consentimiento de ejemplo, orientación sobre retención y notas sobre el artículo 22 del GDPR, la revisión del IRB y las políticas de rechazo de las plataformas.
writing_process_risk aparece en el panel Writing Process del panel de administración como ayuda para ordenar, separado del suspicion_score que ya existía. Ninguno de los dos números se incorpora al otro, así que ninguno cambia en silencio lo que significa el otro.
Cómo activarlo
La funcionalidad llega con Potato 2.7.2. Una configuración más completa que la de una línea del principio:
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 impide pegar en los campos instrumentados. También bloquea las citas legítimas, y quien esté decidido puede volver a teclear el texto, así que flag y revisar después suele salir mejor.
Hay un proyecto ejecutable en examples/advanced/keystroke-logging/, y el recorrido de calibración en examples/advanced/keystroke-calibration/.
Documentación
- Registro de pulsaciones — cada campo capturado, las características de resumen, el almacenamiento y la resolución de problemas
- Detección del proceso de escritura — las seis reglas, los tres niveles, la tabla de falsos positivos y las citas
- Ética del registro de pulsaciones — consentimiento, IRB, retención, derechos de los participantes
- Seguimiento de comportamiento — el sistema más amplio de seguimiento de interacciones en el que esto se inscribe
- Control de calidad — verificaciones de atención y estándares de oro
- Panel de administración — donde vive el panel Writing Process
- Crowdsourcing en Prolific y MTurk — reglas de las plataformas sobre monitorización y rechazo
Actualizar
pip install --upgrade potato-annotation==2.7.2Nada cambia en un proyecto existente hasta que lo pidas. keystroke_logging.enabled es false por defecto, así que actualizar nunca empieza a grabar a nadie.