Steven
Steven8 min de leitura

O silêncio era estrutural

As duas últimas releases da GeekBye v1 giram em torno da mesma verdade incômoda: a transcrição em tempo real sobre uma rede real não é sem perdas, e o gesto honesto é parar de fingir que é. A v1.8.20 guardava em disco uma cópia de cada chunk de áudio antes de descartá-lo durante uma reconexão, e começou a marcar em voz alta os buracos na transcrição. A v1.9.0 parou de enviar silêncio para economizar largura de banda — e descobriu que o silêncio era justamente o sinal que o transcritor usava para saber que uma frase havia terminado. Duas releases sobre o custo de jogar coisas fora.

Engenharia
Audio
Confiabilidade
Lançamentos da GeekBye
O silêncio era estrutural

As duas últimas releases da GeekBye v1 — a v1.8.20 e a v1.9.0, as que ficam logo antes do marco da versão 2 — compartilham um tema que só fica óbvio quando você as lê juntas. Ambas tratam de coisas que o app jogava fora em silêncio, e ambas chegam à mesma conclusão: aquilo que você descarta para ser eficiente estava fazendo mais trabalho do que você imaginava. Uma release descartava áudio durante as reconexões de rede. A outra descartava silêncio para economizar largura de banda. Nos dois casos, o que foi descartado acabou sendo estrutural.

A transcrição em tempo real não é sem perdas, e a v1.8.20 admite isso

Aqui vai um fato sobre o speech-to-text por streaming sobre um WebSocket que é fácil desviar o olhar: quando a conexão cai e se reconecta — ou quando o app faz failover de um provedor de transcrição para outro, do Deepgram para o ElevenLabs — o cliente descarta de propósito os chunks de áudio capturados durante esse buraco. Ele precisa. Um provedor recém-conectado que de repente recebe um acúmulo de áudio velho vai se engasgar, ou pior, prender seu detector de atividade de voz na coisa errada. Então o cliente joga esses chunks fora.

O problema não é o descarte. O problema é o que o descarte significava: essas palavras simplesmente nunca apareciam na transcrição, e nada te avisava de que faltavam. A gravação parecia completa. Não estava.

A v1.8.20 ataca isso em duas frentes, e nenhuma delas é «fazer as reconexões nunca perderem áudio» — porque você não pode, não em tempo real. Em vez disso, ela torna a perda recuperável e honesta.

A metade recuperável é um tee de PCM durável. No handler de captura do processo principal, cada chunk capturado agora é escrito em um arquivo no disco antes de chegar às comportas de descarte de reconexão e de período de carência:

every captured chunk is written to <userData>/sessions/session-<ts>.wav before realtime drop gates so reconnect-lost audio is preserved for a future batch-STT reconciliation pass

O mecanismo é de uma simplicidade agradável: teeChunkToDisk abre um WriteStream, reserva de antemão um cabeçalho WAV de 44 bytes, vai anexando amostras mono cruas de 16 bits à medida que chegam, e remenda o cabeçalho com os tamanhos finais ao parar (fs.openSync(..., 'r+'), escrever 44 bytes no offset 0). O resultado é um WAV reproduzível do áudio completo — incluindo as partes que a transcrição em tempo real nunca viu. Quando a gravação termina, ele emite um evento session-audio-saved com o caminho do arquivo e a contagem de bytes.

Duas notas de honestidade importam aqui, porque é fácil prometer demais com isso. Primeira: o tee não preenche o buraco nestas releases. Ele preserva o áudio bruto para uma passagem de reconciliação posterior — uma retranscrição em lote que reconcilia o arquivo com a transcrição ao vivo — que é um gancho que estas releases deixam posto, não um recurso que elas entregam. O commit de acompanhamento até anota session-audio-saved com um TODO apontando para os issues de reconciliação para que ninguém o confunda com código morto. Segunda: isso custa disco. O time estimou cerca de 86 MB por gravação de 30 minutos, então a mesma release adiciona um StartupCleanupService que poda as sessões salvas com mais de 7 dias. Uma rede de segurança que cresce sem limite é um vazamento; a limpeza é o que torna o tee entregável.

A metade honesta é ainda mais simples. Na primeira tentativa de reconexão, o cliente escreve uma entrada real e visível na transcrição:

[reconnecting — audio may be missing]

Essa única linha é toda a filosofia da release em miniatura. O app não pode garantir que ouviu tudo, então ele para de dar a entender que ouviu. Um buraco que você pode ver é um buraco que você pode contornar; um buraco que você não pode ver é um relatório de bug esperando para acontecer.

v1.9.0: o silêncio que você otimizou até sumir era o sinal

A v1.9.0 é a última release da v1, e é onde o backend repassou ao cliente um conjunto de responsabilidades da transcrição ao vivo — descartar silêncio, atribuir cada linha da transcrição ao microfone ou ao áudio do sistema a partir de uma linha do tempo local de atividade de voz, ser dono do loop de reconexão, impor os limites de gravação. A maior parte disso é alicerce para a arquitetura da versão 2 e foi contada em outro lugar. Mas um dos seus fios é a melhor pequena história de qualquer uma das duas releases, porque é uma correção que teve que desfazer o dano de uma otimização que era completamente correta isoladamente.

Começa com uma mudança obviamente boa: parar de enviar silêncio. O cliente começou a filtrar os chunks cuja fonte dominante era silêncio antes de despachá-los pelo socket — menos largura de banda, menos áudio inútil para o backend processar, e isso honrava um contrato de cliente explícito do backend que dizia «não envie silêncio». Toda razão para fazê-lo era sólida.

E quebrou por completo a finalização das utterances. O commit que corrige isso expõe a cadeia causal com uma franqueza incomum:

After the client began filtering silence chunks … ElevenLabs Scribe's VAD-based commit strategy no longer fires — EL needs ~1s of silence in the audio stream to commit a partial, and that silence never reaches it. Result: partials accumulate forever, UI shows trailing "...", utterances never finalize.

Esta é a forma platônica de uma abstração com vazamentos revidando a mordida. O detector de atividade de voz do transcritor não tem uma entrada separada para «o falante terminou» — ele infere o fim de uma frase ao ouvir cerca de um segundo de silêncio no áudio. O silêncio não era ruído no fio; era uma mensagem. Ao filtrá-lo para economizar largura de banda, o cliente acidentalmente parou de enviar o único sinal que dizia ao transcritor que uma frase havia terminado. Então cada transcrição parcial só continuava crescendo, com a cauda se dissolvendo em uns ... que nunca se resolviam.

Você poderia corrigir isso recolocando o silêncio — mas aí você desfez a otimização. A correção de verdade é melhor: enviar o significado do silêncio sem enviar o silêncio. O cliente agora rastreia há quanto tempo o áudio está em silêncio localmente, e quando isso cruza SILENCE_COMMIT_THRESHOLD_MS = 1000 — deliberadamente igualado ao próprio limiar de silêncio do VAD do backend — dispara um sinal commit explícito fora de banda: sendMixedChunk nota o silêncio prolongado, envia um realtime-audio:commit via IPC, o processo principal encaminha um { type: 'commit' } pelo socket, e o backend diz ao ElevenLabs para finalizar o parcial. Dois pequenos pedaços de estado, silenceStartedAt e silenceCommitFired, evitam que ele dispare repetidamente. A vitória de largura de banda sobrevive; a finalização volta a funcionar; os ... pendurados se resolvem.

Há também uma pequena nota de rodapé honesta sobre esta release: a lógica de reconexão sem limite que a v1.9.0 introduziu foi primeiro construída no renderer como um WsReconnectController, e depois deletada dentro da mesma release assim que a implementação de verdade aterrissou no handler do processo principal, onde ela pertencia. A história da reconexão em si — backoff sem limite, alternando provedores a cada cinco falhas — é contada no próprio capítulo dela; o que a v1.9.0 acrescenta é que é onde essa arquitetura nasceu, e onde o cliente tomou posse dela pela primeira vez.

Três coisas que estas releases ensinaram

  1. Quando você não pode evitar a perda, torne-a recuperável e torne-a visível. A v1.8.20 não impede as reconexões de descartarem áudio — isso não é fisicamente possível em tempo real. Ela guarda uma cópia do áudio bruto para depois, e rotula o buraco na transcrição. Nenhuma das duas é glamorosa; juntas, elas transformam a perda silenciosa de dados numa condição conhecida, limitada e recuperável.
  2. Uma rede de segurança precisa dos próprios limites. O tee durável encheria o disco em silêncio se deixado por conta própria — uma correção que vira um bug em câmera lenta. A limpeza de 7 dias entregue na mesma release não é um pensamento tardio; é o que torna o tee seguro de entregar, ponto. Se você adiciona um mecanismo que acumula, adicione no mesmo fôlego o mecanismo que o poda.
  3. Aquilo que você descarta para ser eficiente pode ser um sinal. A lição mais afiada de qualquer uma das duas releases: filtrar o silêncio era correto em cada eixo que você normalmente checaria — largura de banda, carga, conformidade de contrato — e ainda assim quebrou o produto, porque um sistema a jusante estava lendo um significado no silêncio. Quando você remove dados por eficiência, pergunte-se o que estava inferindo algo a partir desses dados. Às vezes você tem que enviar o significado explicitamente justamente porque parou de enviar aquilo de que ele costumava ser inferido.

E esse é o fim da história da versão 1 — o último endurecimento antes da reescrita. Para o capítulo anterior, os três verbos que mantêm o Web Audio vivo (v1.8.16 + v2.0.2); para onde tudo isso estava indo, o que uma versão 2 realmente exige: 206 commits (v2.0.0); e para todo o arco, a anatomia de entregar software até a perfeição.

Artigos Relacionados

Os três verbos que mantêm o Web Audio vivo
Steven
Steven10 min de leitura

Os três verbos que mantêm o Web Audio vivo

Duas releases pontuais da GeekBye, com dois meses de diferença e em dois arquivos distintos, ensinaram ao nosso código de áudio a mesma lição a partir de extremos opostos: parar de tratar o AudioContext do navegador como descartável. Uma release aprendeu a fazer resume() de um contexto que o macOS havia suspendido silenciosamente no meio da gravação; a outra aprendeu a fazer suspend() em vez de close() para que sessões consecutivas parem de se espatifar contra o teto de aproximadamente seis contextos do Chromium. Resume, suspend, close — é esse o enredo inteiro.

Engenharia
Audio
Desktop
Distinguir uma chamada de um app aberto
Steven
Steven9 min de leitura

Distinguir uma chamada de um app aberto

A GeekBye pode notar que você entrou em uma reunião por vídeo e oferecer gravá-la. A detecção acaba sendo a metade fácil — um binário Swift que lê títulos de janela a cada dez segundos. A metade difícil é a precisão: não disparar quando o Zoom está meramente aberto, não oferecer gravar uma reunião que você já está gravando, e não silenciar o microfone na chamada em que você realmente está. Três releases, e cada uma é uma salvaguarda que teve que aprender a não se sabotar.

Engenharia
macOS
Desktop
Tirando o backend do caminho de upload
Steven
Steven9 min de leitura

Tirando o backend do caminho de upload

A GeekBye grava sua tela e salva o vídeo no seu Google Drive. A primeira versão enviava cada gravação pelos próprios servidores da GeekBye no caminho; uma release depois, o arquivo ia direto da sua máquina para o Drive, e o backend foi rebaixado a segurar um único ponteiro. A parte interessante é o quão pouco código a versão «direta e retomável» realmente contém — porque a retomabilidade veio de apagar um proxy, não de escrever um.

Engenharia
Arquitetura
Desktop