Community影像github.com

photo-skill(Apple Photos 照片技能)

在 Mac 的 Apple Photos 图库(或任意图片文件夹)中查找、查看、评判和组织照片。适用于搜图、按内容搜索、找相似图片、批量排序等场景。

photo-skill(Apple Photos 照片技能) 是什麼?

photo-skill 是一个专为 Mac 上 Apple Photos 图库(或任意图片目录)设计的 AI 辅助照片管理工具。其核心理念是“代码负责排名和获取,模型负责查看和判断”:通过 SQL、osxphotos 和嵌入索引将数万张照片缩小到几百个候选,然后以联系表(contact sheet)形式呈现,用户或子智能体逐张评判。 该技能支持多种查询方式:基于日期、地点、人物、相册的结构化查询(通过 sqlite3 或 osxphotos),基于图片内容的语义搜索(使用本地 SigLIP 2 嵌入索引,不上传任何数据),以及近重复检测和“找更多类似照片”。它还提供了照片下载(photokit)、导入、日期设置、隐藏/收藏等修改功能,以及可选的照片自动更新索引(通过 launchd 实现)。 平台支持:macOS,需要 Apple Photos 图库或任意图片文件夹。首次使用需授予“完全磁盘访问权限”,并运行 setup.sh 完成环境配置。所有照片处理均在本地完成,隐私安全。对于视频处理,可配合 video-skill 使用。

相容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/kylefritz/photo-skill/tree/HEAD

Installed? Explore more 影像 skills: steipete/songsee, affaan-m/frontend-design, mattpocock/improve-codebase-architecture · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

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 生成的 JSONL30 秒 + 每项毫秒级
地点、标签、照片中的文字、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)。此方法会遗漏远处的人群和后脑勺,因此在发布前仍需查看。

查看照片

  1. sh $S/snapshot.sh <scratch>/db
  2. 将 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 直到它被运行过。

修改图库(仅当被要求时)

  1. 在图库外部暂存副本(cp -c 克隆是免费的)并先修复它们:使用 exiftool 处理日期和 Orientation、视频的 Rotation。
  2. python3 -I $S/import.py --album NAME --log LOG.jsonl [--dry-run] FILES...(或从 stdin 读取路径):每次调用 50 个文件,每 1500 个文件重启一次 Photos,遇到第一批短批次时停止,可从 LOG 文件恢复。如果它停止,说明 Photos 有一个错误对话框打开:请用户关闭它,然后重新运行。
  3. 为没有内嵌日期的文件设置日期:python3 -I $S/set_dates.py < id<TAB>YYYY-MM-DD HH:MM:SS。
  4. 隐藏/收藏(AppleScript 无法隐藏):swift $S/set_flags.swift < id<TAB>hide<TAB>fav。隐藏的资源会从获取结果中消失,因此“still visible: 0”表示操作成功。
  5. 对照新的快照验证:每个记录的 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 临时目录会在会话之间被清空;请将长期运行的工作保留在项目文件夹中。

相關技能