Lendo o processo de escrita: registro de teclas para anotação de texto livre
O Potato agora consegue registrar como os anotadores produzem respostas de texto livre, sem registrar o que eles digitam, e transformar as pausas, revisões e colagens em sinalizações auditáveis para respostas que foram coladas em vez de escritas.
Um trabalhador de crowdsourcing abre sua tarefa de anotação, lê a passagem, muda para outra aba, volta vinte segundos depois, e uma justificativa de 280 caracteres aparece na caixa em um único movimento.
A justificativa está boa. Está no tema, está gramatical, faz referência à passagem. Leia-a ao lado de outras quarenta e nada chama atenção. Qualquer classificador que você aponte para ela vai dizer algo entre «provavelmente humano» e «indefinido», que é o que esses classificadores dizem sobre quase todo texto.
A resposta pronta não conta de onde veio. Os vinte segundos e o movimento único, sim.
O Potato agora registra isso. Campos de texto livre podem capturar um fluxo cego ao conteúdo de como uma resposta foi produzida, resumi-lo em cerca de quarenta atributos e rodar um pequeno conjunto de regras nomeadas sobre o resultado. Vem desativado por padrão e liga com uma linha:
keystroke_logging:
enabled: trueRegistro de teclas
Redigido, transcrito, colado
A pesquisa em que isso se apoia já está razoavelmente assentada. Crossley e colegas coletaram 500 redações argumentativas, pediram a um segundo grupo de trabalhadores que as transcrevesse e separaram escrita autêntica de transcrição com 99% de acurácia usando uma random forest. Deane et al. e Zhang et al. encontraram a mesma separação de forma independente. Asher et al. construíram a versão para crowdsourcing no Prolific, sinalizando participantes cuja contagem de teclas era baixa demais para o tamanho do que enviaram.
A assinatura é consistente em todos eles. A composição real tem pausas mais longas antes de frases e palavras, mais inserções e exclusões, e alta variância nos intervalos entre teclas. A cópia digitada é linear, orientada a rajadas e de baixa variância. A colagem quase não tem intervalos.
Nada disso aparece no texto. Tudo isso aparece no registro.
Por que beforeinput e não keydown
Esta é a decisão técnica que determina se o resto do recurso funciona.
O jeito óbvio de escrever um registrador de teclas é escutar keydown. É também o jeito que falha, porque colagem, arrastar e soltar, composição por IME, ditado, autopreenchimento e desfazer alteram o conteúdo de um campo sem disparar keydown uma única vez. Um registrador que só usa keydown é cego exatamente nos casos que ele existe para pegar.
O sinal principal do Potato é o InputEvent.inputType no beforeinput, que dispara para todos eles e diz qual aconteceu. keydown e keyup continuam sendo escutados, mas com outra finalidade: contar as teclas que uma pessoa apertou fisicamente.
A diferença entre esses dois números é a coisa mais útil que se coleta. Caracteres que apareceram no campo sem uma tecla correspondente são registrados como silent_insert_chars, e sua fração da resposta como silent_insert_ratio. Uma colagem que a página suprimiu, uma extensão que injetou texto, um fluxo de ditado, um script preenchendo a caixa: nenhum deles produz teclas, todos eles produzem caracteres.
O que é registrado, e o que não é
Cada evento carrega um carimbo de tempo, um tipo de entrada, uma classe de tecla, uma posição do cursor e uma variação de comprimento:
{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"}}
A tecla em si nunca é armazenada, apenas a família a que pertence: letter, digit, punct, space, enter, bksp, del, nav, mod, func, unknown. Texto colado é reduzido a um comprimento, um rótulo de origem e um hash com sal por sessão. Campos de senha são recusados de saída. Não dá para reconstruir a resposta a partir do fluxo, e é esse o ponto: o fluxo descreve o processo e não diz nada sobre o conteúdo.
A classificação da origem da colagem é o que mantém o comportamento comum fora das sinalizações. Quando chega uma colagem, o Potato a compara com a passagem em anotação, com qualquer sugestão de IA exibida na página e com o que já havia no campo, depois guarda o rótulo e descarta a comparação. Citar a passagem é lido como instance_text. Mover o seu próprio rascunho é lido como self. Nenhum dos dois conta como inserção externa.
Seis regras, e nada fingindo ser um modelo
A detecção roda no servidor em três níveis. Só o primeiro vem ativado.
O primeiro nível são seis sinalizações nomeadas, cada uma com um limiar explícito, cada uma devolvendo os valores dos atributos que a dispararam:
| Sinalização | Dispara quando | Severidade |
|---|---|---|
paste_dominant | Metade ou mais do texto final chegou por colagem | suspect |
silent_insertion | ≥30% dos caracteres inseridos não tiveram tecla por trás | suspect |
transcription_rhythm | Ritmo metronômico e nenhuma revisão e quase nenhuma pausa | review |
offscreen_composition | Uma inserção externa grande logo depois de ≥10s fora da página | suspect |
implausible_speed | Acima de ~180 wpm de forma sustentada ao longo de uma resposta inteira | review |
synthetic_input | O navegador informa isTrusted === false | suspect |
transcription_rhythm é conjuntiva de propósito. Digitação metronômica sozinha é alguém que digita rápido. Nunca apagar nada é alguém cuidadoso. Quase não pausar é uma resposta curta. Só quando as três valem ao mesmo tempo você tem a assinatura da cópia digitada, e a regra ignora por completo respostas com menos de 80 caracteres, onde não há ritmo a ler.
O segundo nível é a calibração. Os atributos de teclas dependem muito da tarefa de escrita, então um limiar ajustado para uma justificativa de uma frase está errado para cinco parágrafos. python -m potato.typing_detect calibrate config.yaml reajusta cada corte a um percentil de cauda das sessões do seu próprio projeto. São necessárias pelo menos 30 sessões utilizáveis, e os valores calibrados ficam limitados a até 3× os padrões embutidos, para que uma população homogênea não consiga arrastar um limiar até a própria mediana.
O terceiro é supervisionado. Se você tiver rótulos, fit_supervised() treina um classificador de verdade sobre a matriz de atributos. O scikit-learn é importado sob demanda e não é uma dependência do Potato.
Nenhum modelo pré-treinado é distribuído. Não há corpus rotulado no repositório, e distribuir coeficientes ajustados derivados de nada seria inventar um número de validação. O que é distribuído, em vez disso, são seis regras que você pode ler, discordar e sobrescrever.
Se você quiser rótulos, a fase de treinamento pode produzi-los dentro do seu próprio estudo. Dê aos anotadores um aquecimento de copiar a passagem: essas sessões são exemplares genuínos de transcrição, dos seus anotadores e na sua tarefa, e as respostas comuns deles são a classe redigida. Foi assim que o corpus de Crossley foi construído, e o exemplo de calibração monta isso de ponta a ponta.
Os limiares são avaliados no servidor e nunca enviados ao navegador. Publicá-los diria a um anotador exatamente com que lentidão colar.
Onde os dados ficam
Os fluxos brutos vão para o SQLite, em <task_dir>/project.sqlite, uma linha por sessão, pela mesma camada de persistência dos memorandos e do livro de códigos. Os eventos são codificados por delta e compactados com zlib a 1,7 byte por evento, medido, então uma resposta de 500 palavras custa cerca de 5 KB.
Eles deliberadamente não vão para o user_state.json. Esse arquivo é reescrito por inteiro a cada salvamento de anotação, e uma resposta longa tem por volta de 3.000 eventos. Só o resumo compacto é espelhado nos dados comportamentais, com a chave "{schema}:::{label}", de modo que ele acompanha a anotação até o painel e as exportações.
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__ já existente e são identificadas por phase e page. É isso que faz o truque de copiar a passagem funcionar: os exemplares de transcrição são separáveis das respostas comuns só pela fase.
As duas exportações são opcionais. export_include_typing_dynamics: true grava um arquivo typing_dynamics.csv ao lado das suas anotações, e python -m potato.export.cli <config.yaml> --format keystrokes grava os fluxos brutos em Parquet, com queda para JSONL sem o pyarrow. Dados comportamentais nunca entram numa publicação de dataset por acidente.
Antes de apontar isso para pessoas
Uma sinalização é indício para uma pessoa revisar. Não é prova, e não pode ser ligada a rejeição automática, retenção de pagamento ou banimento.
O modo de falha aqui é acusar um anotador honesto, e os casos que disparam as regras não são exóticos. Alguns estão tratados: citar a passagem e reorganizar o próprio rascunho são suprimidos pela classificação de origem, e teclados virtuais e composição por IME suprimem silent_insertion, já que nenhum dos dois emite keydown de forma confiável e toda inserção pareceria silenciosa. Outros não estão. Ditado vai sinalizar. Extensões de gramática vão sinalizar. Parte da tecnologia assistiva produz eventos não confiáveis e vai disparar synthetic_input, e por isso a documentação diz sem rodeios que, se o seu estudo estiver aberto a pessoas que usam tecnologia assistiva, é melhor desligar essa regra do que explicar uma sinalização a alguém que estava usando as ferramentas de que precisa para trabalhar.
E há a aritmética. Se 5% das suas respostas são coladas e sua regra sinaliza 5% das sessões, a maior parte do que você sinalizar ainda pode ser trabalho honesto. Numa plataforma em que a má conduta é de fato rara, uma regra com uma taxa de falsos positivos até modesta produz mais acusações falsas do que capturas verdadeiras. Um limiar calibrado deixa isso explícito, e não melhor: um percentil de cauda sinaliza sua fração de cauda de qualquer população, inclusive de uma em que ninguém fez nada de errado.
A divulgação vem ativada por padrão, e desligá-la registra um aviso na inicialização. Esse padrão existe porque padrões de tempo são um biométrico comportamental. Eles podem identificar uma pessoa e vincular contas entre contextos, e a literatura de pesquisa já os usou para inferir habilidade de digitação, condição de falante de segunda língua e carga cognitiva. Ninguém que se inscreveu para rotular frases esperava isso. O Potato não calcula nada disso e não distribui ferramenta alguma para isso, mas os dados que você guardar sustentariam a análise, e administrar isso é problema seu, não da ferramenta. typing_store.delete_for_user() remove os fluxos de um participante, e fidelity: summary mantém os atributos e descarta o detalhe biométrico. A página de ética tem texto de consentimento de exemplo, orientação de retenção e notas sobre o Artigo 22 do GDPR, revisão por IRB e políticas de rejeição das plataformas.
writing_process_risk aparece no painel Processo de Escrita do painel administrativo como auxílio de ordenação, separado do suspicion_score já existente. Nenhum dos dois números entra no outro, então nenhum deles muda em silêncio o que o outro significa.
Como ligar
O recurso chega no Potato 2.7.2. Uma configuração mais completa do que a linha única lá de cima:
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 impede colar nos campos instrumentados. Também bloqueia citações legítimas, e quem estiver decidido pode redigitar, então flag e revisão costuma ser a troca melhor.
Um projeto executável fica em examples/advanced/keystroke-logging/, e o passo a passo de calibração em examples/advanced/keystroke-calibration/.
Documentação
- Registro de teclas — cada campo capturado, os atributos de resumo, o armazenamento e a solução de problemas
- Detecção do processo de escrita — as seis regras, os três níveis, a tabela de falsos positivos e as citações
- É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
- Crowdsourcing no Prolific e no MTurk — as regras das plataformas sobre monitoramento e rejeição
Atualizando
pip install --upgrade potato-annotation==2.7.2Nada muda em um projeto existente até você pedir. keystroke_logging.enabled é false por padrão, então atualizar nunca começa a gravar ninguém.