Steven
Steven5 min czytania

Jak transmitować raport na żywo bez migotania

Gdy kończysz spotkanie, podsumowanie GeekBye teraz wypełnia się na żywo, zamiast zmuszać cię do wpatrywania się w kręcący się wskaźnik. Sprawienie, by interfejs strumieniowy sprawiał wrażenie spokojnego, a nie nerwowego, wymagało rozwiązania migotania dwukrotnie — raz dla pól strukturalnych, raz dla markdown — a drugą naprawą był renderer, który już mieliśmy.

Inżynieria
Frontend
Streaming
Wydania GeekBye
Jak transmitować raport na żywo bez migotania

Przed tym wydaniem zakończenie spotkania w GeekBye oznaczało wpatrywanie się w kręcący się wskaźnik. Aplikacja wysyłała całą twoją transkrypcję do analizy, czekała na całe podsumowanie — ocenę, kluczowe punkty, zadania do wykonania, wszystko — a potem zrzucała to na ekran naraz. Działało, ale sprawiało wrażenie powolnego, bo wpatrywałeś się w nic, gdy pracowało.

GeekBye v1.6.13–v1.6.15 zmienił to w raport, który wpływa na żywo, pole po polu, w miarę jak jest generowany. Ciekawa część to nie strumieniowanie — to wszystko, co musieliśmy zrobić, by strumieniowanie nie wyglądało nerwowo. Spokojne strumieniowanie i toporne strumieniowanie to ta sama funkcja o bardzo różnym wykonaniu.

Migotanie, część pierwsza: nie pozwól, by każdy kawałek szarpał Reactem

Podsumowanie to nie jeden blok — to pola strukturalne (ogólna ocena, kluczowe punkty, zadania do wykonania i tak dalej), które backend emituje pojedynczo przez server-sent events. Naiwny sposób renderowania tego to aktualizacja stanu Reacta przy każdym zdarzeniu, gdy nadchodzi.

Zrób tak, a dostaniesz migotanie. Każde nadchodzące pole wyzwala własny ponowny render; komponenty pokazujące „puste" montują się, potem odmontowują, potem znów montują jako „wypełnione"; układ drga przy każdym pakiecie. Raport składa się sam na twoich oczach w najgorszy możliwy sposób — widocznie, nerwowo.

Naprawa to dwuczęściowa dyscyplina. Po pierwsze, akumuluj nadchodzące pola w ref — mutowalnym uchwycie, który nie wyzwala renderów — i publikuj do stanu Reacta raz na kawałek ze świeżą kopią, tak by drzewo aktualizowało się z rozmysłem, a nie przy każdym mikro-zdarzeniu. Po drugie, renderuj jedno stabilne drzewo komponentów przez cały czas, czy to podczas strumieniowania, czy po zakończeniu, z placeholderami w miejscu dla pól, które jeszcze nie dotarły:

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

Komponent pokazujący ocenę nigdy się nie odmontowuje — po prostu pokazuje , dopóki liczba nie wyląduje, a potem liczbę. Nic nie drga. A w pełni złożony obiekt na końcu i tak trafia do lokalnej bazy danych, więc strumieniowany interfejs i trwałe przechowywanie współistnieją bez walki.

Migotanie, część druga: renderer, który już mieliśmy

Drugie migotanie jest subtelniejsze i żyje w samym markdown. Raport renderuje bogaty markdown — nagłówki, pogrubienia, listy, bloki kodu. Ale markdown nadchodzący w trakcie strumienia jest, w każdej danej chwili, niedokończony. Lista z jednym punktem jak dotąd. Blok kodu, który został otwarty, ale nie zamknięty. Znacznik pogrubienia bez jeszcze pary.

Konwencjonalny renderer markdown re-parsuje cały ciąg przy każdym tokenie, a ten niedokończony stan parsuje się za każdym razem na coś innego — więc lista miga, blok kodu błyska otwarciem i zamknięciem, układ skacze, gdy tokeny się kończą. To to samo nerwowe składanie co wcześniej, jedną warstwę niżej.

Naprawa była niemal krępująco dostępna: już używaliśmy renderera markdown świadomego strumieniowania do czatu AI na żywo. Renderera zbudowanego, by tolerować niedokończone tokeny — by renderować niedokończony markdown stabilnie i osadzić go dopiero, gdy tokeny się dopełnią — zamiast re-parsować od zera za każdym razem. Raport potrzebował po prostu użyć tego samego. Rozwiązaliśmy dokładnie ten problem dla czatu miesiące wcześniej; „naprawą" dla raportu było uświadomienie sobie, że już mamy narzędzie, i skierowanie go na drugą powierzchnię. Ponowne użycie pobiło przebudowę.

Bonus: kopiowanie sformatowane to problem formatu schowka

Gdy raport już dobrze wyglądał, ludzie chcieli go wkleić — do dokumentu, do maila — i zachować formatowanie oraz zrzuty ekranu. Instynkt to zserializować raport do ciągu markdown i mieć nadzieję, że cel go wyrenderuje. Zwykle tego nie robi.

Prawdziwa odpowiedź jest taka, że schowek trzyma wiele reprezentacji naraz. Więc zapisujemy dwie: wersję HTML z nietkniętym formatowaniem i zrzutami ekranu osadzonymi jako obrazy inline oraz tekstowy fallback dla celów, które nie przyjmują HTML. Wklej do edytora sformatowanego, a dostaniesz sformatowany raport z obrazkami; wklej do zwykłego pola tekstowego, a dostaniesz czysty tekst. „Kopiowanie sformatowane" nigdy nie było problemem serializacji — to był problem wybierz-właściwy-format-schowka.

To samo wydanie dało też raportowi etykiety Ja / Oni w stylu czatu z dwoma mówcami wyrównanymi do przeciwnych stron, tak by transkrypcja czytała się jak rozmowa, którą była, i przeniosło twoje własne zrzuty ekranu na ich własną stronę, by pasowały. (Jedno wydanie w tym oknie, v1.6.14, było czystą przebudową — żadnej historii, i uczciwie jest to powiedzieć.)

Trzy rzeczy, których nauczył nas strumieniowany interfejs

  1. Buforuj strumieniowane pola, publikuj raz na kawałek. Pozwolić każdemu server-sent event napędzać własną aktualizację stanu to właśnie migotanie. Akumulator w ref plus pojedyncza publikacja na kawałek, do drzewa, które nigdy się nie odmontowuje, zmienia nerwowe składanie w spokojne wypełnianie.
  2. Cokolwiek się strumieniuje, potrzebuje renderera świadomego strumieniowania. Renderer re-parsujący cały ciąg przy każdym tokenie będzie migać na niedokończonym markdown. Użyj takiego zbudowanego dla niekompletnego wejścia — a jeśli masz już jeden do innej powierzchni, użyj go ponownie, zanim zbudujesz drugi.
  3. Kopiowanie sformatowane dotyczy formatów schowka, nie serializacji. Zapisz text/html z osadzonymi obrazami oraz fallback text/plain. Schowek zaprojektowano, by nieść oba; korzystaj z tego.

To piąty rozdział opowieści o niezawodności i dopracowaniu, która staje się GeekBye v2. Po poprzedni rozdział zobacz błąd słuchawek dousznych (v1.6.12); po miejsce, gdzie ten sam pomysł server-sent events stał się później całym transportem awaryjnym, transkrypcja na żywo, gdy firewall blokuje WebSockety (v2.0.8); a po cały łuk, anatomię dostarczania oprogramowania do perfekcji.

Powiązane artykuły

Cisza była nośna
Steven
Steven7 min czytania

Cisza była nośna

Dwa ostatnie wydania GeekBye v1 mówią o tej samej niewygodnej prawdzie: transkrypcja w czasie rzeczywistym przez prawdziwą sieć nie jest bezstratna, a uczciwym ruchem jest przestać udawać, że jest. v1.8.20 zachowywało kopię każdego fragmentu dźwięku na dysku, zanim porzuciło go podczas ponownego połączenia, i zaczęło głośno oznaczać luki w transkrypcji. v1.9.0 przestało wysyłać ciszę, by oszczędzić pasmo — i odkryło, że cisza była dokładnie tym sygnałem, po którym transkryber poznawał, że zdanie się skończyło. Dwa wydania o koszcie wyrzucania rzeczy.

Inżynieria
Audio
Niezawodność
Trzy czasowniki, które utrzymują Web Audio przy życiu
Steven
Steven8 min czytania

Trzy czasowniki, które utrzymują Web Audio przy życiu

Dwa wydania punktowe GeekBye, dwa miesiące od siebie i w dwóch różnych plikach, nauczyły nasz kod audio tej samej lekcji z przeciwnych stron: przestań traktować przeglądarkowy AudioContext jak obiekt jednorazowy. Jedno wydanie nauczyło się resume() kontekstu, który macOS po cichu zawiesił w środku nagrywania; drugie nauczyło się suspend() zamiast close(), żeby kolejne po sobie sesje przestały uderzać w narzucony przez Chromium pułap mniej więcej sześciu kontekstów. Resume, suspend, close — oto cała fabuła.

Inżynieria
Audio
Desktop
Odróżnić rozmowę od otwartej aplikacji
Steven
Steven8 min czytania

Odróżnić rozmowę od otwartej aplikacji

GeekBye potrafi zauważyć, że dołączyłeś do spotkania wideo, i zaproponować jego nagranie. Wykrywanie okazuje się łatwiejszą połową — natywny plik Swift czytający tytuły okien co dziesięć sekund. Trudną połową jest precyzja: nie odpalać, gdy Zoom jest tylko otwarty, nie proponować nagrywania spotkania, które już nagrywasz, i nie wyciszać mikrofonu w rozmowie, w której faktycznie jesteś. Trzy wydania, a każde z nich to zabezpieczenie, które musiało nauczyć się nie pokonywać samego siebie.

Inżynieria
macOS
Desktop