Skip to content

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

yaml
keystroke_logging:
  enabled: true

Esa 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:

bash
python potato/flask_server.py start examples/advanced/keystroke-logging/config.yaml -p 8000

Advertencia: enabled es false por 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:

text
{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 capturaPor qué
Los caracteres tecleadosEl flujo reconstruye el proceso, no el texto
El texto pegadoSolo una longitud, una etiqueta de origen y un hash con sal
Los borradores intermediosNo se pueden reconstruir solo con las variaciones de longitud
Nada de un campo de contraseñagetFieldIdentity rechaza type="password" sin más
El contenido del portapapeles en generalSe 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:

yaml
keystroke_logging:
  enabled: true
  include_schemas: [rationale]      # allowlist; empty = all fields
  exclude_schemas: [scratch_notes]  # denylist

O excluye un solo elemento en HTML personalizado:

html
<textarea data-keystroke-logging="off" ...></textarea>

Referencia de configuración

yaml
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
ClavePor defectoSignificado
enabledfalseInterruptor principal. No se captura nada cuando es falso.
fidelityeventsoff 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_eventstruePersistir los flujos en bruto. Se ignora salvo con fidelity: events.
classify_paste_sourcetrueComparar los pegados con el pasaje, las sugerencias de IA y el propio contenido del campo.
idle_session_ms30000Inactividad antes de cerrar y enviar una sesión.
flush_interval_ms5000Cadencia de envío del navegador.
pause_thresholds_ms[500,1000,2000,5000,10000]Los recuentos de pausas se informan en cada uno.
disclose_to_annotatorstrueMostrar 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

FidelidadFlujo guardado¿Recalcular métricas nuevas después?Úsala cuando
offLa funcionalidad está desactivada para este proyecto
summaryNoNoTienes claro qué características necesitas, o tu aprobación ética no cubre conservar los flujos
eventsPor 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

CampoSignificado
keystrokesPulsaciones físicas que produjeron texto
final_charsLongitud del campo al terminar la sesión
chars_typed / chars_insertedCaracteres insertados tecleando / por cualquier vía
chars_deletedCaracteres eliminados
chars_per_keystrokePor encima de ~1,1 implica texto que llega sin pulsaciones
active_ms / wall_msTiempo en el campo, sin contar / contando el tiempo fuera

Ritmo

CampoSignificado
iki_median_ms, iki_mean_msTendencia central del intervalo entre teclas
iki_p10/p25/p75/p90_msForma de la distribución de intervalos
iki_log_sd, iki_log_cvDispersió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

CampoSignificado
pause_countsRecuentos en cada umbral configurado
pause_total_msTiempo total en pausas
pre_word_pause_mean_msPausa media antes de empezar una palabra
pre_sentence_pause_mean_msPausa media después de un signo de puntuación
intraword_iki_median_msIntervalo mediano dentro de las palabras, un indicador indirecto de destreza al teclado

Ráfagas

CampoSignificado
bursts, burst_mean_chars, burst_max_charsEstadísticas de las tandas de producción
p_burstsRáfagas terminadas por una pausa
r_burstsRáfagas terminadas por una revisión

Revisión

CampoSignificado
backspaces, deletes, undo_eventsComportamiento de borrado
non_terminal_editsEdiciones hechas por detrás del final del texto, es decir, quien escribe volvió atrás a revisar
caret_jumpsMovimientos no adyacentes del cursor
revision_ratiochars_deleted / chars_typed

Inserción externa

CampoSignificado
paste_events, pasted_chars, largest_paste_charsVolumen de pegado
pasted_fractionProporción del texto final que se pegó
drop_eventsInserciones por arrastrar y soltar
silent_insert_chars / silent_insert_ratioCaracteres sin una pulsación correspondiente
external_insert_chars / external_insert_ratioComo el anterior, excluyendo autocitas y citas del pasaje
paste_sources, paste_chars_by_sourceRecuentos 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

CampoSignificado
blur_events, blur_total_msTiempo fuera de la página
max_blur_before_insert_msAusencia más larga inmediatamente anterior a una inserción grande
first_keystroke_latency_msTiempo de reflexión antes del primer carácter

Integridad

CampoSignificado
untrusted_eventsInputEvent.isTrusted === false, es decir, entrada por script o automatizada
composition_eventsComposición IME
virtual_keyboardTeclado 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:

bash
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:

sql
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

yaml
export_include_typing_dynamics: true

Produce 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

bash
python -m potato.export.cli <config.yaml> --format keystrokes

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

python
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étodoRutaPropósito
POST/api/track_typingRecibir del navegador las sesiones completadas
GET/api/typing_summary/<instance_id>Resúmenes de una instancia, del usuario actual
GET/admin/api/writing_processRecopilació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:

js
const el = document.querySelector('textarea');
window.keystrokeTracker.getFieldIdentity(el);   // null means it is not tracked

null 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

Para detalles de implementación, consulta la documentación fuente.