Steven
Steven6 min de lectura

Cómo transmitir un informe en vivo sin el parpadeo

Cuando termina tu reunión, el resumen de GeekBye ahora se rellena en vivo en lugar de hacerte mirar un spinner. Lograr que la interfaz en streaming se sintiera calmada en vez de nerviosa exigió resolver el parpadeo dos veces — una para los campos estructurados, otra para el markdown — y el segundo arreglo fue un renderizador que ya teníamos.

Ingeniería
Frontend
Streaming
Lanzamientos de GeekBye
Cómo transmitir un informe en vivo sin el parpadeo

Antes de este lanzamiento, terminar una reunión de GeekBye significaba mirar un spinner. La app enviaba toda tu transcripción para su análisis, esperaba el resumen completo — puntuación, puntos clave, tareas pendientes, todo — y luego lo soltaba en pantalla de golpe. Funcional, pero se sentía lento, porque te quedabas mirando la nada mientras trabajaba.

GeekBye v1.6.13–v1.6.15 convirtió eso en un informe que se transmite en vivo, campo a campo, a medida que se genera. La parte interesante no es el streaming — es todo lo que tuvimos que hacer para evitar que el streaming se viera nervioso. El streaming calmado y el streaming brusco son la misma función con ejecuciones muy distintas.

Parpadeo, parte uno: no dejes que cada fragmento sacuda a React

El resumen no es un solo bloque — son campos estructurados (una puntuación general, puntos clave, tareas pendientes, etc.) que el backend emite uno a uno sobre server-sent events. La forma ingenua de renderizarlo es actualizar el estado de React en cada evento a medida que llega.

Haz eso, y obtienes parpadeo. Cada campo que llega dispara su propio re-renderizado; componentes que mostraban "vacío" se montan, luego se desmontan, luego se vuelven a montar como "cargado"; el diseño se sacude con cada paquete. El informe se ensambla solo frente a ti de la peor manera posible — visible, nerviosamente.

El arreglo es una disciplina de dos partes. Primero, acumula los campos entrantes en un ref — un contenedor mutable que no dispara renderizados — y publica al estado de React una vez por fragmento con una copia nueva, para que el árbol se actualice de forma deliberada y no en cada micro-evento. Segundo, renderiza un único árbol de componentes estable todo el tiempo, ya sea en streaming o terminado, con marcadores en línea para los campos que aún no han llegado:

// 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>

El componente que muestra la puntuación nunca se desmonta — solo muestra hasta que aterriza el número, y luego el número. Nada se sacude. Y el objeto ya completamente ensamblado sigue guardándose en caché en la base de datos local al final, así que la interfaz en streaming y el almacenamiento duradero conviven sin pelearse.

Parpadeo, parte dos: el renderizador que ya teníamos

El segundo parpadeo es más sutil y vive en el propio markdown. El informe renderiza markdown enriquecido — encabezados, negritas, listas, bloques de código. Pero el markdown que llega a mitad del stream está, en cualquier instante dado, a medio terminar. Una lista con un solo punto por ahora. Un bloque de código que se ha abierto pero no cerrado. Un marcador de negrita sin pareja todavía.

Un renderizador de markdown convencional reanaliza toda la cadena en cada token, y ese estado a medio terminar se analiza como algo distinto cada vez — así que la lista parpadea, el bloque de código destella abriéndose y cerrándose, el diseño salta a medida que los tokens se completan. Es el mismo ensamblaje nervioso de antes, una capa más abajo.

El arreglo estaba casi vergonzosamente a mano: ya estábamos usando un renderizador de markdown consciente del streaming para el chat de IA en vivo. Un renderizador construido para tolerar tokens incompletos — para renderizar markdown a medio terminar de forma estable y asentarlo solo cuando los tokens se completan — en lugar de reanalizar desde cero cada vez. El informe solo necesitaba usar el mismo. Habíamos resuelto exactamente este problema para el chat meses antes; el "arreglo" para el informe fue reconocer que ya teníamos la herramienta y apuntarla a una segunda superficie. Reutilizar le ganó a reconstruir.

El extra: la copia enriquecida es un problema de formato de portapapeles

Una vez que el informe se veía bien, la gente quería pegarlo — en un documento, un correo — y conservar el formato y las capturas de pantalla. El instinto es serializar el informe a una cadena de markdown y esperar que el destino la renderice. Normalmente no lo hace.

La verdadera respuesta es que el portapapeles guarda varias representaciones a la vez. Así que escribimos dos: una versión HTML con el formato intacto y las capturas incrustadas como imágenes en línea, y una alternativa en texto plano para destinos que no aceptan HTML. Pega en un editor enriquecido y obtienes el informe con formato y con imágenes; pega en una caja de texto plano y obtienes texto limpio. La "copia enriquecida" nunca fue un problema de serialización — fue un problema de elegir-el-formato-de-portapapeles-correcto.

El mismo lanzamiento también dio al informe etiquetas al estilo del chat Yo / Ellos con los dos hablantes alineados a lados opuestos, para que una transcripción se lea como la conversación que fue, y movió tus propias capturas de pantalla a su propio lado para que coincidieran. (Un lanzamiento dentro de esta ventana, v1.6.14, fue una recompilación pura — no hay historia ahí, y es honesto decirlo.)

Tres cosas que nos enseñó la interfaz en streaming

  1. Almacena en búfer los campos en streaming, publica una vez por fragmento. Dejar que cada server-sent event impulse su propia actualización de estado es el parpadeo. Un acumulador con ref más una única publicación por fragmento, en un árbol que nunca se desmonta, convierte el ensamblaje nervioso en un rellenado calmado.
  2. Cualquier cosa que se transmita necesita un renderizador consciente del streaming. Un renderizador que reanaliza toda la cadena por token parpadeará con markdown a medio terminar. Usa uno construido para entradas incompletas — y si ya tienes uno para otra superficie, reutilízalo antes de construir un segundo.
  3. La copia enriquecida trata de formatos de portapapeles, no de serialización. Escribe text/html con imágenes incrustadas y una alternativa text/plain. El portapapeles fue diseñado para llevar ambos; úsalo.

Este es el quinto capítulo de la historia de fiabilidad y pulido que se convierte en GeekBye v2. Para el capítulo anterior, mira el bug de los auriculares (v1.6.12); para el punto donde la misma idea de los server-sent events se convirtió después en todo un transporte de reserva, transcripción en vivo cuando el firewall bloquea WebSockets (v2.0.8); y para todo el arco, la anatomía de publicar software hasta la perfección.

Artículos Relacionados

El silencio era estructural
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
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
Distinguir una llamada de una app abierta
Steven
Steven10 min de lectura

Distinguir una llamada de una app abierta

GeekBye puede notar que te has unido a una reunión por video y ofrecerte grabarla. La detección resulta ser la mitad fácil — un binario Swift que lee títulos de ventana cada diez segundos. La mitad difícil es la precisión: no dispararse cuando Zoom está meramente abierto, no ofrecerte grabar una reunión que ya estás grabando, y no silenciar el micrófono en la llamada en la que realmente estás. Tres releases, y cada una es una salvaguarda que tuvo que aprender a no derrotarse a sí misma.

Ingeniería
macOS
Desktop