
Dari OCR ke Piksel Tanpa Kehilangan Fallback
GeekBye berhenti membaca screenshot-mu dengan OCR dan mulai mengirim gambar sebenarnya ke model vision — yang merupakan bagian mudahnya. Bagian yang memberi pelajaran adalah optimisasi yang melewati OCR sepenuhnya untuk pengguna vision, diam-diam mematahkan fallback ke model non-vision, dan harus ditarik kembali ke sebuah versi di mana jaring pengaman berjalan paralel dan tak berbiaya apa pun.
Ada sebuah persimpangan sejati bagi aplikasi mana pun yang membiarkanmu men-screenshot sesuatu dan bertanya kepada AI tentangnya. Kamu bisa menjalankan OCR — mengekstrak teksnya di perangkat, mengirim sebuah string mungil, dan membuang gambarnya. Atau kamu bisa mengirim pikselnya — mengirim gambar sebenarnya dan membiarkan sebuah model berkapabilitas vision membacanya. OCR murah dan kecil tapi buta terhadap segala yang bukan teks: tata letak, diagram, indentasi kode, garis merah bergelombang di bawah error. Piksel mempertahankan semua itu tapi berbiaya lebih banyak byte dan membutuhkan sebuah model yang bisa melihat. GeekBye memulai di sisi OCR dari persimpangan itu dan, sepanjang v1.8.6 dan v1.8.7, menyeberang ke sisi piksel. Peralihan itu adalah bagian mudah dari kisah ini. Bagian yang menarik adalah optimisasi yang datang bersamanya, mematahkan fallback, dan harus dibatalkan.
Sebelumnya: baca teksnya, buang gambarnya
Sampai babak ini, sebuah screenshot menjadi sebuah jawaban seperti ini: kamu menekan pintasan tangkapan, client menjalankan OCR — framework Vision milik Apple melalui sebuah binary Swift VisionOCR di macOS, sebuah skrip PowerShell di Windows, keduanya di balik satu abstraksi IOcrEngine — dan teks yang diekstrak dibungkus ke dalam sebuah string dan dikirim ke backend sebagai screenshotText. Gambar itu sendiri tak pernah meninggalkan mesin. Itu efisien, dan untuk sebuah dinding teks polos itu baik-baik saja. Tapi sebuah screenshot jarang hanya teks. Ia adalah lowongan kerja dengan tata letak dua kolom, sebuah soal pemrograman dengan diagram, sebuah error dengan stack trace yang indentasinya adalah petunjuknya. OCR meratakan semua itu menjadi transkripsi baris demi baris dan model menjawab pertanyaan yang lebih tipis daripada yang kamu ajukan.
Peralihannya: kirim pikselnya (v1.8.6)
v1.8.6 menambahkan jalur yang satunya. Sebuah imageOptimizer.ts baru, dibangun di atas sharp dan karena itu lintas-platform, melakukan tepat tiga hal pada sebuah screenshot mentah:
sharp(pngFilePath).resize({ width: 1920, withoutEnlargement: true }).jpeg({ quality: 80 })
Ubah ukuran menjadi paling lebar 1920px (jangan pernah memperbesar tangkapan yang lebih kecil), konversi PNG ke JPEG, kualitas 80. Keluarannya adalah base64 mentah dengan tipe media dibawa dalam sebuah field terpisah, dan ia meluncur ke backend dalam dua field request baru di samping yang lama: screenshotImage dan screenshotImageMediaType, independen dari screenshotText. Backend kini bisa menerima sebuah gambar, teks, atau keduanya.
Langkah optimisasi itu bukan kosmetik. Sebuah PNG screenshot mentah berukuran 3–8 MB; JPEG yang diubah ukurannya kira-kira 200–500 KB. Itu selisih satu orde besaran pada unggahan, dan ia juga memangkas tagihan model — model vision membagi sebuah gambar menjadi ubin berdasarkan resolusinya dan menagih per ubin, jadi membatasi lebar pada 1920 dan mengompres ulang langsung mengurangi baik byte di kabel maupun token-gambar yang kamu bayar, tanpa kehilangan keterbacaan yang berarti untuk teks di layar.
Bagaimana aplikasi tahu apakah model bahkan bisa melihat? Backend mengumumkannya. Respons /api/config mendapat sebuah blok ai.vision yang diindeks per paket, dan client meng-cache satu boolean tunggal — aiSupportsVision — pada setiap pengambilan config. Ketika vision didukung, optimasi gambarnya; ketika tidak, jatuh ke OCR. Jadi kapabilitas itu adalah fakta backend, di-cache di sisi client, dan keputusan per-screenshot untuk menjalankan atau melewati OCR dibuat di client dari flag itu. Backend tetap memiliki keputusan model mana yang sebenarnya menjawab.
Optimisasi yang terlalu pintar
Di sinilah ia menjadi memberi pelajaran. Versi pertama dari jalur vision menjalankan OCR dan optimisasi gambar bersama-sama, secara paralel. Lalu sebuah commit "perf" mengetatkannya menjadi salah-satu-atau: jika paket mendukung vision, optimasi gambarnya dan lewati OCR sepenuhnya; jika tidak, jalankan OCR dan lewati pekerjaan gambar. Di atas kertas ini adalah kemenangan yang jelas — mengapa mengekstrak teks yang tak dibutuhkan model vision? Changelog bahkan mengiklankannya: "OCR is skipped when the AI can read the image directly."
Masalahnya adalah satu kata itu: fallback. Backend tak punya satu model; ia punya sebuah primer dan sebuah fallback, dan fallback itu bisa jadi sebuah model non-vision. Bayangkan urutannya: model primer paketmu mendukung vision, jadi client dengan bangga melewati OCR dan hanya mengirim gambar. Penyedia primer tersendat. Backend jatuh ke sebuah model teks-saja — yang kini diberi sebuah gambar yang tak bisa ia baca dan tanpa teks, karena client telah mengoptimasi teksnya hingga hilang. Jalur cepat itu diam-diam telah menghapus jaring pengaman. Kamu bisa membuang sebuah jalur cepat yang berlebihan dengan aman; kamu tak bisa membuang benda yang jalur cepat itu hanya jalan pintas untuk menghindarinya.
Perbaikannya: bersandar pada fallback, bukan primer
Perbaikannya adalah bagian yang layak dicuri. Ia bukan kembali ke selalu menjalankan OCR — itu akan membuang kemenangan untuk kasus umum. Ia adalah membuat pelewatan bersyarat pada sebuah flag kapabilitas kedua: apakah fallback mendukung vision? Config backend mendapat sebuah blok fallbackVision; client meng-cache aiFallbackSupportsVision di samping flag pertama. Kini keputusan itu punya empat cabang:
- Primer dan fallback keduanya melihat → optimasi gambar, lewati OCR. Jalur cepat yang umum.
- Primer melihat tapi fallback buta → optimasi gambar dan jalankan OCR, sebagai asuransi untuk fallback.
- Optimisasi gambar melempar error → OCR sebagai fallback keras.
- Paket tak punya vision sama sekali → hanya OCR.
Dan cabang kedua — kasus asuransi — adalah tempat gerakan terakhir itu penting. Menjalankan OCR setelah optimisasi gambar akan menambahkan latensinya kembali di atasnya. Jadi keduanya berjalan bersama di bawah satu Promise.all: optimisasi gambar dan OCR bertumpang tindih, dan OCR yang ada hanya untuk berjaga-jaga kalau backend membutuhkannya selesai dalam waktu di mana gambar toh sedang dioptimasi. Jaring pengaman itu gratis dalam istilah waktu-nyata kapan pun ia tak dipakai. Itulah seluruh triknya dalam satu kalimat: pertahankan fallback yang murah, sandarkan ia pada apakah penyedia fallback-mu benar-benar membutuhkannya, dan paralelkan ia sehingga ia tak berbiaya apa pun sampai hari ia menyelamatkanmu.
Dua jebakan lebih kecil yang dipicu oleh pelewatan itu
Melewati pekerjaan yang ditangguhkan secara agresif punya kebiasaan memunculkan setiap tempat yang diam-diam mengasumsikan bahwa pekerjaan itu akan terjadi. Dua muncul di review PR pada minggu yang sama.
Sebuah deadlock antrean. Antrean screenshot menganggap sebuah item "siap" ketika ia punya teks OCR. Screenshot vision kini tak pernah mendapat teks OCR — memang disengaja — jadi pemeriksaan kelengkapan hasIncompleteOCR menunggu selamanya dan antrean menggantung. Perbaikannya mendefinisikan ulang "siap" sebagai punya sebuah gambar teroptimasi atau punya teks OCR, yang memang benar-benar dijamin pipeline sekarang.
Sebuah flag kapabilitas basi. aiSupportsVision di-cache, yang berarti ia bisa menjadi basi tepat ke arah yang salah: seorang pengguna logout, atau sebuah pengambilan config gagal, dan client tertinggal percaya bahwa ia mungkin masih boleh mengirim gambar ke sebuah model yang tak lagi mendukungnya. Flag itu kini di-reset saat logout dan saat kegagalan config-fetch — sebuah kapabilitas yang di-cache harus punya jawaban untuk saat benda yang ia gambarkan menghilang.
Sepatah kata tentang angka-angkanya
Karena kejujuran adalah inti dari seri ini: pesan-pesan commit membawa angka — optimisasi gambar "~30ms" versus OCR "~100–150ms," payload "~200–500 KB vs ~3–8 MB PNG" — dan mereka masuk akal, tapi mereka adalah estimasi inline milik penulis sendiri, bukan keluaran sebuah benchmark. Tak ada latensi end-to-end sebelum/sesudah yang terukur di mana pun dalam riwayatnya. Perlakukan argumen kecepatan sebagai niat desain yang solid, bukan hasil yang terbukti. Pengurangan ukuran payload adalah satu-satunya angka yang bisa kamu percayai, karena ia jatuh langsung dari mengubah ukuran dan mengompres ulang.
Tiga hal yang diajarkan release ini kepada kami
- Mengirim piksel mengalahkan membaca teks — ketika model bisa melihat. OCR membuang segala yang dikomunikasikan sebuah screenshot selain karakter. Sebuah model vision membaca tata letak, diagram, dan indentasi, dan sebuah JPEG teroptimasi membuat itu terjangkau. Tapi itu sebuah taruhan kapabilitas, itulah sebabnya ia butuh sebuah fallback.
- Kamu bisa mengoptimasi hingga hilang sebuah jalur cepat, tak pernah jaring pengaman. Melewati OCR untuk pengguna vision benar untuk primer dan salah untuk fallback. Bug-nya bukan pelewatan itu; ia adalah melewati berdasarkan kapabilitas primer padahal kapabilitas fallback-lah yang penting.
- Sebuah jaring pengaman gratis adalah yang diparalelkan. Sandarkan pekerjaan asuransi pada apakah ia benar-benar dibutuhkan, lalu jalankan ia berdampingan dengan jalur cepat sehingga ia bertumpang tindih. Dilakukan dengan benar, degradasi anggun tak berbiaya apa pun sampai hari ia adalah satu-satunya alasan fiturmu masih bekerja.
Untuk bab sebelumnya dalam kisah v1, dua mode kegagalan sebuah overlay click-through (v1.8.5); dan untuk keseluruhan alurnya, anatomi merilis software sampai sempurna.