Steven
Steven7 phút đọc

Cách phát trực tiếp một báo cáo mà không bị nhấp nháy

Khi cuộc họp của bạn kết thúc, bản tóm tắt của GeekBye giờ đây tự điền vào theo thời gian thực thay vì bắt bạn ngồi nhìn một vòng xoay tải. Để giao diện phát trực tiếp trở nên êm ả thay vì giật cục, chúng tôi phải giải quyết nhấp nháy hai lần — một lần cho các trường có cấu trúc, một lần cho markdown — và cách sửa lần thứ hai là một bộ render mà chúng tôi đã sở hữu sẵn.

Kỹ thuật
Frontend
Phát trực tiếp
Bản phát hành GeekBye
Cách phát trực tiếp một báo cáo mà không bị nhấp nháy

Trước bản phát hành này, kết thúc một cuộc họp GeekBye nghĩa là ngồi nhìn một vòng xoay tải. Ứng dụng sẽ gửi toàn bộ bản ghi lời nói của bạn đi phân tích, chờ toàn bộ bản tóm tắt — điểm số, điểm chính, các mục hành động, tất tần tật — rồi đổ hết lên màn hình một lượt. Có chạy được, nhưng cảm giác chậm, bởi bạn chỉ ngồi nhìn khoảng trống trong lúc nó làm việc.

GeekBye v1.6.13–v1.6.15 đã biến điều đó thành một báo cáo chảy vào trực tiếp, theo từng trường, ngay khi nó đang được tạo ra. Phần thú vị không phải là bản thân việc streaming — mà là tất cả những gì chúng tôi phải làm để việc streaming không trông giật cục. Streaming êm ả và streaming lục cục là cùng một tính năng, chỉ khác nhau ở cách thực thi mà thôi.

Nhấp nháy, phần một: đừng để mỗi chunk giằng xé React

Bản tóm tắt không phải một khối duy nhất — nó là các trường có cấu trúc (điểm tổng thể, điểm chính, mục hành động, v.v.) mà backend phát ra từng cái một qua server-sent events. Cách render ngây thơ là cập nhật React state mỗi khi một sự kiện đến.

Làm vậy, bạn sẽ bị nhấp nháy. Mỗi trường đến đều kích hoạt một lần re-render riêng của nó; các component đang hiển thị "rỗng" được mount, rồi unmount, rồi remount thành "đã tải"; bố cục giật lên theo từng gói tin. Báo cáo tự lắp ráp trước mắt bạn theo cách tệ nhất có thể — thấy rõ, và bồn chồn.

Cách sửa là một kỷ luật gồm hai phần. Thứ nhất, tích lũy các trường đến trong một ref — một vật chứa có thể thay đổi mà không kích hoạt render — và công bố sang React state một lần mỗi chunk với một bản sao mới, để cây được cập nhật một cách có chủ đích thay vì theo từng vi-sự-kiện. Thứ hai, render một cây component ổn định duy nhất suốt cả quá trình, dù đang streaming hay đã xong, với các placeholder inline cho những trường chưa đến:

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

Component hiển thị điểm số không bao giờ bị unmount — nó chỉ hiển thị cho đến khi con số đáp xuống, rồi hiển thị con số. Không có gì giật cả. Và đối tượng đã lắp ráp hoàn chỉnh vẫn được cache vào cơ sở dữ liệu cục bộ ở phần cuối, nên giao diện streaming và lưu trữ bền vững cùng tồn tại mà không giành nhau.

Nhấp nháy, phần hai: bộ render chúng tôi đã sở hữu sẵn

Loại nhấp nháy thứ hai tinh vi hơn và nằm trong chính markdown. Báo cáo render markdown phong phú — tiêu đề, chữ đậm, danh sách, khối code. Nhưng markdown đến giữa chừng luồng thì, tại bất kỳ khoảnh khắc nào, đều còn dang dở. Một danh sách mới có một gạch đầu dòng cho tới lúc này. Một code fence đã mở nhưng chưa đóng. Một dấu chữ đậm chưa có bạn đồng hành.

Một bộ render markdown thông thường phân tích lại toàn bộ chuỗi trên mỗi token, và trạng thái dang dở đó lần nào cũng phân tích ra thành thứ khác nhau — nên danh sách nhấp nháy, khối code chớp mở rồi đóng, bố cục nhảy lên khi các token hoàn tất. Vẫn là kiểu lắp ráp bồn chồn như trước, thấp xuống một tầng.

Cách sửa gần như hiển nhiên đến mức ngượng: chúng tôi vốn đã dùng một bộ render markdown nhận biết streaming cho chat AI trực tiếp. Một bộ render được xây để chịu được các token chưa hoàn chỉnh — render markdown còn dang dở một cách ổn định và chỉ để nó an vị khi các token hoàn tất — thay vì phân tích lại từ đầu mỗi lần. Báo cáo chỉ cần dùng đúng cái đó. Chúng tôi đã giải quyết chính bài toán này cho chat từ mấy tháng trước; "cách sửa" cho báo cáo là nhận ra chúng tôi đã có sẵn công cụ và chĩa nó vào một bề mặt thứ hai. Tái sử dụng thắng xây lại.

Phần thưởng: sao chép có định dạng là một bài toán về định dạng clipboard

Một khi báo cáo trông đẹp, người ta muốn dán nó — vào một tài liệu, một email — và giữ nguyên định dạng lẫn ảnh chụp màn hình. Bản năng là tuần tự hóa báo cáo thành một chuỗi markdown rồi hy vọng nơi đến sẽ render nó ra. Thường thì nó không.

Câu trả lời thực sự là clipboard giữ nhiều biểu diễn cùng một lúc. Nên chúng tôi ghi hai bản: một bản HTML với định dạng còn nguyên và ảnh chụp màn hình được nhúng thành ảnh inline, và một bản văn bản thuần dự phòng cho những nơi đến không nhận HTML. Dán vào một trình soạn thảo có định dạng và bạn nhận được báo cáo có định dạng kèm hình ảnh; dán vào một ô văn bản thuần và bạn nhận được văn bản sạch sẽ. "Sao chép có định dạng" chưa bao giờ là một bài toán tuần tự hóa — nó là một bài toán chọn đúng định dạng clipboard.

Cùng bản phát hành đó cũng cho báo cáo các nhãn kiểu chat Me / Them với hai người nói được căn về hai phía đối diện, để một bản ghi lời nói đọc lên như đúng cuộc trò chuyện mà nó vốn là, và dời ảnh chụp màn hình của chính bạn sang phía của nó cho khớp. (Một bản phát hành trong quãng này, v1.6.14, là một bản rebuild thuần túy — không có câu chuyện nào ở đó, và nói thẳng ra như vậy là trung thực.)

Ba điều mà giao diện streaming đã dạy chúng tôi

  1. Đệm các trường được stream, công bố một lần mỗi chunk. Để mỗi server-sent event tự lái một lần cập nhật state riêng chính là nhấp nháy. Một bộ tích lũy ref cộng với một lần công bố duy nhất mỗi chunk, vào một cây không bao giờ unmount, biến sự lắp ráp bồn chồn thành sự điền vào êm ả.
  2. Bất cứ thứ gì stream đều cần một bộ render nhận biết streaming. Một bộ render phân tích lại toàn bộ chuỗi trên mỗi token sẽ nhấp nháy với markdown còn dang dở. Hãy dùng một bộ được xây cho đầu vào chưa hoàn chỉnh — và nếu bạn đã có một cái cho một bề mặt khác, hãy tái sử dụng nó trước khi xây cái thứ hai.
  3. Sao chép có định dạng là về định dạng clipboard, không phải tuần tự hóa. Hãy ghi text/html có nhúng ảnh một bản dự phòng text/plain. Clipboard được thiết kế để mang cả hai; hãy dùng nó.

Đây là chương thứ năm của câu chuyện về độ tin cậy và sự trau chuốt sẽ trở thành GeekBye v2. Chương trước, xem lỗi tai nghe (v1.6.12); còn nơi chính ý tưởng server-sent-events về sau trở thành cả một kênh truyền dự phòng, xem phiên âm trực tiếp khi tường lửa chặn WebSocket (v2.0.8); và cho toàn bộ vòng cung, xem giải phẫu việc xuất xưởng phần mềm đến độ hoàn hảo.

Bài Viết Liên Quan

Sự Im Lặng Vốn Chịu Tải
Steven
Steven10 phút đọc

Sự Im Lặng Vốn Chịu Tải

Hai bản phát hành cuối cùng của GeekBye v1 đều xoay quanh cùng một sự thật khó chịu: phiên âm thời gian thực trên một mạng thật không phải là không mất mát, và nước đi trung thực là thôi giả vờ rằng nó như vậy. v1.8.20 giữ một bản sao của mỗi audio chunk trên đĩa trước khi bỏ nó trong một lần kết nối lại, và bắt đầu đánh dấu các khoảng trống trong transcript ra thành tiếng. v1.9.0 thôi gửi im lặng để tiết kiệm bandwidth — rồi phát hiện ra im lặng chính là tín hiệu chính xác mà bộ phiên âm dùng để biết một câu đã kết thúc. Hai bản phát hành về cái giá của việc vứt đồ đi.

Kỹ thuật
Audio
Độ tin cậy
Ba Động Từ Giữ Cho Web Audio Sống
Steven
Steven11 phút đọc

Ba Động Từ Giữ Cho Web Audio Sống

Hai bản point release của GeekBye, cách nhau hai tháng và nằm trong hai file khác nhau, đã dạy code âm thanh của chúng tôi cùng một bài học từ hai đầu đối nghịch: thôi coi AudioContext của browser như một thứ dùng-rồi-bỏ. Một bản học được cách resume() một context mà macOS đã lặng lẽ suspend giữa lúc ghi; bản kia học được cách suspend() thay vì close() để những session nối tiếp nhau thôi đâm sầm vào cái trần khoảng-sáu-context của Chromium. resume, suspend, close — đó là toàn bộ cốt truyện.

Kỹ thuật
Audio
Desktop
Phân Biệt Một Cuộc Gọi Với Một App Đang Mở
Steven
Steven11 phút đọc

Phân Biệt Một Cuộc Gọi Với Một App Đang Mở

GeekBye có thể nhận ra bạn vừa vào một cuộc họp video và đề nghị ghi lại nó chỉ bằng một cú nhấp. Việc phát hiện hóa ra là nửa dễ — một Swift binary đọc tiêu đề cửa sổ mỗi mười giây. Nửa khó là độ chính xác: không phát hỏa khi Zoom chỉ đang mở, không nhắc cho một cuộc họp bạn đã đang ghi, và không tắt tiếng cái mic trong cuộc gọi bạn thực sự đang ngồi. Ba bản phát hành, và mỗi bản là một guard phải học cách không tự đánh bại chính mình.

Kỹ thuật
macOS
Desktop