Registro de Pulsaciones
Potato puede registrar las pausas, las ráfagas, las revisiones y los pegados que hay detrás de una respuesta de texto libre sin registrar ninguno de los caracteres que teclea un anotador.
Potato puede registrar cómo se produjo una respuesta de texto libre sin registrar la respuesta. Cada evento lleva una marca de tiempo, un tipo de entrada, una clase de tecla y una variación de longitud; ninguno lleva el carácter que se tecleó. A partir de ese flujo, Potato calcula unas cuarenta características de resumen y las almacena junto a la anotación.
La idea es distinguir una respuesta redactada, tecleada con las pausas y las dudas de alguien que la va resolviendo, de una transcrita a partir de otra ventana o de una pegada desde un chatbot. Si lees las respuestas terminadas, se parecen. Los registros no.
El registro de pulsaciones necesita Potato 2.7.2 o posterior, y viene desactivado por defecto: keystroke_logging.enabled es false hasta que lo cambies, así que una actualización nunca empieza a grabar a nadie. Para las reglas construidas sobre estos datos, consulta Detección del Proceso de Escritura. Antes de apuntarlo a participantes humanos, lee Ética del Registro de Pulsaciones.
Inicio rápido
keystroke_logging:
enabled: trueEsa es toda la configuración mínima. Cada campo de texto libre del proyecto empieza a producir un flujo de eventos ciego al contenido, un resumen y un conjunto de marcas de detección.
Potato incluye un ejemplo ejecutable:
python potato/flask_server.py start examples/advanced/keystroke-logging/config.yaml -p 8000Advertencia:
enabledesfalsepor defecto. Actualizar Potato nunca empieza a grabar a tus anotadores en silencio.
Qué se captura
Cada evento registra una marca de tiempo, un tipo de entrada, una clase de tecla, la posición del cursor y el cambio en la longitud 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"}}
Qué no se captura a propósito
| No se captura | Por qué |
|---|---|
| Los caracteres tecleados | El flujo reconstruye el proceso, no el texto |
| El texto pegado | Solo una longitud, una etiqueta de origen y un hash con sal |
| Los borradores intermedios | No se pueden reconstruir solo con las variaciones de longitud |
| Nada de un campo de contraseña | getFieldIdentity rechaza type="password" sin más |
| El contenido del portapapeles en general | Se lee al pegar para clasificarlo y luego se descarta |
Clases de tecla
La tecla en sí nunca se almacena, solo a qué familia pertenece:
letter, digit, punct, space, enter, bksp, del, nav, mod, func, unknown
Tipos de entrada
La señal principal de Potato es InputEvent.inputType en beforeinput, no keydown. Esta es la decisión técnica central. El pegado, arrastrar y soltar, la composición IME, el dictado, el autorrelleno y deshacer modifican un campo sin disparar keydown en ningún momento, así que un registrador que solo mire keydown es ciego justo a los casos que esta funcionalidad existe para detectar.
Tipos de entrada capturados: insertText, insertReplacementText, insertFromPaste, insertFromDrop, insertCompositionText, insertLineBreak, insertParagraph, deleteContentBackward, deleteContentForward, deleteWordBackward, deleteWordForward, deleteByCut, deleteByDrag, historyUndo, historyRedo, más los sintéticos focus, blur y keydown.
keydown y keyup se siguen escuchando, pero solo para contar las pulsaciones físicas y medir el tiempo de mantenimiento de la tecla. La diferencia entre los caracteres que aparecieron y las teclas realmente pulsadas es la señal individual más fuerte que se recoge. Consulta silent_insert_ratio más abajo.
Qué campos se instrumentan
Por defecto, todos los campos de texto libre: el esquema text, las casillas de respuesta libre dentro de radio y multiselect, y las áreas de texto de justificación o notas en text_edit, pairwise, trajectory_eval y esquemas similares.
Los campos se identifican por los atributos schema y label_name que Potato ya coloca en cada entrada de anotación, y si no, dividiendo el atributo name por :::.
Restringe el alcance con cualquiera de las dos listas:
keystroke_logging:
enabled: true
include_schemas: [rationale] # allowlist; empty = all fields
exclude_schemas: [scratch_notes] # denylistO excluye un solo elemento en HTML personalizado:
<textarea data-keystroke-logging="off" ...></textarea>Referencia de configuración
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| Clave | Por defecto | Significado |
|---|---|---|
enabled | false | Interruptor principal. No se captura nada cuando es falso. |
fidelity | events | off desactiva; summary calcula las características pero no guarda el flujo; events guarda ambos. |
include_schemas | [] | Lista de esquemas permitidos. Vacía significa todos. |
exclude_schemas | [] | Lista de exclusión, aplicada después de la lista de permitidos. |
store_events | true | Persistir los flujos en bruto. Se ignora salvo con fidelity: events. |
classify_paste_source | true | Comparar los pegados con el pasaje, las sugerencias de IA y el propio contenido del campo. |
idle_session_ms | 30000 | Inactividad antes de cerrar y enviar una sesión. |
flush_interval_ms | 5000 | Cadencia de envío del navegador. |
pause_thresholds_ms | [500,1000,2000,5000,10000] | Los recuentos de pausas se informan en cada uno. |
disclose_to_annotators | true | Mostrar un aviso de grabación. Desactivarlo registra una advertencia. |
Las claves de detección están documentadas en Detección del Proceso de Escritura.
Elegir una fidelidad
| Fidelidad | Flujo guardado | ¿Recalcular métricas nuevas después? | Úsala cuando |
|---|---|---|---|
off | — | — | La funcionalidad está desactivada para este proyecto |
summary | No | No | Tienes claro qué características necesitas, o tu aprobación ética no cubre conservar los flujos |
events | Sí | Sí | Por defecto. Unos 2 bytes por pulsación |
events es el ajuste recomendado. Una respuesta de 500 palabras cuesta unos 5 KB, y significa que una métrica que se te ocurra después de la recogida de datos todavía se puede calcular.
Características de resumen
Un resumen por (usuario, instancia, campo). Las familias de características siguen a Crossley et al. (2024); consulta la base en la investigación.
Volumen y relación producto-proceso
| Campo | Significado |
|---|---|
keystrokes | Pulsaciones físicas que produjeron texto |
final_chars | Longitud del campo al terminar la sesión |
chars_typed / chars_inserted | Caracteres insertados tecleando / por cualquier vía |
chars_deleted | Caracteres eliminados |
chars_per_keystroke | Por encima de ~1,1 implica texto que llega sin pulsaciones |
active_ms / wall_ms | Tiempo en el campo, sin contar / contando el tiempo fuera |
Ritmo
| Campo | Significado |
|---|---|
iki_median_ms, iki_mean_ms | Tendencia central del intervalo entre teclas |
iki_p10/p25/p75/p90_ms | Forma de la distribución de intervalos |
iki_log_sd, iki_log_cv | Dispersión en escala logarítmica. Poca dispersión significa metronómico, que significa transcripción. |
En escala logarítmica porque las distribuciones de intervalos entre teclas están muy sesgadas a la derecha. Los intervalos de más de 30 segundos se excluyen de estas estadísticas para que una pausa para el café no las domine.
Pausas
| Campo | Significado |
|---|---|
pause_counts | Recuentos en cada umbral configurado |
pause_total_ms | Tiempo total en pausas |
pre_word_pause_mean_ms | Pausa media antes de empezar una palabra |
pre_sentence_pause_mean_ms | Pausa media después de un signo de puntuación |
intraword_iki_median_ms | Intervalo mediano dentro de las palabras, un indicador indirecto de destreza al teclado |
Ráfagas
| Campo | Significado |
|---|---|
bursts, burst_mean_chars, burst_max_chars | Estadísticas de las tandas de producción |
p_bursts | Ráfagas terminadas por una pausa |
r_bursts | Ráfagas terminadas por una revisión |
Revisión
| Campo | Significado |
|---|---|
backspaces, deletes, undo_events | Comportamiento de borrado |
non_terminal_edits | Ediciones hechas por detrás del final del texto, es decir, quien escribe volvió atrás a revisar |
caret_jumps | Movimientos no adyacentes del cursor |
revision_ratio | chars_deleted / chars_typed |
Inserción externa
| Campo | Significado |
|---|---|
paste_events, pasted_chars, largest_paste_chars | Volumen de pegado |
pasted_fraction | Proporción del texto final que se pegó |
drop_events | Inserciones por arrastrar y soltar |
silent_insert_chars / silent_insert_ratio | Caracteres sin una pulsación correspondiente |
external_insert_chars / external_insert_ratio | Como el anterior, excluyendo autocitas y citas del pasaje |
paste_sources, paste_chars_by_source | Recuentos y caracteres por etiqueta de origen |
Usa external_insert_ratio para la detección. silent_insert_ratio cuenta toda la inserción silenciosa, incluida la legítima.
Atención
| Campo | Significado |
|---|---|
blur_events, blur_total_ms | Tiempo fuera de la página |
max_blur_before_insert_ms | Ausencia más larga inmediatamente anterior a una inserción grande |
first_keystroke_latency_ms | Tiempo de reflexión antes del primer carácter |
Integridad
| Campo | Significado |
|---|---|
untrusted_events | InputEvent.isTrusted === false, es decir, entrada por script o automatizada |
composition_events | Composición IME |
virtual_keyboard | Teclado móvil o en pantalla detectado |
Dónde se almacenan los datos
Dos destinos, por dos motivos distintos.
Los flujos en bruto van a SQLite
<task_dir>/project.sqlite, tabla typing_sessions, una fila por sesión, por la misma capa de persistencia que los memos y el libro de códigos.
Las columnas de resumen consultables se desnormalizan junto a un resumen JSON completo y un blob de eventos empaquetado 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;"El flujo se guarda como un blob empaquetado por sesión en lugar de una fila por pulsación. Solo se lee entero, y a unos 2 bytes por evento, un esquema de fila por pulsación metería decenas de millones de filas en el archivo del proyecto sin ninguna ventaja de consulta.
Páginas de fase
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 usa el resto del sistema de comportamiento, y se identifican por sus columnas phase y page:
SELECT phase, page, count(*) FROM typing_sessions GROUP BY phase, page;Esto es lo que hace funcionar el ejemplo de calibración. Una tarea de copiar el pasaje en la fase de entrenamiento produce ejemplos de transcripción que se pueden distinguir de las respuestas redactadas normales solo por phase.
Los resúmenes van a user_state.json
El esbozo compacto se replica en <output_annotation_dir>/<user>/user_state.json bajo instance_id_to_behavioral_data.<instance>.typing_summaries, con la clave "{schema}:::{label}", de modo que viaja con la anotación hasta el panel de administración y las exportaciones.
Los flujos en bruto no van ahí a propósito. Ese archivo se vuelve a serializar entero y se reescribe de forma atómica en cada guardado de anotación, y una respuesta larga son miles de eventos.
Exportar
Ambas exportaciones son opcionales, así que los datos de comportamiento nunca se incluyen en una publicación de datos por accidente.
Características de resumen junto a las anotaciones
export_include_typing_dynamics: trueProduce typing_dynamics.csv (o .tsv) junto a annotations.csv, una fila por (usuario, instancia, campo), con las características de resumen y el veredicto del detector.
Flujos en bruto
python -m potato.export.cli <config.yaml> --format keystrokesEscribe keystroke_sessions.parquet y keystroke_events.parquet, con JSONL como alternativa cuando pyarrow no está instalado. Consulta Exportación a Parquet para el exportador general.
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"])Endpoints de la API
| Método | Ruta | Propósito |
|---|---|---|
POST | /api/track_typing | Recibir del navegador las sesiones completadas |
GET | /api/typing_summary/<instance_id> | Resúmenes de una instancia, del usuario actual |
GET | /admin/api/writing_process | Recopilación por anotador (requiere clave de administración) |
Las sesiones se resumen en el servidor. El navegador nunca envía un resumen calculado, así que las cifras no se pueden falsificar con un cliente modificado, y una métrica añadida más adelante se puede recalcular a partir de los flujos almacenados.
Cómo funcionan las sesiones
Una sesión empieza cuando un campo recibe el foco y termina con lo que ocurra primero: perder el foco, navegar a otra instancia, idle_session_ms de inactividad o la descarga de la página. Las sesiones completadas se envían cada flush_interval_ms, y por navigator.sendBeacon al descargar la página para que no se pierda una sesión en curso.
Varias sesiones sobre el mismo campo se fusionan antes de escribir el resumen en el estado del usuario, de modo que salir de un campo y volver se lee como una respuesta y no como varias sospechosamente cortas. Los recuentos y las duraciones se suman. Las estadísticas de distribución son aproximaciones ponderadas por pulsaciones, así que usa los flujos en bruto si necesitas una distribución agrupada exacta.
Solución de problemas
No se está registrando ningún dato
Comprueba keystroke_logging.enabled: true y que fidelity no sea off. En la consola del navegador, window.keystrokeTracker debería existir con isInitialized === true. Si es undefined, la configuración nunca llegó a la plantilla.
El rastreador existe pero no aparecen sesiones
Comprueba la identificación del campo:
const el = document.querySelector('textarea');
window.keystrokeTracker.getFieldIdentity(el); // null means it is not trackednull significa que el elemento no tiene atributos schema ni label_name ni un name separado por :::, o que la configuración lo excluye.
silent_insertion marca a todos los anotadores móviles
No debería, porque la regla se suprime cuando virtual_keyboard es verdadero. Si la detección falla, comprueba que el cliente puso esa marca. Consulta la tabla de falsos positivos.
project.sqlite está creciendo
Unos 2 bytes por pulsación. Pon fidelity: summary para conservar las características y descartar los flujos, o usa typing_store.delete_for_user() para eliminar los datos de un participante.
Las cifras salen raras en las pruebas automatizadas
La automatización de navegador teclea con intervalos casi nulos, lo que dispara implausible_speed de verdad. Eso es la marca funcionando, no un error.
Lecturas Adicionales
- Detección del Proceso de Escritura - las seis reglas y los tres niveles de detección
- É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
Para detalles de implementación, consulta la documentación fuente.