photo-skill(照片技能)
脚本位于 scripts/ 目录(与本文档同级);下面用 $S 表示该目录,但实际使用时请写出绝对路径,因为 shell 变量在 Bash 调用之间不传递。库路径为 ~/Pictures/Photos Library.photoslibrary,除非通过 PHOTOS_LIBRARY 环境变量指定。
核心理念:代码负责排名和获取,模型负责查看和判断。 SQL、osxphotos 和嵌入索引将 10 万张照片的图库缩小到几百个候选;联系表将这些候选以带标签的网格形式呈现给您;您(或子智能体)逐张评判每个图块并为每个 id 写下裁决;脚本会检查每个 id 是否恰好得到一个答案。
首次在 Mac 上使用
请自行运行 sh $S/setup.sh(添加 --mcp 可安装可选的 MCP 服务器,添加 --index 可安装夜间索引任务)。它会检查 uv、sqlite3、命令行工具和库,预热 osxphotos 和 Pillow,并通过拍摄快照来验证“完全磁盘访问权限”。“完全磁盘访问权限”是唯一需要用户手动授予的权限:系统设置 → 隐私与安全性 → 完全磁盘访问权限 → 打开 Claude 运行所在的应用程序(终端、Ghostty、Claude 应用……)的开关,然后退出并重新打开该应用。
规则
- 只读操作,除非所有者在本次对话中要求更改(导入、日期、隐藏、收藏、相册)。只读也禁止使用
osxphotos query --add-to-album以及 osxphotos 的import、batch-edit、timewarp、push-exif、sync、add-locations。 - 切勿打开正在使用的
Photos.sqlite,即使是只读也不行:SQLite 会写入其-shm文件。请查询由sh $S/snapshot.sh <dir>创建的 APFS 克隆(即时、免费)。切勿在.photoslibrary内部写入。 - 从 iCloud 下载原件是可以的。 使用“优化 Mac 存储”时,大多数原件仅存储在 iCloud 中(在一个约 10 万项的图库中,仅 1.4% 在磁盘上),但每个项目都有本地预览。预览图适用于联系表和是/否判断,但对于读取文本或人脸则尺寸太小;请使用
photokit download获取原件(每张约 1.7 秒)。 - 照片是私密的。 请在会话临时目录或已加入 .gitignore 的文件夹中工作,绝不提交它们,也不要发布他人的照片(网页、PR、上传),除非对方要求。从机器输出的任何内容都应剥离 EXIF(包括 GPS!):通过 Pillow 重新保存即可。
选择工具
| 需求 | 使用工具 | 速度 |
|---|---|---|
| 计数、日期、相册、人物、回收站/隐藏、GPS 范围、Apple 评分 | 对快照使用 sqlite3(reference.md 中有查询示例) | 即时 |
| 图片内容(“红色鸟居”)、找相似、近重复 | photo_index.sh search / like / query(见下文) | 每次调用约 15 秒 |
| Apple 自带标签、AI 标题、地名、命名人物、美学评分 | 运行一次 photo_meta.py → 然后 grep 生成的 JSONL | 30 秒 + 每项毫秒级 |
| 地点、标签、照片中的文字、GPS 半径(通过常驻进程) | 可选的 apple-photos MCP(setup.sh --mcp)。务必传递 library | 首次调用 30-40 秒,之后 <1 秒 |
| 来自 shell 的相同过滤条件、JSON 格式元数据 | uvx --from osxphotos osxphotos query --library "$HOME/Pictures/Photos Library.photoslibrary" --mute ... --json | 每次调用约 30 秒 |
| 多次 osxphotos 查询 | 单个脚本:uv run --no-project --with osxphotos,osxphotos.PhotosDB(dbfile=LIB) 加载一次 | 一次性 30 秒 |
| 查看照片 | id → uuid_sheets.sh → 读取联系表 | 几秒 |
| 全分辨率文件 | photokit download(重用已在磁盘上的文件) | 从 iCloud 下载每张约 1.7 秒 |
| 纯图片文件夹(非 Apple Photos) | sheets.py DIR,以及 photo_index.sh build DIR | |
| 修改图库 | import.py、set_dates.py、set_flags.swift(见下文) |
始终为 osxphotos 和 MCP 提供图库路径(--library / "library" / PhotosDB(dbfile=...))。如果不提供,它们会从 Photos 应用偏好设置中查找“最后打开的图库”(在它的应用容器内);macOS 会阻止这种读取,并弹出一个“访问其他应用数据”的提示(该提示在终端中永远不会显示),导致调用永远挂起(CPU 为 0%)。将 --library 指向快照也可以,但那样会丢失标签且文件路径不正确。
哪些项目计入统计
ZTRASHEDSTATE = 0始终如此。ZKIND = 0表示照片,1表示视频。ZSAVEDASSETTYPE:6 = 所有者的 iCloud 图库(他们的手机),3 = 已导入,4 = 共享相册中他人的照片,12 = 来自“与你共享”的 Messages。 “我拍的照片”指 6 或 3。共享项目会出现在日期范围中,因此如果包含它们请说明。ZHIDDEN = 1的项目在计数时会包含,除非进行过滤;如果有任何出现请提及。- 日期:
>= 'YYYY-MM-DD'且< 'next day'(使用datetime(ZDATECREATED+978307200,'unixepoch','localtime'))。between '…-20' and '…-22'会丢失 22 日的大部分数据。每年的同一天:strftime('%m-%d', …) = '10-31'。'localtime'是当前 Mac 的时区;要获取拍摄时的时区,请加上ZADDITIONALASSETATTRIBUTES.ZTIMEZONEOFFSET。 - 无位置信息时值为
-180.0,因此 GPS 过滤会静默排除这些项目;请说明有多少被排除。 - 未检测到人脸:
not exists (select 1 from ZDETECTEDFACE f where f.ZASSETFORFACE = a.Z_PK)。此方法会遗漏远处的人群和后脑勺,因此在发布前仍需查看。
查看照片
sh $S/snapshot.sh <scratch>/db- 将 id 查询直接通过管道传入联系表构建脚本。切勿将 id 列表打印到对话中:
sqlite3 <scratch>/db/Photos.sqlite "select ZUUID from ZASSET where ZTRASHEDSTATE=0 and ZKIND=0 and datetime(ZDATECREATED+978307200,'unixepoch','localtime') >= '2024-10-31' and datetime(ZDATECREATED+978307200,'unixepoch','localtime') < '2024-11-01'" | sh $S/uuid_sheets.sh <scratch>/sheets
上述命令会生成 6×5 的 236 像素图块网格,每个图块标记为 0001、0002……(id 是全局唯一的,不会在不同联系表间混淆),同时生成 manifest.json(id → 文件,以及任何跳过项及其原因)和 missing.txt。它会优先使用磁盘上的原件,否则使用最大的本地预览,并会查找共享相册和 Messages 文件夹。非空的 missing.txt 需要报告:照片不会无声消失。对于文件夹:uv run --no-project --with pillow python -I $S/sheets.py OUT DIR(可用 --cols、--rows、--tile 调整大小;每张联系表的长边最好保持在约 2500 像素以下)。
- 少量联系表: 自己阅读。
- 大量联系表: 分发给子智能体,每个智能体分配一个联系表范围。使用擅长视觉的模型(测试中,最小模型过于宽容)。提示词必须要求每张图块 id 一行 JSONL(
{"id": "0007", "verdict": ...}),并告诉子智能体逐块查看并自己读取图像,不要编写代码。然后运行python3 -I $S/check_verdicts.py manifest.json out/*.jsonl:它会检查缺失、重复或虚构的 id,并标记那些所有图块都得到相同答案的联系表(这通常意味着子智能体略读);请亲自查看这些联系表。 - 对于任何会更改文件的操作,请执行两轮,仅对结果一致的地方执行操作,然后查看所有将要更改的图块。
- 在项目的 README 中记录模型、智能体数量和大致的 token 数。
按图片内容搜索
一个私有索引为每张照片保存一个嵌入向量(默认使用 SigLIP 2),该索引基于本地预览构建,因此不会从 iCloud 下载任何数据。它位于 ~/Library/Application Support/photo-skill/index(可通过 PHOTO_INDEX_DIR 环境变量覆盖),绝不会放入 git 仓库。实测性能:在 M4 上从 Apple Photos 图库构建约为 28 张/秒(10 万张约需一小时,外加一次约 400 MB 的模型下载),从 JPEG 文件夹构建约为 70 张/秒。
sh $S/photo_index.sh status
sh $S/photo_index.sh build # Apple Photos;仅添加新增内容,可恢复
sh $S/photo_index.sh build ~/Pictures/scans # 或任意文件夹(使用自己的 PHOTO_INDEX_DIR)
sh $S/photo_index.sh search "红色的鸟居" 60 | cut -f1 | sh $S/uuid_sheets.sh <scratch>/hits
sh $S/photo_index.sh like <scratch>/examples.txt 60 # 寻找与给定示例 id 最近的图片
sh $S/photo_index.sh query --text "一碗拉面" --text "拉面汤" --minus "菜单" --within <scratch>/trip.txt --top 40
输出格式为 id<TAB>score,最佳匹配在前。索引负责排名;您负责评判。 务必阅读联系表:搜索“自动售货机”也会返回便利店冰箱和售票机的图片。
- 分数。 SigLIP 的文本→图片余弦相似度较小(好的匹配约为 0.10–0.20);只有排序有意义,因此切勿使用阈值。默认模型下的图片→图片相似度:≥ 0.97 为同一张照片,≥ 0.95 为同一瞬间(连拍、重拍),约 0.90 为同一地点不同角度拍摄,约 0.85 为同一类场景。更换模型后请重新测量这些值。
- 先过滤,再排序。 日期、地点、相册、人物以及共享/自己的照片来自快照上的 SQL 查询;将这些 id 写入文件并传递
--within。这比让嵌入向量搜索“十一月的京都照片”效果更好。 - 像写标题一样描述(“一张拉面碗的照片”或“一碗拉面”),而不是使用关键词。多个
--text短语会被平均;--minus用于排除某个概念。 - 与 Apple 的元数据结合使用。
photo_meta.py会写入meta.jsonl,其中包含 Photos 自身的标签、AI 标题、地名、命名人物和美学评分。它在地点和人物方面表现强劲(例如地名“…神社”),但在外观描述方面较弱(标题往往很泛泛,如“一群人站在建筑物前”)。对此进行 grep,然后与索引搜索结果取并集或交集。它的 OCR 字段(text)在 macOS 26 上的测试中返回为空;如需照片中的文字,请使用 MCP 的文字搜索或下载原件自行阅读。 - 近重复检测: 使用
like并传入一个示例,然后阅读得分;≥ 0.95 的对是连拍/重拍。切勿通过名称+大小去重(副本会被重命名和重新编码)。 - 更换模型(通过
PHOTO_INDEX_MODEL环境变量,任何 SigLIP/SigLIP 2 检查点):索引会在model.txt中记录其使用的模型,并拒绝混合使用两种模型;请在新的PHOTO_INDEX_DIR中构建新的索引。README.md中有模型对比信息。
夜间更新(可选):运行 sh $S/photo_index.sh install-nightly(或 setup.sh --index)会安装一个 launchd 任务,在凌晨 3:30 添加当天的照片。launchd 任务没有终端的“完全磁盘访问权限”,因此该任务会启动一个微小的启动器(photo-index-runner,从 index_runner.c 构建),该启动器需要单独授权:命令会打开系统设置的“完全磁盘访问权限”并在 Finder 中显示该文件;用户点击 + 号,选择它,然后打开开关。使用 launchctl kickstart gui/$(id -u)/local.photo-skill.index 检查,然后运行 photo_index.sh status。日志中出现“Operation not permitted”意味着授权缺失。重新运行 install-nightly 会重写任务但保留启动器,因为重新构建启动器会丢失授权。
PhotoKit 辅助工具
每会话编译一次:swiftc -O $S/photokit.swift -o <scratch>/photokit。首次运行时会通过 macOS 对话框请求 Photos 访问权限,需要 Mac 用户允许。<scratch>/photokit download OUT_DIR < uuids.txt 接受纯 UUID,允许从 iCloud 下载,打印每个文件的路径,并在 stderr 上报告失败。Messages(“与你共享”)中的项目对 PhotoKit 和 osxphotos 均不可见:其预览是唯一可用的数据。
import ALBUM FILE...完全绕过 Photos.app 脚本接口(没有每次启动的上限、没有卡在后面的错误对话框),并打印file<TAB>localIdentifier;请记录这些信息。轻量测试:对于数千个文件,先处理几百个并对照新的快照检查它们。date未经测试;请使用set_dates.py直到它被运行过。
修改图库(仅当被要求时)
- 在图库外部暂存副本(
cp -c克隆是免费的)并先修复它们:使用 exiftool 处理日期和Orientation、视频的Rotation。 python3 -I $S/import.py --album NAME --log LOG.jsonl [--dry-run] FILES...(或从 stdin 读取路径):每次调用 50 个文件,每 1500 个文件重启一次 Photos,遇到第一批短批次时停止,可从 LOG 文件恢复。如果它停止,说明 Photos 有一个错误对话框打开:请用户关闭它,然后重新运行。- 为没有内嵌日期的文件设置日期:
python3 -I $S/set_dates.py < id<TAB>YYYY-MM-DD HH:MM:SS。 - 隐藏/收藏(AppleScript 无法隐藏):
swift $S/set_flags.swift < id<TAB>hide<TAB>fav。隐藏的资源会从获取结果中消失,因此“still visible: 0”表示操作成功。 - 对照新的快照验证:每个记录的 id 都存在、没有在回收站中、相册计数正确。
将照片制作成影片
如需制作幻灯片、蒙太奇或任何由照片生成的视频,请使用 video-skill(github.com/kylefritz/video-skill;如果尚未安装,可提供克隆到 ~/.claude/skills/video-skill 的建议)。此技能负责的部分:查找和评判镜头,photokit download 原件(预览图对于 1080p 来说太小),重新保存(剥离 EXIF),并在放入影片前全尺寸查看每张照片中的人物。video-skill 读取视频的方式与此技能读取照片的方式相同,因此其帧联系表可与 check_verdicts.py 一起使用。
注意事项
- Photos 的日期是相对于 2001-01-01 的秒数:需要加上
978307200。使用datetime(...)字符串进行比较;如果将数字与strftime('%s', ...)(文本)比较,会静默地不匹配任何结果。 ZCLOUDLOCALSTATE并不表示原件在磁盘上;请检查文件是否存在。- 没有方向传感器的旧相机拍摄的竖屏照片会横向存储;可以使用 EXIF 的
Orientation标签修复(无损)。 - Photos 中的人脸坐标原点在左下角:
y = (1 - center_y) * height。 - 打开 Dropbox 或 iCloud Drive 的占位符会永久阻塞。请先检查
st_blocks > 0。 - 旧的
-wal文件与新的Photos.sqlite共存时,wal 文件的内容会被回放覆盖;为此snapshot.sh会先清除旧文件。 /private/tmp临时目录会在会话之间被清空;请将长期运行的工作保留在项目文件夹中。