Skip to content

Registro de Teclas

O Potato pode registrar as pausas, rajadas, revisões e colagens por trás de uma resposta de texto livre sem registrar nenhum dos caracteres que o anotador digita.

O Potato pode registrar como uma resposta de texto livre foi produzida sem registrar a resposta. Cada evento carrega um carimbo de tempo, um tipo de entrada, uma classe de tecla e uma variação de comprimento; nenhum deles carrega o caractere digitado. A partir desse fluxo o Potato calcula cerca de quarenta atributos de resumo e os armazena junto com a anotação.

A ideia é distinguir uma resposta redigida, digitada com as pausas e os arrependimentos de quem está pensando enquanto escreve, de uma transcrita a partir de outra janela ou colada de um chatbot. Leia as respostas prontas e elas se parecem. Os registros, não.

O registro de teclas exige o Potato 2.7.2 ou posterior e vem desativado por padrão: keystroke_logging.enabled é false até você mudar, então uma atualização nunca começa a gravar ninguém. Para as regras construídas sobre esses dados, veja Detecção do Processo de Escrita. Antes de apontar isso para participantes humanos, leia Ética do Registro de Teclas.

Início rápido

yaml
keystroke_logging:
  enabled: true

É essa a configuração mínima inteira. Todo campo de texto livre do projeto passa a produzir um fluxo de eventos cego ao conteúdo, um resumo e um conjunto de sinalizações de detecção.

Um exemplo executável acompanha o Potato:

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

Aviso: enabled é false por padrão. Atualizar o Potato nunca começa a gravar seus anotadores em silêncio.

O que é capturado

Cada evento registra um carimbo de tempo, um tipo de entrada, uma classe de tecla, a posição do cursor e a variação no comprimento do 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"}}

O que deliberadamente não é capturado

Não capturadoPor quê
Os caracteres digitadosO fluxo reconstrói o processo, não o texto
Texto coladoApenas um comprimento, um rótulo de origem e um hash com sal
Rascunhos intermediáriosNão são reconstruíveis só a partir de variações de comprimento
Qualquer coisa em campo de senhagetFieldIdentity recusa type="password" de saída
Conteúdo da área de transferência em geralLido no momento da colagem para classificar e descartado em seguida

Classes de tecla

A tecla em si nunca é armazenada, apenas a família a que pertence:

letter, digit, punct, space, enter, bksp, del, nav, mod, func, unknown

Tipos de entrada

O sinal principal do Potato é o InputEvent.inputType no beforeinput, não o keydown. Essa é a decisão técnica central. Colagem, arrastar e soltar, composição por IME, ditado, autopreenchimento e desfazer alteram um campo sem disparar keydown uma única vez, então um registrador que só usa keydown é cego exatamente nos casos que o recurso existe para detectar.

Tipos de entrada capturados: insertText, insertReplacementText, insertFromPaste, insertFromDrop, insertCompositionText, insertLineBreak, insertParagraph, deleteContentBackward, deleteContentForward, deleteWordBackward, deleteWordForward, deleteByCut, deleteByDrag, historyUndo, historyRedo, além dos sintéticos focus, blur e keydown.

keydown e keyup continuam sendo escutados, mas só para contar as teclas físicas e medir o tempo de pressionamento. A diferença entre os caracteres que apareceram e as teclas de fato apertadas é o sinal isolado mais forte que se coleta. Veja silent_insert_ratio abaixo.

Quais campos são instrumentados

Por padrão, todo campo de texto livre: o esquema text, as caixas de resposta livre dentro de radio e multiselect, e as áreas de texto de justificativa ou anotações em text_edit, pairwise, trajectory_eval e esquemas parecidos.

Os campos são identificados pelos atributos schema e label_name que o Potato já marca em toda entrada de anotação, com queda para dividir o atributo name em :::.

Restrinja o alcance com qualquer uma das duas listas:

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

Ou deixe um único elemento de fora em HTML personalizado:

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

Referência de configuração

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
ChavePadrãoSignificado
enabledfalseChave mestra. Nada é capturado quando é false.
fidelityeventsoff desativa; summary calcula os atributos mas não guarda o fluxo; events guarda os dois.
include_schemas[]Lista de permissão de nomes de esquema. Vazia significa todos.
exclude_schemas[]Lista de bloqueio, aplicada depois da lista de permissão.
store_eventstruePersiste os fluxos brutos. Ignorado a menos que fidelity: events.
classify_paste_sourcetrueCompara as colagens com a passagem, com as sugestões de IA e com o conteúdo do próprio campo.
idle_session_ms30000Inatividade antes de a sessão ser fechada e enviada.
flush_interval_ms5000Cadência de envio do navegador.
pause_thresholds_ms[500,1000,2000,5000,10000]As contagens de pausa são reportadas em cada um.
disclose_to_annotatorstrueExibe um aviso de gravação. Desligar registra um alerta.

As chaves de detecção estão documentadas em Detecção do Processo de Escrita.

Escolhendo uma fidelidade

FidelidadeFluxo armazenadoRecalcular métricas novas depois?Use quando
offO recurso está desativado neste projeto
summaryNãoNãoVocê tem certeza de quais atributos precisa, ou sua aprovação ética não cobre reter os fluxos
eventsSimSimPadrão. Cerca de 2 bytes por tecla

events é o ajuste recomendado. Uma resposta de 500 palavras custa por volta de 5 KB, e significa que uma métrica que você pensar depois da coleta ainda pode ser calculada.

Atributos de resumo

Um resumo por (usuário, instância, campo). As famílias de atributos seguem Crossley et al. (2024); veja a base de pesquisa.

Volume e relação produto-processo

CampoSignificado
keystrokesTeclas apertadas fisicamente que produziram texto
final_charsComprimento do campo ao fim da sessão
chars_typed / chars_insertedCaracteres inseridos por digitação / por qualquer meio
chars_deletedCaracteres removidos
chars_per_keystrokeAcima de ~1,1 implica texto chegando sem teclas
active_ms / wall_msTempo no campo, excluindo / incluindo o tempo fora

Ritmo

CampoSignificado
iki_median_ms, iki_mean_msTendência central do intervalo entre teclas
iki_p10/p25/p75/p90_msFormato da distribuição dos intervalos
iki_log_sd, iki_log_cvDispersão em escala logarítmica. Baixa significa metronômico, o que significa transcrição.

Escala logarítmica porque as distribuições de intervalo entre teclas são fortemente assimétricas à direita. Intervalos acima de 30 segundos ficam de fora dessas estatísticas, para que uma pausa para o café não domine tudo.

Pausas

CampoSignificado
pause_countsContagens em cada limiar configurado
pause_total_msTempo total em pausas
pre_word_pause_mean_msPausa média antes de começar uma palavra
pre_sentence_pause_mean_msPausa média depois de pontuação
intraword_iki_median_msIntervalo mediano dentro das palavras, um substituto para a habilidade de digitação

Rajadas

CampoSignificado
bursts, burst_mean_chars, burst_max_charsEstatísticas dos trechos de produção contínua
p_burstsRajadas encerradas por uma pausa
r_burstsRajadas encerradas por uma revisão

Revisão

CampoSignificado
backspaces, deletes, undo_eventsComportamento de exclusão
non_terminal_editsEdições feitas atrás do fim do texto, ou seja, quem escreve voltou para revisar
caret_jumpsMovimentos não adjacentes do cursor
revision_ratiochars_deleted / chars_typed

Inserção externa

CampoSignificado
paste_events, pasted_chars, largest_paste_charsVolume de colagem
pasted_fractionParcela do texto final que foi colada
drop_eventsInserções por arrastar e soltar
silent_insert_chars / silent_insert_ratioCaracteres sem tecla correspondente
external_insert_chars / external_insert_ratioO mesmo, excluindo autocitações e citações da passagem
paste_sources, paste_chars_by_sourceContagens e caracteres por rótulo de origem

Use external_insert_ratio para detecção. silent_insert_ratio conta toda inserção silenciosa, inclusive a legítima.

Atenção

CampoSignificado
blur_events, blur_total_msTempo fora da página
max_blur_before_insert_msAusência mais longa imediatamente antes de uma inserção grande
first_keystroke_latency_msTempo de reflexão antes do primeiro caractere

Integridade

CampoSignificado
untrusted_eventsInputEvent.isTrusted === false, ou seja, entrada por script ou automatizada
composition_eventsComposição por IME
virtual_keyboardTeclado virtual ou móvel detectado

Onde os dados ficam

Dois destinos, por dois motivos diferentes.

Os fluxos brutos vão para o SQLite

<task_dir>/project.sqlite, tabela typing_sessions, uma linha por sessão, pela mesma camada de persistência dos memorandos e do livro de códigos.

Colunas de resumo consultáveis ficam desnormalizadas ao lado de um resumo completo em JSON e de um blob de eventos compactado com 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;"

O fluxo é guardado como um blob compactado por sessão, e não como uma linha por tecla. Ele só é lido de volta por inteiro, e a cerca de 2 bytes por evento um esquema de uma linha por tecla colocaria dezenas de milhões de linhas no arquivo do projeto sem nenhum ganho de consulta.

Páginas de fase

Respostas de texto livre na fase de treinamento e em pesquisas de pré-estudo ou pós-estudo também são capturadas. Essas páginas não têm id de instância, então suas sessões ficam sob a sentinela __phase_page__ que o resto do sistema comportamental já usa, e são identificadas pelas colunas phase e page:

sql
SELECT phase, page, count(*) FROM typing_sessions GROUP BY phase, page;

É isso que faz o exemplo de calibração funcionar. Uma tarefa de copiar a passagem na fase de treinamento gera exemplares de transcrição que se distinguem das respostas redigidas comuns só pela phase.

Os resumos vão para o user_state.json

O esboço compacto é espelhado em <output_annotation_dir>/<user>/user_state.json, sob instance_id_to_behavioral_data.<instance>.typing_summaries, com a chave "{schema}:::{label}", de modo que acompanha a anotação até o painel administrativo e as exportações.

Os fluxos brutos deliberadamente não vão para lá. Esse arquivo é reserializado por inteiro e reescrito atomicamente a cada salvamento de anotação, e uma resposta longa tem milhares de eventos.

Exportando

As duas exportações são opcionais, então dados comportamentais nunca entram numa publicação de dataset por acidente.

Atributos de resumo junto com as anotações

yaml
export_include_typing_dynamics: true

Produz typing_dynamics.csv (ou .tsv) ao lado de annotations.csv, uma linha por (usuário, instância, campo), com os atributos de resumo e o veredito do detector.

Fluxos brutos

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

Grava keystroke_sessions.parquet e keystroke_events.parquet, com queda para JSONL quando o pyarrow não está instalado. Veja Exportação em Parquet para o exportador mais amplo.

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 da API

MétodoRotaFinalidade
POST/api/track_typingRecebe do navegador as sessões concluídas
GET/api/typing_summary/<instance_id>Resumos de uma instância, do usuário atual
GET/admin/api/writing_processConsolidado por anotador (exige chave de administrador)

As sessões são resumidas no servidor. O navegador nunca envia um resumo pronto, então os números não podem ser forjados por um cliente modificado, e uma métrica adicionada depois pode ser recalculada a partir dos fluxos guardados.

Como funcionam as sessões

Uma sessão começa quando um campo recebe foco e termina no que vier primeiro: perder o foco, navegar para outra instância, idle_session_ms de inatividade ou o descarregamento da página. As sessões concluídas são enviadas a cada flush_interval_ms, e via navigator.sendBeacon no descarregamento, para que uma sessão em andamento não se perca.

Várias sessões no mesmo campo são fundidas antes de o resumo ser gravado no estado do usuário, então sair de um campo e voltar é lido como uma resposta só, e não como várias suspeitamente curtas. Contagens e durações somam. As estatísticas de distribuição são aproximações ponderadas por teclas, então use os fluxos brutos se precisar da distribuição agrupada exata.

Solução de problemas

Nenhum dado está sendo gravado

Verifique keystroke_logging.enabled: true e que fidelity não é off. No console do navegador, window.keystrokeTracker deve existir com isInitialized === true. Se for undefined, a configuração nunca chegou ao template.

O rastreador existe, mas nenhuma sessão aparece

Verifique a identificação dos campos:

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

null significa que o elemento não tem atributos schema ou label_name nem um name separado por :::, ou que está excluído pela configuração.

silent_insertion sinaliza todo anotador em celular

Não deveria, porque a regra é suprimida quando virtual_keyboard é true. Se a detecção estiver disparando errado, verifique se o cliente marcou essa flag. Veja a tabela de falsos positivos.

O project.sqlite está crescendo

Cerca de 2 bytes por tecla. Use fidelity: summary para manter os atributos e descartar os fluxos, ou typing_store.delete_for_user() para remover os dados de um participante.

Os números parecem errados em testes automatizados

A automação de navegador digita com intervalos quase nulos, o que de fato dispara implausible_speed. Isso é a sinalização funcionando, não um bug.

Leitura Complementar

Para detalhes de implementação, consulte a documentação de origem.