Community图像github.com

kylefritz/photo-skill

在 Mac 上的 Apple 照片图库(或任意图片文件夹)中查找、查看、评判和整理照片。当用户想要查找特定照片、根据画面内容搜索、找相似图片、批量评分或管理照片时使用。

photo-skill 是什么?

**photo-skill** 是一个专为 Mac 上 Apple 照片图库设计的 AI 辅助管理工具。其核心理念是「代码负责排序和获取,模型负责查看和评判」:通过 SQL 查询、osxphotos 和私有嵌入索引将 10 万张照片快速缩小到几百个候选,然后用联系表(contact sheets)将候选照片以带标签的网格形式呈现给用户或子代理进行逐张评分,最终脚本确保每个 ID 都有且只有一个评判结果。 该技能支持 Apple 照片图库、任意图片文件夹,以及 iCloud 原片下载。它提供基于画面内容的语义搜索(使用本地 SigLIP 2 嵌入索引,不依赖 iCloud)、近重复检测、按日期/地点/人物/相册筛选、隐藏/删除照片查看、导入文件并设置日期、隐藏或标记收藏等功能。所有操作遵循严格的安全规则:除非用户要求更改,否则只读;从不直接访问实时数据库,而是使用 APFS 快照;可选的夜间索引更新任务需要额外的全磁盘访问授权。 **支持的平台:** macOS(需要 Apple 照片图库或本地图片文件夹)。 **工作原理:** 1) 使用 `sqlite3` 对快照进行快速计数和筛选;2) 使用 `photo_index.sh` 进行语义搜索和相似度查询;3) 使用 `uuid_sheets.sh` 生成联系表供视觉模型评判;4) 使用 `photokit` 工具下载 iCloud 中的原片;5) 使用 `check_verdicts.py` 验证评分结果。所有关键操作均通过 Swift 脚本或 Python 脚本在本地执行。 **使用场景:** 查找特定照片(“找那张东京塔的夜拍”)、根据画面内容搜索(“一张红色鸟居的照片”)、整理照片(按日期/人物/地点)、删除重复或相似照片、批量设置日期或标记,以及将照片用于视频制作(与 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 万张照片的图库缩小到几百个候选;联系表(contact sheets)将这些候选照片以带标签的网格形式呈现给你;你(或子代理)评判每个图块并为每个 ID 写下评判结果;然后脚本检查每个 ID 是否都得到了唯一一个答案。

首次在 Mac 上使用

自行运行 sh $S/setup.sh(添加 --mcp 可安装可选的 MCP 服务器,--index 可安装夜间索引任务)。该脚本会检查 uv、sqlite3、命令行工具和照片库,预热 osxphotos 和 Pillow,并通过拍摄快照来验证全磁盘访问权限。全磁盘访问是唯一需要用户手动授权的步骤:系统设置 → 隐私与安全性 → 全磁盘访问 → 开启 Claude 运行的应用程序(Terminal、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 照片)sheets.py DIR,以及 photo_index.sh build DIR
更改照片库import.py、set_dates.py、set_flags.swift(见下文)

始终给 osxphotos 和 MCP 传递照片库路径(--library / "library" / PhotosDB(dbfile=...))。不传递路径时,它们会在照片应用偏好设置(位于其应用容器内)中查找“最后打开的照片库”;macOS 会阻止该读取操作,背后隐藏着一个“允许访问其他应用的数据”提示(该提示在终端中永远不会显示),导致调用永远挂起(CPU 0%)。将 --library 指向快照也可以,但那样会缺失标签且文件路径不正确。

哪些项目计入统计

  • ZTRASHEDSTATE = 0 始终如此。ZKIND = 0 表示照片,1 表示视频。
  • ZSAVEDASSETTYPE:6 = 照片所有者的 iCloud 图库(其手机上的),3 = 已导入,4 = 共享相册中其他人的照片,12 = 通过信息共享给你的照片。“我拍的照片”意味着 6 或 3。共享项目会出现在日期范围内,因此如果包含它们,请说明。
  • ZHIDDEN = 1 的项目默认会包含在计数中;如果有隐藏项目出现,请指出。
  • 日期:在 datetime(ZDATECREATED+978307200,'unixepoch','localtime') 上使用 >= 'YYYY-MM-DD' 且 < 'next day'。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。脚本会优先使用本地磁盘上的原片,如果没有则使用最大的本地预览,同时也会在共享相册和信息文件夹中查找。如果 missing.txt 非空,请报告:照片永远不应静默丢失。对于文件夹:uv run --no-project --with pillow python -I $S/sheets.py OUT DIR(可使用 --cols、--rows、--tile 调整大小;单个联系表的长边应保持在约 2,500 像素以下)。

  • 少量联系表: 自行阅读。
  • 大量联系表: 分发给子代理,每个子代理分配一定范围的联系表。使用视觉能力强的模型(测试中发现最小的模型过于宽容)。提示中必须要求每个图块输出一行 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 照片库构建索引约 28 张/秒(10 万张照片约需 1 小时,另加一次约 400 MB 模型下载),从 JPEG 文件夹构建约 70 张/秒。

sh $S/photo_index.sh status
sh $S/photo_index.sh build                          # Apple 照片库;仅添加新增内容,可中断恢复
sh $S/photo_index.sh build ~/Pictures/scans         # 或任意文件夹(使用其自己的 PHOTO_INDEX_DIR)
sh $S/photo_index.sh search "a red torii gate" 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 "a bowl of ramen" --text "noodle soup" --minus "a menu" --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 参数。这比要求嵌入模型搜索“11 月京都的照片”效果更好。
  • 像写描述一样措辞(“a photo of a bowl of ramen” 或 “a bowl of ramen”),而不是关键词。多个 --text 表述会被取平均;--minus 用于排除某个概念。
  • 结合 Apple 的元数据。 photo_meta.py 会输出 meta.jsonl,包含 Apple 照片自身的标签、AI 描述、地名、已命名人物和美学评分。它在地点和人物方面表现很好(地名如“… Shrine”),但在外观方面较弱(描述通常比较笼统:“一群人站在建筑物前”)。可以先 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 弹窗中请求照片访问权限,需要 Mac 用户手动允许。 <scratch>/photokit download OUT_DIR < uuids.txt 接受纯 UUID 列表,允许从 iCloud 下载,打印每个文件的路径,并将失败信息输出到 stderr。Messages(通过信息共享给你的照片)项目对 PhotoKit 和 osxphotos 均不可见:它们只有预览图可用。

  • import ALBUM FILE... 完全跳过 AppleScript 脚本控制(没有每启动次数限制,没有会卡在后面的错误对话框),输出 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 个文件,每处理 1,500 个文件重启一次照片应用,在第一批较少数量的文件后停止,可从日志文件恢复。如果它停止,说明照片应用有一个错误对话框打开:请用户关闭该对话框,然后重新运行。
  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。隐藏的资源会从获取结果中消失,因此“仍然可见:0”表示操作成功。
  5. 对照新的快照进行验证:所有记录的 ID 都已存在,没有已删除的,相册计数正确。

将照片制作成影片

如需制作幻灯片、蒙太奇或其他使用照片的视频,请使用 video-skill 技能(github.com/kylefritz/video-skill;如果未安装,可克隆到 ~/.claude/skills/video-skill)。本技能的作用:找到并评判镜头,使用 photokit download 下载原片(预览图对于 1080p 来说太小),重新保存时去除 EXIF 信息,并在放入影片前全尺寸查看每张照片中的人物。video-skill 读取视频的方式与本技能读取照片的方式相同,因此其帧联系表可以与 check_verdicts.py 配合使用。

注意事项

  • 照片的日期是从 2001-01-01 开始的秒数:需加上 978307200。比较 datetime(...) 字符串;将数字与 strftime('%s', ...)(文本类型)进行比较会静默地不匹配任何结果。
  • ZCLOUDLOCALSTATE 并不表示原片在磁盘上;请检查文件本身是否存在。
  • 没有方向传感器的旧相机拍摄的竖屏照片可能被存储为横屏;使用 EXIF 的 Orientation 标签修复(无损)。
  • 照片中人脸坐标的原点在左下角:y = (1 - center_y) * height。
  • 打开 Dropbox 或 iCloud Drive 的占位符文件会永久阻塞。请先检查 st_blocks > 0。
  • 在最新的 Photos.sqlite 旁边如果存在过时的 -wal 文件,会回放覆盖;snapshot.sh 在创建快照前会先清除旧文件。
  • /private/tmp 暂存区会在会话之间被清空;请将长期运行的工作保存在项目文件夹中。

相关技能