Steven
Steven6 min de leitura

Como transmitir um relatório ao vivo sem a cintilação

Quando a sua reunião termina, o resumo do GeekBye agora se preenche ao vivo em vez de o obrigar a olhar para um spinner. Fazer com que a interface em streaming parecesse calma em vez de trêmula exigiu resolver a cintilação duas vezes — uma para os campos estruturados, outra para o markdown — e a segunda correção foi um renderizador que já tínhamos.

Engenharia
Frontend
Streaming
Lançamentos do GeekBye
Como transmitir um relatório ao vivo sem a cintilação

Antes deste lançamento, terminar uma reunião do GeekBye significava olhar para um spinner. A app enviava toda a sua transcrição para análise, esperava pelo resumo inteiro — pontuação, pontos-chave, ações a tomar, tudo — e depois despejava-o no ecrã de uma só vez. Funcional, mas parecia lento, porque você ficava a olhar para o nada enquanto ela trabalhava.

O GeekBye v1.6.13–v1.6.15 transformou isso num relatório que se transmite ao vivo, campo a campo, à medida que é gerado. A parte interessante não é o streaming — é tudo o que tivemos de fazer para impedir que o streaming parecesse trêmulo. O streaming calmo e o streaming brusco são a mesma funcionalidade com execuções muito diferentes.

Cintilação, parte um: não deixe cada fragmento sacudir o React

O resumo não é um único bloco — são campos estruturados (uma pontuação geral, pontos-chave, ações a tomar, e assim por diante) que o backend emite um a um sobre server-sent events. A forma ingénua de renderizar isso é atualizar o estado do React a cada evento assim que chega.

Faça isso, e obtém cintilação. Cada campo que chega dispara a sua própria re-renderização; componentes que mostravam "vazio" montam, depois desmontam, depois voltam a montar como "carregado"; o layout treme a cada pacote. O relatório monta-se sozinho à sua frente da pior maneira possível — visível, nervosamente.

A correção é uma disciplina em duas partes. Primeiro, acumule os campos que chegam num ref — um contentor mutável que não dispara renderizações — e publique para o estado do React uma só vez por fragmento com uma cópia nova, para que a árvore se atualize de forma deliberada em vez de a cada micro-evento. Segundo, renderize uma única árvore de componentes estável o tempo todo, esteja em streaming ou terminada, com marcadores em linha para os campos que ainda não chegaram:

// accumulate in a ref, publish once per chunk
partialRef.current = { ...partialRef.current, [field]: value }
setPartial({ ...partialRef.current })

// one tree, never swapped; missing fields show a placeholder in place
const report = isStreaming ? partial : saved
<Score>{report.overallScore ?? '…'}</Score>

O componente que mostra a pontuação nunca é desmontado — apenas mostra até o número aterrar, e depois o número. Nada treme. E o objeto totalmente montado continua a ser guardado em cache na base de dados local no fim, por isso a interface em streaming e o armazenamento durável coexistem sem brigar.

Cintilação, parte dois: o renderizador que já tínhamos

A segunda cintilação é mais subtil e vive no próprio markdown. O relatório renderiza markdown formatado — títulos, negrito, listas, blocos de código. Mas o markdown que chega a meio do fluxo está, em qualquer instante, meio terminado. Uma lista com um só marcador por agora. Um bloco de código que foi aberto mas não fechado. Um marcador de negrito ainda sem par.

Um renderizador de markdown convencional reanalisa toda a string a cada token, e esse estado meio terminado analisa-se como algo diferente de cada vez — por isso a lista cintila, o bloco de código pisca abrindo e fechando, o layout salta à medida que os tokens se completam. É a mesma montagem nervosa de antes, uma camada abaixo.

A correção estava quase embaraçosamente à mão: já estávamos a usar um renderizador de markdown ciente do streaming para o chat de IA ao vivo. Um renderizador construído para tolerar tokens incompletos — para renderizar markdown meio terminado de forma estável e só o assentar quando os tokens se completam — em vez de reanalisar do zero a cada vez. O relatório só precisava de usar o mesmo. Tínhamos resolvido exatamente este problema para o chat meses antes; a "correção" para o relatório foi reconhecer que já tínhamos a ferramenta e apontá-la para uma segunda superfície. Reutilizar venceu reconstruir.

O bónus: a cópia formatada é um problema de formato da área de transferência

Assim que o relatório ficou com bom aspeto, as pessoas quiseram colá-lo — num documento, num e-mail — e manter a formatação e as capturas de ecrã. O instinto é serializar o relatório numa string de markdown e torcer para que o destino a renderize. Normalmente não renderiza.

A verdadeira resposta é que a área de transferência guarda várias representações ao mesmo tempo. Por isso escrevemos duas: uma versão HTML com a formatação intacta e as capturas incorporadas como imagens em linha, e uma alternativa em texto simples para destinos que não aceitam HTML. Cole num editor formatado e obtém o relatório formatado com imagens; cole numa caixa de texto simples e obtém texto limpo. A "cópia formatada" nunca foi um problema de serialização — foi um problema de escolher-o-formato-certo-da-área-de-transferência.

O mesmo lançamento também deu ao relatório rótulos ao estilo do chat Eu / Eles com os dois interlocutores alinhados em lados opostos, para que uma transcrição se leia como a conversa que foi, e moveu as suas próprias capturas de ecrã para o seu próprio lado para combinar. (Um lançamento nesta janela, o v1.6.14, foi uma pura recompilação — não há história aí, e é honesto dizê-lo.)

Três coisas que a interface em streaming nos ensinou

  1. Guarde em buffer os campos transmitidos, publique uma vez por fragmento. Deixar cada server-sent event conduzir a sua própria atualização de estado é a cintilação. Um acumulador com ref mais uma única publicação por fragmento, numa árvore que nunca é desmontada, transforma a montagem nervosa num preenchimento calmo.
  2. Tudo o que transmite precisa de um renderizador ciente do streaming. Um renderizador que reanalisa toda a string por token vai cintilar sobre markdown meio terminado. Use um construído para entradas incompletas — e se já tem um para outra superfície, reutilize-o antes de construir um segundo.
  3. A cópia formatada é sobre formatos da área de transferência, não serialização. Escreva text/html com imagens incorporadas e uma alternativa text/plain. A área de transferência foi concebida para transportar ambos; use-a.

Este é o quinto capítulo da história de fiabilidade e polimento que se torna o GeekBye v2. Para o capítulo anterior, veja o bug dos auriculares (v1.6.12); para o ponto onde a mesma ideia de server-sent events se tornou depois todo um transporte de reserva, transcrição ao vivo quando a firewall bloqueia WebSockets (v2.0.8); e para todo o arco, a anatomia de lançar software até à perfeição.

Artigos Relacionados

Uma conexão perdida não deveria derrubar o app inteiro — mas o nosso derrubava
Steven
Steven7 min de leitura

Uma conexão perdida não deveria derrubar o app inteiro — mas o nosso derrubava

Quando nosso backend ficou offline no meio de uma reunião, ele não só pausou a transcrição — derrubou o app inteiro. A causa era um único evento não tratado, e a correção tinha dez linhas. Este é o grupo de lançamentos que fez o GeekBye continuar logado e continuar conectado através das coisas que antes o matavam.

Engenharia
Confiabilidade
Electron
Uma variável CSS, cinco rodadas de revisão e uma cadeia de ferramentas Swift que mentiu
Steven
Steven8 min de leitura

Uma variável CSS, cinco rodadas de revisão e uma cadeia de ferramentas Swift que mentiu

A v2.0.7 do GeekBye tornou todo o overlay translúcido de forma ajustável — o que soa como uma mudança de CSS de uma linha e definitivamente não foi. A história de verdade é o que a revisão de código pegou: duas superfícies que se recusavam a esmaecer, uma barra de gravação que parecia errada na mesma opacidade e um binário compilado que oscilou 672 bytes porque nossa própria documentação disse a um revisor a versão errada do Swift.

Engenharia
Design
Build
Apagámos 5,000 Linhas de Código de Áudio — e as Transcrições Começaram a Aparecer em Duplicado
Steven
Steven6 min de leitura

Apagámos 5,000 Linhas de Código de Áudio — e as Transcrições Começaram a Aparecer em Duplicado

O GeekBye v1.6 removeu dois transcritores Swift on-device e substituiu-os por um único pipeline unificado — uma eliminação líquida de mais de 5,000 linhas, feita enquanto as pessoas estavam a meio de reuniões na app. Depois as transcrições começaram a aparecer em duplicado, porque o bug migrou para a única camada que ainda pensava existirem dois motores.

Engenharia
Transcrição
Arquitetura