
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.
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>.wavbefore 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
- 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.
- 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.
- 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.