Steven
Steven6 min de lectura

Com transmetre un informe en directe sense el parpelleig

Quan s'acaba la teva reunió, el resum de GeekBye ara s'omple en directe en lloc de fer-te mirar un indicador de càrrega. Aconseguir que la interfície de streaming se sentís tranquil·la en lloc de nerviosa va requerir resoldre el parpelleig dues vegades — un cop per als camps estructurats, un cop per al markdown — i la segona solució va ser un renderitzador que ja teníem.

Enginyeria
Frontend
Streaming
GeekBye Releases
Com transmetre un informe en directe sense el parpelleig

Abans d'aquesta versió, acabar una reunió de GeekBye volia dir mirar un indicador de càrrega. L'aplicació enviava tot el teu transcript per analitzar-lo, esperava el resum complet — puntuació, punts clau, tasques a fer, tot — i llavors ho deixava caure a la pantalla de cop. Funcional, però feia la sensació de ser lent, perquè estaves mirant el no-res mentre treballava.

GeekBye v1.6.13–v1.6.15 va convertir això en un informe que arriba en directe, camp a camp, a mesura que es genera. La part interessant no és el streaming — és tot el que vam haver de fer perquè el streaming no semblés nerviós. El streaming tranquil i el streaming irregular són la mateixa funcionalitat amb una execució molt diferent.

Parpelleig, part u: no deixis que cada fragment sacsegi React

El resum no és una sola massa — són camps estructurats (una puntuació global, punts clau, tasques a fer, etc.) que el backend emet un a un mitjançant server-sent events. La manera ingènua de renderitzar-ho és actualitzar l'estat de React a cada esdeveniment tal com arriba.

Fes això, i tindràs parpelleig. Cada camp que arriba desencadena el seu propi re-render; els components que mostraven "buit" es munten, després es desmunten, i es tornen a muntar com a "carregats"; el disseny es sacseja a cada paquet. L'informe es munta a si mateix davant teu de la pitjor manera possible — visiblement, nerviosament.

La solució és una disciplina de dues parts. Primer, acumula els camps entrants en una ref — un contenidor mutable que no desencadena renders — i publica a l'estat de React un cop per fragment amb una còpia nova, de manera que l'arbre s'actualitzi de manera deliberada en lloc de a cada micro-esdeveniment. Segon, renderitza un sol arbre de components estable tota l'estona, tant si s'està transmetent com si ha acabat, amb marcadors de posició en línia per als camps que encara no han arribat:

// 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 component que mostra la puntuació no es desmunta mai — simplement mostra fins que arriba el número, i llavors el número. Res es sacseja. I l'objecte totalment muntat encara es desa a la memòria cau de la base de dades local al final, de manera que la interfície de streaming i l'emmagatzematge durador conviuen sense barallar-se.

Parpelleig, part dos: el renderitzador que ja teníem

El segon parpelleig és més subtil i viu en el mateix markdown. L'informe renderitza markdown ric — capçaleres, negreta, llistes, blocs de codi. Però el markdown que arriba a mig stream està, en qualsevol instant donat, a mig acabar. Una llista amb un sol punt fins ara. Un bloc de codi que s'ha obert però no s'ha tancat. Un marcador de negreta encara sense parella.

Un renderitzador de markdown convencional torna a analitzar tota la cadena a cada token, i aquest estat a mig acabar s'analitza com una cosa diferent cada vegada — així que la llista parpelleja, el bloc de codi s'obre i es tanca de cop, el disseny salta a mesura que els tokens es completen. És el mateix muntatge nerviós d'abans, una capa més avall.

La solució era gairebé vergonyosament a l'abast: ja fèiem servir un renderitzador de markdown conscient del streaming per al xat d'IA en directe. Un renderitzador construït per tolerar tokens incomplets — per renderitzar markdown a mig acabar de manera estable i assentar-lo només quan els tokens es completen — en lloc de tornar a analitzar des de zero cada vegada. L'informe només necessitava fer servir el mateix. Havíem resolt aquest problema exacte per al xat mesos abans; la "solució" per a l'informe va ser adonar-nos que ja teníem l'eina i apuntar-la a una segona superfície. Reutilitzar va guanyar a reconstruir.

L'extra: la còpia rica és un problema de format del porta-retalls

Un cop l'informe tenia bon aspecte, la gent volia enganxar-lo — en un document, un correu — i conservar el format i les captures de pantalla. L'instint és serialitzar l'informe a una cadena de markdown i esperar que la destinació el renderitzi. Normalment no ho fa.

La resposta real és que el porta-retalls conté múltiples representacions alhora. Així que n'escrivim dues: una versió HTML amb el format intacte i les captures de pantalla incrustades com a imatges en línia, i una alternativa de text pla per a les destinacions que no accepten HTML. Enganxa en un editor ric i obtens l'informe amb format i imatges; enganxa en una casella de text pla i obtens text net. La "còpia rica" mai va ser un problema de serialització — era un problema de tria-el-format-correcte-del-porta-retalls.

La mateixa versió també va donar a l'informe etiquetes d'estil xat Me / Them amb els dos parlants alineats a costats oposats, de manera que un transcript es llegeix com la conversa que era, i va moure les teves pròpies captures de pantalla al seu propi costat perquè quadressin. (Una versió d'aquesta finestra, la v1.6.14, va ser una reconstrucció pura — cap història allà, i és honest dir-ho.)

Tres coses que la interfície de streaming ens va ensenyar

  1. Fes buffer dels camps transmesos, publica un cop per fragment. Deixar que cada server-sent event condueixi la seva pròpia actualització d'estat és el parpelleig. Un acumulador ref més una sola publicació per fragment, en un arbre que no es desmunta mai, converteix el muntatge nerviós en un ompliment tranquil.
  2. Qualsevol cosa que es transmeti necessita un renderitzador conscient del streaming. Un renderitzador que torna a analitzar tota la cadena per cada token parpellejarà amb markdown a mig acabar. Fes servir un construït per a entrada incompleta — i si ja en tens un per a una altra superfície, reutilitza'l abans de construir-ne un segon.
  3. La còpia rica tracta de formats del porta-retalls, no de serialització. Escriu text/html amb imatges incrustades i una alternativa text/plain. El porta-retalls es va dissenyar per portar tots dos; fes-lo servir.

Aquest és el cinquè capítol de la història de fiabilitat-i-poliment que esdevé GeekBye v2. Per al capítol anterior, mira el bug dels auriculars (v1.6.12); per on la mateixa idea de server-sent events més tard va esdevenir tot un transport alternatiu, transcripció en directe quan el tallafocs bloqueja els WebSockets (v2.0.8); i per a tot l'arc, l'anatomia de llançar programari fins a la perfecció.

Articles Relacionats

Una connexió perduda no hauria de fer petar tota l'aplicació — però la nostra ho feia
Steven
Steven7 min de lectura

Una connexió perduda no hauria de fer petar tota l'aplicació — però la nostra ho feia

Quan el nostre backend es va desconnectar en plena reunió, no només va aturar la transcripció: va fer petar tota l'aplicació. La causa era un únic esdeveniment sense gestionar, i la solució eren deu línies. Aquest és el grup de versions que va fer que GeekBye es mantingués amb la sessió iniciada i connectat a través de les coses que abans el mataven.

Enginyeria
Fiabilitat
Electron
La teva app de Mac obté accés al micròfon — i l'oblida a cada arrencada
Steven
Steven6 min de lectura

La teva app de Mac obté accés al micròfon — i l'oblida a cada arrencada

GeekBye va demanar permís per al micròfon, li'l vas donar i funcionava. A la següent arrencada: desaparegut. I l'app no apareixia enlloc a Configuració del Sistema → Micròfon. El culpable era una funció de seguretat de macOS que executava l'app en silenci des d'una ruta que desapareix — aquí tens el diagnòstic i la solució d'un sol avís.

macOS
Permisos
Enginyeria
Vam esborrar 5.000 línies de codi d'àudio — i les transcripcions van començar a aparèixer duplicades
Steven
Steven7 min de lectura

Vam esborrar 5.000 línies de codi d'àudio — i les transcripcions van començar a aparèixer duplicades

GeekBye v1.6 va arrencar dos transcriptors Swift al dispositiu i els va substituir per una única canonada unificada — una eliminació neta de més de 5.000 línies, feta mentre la gent era enmig d'una reunió a l'aplicació. Llavors les transcripcions van començar a aparèixer dues vegades, perquè l'error va migrar a l'única capa que encara pensava que hi havia dos motors.

Enginyeria
Transcripció
Arquitectura