Steven
Steven8 min de lectura

El silencio era estructural

Las dos últimas releases de GeekBye v1 giran en torno a la misma verdad incómoda: la transcripción en tiempo real sobre una red real no es sin pérdidas, y lo honesto es dejar de fingir que lo es. La v1.8.20 guardaba en disco una copia de cada chunk de audio antes de descartarlo durante una reconexión, y empezó a marcar en voz alta los huecos en la transcripción. La v1.9.0 dejó de enviar silencio para ahorrar ancho de banda — y descubrió que el silencio era justo la señal que el transcriptor usaba para saber que una frase había terminado. Dos releases sobre el coste de tirar cosas.

Ingeniería
Audio
Fiabilidad
Lanzamientos de GeekBye
El silencio era estructural

Las dos últimas releases de GeekBye v1 — la v1.8.20 y la v1.9.0, las que están justo antes del hito de la versión 2 — comparten un tema que solo se vuelve obvio cuando las lees juntas. Ambas tratan de cosas que la app tiraba en silencio, y ambas llegan a la misma conclusión: aquello que descartas para ser eficiente estaba haciendo más trabajo del que creías. Una release descartaba audio durante las reconexiones de red. La otra descartaba silencio para ahorrar ancho de banda. En ambos casos, lo descartado resultó ser estructural.

La transcripción en tiempo real no es sin pérdidas, y la v1.8.20 lo admite

Aquí va un hecho sobre el speech-to-text por streaming sobre un WebSocket que es fácil de esquivar: cuando la conexión se cae y se reconecta — o cuando la app hace failover de un proveedor de transcripción a otro, de Deepgram a ElevenLabs — el cliente descarta a propósito los chunks de audio capturados durante ese hueco. No le queda otra. Un proveedor recién conectado que de pronto recibe una acumulación de audio rancio se atraganta, o peor, engancha su detector de actividad de voz a lo que no debe. Así que el cliente tira esos chunks.

El problema no es el descarte. El problema es lo que el descarte significaba: esas palabras sencillamente nunca aparecían en la transcripción, y nada te avisaba de que faltaban. La grabación parecía completa. No lo estaba.

La v1.8.20 ataca eso en dos frentes, y ninguno de ellos es «hacer que las reconexiones nunca pierdan audio» — porque no puedes, no en tiempo real. En su lugar, vuelve la pérdida recuperable y honesta.

La mitad recuperable es un tee de PCM duradero. En el manejador de captura del proceso principal, cada chunk capturado se escribe ahora a un archivo en disco antes de llegar a las compuertas de descarte de reconexión y de periodo de gracia:

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

El mecanismo es de una simplicidad agradable: teeChunkToDisk abre un WriteStream, reserva por adelantado una cabecera WAV de 44 bytes, va añadiendo muestras mono crudas de 16 bits a medida que llegan, y parchea la cabecera con los tamaños finales al detener (fs.openSync(..., 'r+'), escribir 44 bytes en el offset 0). El resultado es un WAV reproducible del audio completo — incluidas las partes que la transcripción en tiempo real nunca vio. Cuando la grabación termina, emite un evento session-audio-saved con la ruta del archivo y el número de bytes.

Aquí importan dos notas de honestidad, porque es fácil prometer de más con esto. Primera: el tee no rellena el hueco en estas releases. Preserva el audio crudo para una pasada de reconciliación posterior — una retranscripción por lotes que concilia el archivo con la transcripción en vivo — que es un gancho que estas releases dejan puesto, no una función que entregan. El commit de seguimiento incluso anota session-audio-saved con un TODO que apunta a los issues de reconciliación para que nadie lo confunda con código muerto. Segunda: esto cuesta disco. El equipo estimó unos 86 MB por grabación de 30 minutos, así que la misma release añade un StartupCleanupService que poda las sesiones guardadas de más de 7 días. Una red de seguridad que crece sin límite es una fuga; la limpieza es lo que hace que el tee sea entregable.

La mitad honesta es aún más simple. En el primer intento de reconexión, el cliente escribe una entrada real y visible en la transcripción:

[reconnecting — audio may be missing]

Esa única línea es toda la filosofía de la release en miniatura. La app no puede garantizar que lo oyó todo, así que deja de dar a entender que sí. Un hueco que puedes ver es un hueco que puedes sortear; un hueco que no puedes ver es un informe de bug esperando a ocurrir.

v1.9.0: el silencio que optimizaste hasta hacerlo desaparecer era la señal

La v1.9.0 es la última release de la v1, y es donde el backend le pasó al cliente un conjunto de responsabilidades de la transcripción en vivo — descartar silencio, atribuir cada línea de la transcripción al micrófono o al audio del sistema a partir de una línea de tiempo local de actividad de voz, ser dueño del bucle de reconexión, imponer los límites de grabación. La mayor parte de eso es cimiento para la arquitectura de la versión 2 y se ha contado en otro sitio. Pero uno de sus hilos es la mejor pequeña historia de cualquiera de las dos releases, porque es un arreglo que tuvo que deshacer el daño de una optimización que era completamente correcta de forma aislada.

Empieza con un cambio obviamente bueno: dejar de enviar silencio. El cliente empezó a filtrar los chunks cuya fuente dominante era silencio antes de mandarlos por el socket — menos ancho de banda, menos audio inútil que procesar para el backend, y honraba un contrato de cliente explícito del backend que decía «no envíes silencio». Todas las razones para hacerlo eran sólidas.

Y rompió por completo la finalización de utterances. El commit que lo arregla expone la cadena causal con una franqueza inusual:

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 es la forma platónica de una abstracción con fugas devolviendo el mordisco. El detector de actividad de voz del transcriptor no tiene una entrada aparte para «el hablante ha terminado» — infiere el final de una frase al oír cerca de un segundo de silencio en el audio. El silencio no era ruido en el cable; era un mensaje. Al filtrarlo para ahorrar ancho de banda, el cliente dejó de enviar sin querer la única señal que le decía al transcriptor que una frase había terminado. Así que cada transcripción parcial seguía creciendo sin más, con la cola difuminándose en unos ... que nunca se resolvían.

Podrías arreglar esto volviendo a poner el silencio — pero entonces habrías deshecho la optimización. El arreglo de verdad es mejor: enviar el significado del silencio sin enviar el silencio. El cliente rastrea ahora cuánto tiempo lleva el audio en silencio de forma local, y cuando eso cruza SILENCE_COMMIT_THRESHOLD_MS = 1000 — igualado a propósito con el propio umbral de silencio del VAD del backend — dispara una señal commit explícita fuera de banda: sendMixedChunk nota el silencio sostenido, envía un realtime-audio:commit por IPC, el proceso principal reenvía un { type: 'commit' } por el socket, y el backend le dice a ElevenLabs que finalice el parcial. Dos pequeños trozos de estado, silenceStartedAt y silenceCommitFired, evitan que se dispare repetidamente. La victoria de ancho de banda sobrevive; la finalización vuelve a funcionar; los ... colgantes se resuelven.

Hay también una pequeña nota al pie honesta sobre esta release: la lógica de reconexión sin límite que introdujo la v1.9.0 se construyó primero en el renderer como un WsReconnectController, y luego se eliminó dentro de la misma release una vez que la implementación de verdad aterrizó en el manejador del proceso principal, donde le correspondía. La historia de la reconexión en sí — backoff sin límite, alternando proveedores cada cinco fallos — se cuenta en su propio capítulo; lo que la v1.9.0 aporta es que es donde nació esa arquitectura, y donde el cliente tomó posesión de ella por primera vez.

Tres cosas que estas releases enseñaron

  1. Cuando no puedes evitar la pérdida, vuélvela recuperable y vuélvela visible. La v1.8.20 no impide que las reconexiones descarten audio — eso no es físicamente posible en tiempo real. Guarda una copia del audio crudo para después, y etiqueta el hueco en la transcripción. Ninguna de las dos cosas es glamorosa; juntas convierten la pérdida silenciosa de datos en una condición conocida, acotada y recuperable.
  2. Una red de seguridad necesita sus propios límites. El tee duradero llenaría el disco en silencio si se le dejara a su aire — un arreglo que se convierte en un bug a cámara lenta. La limpieza a los 7 días que se entregó en la misma release no es una ocurrencia tardía; es lo que hace que el tee sea seguro de entregar siquiera. Si añades un mecanismo que acumula, añade en el mismo aliento el mecanismo que lo poda.
  3. Aquello que descartas para ser eficiente puede ser una señal. La lección más afilada de cualquiera de las dos releases: filtrar el silencio era correcto en cada eje que comprobarías normalmente — ancho de banda, carga, cumplimiento del contrato — y aun así rompió el producto, porque un sistema aguas abajo estaba leyendo un significado en el silencio. Cuando quitas datos por eficiencia, pregúntate qué estaba infiriendo algo a partir de esos datos. A veces tienes que enviar el significado de forma explícita precisamente porque dejaste de enviar aquello de lo que solía inferirse.

Y ese es el final de la historia de la versión 1 — el último endurecimiento antes de la reescritura. Para el capítulo anterior, los tres verbos que mantienen vivo el Web Audio (v1.8.16 + v2.0.2); para hacia dónde se dirigía todo esto, lo que de verdad cuesta una versión 2: 206 commits (v2.0.0); y para todo el arco, la anatomía de publicar software hasta la perfección.

Artículos Relacionados

Los tres verbos que mantienen vivo el Web Audio
Steven
Steven10 min de lectura

Los tres verbos que mantienen vivo el Web Audio

Dos releases puntuales de GeekBye, con dos meses de diferencia y en dos archivos distintos, le enseñaron a nuestro código de audio la misma lección desde extremos opuestos: dejar de tratar el AudioContext del navegador como desechable. Una release aprendió a hacer resume() de un contexto que macOS había suspendido silenciosamente a mitad de grabación; la otra aprendió a hacer suspend() en vez de close() para que las sesiones consecutivas dejen de estrellarse contra el techo de aproximadamente seis contextos de Chromium. Resume, suspend, close — ese es todo el argumento.

Ingeniería
Audio
Desktop
Una conexión caída no debería tumbar toda tu app — pero la nuestra lo hacía
Steven
Steven7 min de lectura

Una conexión caída no debería tumbar toda tu app — pero la nuestra lo hacía

Cuando nuestro backend se quedó sin conexión en mitad de una reunión, no solo pausó la transcripción — tumbó la app entera. La causa era un único evento sin gestionar, y el arreglo eran diez líneas. Este es el grupo de lanzamientos que hizo que GeekBye siguiera con la sesión iniciada y siguiera conectado a través de las cosas que antes lo mataban.

Ingeniería
Fiabilidad
Electron
La comprobación de seguridad que hizo imposible cerrar nuestra app
Steven
Steven6 min de lectura

La comprobación de seguridad que hizo imposible cerrar nuestra app

La actualización automática fue la función más difícil que hemos lanzado — seis versiones en cuatro días para que dejara de dejar la app inservible. El peor error fue uno que introdujimos por querer ser prudentes: una comprobación "de seguridad" de 500 milisegundos que convertía una actualización fallida en un proceso que, literalmente, no podías cerrar.

Ingeniería
Electron
Fiabilidad