Steven
Steven1 分で読める

ライブレポートをちらつきなしでストリーミングする方法

会議が終わると、GeekByeのサマリーはスピナーを見つめさせる代わりに、いまやライブで埋まっていきます。ストリーミングUIをそわそわではなく落ち着いた感触にするには、ちらつきを二度解決する必要がありました — 一度は構造化フィールドのために、もう一度はmarkdownのために。そして二番目の修正は、すでに自分たちが持っていたレンダラーでした。

エンジニアリング
フロントエンド
ストリーミング
GeekByeリリース
ライブレポートをちらつきなしでストリーミングする方法

このリリース以前、GeekByeの会議を終えるということはスピナーを見つめることを意味しました。アプリは文字起こし全体を分析へ送り、サマリーのすべて — スコア、キーポイント、アクションアイテム、その一切 — を待ち、それから一度に画面へ落とすのです。機能はしますが、遅く感じました。動いている間、あなたは何もない画面を見つめていたからです。

GeekBye v1.6.13〜v1.6.15は、それを生成されるそばからフィールドごとにライブで流れ込むレポートへと変えました。おもしろいのはストリーミングそのものではありません — ストリーミングがそわそわして見えないようにするために私たちがやらねばならなかったすべてです。落ち着いたストリーミングとぎくしゃくしたストリーミングは、実装がまるで違うだけで同じ機能なのです。

ちらつき、その一: すべてのチャンクにReactを揺さぶらせない

サマリーは一つの塊ではありません — バックエンドがserver-sent eventsで一つずつ送り出す構造化フィールド(全体スコア、キーポイント、アクションアイテムなど)です。それを描画する素朴なやり方は、イベントが届くたびにReactのstateを更新することです。

そうすると、ちらつきが出ます。各フィールドの到着はそれぞれの再レンダリングを引き起こし、「空」を表示していたコンポーネントがマウントされ、アンマウントされ、そして「読み込み済み」として再マウントされる。レイアウトはパケットごとにびくつきます。レポートは最悪のやり方で目の前で自らを組み立てていく — 目に見えて、神経質に。

修正は二部構成の規律です。第一に、届いてくるフィールドをref — レンダリングを引き起こさない可変のホルダー — に蓄積し、チャンクごとに一度だけ新しいコピーでReactのstateへ公開する。そうすればツリーはあらゆる微小イベントごとにではなく、意図的に更新されます。第二に、ストリーミング中でも完了後でも、まだ届いていないフィールドにはインラインのプレースホルダーを添えて、終始一つの安定したコンポーネントツリーを描画するのです:

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

スコアを表示するコンポーネントは決してアンマウントされません — 数字が届くまでただ を表示し、それから数字を表示します。何もびくつきません。そして完全に組み上がったオブジェクトは最後にローカルデータベースへキャッシュされるので、ストリーミングUIと永続的なストレージはぶつかることなく共存します。

ちらつき、その二: すでに自分たちが持っていたレンダラー

二番目のちらつきはもっと微妙で、markdownそのものの中に潜んでいます。レポートはリッチなmarkdown — 見出し、太字、リスト、コードブロック — を描画します。しかしストリームの途中で届くmarkdownは、任意の瞬間においては中途半端です。今のところ箇条書きが一つだけのリスト。開かれたが閉じられていないコードフェンス。まだ相方のいない太字マーカー。

従来のmarkdownレンダラーはトークンごとに文字列全体を再パースし、その中途半端な状態は毎回違うものへパースされます — だからリストはちらつき、コードブロックは開いたり閉じたりを繰り返し、トークンが完成するにつれてレイアウトが跳ねます。一つ下の層で起きる、先ほどと同じ神経質な組み立てです。

修正はほとんど恥ずかしいほど手近にありました: 私たちはすでにライブAIチャットのためにストリーミング対応のmarkdownレンダラーを使っていたのです。不完全なトークンに耐えるよう作られたレンダラー — 毎回ゼロから再パースするのではなく、中途半端なmarkdownを安定して描画し、トークンが完成したときにだけ落ち着かせる。レポートは同じものを使えばよいだけでした。私たちはこのまさに同じ問題を数か月前にチャットで解決していたのです。レポートの「修正」とは、すでにその道具を持っていると気づき、それを二つ目の面に向けることでした。作り直しより再利用が勝ったのです。

おまけ: リッチコピーはクリップボード形式の問題

レポートが見栄えよくなると、人々はそれを貼り付けたがりました — ドキュメントへ、メールへ — そして書式とスクリーンショットを保ったままで。とっさの発想は、レポートをmarkdown文字列にシリアライズして、貼り付け先がそれを描画してくれると願うことです。たいてい描画してくれません。

本当の答えは、クリップボードは複数の表現を同時に保持できるということです。そこで私たちは二つ書き込みます: 書式をそのまま保ち、スクリーンショットをインライン画像として埋め込んだHTML版と、HTMLを受け付けない貼り付け先のためのプレーンテキストのフォールバックです。リッチエディタに貼れば書式付きのレポートが画像付きで得られ、プレーンテキストのボックスに貼ればきれいなテキストが得られます。「リッチコピー」はけっしてシリアライズの問題ではなかったのです — それは正しいクリップボード形式を選ぶ問題だったのです。

同じリリースはまた、レポートにチャット風のMe / Themラベルを与え、二人の話者を左右反対の側へ揃えました。おかげで文字起こしは、それが本来そうであった会話のように読めます。そしてあなた自身のスクリーンショットも、それに合わせて自分の側へ移しました。(この期間のうち一つのリリース、v1.6.14は純粋な再ビルドでした — 語るべき物語はなく、そう正直に言っておきます。)

ストリーミングUIが教えた三つのこと

  1. ストリーミングされるフィールドはバッファし、チャンクごとに一度だけ公開せよ。 あらゆるserver-sent eventにそれぞれのstate更新を駆動させることこそがちらつきだ。refのアキュムレータに、チャンクごとの単一の公開を加え、決してアンマウントしないツリーへ流し込むことが、神経質な組み立てを落ち着いた埋め込みへ変える。
  2. ストリーミングするものはすべて、ストリーミング対応のレンダラーを必要とする。 トークンごとに文字列全体を再パースするレンダラーは、中途半端なmarkdownでちらつく。不完全な入力のために作られたものを使え — そしてもし別の面のためにすでに持っているなら、二つ目を作る前に再利用せよ。
  3. リッチコピーはシリアライズではなくクリップボード形式の話だ。 画像を埋め込んだ text/html text/plain のフォールバックを書き込め。クリップボードは両方を運ぶよう設計されている。それを使え。

これは、GeekBye v2になる信頼性と磨き上げの物語の第五章です。前の章はイヤホンのバグ(v1.6.12)を。同じserver-sent-eventsのアイデアが後にまるごとのフォールバック伝送路になった話は、ファイアウォールがWebSocketをブロックするときのライブ文字起こし(v2.0.8)を。そして全体の弧については、ソフトウェアを完璧に出荷することの解剖学をご覧ください。

関連記事

接続が切れただけでアプリ全体が落ちてはいけない — でも私たちのは落ちた
Steven
Steven1 分で読める

接続が切れただけでアプリ全体が落ちてはいけない — でも私たちのは落ちた

バックエンドが会議の途中でオフラインになったとき、それは文字起こしを止めただけではありませんでした — アプリ全体をクラッシュさせたのです。原因はたった一つの処理されないイベント、修正は十行でした。これは、GeekByeがかつて自分を殺していた事態を通り抜けても、サインインを保ち、接続を保つようにしたリリース群の物語です。

エンジニアリング
信頼性
Electron
ソフトウェアを完璧に届けるための解剖学:テストが見逃したものをコードレビューがどう捉えたか
Steven
Steven1 分で読める

ソフトウェアを完璧に届けるための解剖学:テストが見逃したものをコードレビューがどう捉えたか

GeekBye v2シリーズを通して、同じことが何度も起きています。開発者のマシンではすべてのテストに合格した修正が、コードレビューによって「ほぼ全員にとって失敗していたはずだ」と証明されるのです。これは9つのリリースの背後にあるワークフロー——レビューというゲート、修正より先に不具合を捉える仕組み、そして「自分の環境では動く」を「ちゃんと動く」に変える、出荷前テストの規律の物語です。

エンジニアリング
プロセス
コードレビュー
あなたのMacアプリはマイクへのアクセスを許可する — そして起動するたびに、それを忘れる
Steven
Steven1 分で読める

あなたのMacアプリはマイクへのアクセスを許可する — そして起動するたびに、それを忘れる

GeekByeがマイクの許可を求め、あなたは許可し、動きました。次に起動すると、消えている。しかもアプリは、システム設定 → マイク にそもそも一度も現れなかった。犯人は、アプリを「消えてしまうパス」から静かに実行していたmacOSのセキュリティ機能でした — その診断と、たった一つのプロンプトで済む修正をここに記します。

macOS
権限
エンジニアリング