作者完整拆解 Screenshot Vault 技术选型:ML Kit 端侧识别、Expo + SQLite 本地持久化、Gemini API 做分类摘要,隐私优先架构值得参考。
每个人的手机里都有一个装满 500 多张截图的文件夹,想在里面翻找三个月前的收据简直是大海捞针。我受够了这种事,于是做了一个 App 叫 Screenshot Vault——它能读取手机里每张截图中的文字,让你像用 Google 一样去搜索它们。
下面说说它是怎么实现的,以及几个我当初没料到会这么重要的设计决策。
我的第一反应是把截图上传到服务器处理。后来认真想了想——一个典型的截图文件夹里有什么?银行 OTP 验证码、支付凭证、私人聊天记录、家庭地址。把这些上传到我控制的数据库里,既是我的负担,一旦泄露也是用户真实的风险。
Google ML Kit 的文字识别完全运行在设备上,免费,无需服务器往返。所以 OCR 在本地完成,唯一离开手机的东西是已经提取出的文字——发送给 Gemini 做轻量的分类调用,而不是图片本身。这样做还有一个好处:规模大了成本更低,因为不需要为图片存储和带宽付费。
如果有人安装了 App 时已经有了 500 张旧截图,要在展示任何内容之前对它们全部跑 OCR + AI,这意味着首次启动要等 10-20 分钟。这种体验的结果只有一个:立刻卸载。
取而代之的是分阶段、可恢复的处理方式:
CREATE TABLE screenshots (
id TEXT PRIMARY KEY,
uri TEXT,
text_content TEXT,
category TEXT,
ai_title TEXT,
tags TEXT,
ocr_done INTEGER DEFAULT 0,
ai_done INTEGER DEFAULT 0,
created_at INTEGER
);
展示策略:
ocr_done = 0 的记录,按最新优先ocr_done = 1 且 ai_done = 0 的记录,按最新优先如果 App 在扫描中途被关闭,下次启动时它会从标志位断点处继续——不会重复处理,不会丢失进度。这其实就是 Google Photos 用于"正在准备你的照片库"后台索引的同一套思路。
早期版本直接从 App 里调用 Gemini,Key 写在 env var 里。很快我就意识到:任何安装了 APK 的人都能把这个 Key 提取出来,然后在我的账号上疯狂消耗——以 EXPO_PUBLIC_ 开头的 env var 会直接打包进客户端,没有任何例外。
我把它迁移到落地页同一个 Vercel 项目里的一个 Next.js API Route 后面:
export async function POST(req: NextRequest) {
const { text } = await req.json();
const apiKey = process.env.GEMINI_API_KEY; // server-only, never shipped to client
const res = await fetch(GEMINI_ENDPOINT, {
method: 'POST',
headers: { 'x-goog-api-key': apiKey },
body: JSON.stringify({ /* prompt asking for category + title + tags in one call */ }),
});
// ...parse and return
}
一次调用返回 category + title + tags,而不是发三个独立请求——这在 Gemini 免费额度(很快就会触顶)上差异巨大。
目前还在早期,暂只支持 Android,小范围测试中,后续会扩大发布范围。如果你希望关注进展或等它 ready 了想试试:screenshot-vault-lac.vercel.app
很好奇有没有其他人做过类似的 AI 功能端侧 vs. 云端权衡——欢迎在评论区交流探讨。