功能架构
三个核心容器,外加一个 mDNS 服务,不需要独立的消息中间件,也不依赖云服务。照片通过上传或从 NAS 目录导入,依次完成元数据提取、缩略图生成、语义向量化、人脸与文字识别,最终成为可按自然语言、人物和地点检索的相册。
Go 后端、AI 服务、PostgreSQL,和一个广播局域网域名的 mDNS
metadata、thumbnail、clip、face、ocr
Chinese-CLIP 图文向量(按模型档位为 512、768 或 1024 维);ArcFace 人脸特征为 512 维
pgvector 余弦距离索引,搜索和人脸比对在数据库里进行
系统架构
浏览器只连接 Go 后端,由后端负责上传、任务调度、缩略图和权限管理,并将预览图交给 AI 服务推理,结果统一写入 PostgreSQL。AI 服务无状态,不访问数据库、不存储文件,可独立部署到性能更强的机器上。
网页打包后嵌在程序里,部署不需要 Node。启动时自动执行数据库迁移,升级就是换镜像。
内置 AI 服务运行在 Docker 内部网络中,使用 CPU 推理;在网页中填入远程地址,即可切换到家中带显卡的电脑或 Mac。
照片信息、人物、设置、任务队列和向量统一存入 PostgreSQL,业务数据与任务状态可以一起备份;照片文件需要另外备份。
照片处理流水线
照片导入后,先提取元数据、生成缩略图,再并行计算语义向量、识别人脸和文字。各步骤独立入队,重启或 AI 服务掉线也不会丢失任务,恢复后继续处理。
ingest- 网页上传走 tus,断网能续传
- /photos 目录每 15 分钟扫一遍
- 算 SHA-1 校验和,写进 assets
metadata- ExifTool 读尺寸、拍摄时间
- GPS、相机、镜头、光圈快门
- 没有时区的按设置的时区算
thumbnail- libvips 生成缩略图
- 再生成 1440px 预览图
- 视频用 FFmpeg 取帧、转码
clip- Chinese-CLIP 算 1024 维向量
- 自然语言搜索、自动分类
- 美学评分、相似照片
face- SCRFD 找脸、5 个关键点对齐
- ArcFace 算 512 维特征
- 增量归到人物里
ocr- PP-OCR 认出照片里的字
- 按字搜照片
- 判断证件、文档、截图
后台任务调度
没有独立的消息中间件:任务队列就是 PostgreSQL 中的一张 jobs 表,与照片数据在同一事务中写入,确保照片入库、任务入队。worker 通过 SKIP LOCKED 领取任务,用租期机制避免卡死,失败后按平方退避重试。
-- 领取一个任务(server/internal/jobs/worker.go)
UPDATE jobs SET status = 'running', attempts = attempts + 1,
locked_by = $1, locked_until = now() + $2::interval
WHERE id = (
SELECT id FROM jobs
WHERE status = 'pending' AND run_after <= now() AND type = ANY($3)
ORDER BY priority DESC, run_after
FOR UPDATE SKIP LOCKED -- 多个 worker 同时领也不会抢到同一个
LIMIT 1
)
RETURNING id, type, asset_id, payload, attempts, max_attempts;- 同一张照片的同一类任务已经在排队或运行时不再重复入队。
- 队列空时每秒查一次;按 priority 从高到低、到点时间从早到晚领。
- 每个任务的租期(也是超时)10 分钟;进程重启时把自己名下没做完的任务退回队列。
- 有人在上传照片时,AI 任务暂停领取,带宽和 CPU 先给上传。
- AI 服务连不上时整个 AI worker 停下等,不会把任务一个个领出来失败、白白用掉重试次数。
- 「自动」档位会先检测 AI 服务器硬件,选定合适的模型后再领取任务,有助于避免在树莓派等设备上加载超出可用内存的模型。
AI 调度
AI 服务只负责接收预览图,返回人脸、向量和文字。后端决定服务地址、模型和并发数;AI 服务根据硬件选择 CUDA、CoreML / MPS、OpenVINO 或 CPU。
选「自动」时按 AI 服务器的硬件推荐:有可用显卡用高精度;只有 CPU 时,内存不到 6 GB 用轻量,6 GB 及以上用均衡。
| 档位 | 适合的硬件 | 人脸 | 语义搜索 |
|---|---|---|---|
| quality · 高精度 | Apple 芯片 Mac、NVIDIA 显卡 | SCRFD-10G + ArcFace R100(antelopev2) | Chinese-CLIP ViT-H/14 · 1024 维 |
| balanced · 均衡 | Intel i5 级别的 NAS | SCRFD-10G + ArcFace R100(antelopev2) | Chinese-CLIP ViT-L/14 · 768 维 |
| lite · 轻量 | N100、树莓派 5 | SCRFD + ArcFace R50(buffalo_l) | Chinese-CLIP ViT-B/16 · 512 维 |
POST /predict (multipart: image + tasks)
Authorization: Bearer <令牌>
tasks = {
"face": {"model": "antelopev2", "min_score": 0.5},
"clip": {"model": "chinese-clip-vit-h-14"},
"ocr": {}
}
→ {
"face": [{"box": [x, y, w, h], "score": 0.93,
"embedding": [512 个数], "yaw": 0.12, "blur": 340}],
"clip": {"embedding": [1024 个数]},
"ocr": {"text": "...", "lines": [...]},
"model_ids": {"face": "antelopev2", "clip": "chinese-clip-vit-h-14"}
}核心算法
搜索、人物、连拍和 AI 精选背后用到的模型、算法和阈值,有的靠 AI 模型,有的是向量计算和规则。
Chinese-CLIP 把照片和文字放进同一个向量空间:照片入库时算好图片向量,搜索时把「宝宝三岁生日」算成文字向量,在 pgvector 的 HNSW 索引里按余弦距离找最近的照片。
- 三路结果合并:照片里认出这些字的排最前,再是在这个地名拍的,最后是意思最像的。
- 只返回明显对得上的:要和最像的那张差不多像,还要明显比一般照片像,有助于过滤掉相关性弱的照片,而不是返回一堆沾边的结果。
- 搜索词正好是分类名(风景、美食、截图……)时直接返回这一类。
SCRFD 找到脸和 5 个关键点,按关键点对齐裁成 112×112,ArcFace 算出 512 维特征(归一化后点积就是余弦相似度)。每张照片处理完就增量归一次,不用等全部算完。
- 够清楚、够正、够大的「好脸」找最像的脸:有已归人物的按票数归过去;都没归、但像的够 3 张就新建一个人物。
- 模糊、侧脸、太小的脸不新建人物,只在很像某个人时跟着归进去,偶尔出现一次的路人不建。
- 几个任务同时归类时拿同一把数据库咨询锁,不会把一个人建成两个。封面挑清楚(拉普拉斯方差)、正脸(yaw)、够大的单人脸。
- 同一场景接着拍的几张 CLIP 相似度通常在 0.94–0.98,换了场景或姿势一般在 0.7–0.89;以 0.93 作为相似度阈值,有助于找出可能是连拍的照片。
- 推荐保留的那张:有脸时看脸清楚、睁眼看镜头;没脸时看文件大小(同一台相机同样设置,糊的照片细节少、压出来小)。
- 同一个文件存了几份的单独放进「重复」,不用挑。「保留」把其余的放进最近删除,在永久删除之前都能恢复。
不另外跑模型,复用 CLIP 向量:用一组「摄影作品」的描述和一组「普通生活照」的描述各算平均向量,相减得到一个「好看」的方向,照片向量在这个方向上的投影就是它的分。
- 只用来排先后,不当绝对分数,首页的精选轮播按它挑照片。
- 另有一组描述专门分出「拍坏了」的照片(糊、过曝之类),和「好不好看」分开算。
用到的技术
都是成熟的开源组件,没有私有服务,换任何一块都有替代品。
- GoAPI、任务队列、缩略图调度
- libvips缩略图和预览图
- FFmpeg视频取帧、转码
- ExifTool读写照片元数据
- tus断点续传上传
- Python + FastAPI无状态 HTTP 服务
- ONNX Runtime人脸、文字识别推理
- PyTorchChinese-CLIP
- InsightFaceSCRFD + ArcFace
- RapidOCRPP-OCR 文字识别
- PostgreSQL 17业务数据和任务队列
- pgvector向量存储
- HNSW余弦距离索引
- ip2region登录日志的 IP 位置(离线)
- Nominatim地点反查(可关闭)
- React + TypeScript网页
- Vite + Tailwind CSS构建和样式
- MapLibre GL卫星地球
- Docker Composeamd64 / arm64 镜像
- mDNSwanmory.local 局域网域名
隐私与安全
默认情况下,照片和 AI 结果都留在你的私有网络内。如果配置了远程 AI 服务,预览图会发送到该服务器处理。
AI 服务只拿到 1440 像素的预览图,不保存任何文件,不接触数据库、人名和原图。
内置 AI 服务只在 Docker 内部网络里,只有后端连得到;远程 AI 服务设了令牌后,每个请求都要带 Bearer 令牌。
成员只能删除自己上传的照片,管理员能管全部;隐藏的照片只有自己看得到;登录日志按 IP 显示大概位置。
NAS 目录可以用 :ro 只读挂载,相册改不了、删不掉里面的文件。
模型下载完以后识别和搜索都在本地;地点反查、卫星图这些需要联网的功能可以关掉。
InsightFace 人脸模型仅限非商业用途;模型不进镜像,下载前在设置页展示每个模型的许可。