一块存了十几年照片的移动硬盘插上电脑:云相册要你先上传,传统桌面相册要你先导入,两边的潜台词都是「照片得搬一次家」。Lap 反着来:照片原地不动,它在旁边建一份随时可以扔掉的索引。这篇文章要拆的就是它怎么把「不搬家」做成架构——影子目录、增量扫描、自建缩略图协议、BLAKE3 去重、CLIP 检索、人脸聚类,以及所有这些选择背后要付的代价。在 云栈社区 的开发者圈子里,这类本地优先工具最近讨论得不少。
先看看它长什么样:左边是按日期、评分、题材分组的文件夹树,中间是密度很高的照片墙,右边是信息面板——直方图、镜头参数、拍摄地点、小地图。乍看跟 Lightroom 是一个物种,但它没有「导入」这个动作。

顺便交代它的出身,因为后面每一节都跟这个出身有关:后端是 Rust,外壳是 Tauri,界面是 Vue,索引放在 SQLite 里。 一个装了几十兆的桌面程序,没有 Electron 那套几百兆的浏览器运行时,靠的是系统自带的 WebView;而真正吃 CPU 的活——解码 RAW、算哈希、跑 CLIP 推理、建向量索引——全在 Rust 这一侧跑完。这不是为了写着爽,是因为这些活必须本机完成,还不能让界面卡一下。
一、三条老路,各有各的坑
把照片管起来,市面上就三条路,先把每条的账算清楚。
第一条,云相册。 把照片传上去,算法帮你打好标签,多设备同步,体验确实好。代价也实在:几个 TB 的照片先传几个月;容量按月付费;更重要的是,你的全部生活影像躺在一台你管不着的服务器上——服务条款改了、服务停了、账号被封了,连申辩的机会都没有。
第二条,导入式桌面相册。 Lightroom、Photos 这类软件把照片「收编」进自己的库:复制一份、建索引、改组织结构。从此文件有正副本之分——库里是「正本」,硬盘上那份是「原件」。导出等于搬家,库文件损坏等于组织结构全灭,换软件等于重走一遍。
第三条,看图软件。 资源管理器、Finder 直接看。没有搬家问题,但也没有管理:搜不了「海边日落」,认不出 RAW,HEIC 缩略图卡半天,重复文件全靠肉眼。
Lap 走的路一句话能说完:文件夹是唯一真相,软件只做影子。 你的照片还是那些普通文件,躺在原来的目录里,用任何别的软件都能打开。Lap 旁边建一份索引,随时可以删掉重建。
这句话听着轻巧,但「只做影子」四个字,要撑起一个能管十万张照片的相册,每一步都有硬骨头。下面一条一条拆。
二、影子目录:数据库可以删,照片不能丢
Lap 用一个 SQLite 文件当索引,里面的表不多,每张表干的活很直白:afiles 记每个文件——路径、大小、修改时间、拍摄时间、EXIF 里的相机镜头 GPS;afolders 记目录树;atags 和 acollections 记标签和收藏集;faces 和 persons 记人脸和人物;file_hashes 记内容哈希;duplicate_groups 记重复分组。
关键在表里存的东西的性质:全部是派生数据。标签是你点的,但存在库里而不写进照片;评分、选片状态、智能相册规则,同理。也就是说,把 Lap 的数据库删了,损失的是「标签和分类」,照片文件一根毫毛都不少。
这个立场跟大多数相册软件反着来。很多软件的思路是「把组织信息写进照片」:改 EXIF、写 XMP、嵌入关键词。听起来很通用,实际上是在拿原文件冒险——写坏一次就是永久损失,多设备同步时两边抢着写会打架,只读的网络盘和光盘根本写不了。
Lap 的选择是默认不碰原文件,但留了个出口:需要的时候可以把标签、评分、选片状态批量导出成 XMP 边车文件,跟 Lightroom 互通。这个导出写得相当谨慎——全局写锁防并发,先写临时文件再原子改名,写之前检查边车有没有被外部改过,改过就直接拒绝,绝不覆盖别人的修改。

「影子」还有一个反直觉的好处:卸载也是安全的。 README 里专门写了卸载步骤,先卸软件再删数据库和缓存目录,白纸黑字保证不会动到你的照片。这个保证的底气就来自架构——照片从来就没进过它的仓库。
三、第一遍扫描和第一万遍扫描
建索引的第一步是扫描。Lap 用 walkdir 把你指定的文件夹整个递归一遍,每个文件读出路径、大小、修改时间,再抽 EXIF 里的拍摄时间和相机参数,全部写进 SQLite。
第一遍最慢,也最诚实:每个文件都要摸一遍。但第二遍开始就是另一个故事了。每个文件的身份是「路径 + 大小 + 修改时间」三元组,file_hashes 表里记着上次处理时的修改时间——时间戳没变,就当它没变,跳过。你只是浏览了一晚上照片,一个字节都没改,第二次打开时扫描几乎是瞬间完成的。
有两个容易被忽略的细节,都跟「别把用户拖下水」有关。
一是网盘占位文件。OneDrive、iCloud 这类服务会把没下载的文件变成一个几 KB 的占位符,很多软件一扫到就顺手触发下载,几个 TB 的照片就这么被偷偷拉下来了。Lap 识别出 online-only 文件直接跳过,不哈希、不解码、不触发下载。
二是可移动盘的离线浏览。移动硬盘拔了、NAS 断网了,Lap 不把整个库标记为失效,缩略图和元数据还在缓存里,相册照样翻,只是点开大图会提示盘不在。
扫描本身用事务批量写入,不是一条一条提交——SQLite 单条写很慢,攒一批再写是基本操作。改天你在资源管理器里手动挪了文件也没关系,重扫一遍能追上,只是 collections 这类「只存在 Lap 里」的组织信息会断——这一点它自己承认得很干脆,后面细说。

四、缩略图:两条自定义协议和一座可搬迁的缓存
十万张照片的界面要顺滑,命门在缩略图。Lap 在这块的做法很有 Tauri 应用的代表性。
先是通道。网页应用给界面塞图片,通常走两种路:要么把图片编码成 base64 塞进消息里,要么复制到临时目录再给个路径。前者体积膨胀三成,后者会产生一堆垃圾文件。Lap 直接注册了两个自定义 URL 协议:thumb:// 和 preview://。前端里图片地址就写 thumb://123?w=320,Rust 侧收到请求,从缓存取出字节直接回 HTTP 响应——不落盘、不转码、不经过中间层。协议还是异步的,图片解码这种重活在后台线程做,界面不卡。
再是缓存隔离。URL 里带一个库编号,同一个缓存目录里,不同相册库的缩略图各占一个命名空间。你今天挂家里的库、明天挂公司的库,缓存不会串图。
缩略图本身的规格也抠过细节:JPEG 质量 85;短边不得小于目标尺寸的四分之一——这一条是给全景图和长截图留的,否则一张 8000×800 的全景图缩成 320×32 一条线,缩略图就没法看了;绝不放大,小图原样出。
最后是缓存目录本身可以整体搬家。迁移不是简单复制:先写一个迁移日志文件,边移边记进度,中途被杀掉,下次启动发现日志还在,从断点继续;源目录和目标目录有重叠直接拒绝——这种搬来搬去的操作,重叠是最容易把文件搬没的姿势。

五、解码链:HEIC、RAW、JXL 各有各的门
照片格式是这类软件真正的泥潭。Lap 支持六十多种格式,背后是几条各司其职的解码链:HEIC 和 HEIF 走 libheif,RAW 走 LibRaw,JPEG XL 走 jxl-oxide,普通 JPEG 走 libjpeg-turbo,视频帧走 FFmpeg。
RAW 是重头。一张 45MP 的 RAW 全解码要几秒,而缩略图只需要几百像素。Lap 的选择是优先用 RAW 文件里相机写好的内嵌预览——那本来就是一张现成的 JPEG,取出来就行,快几个数量级;想要真色彩再切全解码渲染,这个选项在界面里可以配。顺带把 RAW+JPEG 双格式对(很多相机连拍时的组合)合成一个条目,移动和删除都成对处理,不会留下孤儿文件。
EXIF 这块也能看出踩坑痕迹。它的 EXIF 读取全是「宽容模式」:容器坏了就逐段恢复,解析挂了就捕获异常再降级;方向读不出来,还有一套二进制扫描的兜底逻辑直接从文件头猜。真实世界的照片文件,脏数据比例远超想象,这类代码写得越「讲究」越容易崩。
还有一个冷门但很妙的细节:它能读出一张 PNG 是不是 AI 生成的。 Stable Diffusion 系工具生成图片时,会把提示词和参数写进 PNG 的文本块里,甚至藏进透明通道的最低位(所谓隐写水印)。Lap 把这两处都解析了——你在相册里翻到一张图,信息面板能直接告诉你是哪个模型、什么提示词画的。

六、视频:一个 sidecar 和三条硬超时
视频这块的思路一句话:别信任任何视频文件。
Lap 把 ffmpeg 和 ffprobe 打成 sidecar 随应用分发,每个平台一份对应架构的二进制,不用用户装。所有调用都是异步的,而且每一档都有硬超时:探测元数据五秒,索引阶段的探测放宽到二十秒,转码三十秒。超时不是报错了事,有降级路径——比如播放上,HEVC 和 VP9 在 macOS 上原生能放,别的平台就先转码成 H.264 再给播放器,转码也不行就把文件丢给系统外部播放器,错误码都定义好了。
探测也有讲究:ffprobe 只读文件前 5 兆,分析时长限制五秒。损坏的视频文件最擅长的就是让解析器读到底——几 GB 的坏文件能挂住整个索引流程,限定读取量是唯一的防法。
Linux 上是例外:播放走系统 GStreamer 插件,缺什么 README 会让你补装,这是发行版生态的现实,硬带 sidecar 反而跟系统解码器打架。

七、会动的照片:两种完全不同的「动态照片」
「动态照片」其实是两种东西,Lap 得各写一套检测。
Apple Live Photo 是两个文件:一张 HEIC 加一段同名 MOV,靠一个内容标识符配对,还可能拖着一个 AAE 边车。这套是文件系统层面的配对,找到标识符就成。
Android 阵营的 Motion Photo 就野了:把一小段 MP4 直接接在 JPEG 文件的尾巴上。对软件来说,这是一张 JPEG 和一段视频共享同一份字节,你甚至能用看图软件打开它——只不过只看得见照片那一半。
Lap 的检测顺序很务实:先只读文件头 1 兆,在 XMP 元数据里找视频长度的权威记录,拿到偏移量验证一下那个位置是不是真的 MP4(MP4 文件开头必有 ftyp 盒子);XMP 里没有,再全文扫一段特征字节串做兜底;连文件都太小的(不到 64KB)直接放弃。为什么只读 1 兆?因为一张 4K 动态照片动辄几十兆,机械硬盘上全文件扫一遍,一张就是一秒,一万张就是三个小时。先读头部命中绝大多数情况,兜底才付全文的代价。

八、重复文件:BLAKE3 算的是字节,不是长相
找重复文件,Lap 的路线保守而高效:只认字节级相同。
流程三步:先按文件大小分组——大小不同绝不可能是同一份内容,这一步就把绝大多数文件排除了;然后对每个可疑文件算 BLAKE3 全文件哈希,这是目前最快的密码学哈希之一,天然适合大文件;最后按「哈希 + 大小」分组,组内就是真重复。
第一眼会觉得太严格:两张连拍只差一个字节,就算重复吗?不算,这正是设计——「完全一样」和「长得像」是两种需求,前者归去重,后者归相似搜索。把两件事搅在一起,两边都做不好。
增量逻辑跟扫描共享同一套三元组:修改时间没变,哈希直接复用。识别出是网盘占位文件的照旧跳过。删重复文件也不莽,走系统回收站,删错了还能捞回来。每组给一个「可回收空间」汇总,按组批量删。

九、AI 检索:两个塔,一个可换的头
Lap 的搜索框能打「雪山的日落」这种自然语言,背后是 CLIP 的经典双塔结构:一个视觉模型把每张照片压成一个向量,一个文本模型把你的查询也压成向量,两边比余弦距离。全部在本地跑,ONNX Runtime 推理。
有意思的是文本塔可以换。默认模型随安装包带,英文查询;想要用中文、日文等五十多种语言搜,可以再下一个多语言文本模型。这个按需下载做得挺讲究:下载地址带一份 SHA256 校验清单,下完先验指纹;装的时候下到临时目录,校验通过再原子改名替换,中途断电重启会自动回滚到备份目录,不会留下半座「废墟」。
还有一个工程细节能看出认真程度:推理线程数固定为 2。 CLIP 推理是能吃满 CPU 的活,但它故意限着——索引要在你干活的时候后台跑,把八个核全吃满,风扇一响用户就把它卸了。
向量这边还有个坑要自己防:默认模型和多语言模型产出的向量维度不一样。库里两种向量混存的话,维度不同的向量之间算余弦距离是纯垃圾。Lap 的做法是统计出「主导维度」,只对同维度的向量做相似计算,异维的单独标出来,绝不掺进同一锅。

十、相似图:HNSW 与「每组最多 64 张」
找相似照片要面对一个规模问题:十万张照片两两比距离是百亿次浮点运算,谁也等不起。Lap 用 HNSW——分层可导航小世界图——做近似最近邻,查询时每张图只在图上跳几十步就能找到最像的邻居,参数上检索宽度放到 200,宁可多查一点也要准。
但真正体现产品判断的是另一个数字:每组上限 64 张。 查询时每张图最多带 63 个邻居回来,凑成一个组。为什么是 64?因为相似图的出口是人工复核——一屏看完、决定留哪张删哪张。一组几百张,人直接放弃看;64 张是「一次能看完」的工程化表达,顺带保证组内每张都跟其他所有张直接比过,不会有「隔着两跳的假相似」。
扫描时还有两层过滤:完全重复的文件先排除——那是去重模块的辖区;日期差太远的也不往一组凑——2016 年的雪山和 2024 年的雪山长得再像也不是你要的「同一次拍摄的废片」。

十一、人脸:四道过滤和一条 0.42 的线
人脸是最容易翻车的功能,Lap 的整条流水线都透着「先防误报」四个字。
第一步检测用 RetinaFace:输入统一缩到 640 见方(或按 32 对齐的缩略图尺寸省算力),三个尺度的锚框扫小脸到大脸,非极大值抑制去重叠框。第二步 Embedding 用 MobileFaceNet,每张脸压成 512 维向量。
中间横着四道过滤,一道比一道实际:置信度低于 0.65 的脸不要;太小的脸不要;拉普拉斯方差低于 200 的不要——这一道是拿「清晰度」开刀,糊掉的脸连人都认不出,建进库里只会制造大量「疑似张三」;运动模糊和虚焦在这里就被拦在门外。
聚类用的是图方法:每张脸只跟自己的 80 个近邻连边,余弦距离小于 0.42 才连。0.42 是个值得念叨的数字——它意味着相似度要高于 0.58 才算同一人。这个阈值取的是保守端:漏掉几张,用户手动拖进去就行;把两个长得像的人合并成一个「张三李四混合体」,用户会直接失去信任。先给机器一条严的线,再给人一个宽松的改口渠道,这个顺序不能反。

十二、地图:R 树和跟着筛选走的聚簇
有 GPS 的照片能上地图。这块的技术选型一快一稳:快的是查询——照片坐标进 R 树(rstar),地图每挪一格、每缩放一级,按可视范围做一次范围查询,十万张照片里挑出视野内的那几百张是毫秒级的事;稳的是渲染——Leaflet 加开源瓦片,聚簇不是把照片按坐标分桶了事,而是跟着当前筛选走:你在日期栏选了 2023 年,地图上的簇立刻只剩那一年。
行政区和地名在索引时就算好落库,按国家、地区、地名建了专门的列,按「东京」筛照片走的是数据库索引而不是现场反查。坐标本身不出机器——瓦片图要联网加载,但「哪张照片在哪」这件事,本地算完本地用。

十三、拼图:三种排版和一条 12000 像素的天花板
选一批照片能拼成一张图。三种版式:网格、等高瀑布、照片堆——最后一种是把照片当实体照片散落在桌面上,每张带白边框、随机小角度旋转、投影,参数里旋转角度就写着 ±12 度。
这个模块有两个反直觉的细节。一是上限 12000 像素:不是画布做不大,是内存和打印机都不答应——一张 12000×12000 的 RGB 图要四百多兆内存,超过这个数体验就开始崩,与其让用户等到内存爆,不如在入口就顶死。二是缩略图够不够用的预判:拼 100 张图,如果每张源图都全尺寸解码再缩,内存和时间都遭不住;它先算这张缩略图被裁切、旋转之后还够不够目标格子的分辨率,够就直接用缩略图,不够才碰原图。这种「算清楚了再动手」的判断散布在代码各处,是这类工具和玩具的分水岭。

十四、那些功能列表里不会写的活
翻完代码,最佩服的反而是几件「没有功能名」的事。
成组资产的一致性。 Live Photo 是两个文件,RAW+JPEG 是一对,苹果的 AAE 边车挂在一旁。Lap 的移动、重命名、删除全部按组操作,事务里一起提交——要么全动,要么全不动。拖走一个 RAW 时它的 JPEG 兄弟留在原地,这种事一次就够用户卸载。
移动文件顺带搬家。 在 Lap 里重命名或移动一个已有 XMP 边车的文件,边车跟着走。数据库记录、文件系统、边车文件三方的状态由一个缓存层统一维护,不会出现「照片挪走了,边车变孤儿」。
一条进程,两半世界。 前端那半边只干一件事:发命令、画结果。滚照片墙时它在算排版,后台那半边在解码、在算哈希、在建索引,两边靠消息来回,谁也不等谁。所以它敢在你翻照片的同时继续扫盘——不是扫描变快了,是重活根本没长在界面的线程上。用 Rust 写后端换来的正是这个:这些活换成脚本语言跑,风扇会先替用户表态。
拼音搜索。 依赖列表里躺着一个 pinyin 库——中文用户打「gugong」能命中「故宫」,这是给中文用户做的、功能列表上永远不会出现的一行。
编辑不碰原图。 裁剪、拉直、调色,保存时输出成新文件,原图永远原样。跟「影子目录」的哲学一脉相承。
十五、装机、代价与边界
装起来不费事:macOS 有 Homebrew(brew tap julyx10/lap)和苹果公证过的 dmg;Windows 有 msi,未签名所以 SmartScreen 会拦一下,选「仍要保留」即可;Linux 有 deb 和 AppImage,Arch 还有社区配方可以本地构建。
第一次启动的节奏:选文件夹建库,扫描和缩略图先跑起来,AI 检索和人脸是可选的后续步骤——不想要 AI,可以完全不装模型,它就是一个快的本地相册。
代价也得摆上台面。AI 模型吃内存也吃磁盘,低配机器建议只开检索不开人脸;单机软件没有同步,两台电脑各管各的库,别指望它替你做同步;目录外的改动要靠重扫追,在 Finder 里大动干戈之前,先想想哪些集合和标签只存在 Lap 里,重要的话先备份它的数据库和配置;Windows 包未签名,公司机器可能装不上。
它适合谁?硬盘和 NAS 里躺着几万到几十万张照片、不肯上传、又被资源管理器的缩略图折磨多年的人。它不适合谁?要跨设备实时同步的、要在线分享相册链接的、要多人协作标注的——这些是云相册的主场,别用错工具。

结语
回头看,Lap 最值得学的不是某个算法,而是一个立场:相册软件最重要的部件,是那个可以随时删掉的数据库。
云相册把你的照片变成别人的资产,导入式相册把你的文件夹变成软件的领地,看图软件什么都不管。Lap 选了最难走的一条:索引全部是派生物,软件的全部价值建立在「随时可以推倒重来」之上——所以它必须把扫描写快,把缓存管好,把解码做稳,把每一个「顺手帮你搬家」的冲动按住。
工具和用户的关系,到这一层才算平等:它替你干活,但不占有你。
看一个本地软件可不可信,先问一句:删掉它的数据库,你还剩下什么。剩下的越多,它越值得装。
参考
- Lap 仓库:
https://github.com/julyx10/lap — 源码、README 与架构说明
- Lap 文档站:
https://julyx10.github.io/lap/ — 功能指南与发布说明
- 下载页:
https://github.com/julyx10/lap/releases/latest — 三平台安装包
- HNSW 近似最近邻:
https://github.com/jean-pierreBoth/hnswlib-rs — 相似检索用的图索引库