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
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:
python potato/flask_server.py start examples/advanced/keystroke-logging/config.yaml -p 8000Aviso:
enabledéfalsepor 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:
{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 capturado | Por quê |
|---|---|
| Os caracteres digitados | O fluxo reconstrói o processo, não o texto |
| Texto colado | Apenas um comprimento, um rótulo de origem e um hash com sal |
| Rascunhos intermediários | Não são reconstruíveis só a partir de variações de comprimento |
| Qualquer coisa em campo de senha | getFieldIdentity recusa type="password" de saída |
| Conteúdo da área de transferência em geral | Lido 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:
keystroke_logging:
enabled: true
include_schemas: [rationale] # allowlist; empty = all fields
exclude_schemas: [scratch_notes] # denylistOu deixe um único elemento de fora em HTML personalizado:
<textarea data-keystroke-logging="off" ...></textarea>Referência de configuração
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| Chave | Padrão | Significado |
|---|---|---|
enabled | false | Chave mestra. Nada é capturado quando é false. |
fidelity | events | off 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_events | true | Persiste os fluxos brutos. Ignorado a menos que fidelity: events. |
classify_paste_source | true | Compara as colagens com a passagem, com as sugestões de IA e com o conteúdo do próprio campo. |
idle_session_ms | 30000 | Inatividade antes de a sessão ser fechada e enviada. |
flush_interval_ms | 5000 | Cadê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_annotators | true | Exibe 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
| Fidelidade | Fluxo armazenado | Recalcular métricas novas depois? | Use quando |
|---|---|---|---|
off | — | — | O recurso está desativado neste projeto |
summary | Não | Não | Você tem certeza de quais atributos precisa, ou sua aprovação ética não cobre reter os fluxos |
events | Sim | Sim | Padrã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
| Campo | Significado |
|---|---|
keystrokes | Teclas apertadas fisicamente que produziram texto |
final_chars | Comprimento do campo ao fim da sessão |
chars_typed / chars_inserted | Caracteres inseridos por digitação / por qualquer meio |
chars_deleted | Caracteres removidos |
chars_per_keystroke | Acima de ~1,1 implica texto chegando sem teclas |
active_ms / wall_ms | Tempo no campo, excluindo / incluindo o tempo fora |
Ritmo
| Campo | Significado |
|---|---|
iki_median_ms, iki_mean_ms | Tendência central do intervalo entre teclas |
iki_p10/p25/p75/p90_ms | Formato da distribuição dos intervalos |
iki_log_sd, iki_log_cv | Dispersã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
| Campo | Significado |
|---|---|
pause_counts | Contagens em cada limiar configurado |
pause_total_ms | Tempo total em pausas |
pre_word_pause_mean_ms | Pausa média antes de começar uma palavra |
pre_sentence_pause_mean_ms | Pausa média depois de pontuação |
intraword_iki_median_ms | Intervalo mediano dentro das palavras, um substituto para a habilidade de digitação |
Rajadas
| Campo | Significado |
|---|---|
bursts, burst_mean_chars, burst_max_chars | Estatísticas dos trechos de produção contínua |
p_bursts | Rajadas encerradas por uma pausa |
r_bursts | Rajadas encerradas por uma revisão |
Revisão
| Campo | Significado |
|---|---|
backspaces, deletes, undo_events | Comportamento de exclusão |
non_terminal_edits | Edições feitas atrás do fim do texto, ou seja, quem escreve voltou para revisar |
caret_jumps | Movimentos não adjacentes do cursor |
revision_ratio | chars_deleted / chars_typed |
Inserção externa
| Campo | Significado |
|---|---|
paste_events, pasted_chars, largest_paste_chars | Volume de colagem |
pasted_fraction | Parcela do texto final que foi colada |
drop_events | Inserções por arrastar e soltar |
silent_insert_chars / silent_insert_ratio | Caracteres sem tecla correspondente |
external_insert_chars / external_insert_ratio | O mesmo, excluindo autocitações e citações da passagem |
paste_sources, paste_chars_by_source | Contagens 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
| Campo | Significado |
|---|---|
blur_events, blur_total_ms | Tempo fora da página |
max_blur_before_insert_ms | Ausência mais longa imediatamente antes de uma inserção grande |
first_keystroke_latency_ms | Tempo de reflexão antes do primeiro caractere |
Integridade
| Campo | Significado |
|---|---|
untrusted_events | InputEvent.isTrusted === false, ou seja, entrada por script ou automatizada |
composition_events | Composição por IME |
virtual_keyboard | Teclado 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:
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:
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
export_include_typing_dynamics: trueProduz 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
python -m potato.export.cli <config.yaml> --format keystrokesGrava 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.
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étodo | Rota | Finalidade |
|---|---|---|
POST | /api/track_typing | Recebe 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_process | Consolidado 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:
const el = document.querySelector('textarea');
window.keystrokeTracker.getFieldIdentity(el); // null means it is not trackednull 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
- Detecção do Processo de Escrita - as seis regras e os três níveis de detecção
- Ética do Registro de Teclas - consentimento, IRB, retenção, direitos dos participantes
- Rastreamento Comportamental - o sistema mais amplo de rastreamento de interações em que isso se insere
- Controle de Qualidade - verificações de atenção e padrões-ouro
- Painel Administrativo - onde fica o painel Processo de Escrita
Para detalhes de implementação, consulte a documentação de origem.