Steven
Steven9 phút đọc

Từ OCR Sang Pixel Mà Không Đánh Mất Fallback

GeekBye ngừng đọc các screenshot của bạn bằng OCR và bắt đầu gửi hình ảnh thật tới các vision model — đó là phần dễ. Phần đáng học là tối ưu đã bỏ qua OCR hoàn toàn cho người dùng vision, lặng lẽ làm hỏng cái fallback sang các model không-vision, và phải bị đưa trở lại một phiên bản nơi tấm lưới an toàn chạy song song và chẳng tốn gì cả.

Kỹ thuật
AI
Desktop
Bản phát hành GeekBye
Từ OCR Sang Pixel Mà Không Đánh Mất Fallback

Bất cứ ứng dụng nào cho bạn chụp màn hình một thứ gì đó rồi hỏi AI về nó đều có một ngã rẽ thật sự. Bạn có thể chạy OCR — trích văn bản ngay trên máy, gửi một chuỗi tí hon, và vứt bức ảnh đi. Hoặc bạn có thể gửi các pixel — chuyển đi hình ảnh thật và để một model có khả năng vision đọc nó. OCR rẻ và nhỏ nhưng mù trước mọi thứ không phải là văn bản: layout, sơ đồ, cách thụt lề của code, cái gạch đỏ lượn sóng dưới lỗi. Các pixel giữ lại tất cả những thứ đó nhưng tốn nhiều byte hơn và đòi một model biết nhìn. GeekBye khởi đầu ở phía OCR của ngã rẽ ấy và, xuyên qua v1.8.6 và v1.8.7, bước sang phía pixel. Cú chuyển đó là phần dễ của câu chuyện này. Phần thú vị là tối ưu đi kèm với nó, làm hỏng fallback, và phải bị hoàn tác.

Trước đó: đọc văn bản, bỏ bức ảnh

Cho tới nhánh chuyện này, một screenshot trở thành câu trả lời như thế này: bạn nhấn phím tắt chụp, client chạy OCR — Vision framework của Apple qua một binary Swift tên VisionOCR trên macOS, một script PowerShell trên Windows, cả hai nằm sau một abstraction IOcrEngine duy nhất — và văn bản trích được được bọc vào một chuỗi rồi gửi tới backend dưới dạng screenshotText. Bản thân hình ảnh chưa bao giờ rời khỏi máy. Điều đó hiệu quả, và với một bức tường văn bản thuần thì ổn. Nhưng một screenshot hiếm khi chỉ là văn bản. Nó là một tin tuyển dụng với layout hai cột, một bài toán coding có sơ đồ, một lỗi kèm stack trace mà cách thụt lề chính là manh mối. OCR san phẳng tất cả những thứ đó thành một bản chép lại từng dòng và model trả lời một câu hỏi mỏng hơn câu bạn đã hỏi.

Cú chuyển: gửi các pixel (v1.8.6)

v1.8.6 thêm con đường còn lại. Một imageOptimizer.ts mới, dựng trên sharp và do đó đa nền tảng, làm đúng ba việc với một screenshot thô:

sharp(pngFilePath).resize({ width: 1920, withoutEnlargement: true }).jpeg({ quality: 80 })

Co về rộng nhiều nhất 1920px (không bao giờ phóng to một bản chụp nhỏ hơn), chuyển PNG sang JPEG, quality 80. Đầu ra là base64 thô với media type mang trong một field riêng, và nó theo hai field request mới đi tới backend bên cạnh field cũ: screenshotImagescreenshotImageMediaType, độc lập với screenshotText. Backend giờ có thể nhận một hình ảnh, văn bản, hoặc cả hai.

Bước tối ưu đó không phải để làm đẹp. Một screenshot PNG thô là 3–8 MB; JPEG đã co lại vào khoảng 200–500 KB. Đó là một bậc độ lớn được cắt khỏi việc upload, và nó cũng giảm hóa đơn của model — các vision model chia một ảnh thành các ô theo độ phân giải của nó và tính tiền theo ô, nên chặn chiều rộng ở 1920 và nén lại trực tiếp giảm cả số byte trên đường truyền lẫn số image token bạn phải trả, mà không mất đi ý nghĩa nào về độ dễ đọc của văn bản trên màn hình.

Vậy làm sao ứng dụng biết được model có nhìn thấy được hay không? Backend công bố điều đó. Response của /api/config có thêm một block ai.vision khóa theo gói, và client cache một boolean duy nhất — aiSupportsVision — ở mỗi lần lấy config. Khi vision được hỗ trợ, tối ưu ảnh; khi không, fallback về OCR. Vậy khả năng là một sự thật của backend, được cache ở phía client, và quyết định theo từng screenshot chạy hay bỏ qua OCR được đưa ra trên client từ cái flag đó. Backend vẫn nắm việc model nào thật sự trả lời.

Cái tối ưu quá khôn

Đây là chỗ nó trở nên đáng học. Bản đầu tiên của đường vision chạy OCR và tối ưu ảnh cùng nhau, song song. Rồi một commit "perf" siết nó thành một lựa chọn hoặc/hoặc: nếu gói hỗ trợ vision, tối ưu ảnh và bỏ qua OCR hoàn toàn; nếu không, chạy OCR và bỏ qua phần việc ảnh. Trên giấy đây là cái thắng hiển nhiên — hà cớ gì trích văn bản mà vision model đâu cần? changelog thậm chí còn quảng cáo nó: "OCR is skipped when the AI can read the image directly."

Vấn đề nằm ở chữ fallback. Backend không có một model; nó có một model chính và một fallback, và cái fallback có thể là một model không-vision. Hãy hình dung trình tự: model chính của gói bạn hỗ trợ vision, nên client hãnh diện bỏ qua OCR và chỉ gửi hình ảnh. Provider chính nấc một cái. Backend fallback sang một model chỉ nhận văn bản — mà giờ được trao vào tay một hình ảnh nó không đọc được và không có văn bản, vì client đã tối ưu văn bản đi mất. Đường nhanh đã lặng lẽ xóa mất tấm lưới an toàn. Bạn có thể bỏ đi một đường nhanh dư thừa một cách an toàn; nhưng bạn không thể bỏ đi chính cái thứ mà đường nhanh vốn là một lối tắt đi vòng qua nó.

Cách sửa: đặt điều kiện lên fallback, không phải model chính

Cách sửa là phần đáng chôm về dùng. Nó không phải quay lại luôn luôn chạy OCR — điều đó sẽ ném đi cái thắng cho trường hợp phổ biến. Nó là làm cho việc bỏ qua phụ thuộc vào một capability flag thứ hai: fallback có hỗ trợ vision không? Config của backend có thêm một block fallbackVision; client cache aiFallbackSupportsVision ngay cạnh flag đầu tiên. Giờ quyết định có bốn nhánh:

  • Cả model chính lẫn fallback đều thấy → tối ưu ảnh, bỏ qua OCR. Đường nhanh, phổ biến.
  • Model chính thấy nhưng fallback mù → tối ưu ảnh chạy OCR, như bảo hiểm cho fallback.
  • Tối ưu ảnh ném lỗi → OCR làm fallback cứng.
  • Gói không hề có vision → chỉ OCR.

Và nhánh thứ hai — trường hợp bảo hiểm — là nơi nước đi cuối cùng có ý nghĩa. Chạy OCR sau khi tối ưu ảnh sẽ cộng độ trễ của nó lên trên. Nên cả hai chạy cùng nhau dưới một Promise.all: tối ưu ảnh và OCR chồng lấn, và cái OCR chỉ tồn tại phòng khi backend cần thì đằng nào cũng xong bên trong quãng thời gian ảnh đang được tối ưu. Tấm lưới an toàn miễn phí về mặt thời gian đồng hồ mỗi khi nó không được dùng. Cả mẹo gói gọn trong một câu: giữ cái fallback rẻ, đặt điều kiện lên chuyện provider fallback của bạn có thật sự cần nó không, và cho nó chạy song song để nó chẳng tốn gì cho tới ngày nó cứu bạn.

Hai cái bẫy nhỏ hơn mà việc bỏ qua làm bật ra

Việc bỏ qua công việc bị hoãn một cách hung hăng có một kiểu làm lộ ra mọi chỗ từng lặng lẽ giả định rằng công việc ấy sẽ xảy ra. Hai cái hiện ra trong PR review cùng tuần.

Một queue deadlock. Hàng đợi screenshot coi một mục là "sẵn sàng" khi nó có văn bản OCR. Các vision screenshot giờ không bao giờ có văn bản OCR — theo thiết kế — nên phép kiểm tra tính đầy đủ hasIncompleteOCR chờ mãi mãi và hàng đợi treo. Cách sửa định nghĩa lại "sẵn sàng" là có một ảnh đã tối ưu hoặc có văn bản OCR, đúng là thứ mà pipeline giờ thật sự bảo đảm.

Một capability flag cũ kỹ. aiSupportsVision được cache, nghĩa là nó có thể cũ đi theo đúng cái hướng sai: một người dùng logout, hoặc một lần lấy config thất bại, và client bị bỏ lại với niềm tin rằng nó vẫn có thể gửi hình ảnh tới một model không còn hỗ trợ chúng nữa. Flag giờ reset khi logout và khi lấy config thất bại — một khả năng đã cache phải có một câu chuyện cho lúc cái thứ nó mô tả biến mất.

Một lời về các con số

Vì trung thực là điểm cốt của loạt bài này: các commit message mang theo con số — tối ưu ảnh "~30ms" so với OCR "~100–150ms," payload "~200–500 KB vs ~3–8 MB PNG" — và chúng nghe hợp lý, nhưng chúng là ước lượng inline của chính tác giả, không phải kết quả của một benchmark. Không hề có độ trễ end-to-end trước/sau được đo ở bất cứ đâu trong lịch sử. Hãy coi lập luận về tốc độ như một ý định thiết kế đúng đắn, không phải một kết quả đã được chứng minh. Việc giảm kích thước payload là con số duy nhất bạn có thể tin, vì nó rơi thẳng ra từ việc co và nén lại.

Ba điều bản phát hành này dạy chúng tôi

  1. Gửi các pixel thắng đọc văn bản — khi model nhìn thấy được. OCR vứt bỏ mọi thứ một screenshot truyền đạt ngoài các ký tự. Một vision model đọc được layout, sơ đồ, và cách thụt lề, và một JPEG đã tối ưu làm cho điều đó kham nổi. Nhưng đó là một canh bạc về khả năng, đó là lý do nó cần một fallback.
  2. Bạn có thể tối ưu bỏ đi một đường nhanh, không bao giờ được tấm lưới an toàn. Bỏ qua OCR cho người dùng vision là đúng với model chính và sai với fallback. Bug không phải ở việc bỏ qua; mà ở chỗ bỏ qua dựa trên khả năng của model chính trong khi cái đáng kể lại là khả năng của fallback.
  3. Một tấm lưới an toàn miễn phí là một tấm lưới được cho chạy song song. Đặt công việc bảo hiểm dưới điều kiện nó có thật sự cần hay không, rồi cho nó chạy cạnh đường nhanh để nó chồng lấn. Làm đúng, sự suy giảm nhẹ nhàng chẳng tốn gì cho tới ngày nó là lý do duy nhất khiến tính năng của bạn vẫn còn hoạt động.

Về chương trước trong câu chuyện v1, xem Hai Chế Độ Hỏng Của Một Lớp Phủ Cho Click Xuyên Qua (v1.8.5); 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

Hai Chế Độ Hỏng Của Một Lớp Phủ Cho Click Xuyên Qua
Steven
Steven11 phút đọc

Hai Chế Độ Hỏng Của Một Lớp Phủ Cho Click Xuyên Qua

Cửa sổ của GeekBye nổi lên trên mọi thứ và để các cú click của bạn xuyên qua nó — trừ những chỗ nó có nút. Đó là một hợp đồng hai mặt, và v1.8.5 cùng v1.8.14 chính là dáng vẻ của lúc mỗi mặt vỡ: một bản phát hành lớp phủ nuốt mất một dialog hệ thống, một bản khác nó cướp các phím gõ của bạn. Cách sửa thắng cuộc cho vấn đề thứ hai là xóa code.

Kỹ thuật
Electron
Desktop
Đưa Bản Phát Hành Vào CI, Hai Lần
Steven
Steven10 phút đọc

Đưa Bản Phát Hành Vào CI, Hai Lần

GeekBye v1.8.4 chuyển các bản build phát hành cho macOS và Windows vào CI. Điều changelog không nói là chúng tôi đã thử việc này một lần trước đó, xóa nó đi một tháng sau vì chi phí runner, và chỉ khiến nó trụ được ở lần thứ hai — bởi tới khi ấy release script đã chạy được bằng tay rồi.

Kỹ thuật
CI/CD
Desktop
Ship Ba Mươi Ngôn Ngữ Mà Không Có Lưới An Toàn
Steven
Steven10 phút đọc

Ship Ba Mươi Ngôn Ngữ Mà Không Có Lưới An Toàn

GeekBye v1.8.3 đấu nối react-i18next và dịch toàn bộ ứng dụng sang 30 ngôn ngữ. Phần thư viện chỉ là việc thường ngày. Phần thú vị nằm ở những gì còn thiếu: không có kiểm tra tính đồng nhất của key và không có công cụ trích xuất chuỗi — nên tiếng Anh hardcode lọt lưới, một lỗi chính tả tiếng Đan Mạch bị ship, và cái changelog thì không thống nhất nổi rằng ứng dụng nói 28, 29, hay 30 ngôn ngữ.

Kỹ thuật
i18n
Desktop