Community画像github.com

kylefritz/photo-skill

Apple Photosライブラリ(または任意の画像フォルダ)内の写真を検索、表示、評価、整理します。「あの写真を探して」「2019年の写真は何枚?」「似た写真を見つけて」といった要求に応えます。

photo-skill とは?

このスキルは、Apple Photosライブラリまたは任意の画像フォルダを操作するための強力なツールセットです。SQL、osxphotos、埋め込みインデックスを組み合わせて10万枚のライブラリから候補を絞り込み、コンタクトシート形式で写真を一覧表示し、AIモデルが各写真を評価します。「赤い鳥居の写真を探して」「この写真に似たものを」「重複写真を探して」といった自然言語での検索が可能で、プライベートなオンデバイス埋め込みインデックスにより写真の内容を理解します。対応プラットフォームはmacOSで、Claude Code、Cursor、CodexなどのAIエージェントと連携して動作します。写真の読み取り専用アクセスが基本で、変更(インポート、日付設定、非表示、お気に入り登録)はユーザーの明示的な要求があった場合のみ行います。iCloud上のオリジナル写真のダウンロードや、共有アルバム、メッセージで共有された写真にも対応しており、プライバシーに配慮してローカルでの処理を徹底しています。

対応✓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に質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

photo-skill は何をしますか?

スクリプトはこのファイルと同じディレクトリの scripts/ にあります。以下では $S と表記しますが、各コマンドでは絶対パスを記述してください(シェル変数は Bash 呼び出し間で保持されないため)。ライブラリのパスは ~/Pictures/Photos Library.photoslibrary ですが、PHOTOS_LIBRARY 環境変数で上書き可能です。

核となる考え方:コードがランク付けと取得を行い、モデルが確認と判断を行います。 SQL、osxphotos、埋め込みインデックスで10万枚のライブラリから数百枚の候補に絞り込み、コンタクトシートでグリッド表示します。あなた(またはサブエージェント)が各タイルを確認し、IDごとに判定を書き込み、スクリプトですべてのIDに正確に1つの回答があることを確認します。

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%のみがディスク上にありました)が、すべてのアイテムにローカルプレビューがあります。プレビューはコンタクトシートやyes/noの判定には十分ですが、テキスト読み取りや顔認識には小さすぎます。オリジナルが必要な場合は 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 を一度実行 → JSONLをgrep30秒 + ミリ秒/アイテム
場所、ラベル、写真内テキスト、GPS範囲(常駐プロセス)オプションの apple-photos MCP(setup.sh --mcp)。必ず library を渡す初回30~40秒、以降1秒未満
シェルからの同様のフィルタリング、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がターミナルでの「他のアプリからのデータアクセス」プロンプトをブロックするため、呼び出しが0% CPUで永久にハングします。--library でスナップショットを指定しても動作しますが、ラベルが欠落したりファイルパスが間違ったりします。

対象となるアイテム

  • ZTRASHEDSTATE = 0 常に。ZKIND = 0 写真、1 動画。
  • ZSAVEDASSETTYPE: 6 = 所有者のiCloudライブラリ(自分の電話)、3 = インポート、4 = 共有アルバム内の他人の写真、12 = メッセージから共有。 「自分が撮った写真」は6または3。共有アイテムは日付範囲に現れるので、含める場合はその旨を伝えてください。
  • ZHIDDEN = 1 のアイテムはフィルタリングされない限りカウントに含まれます。見つかった場合は言及してください。
  • 日付: >= 'YYYY-MM-DD' かつ < '翌日' を 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
    

これにより、0001、0002… とラベル付けされた236pxタイルの6×5グリッド(IDはグローバルで、シートごとではないため、2つのシートのタイルが混同されることはありません)、manifest.json(ID → ファイル、スキップされたアイテムとその理由)、および missing.txt が作成されます。ディスク上にオリジナルがある場合はそれを使用し、ない場合は最大のローカルプレビューを使用します。共有アルバムやメッセージのフォルダも検索します。空でない missing.txt を報告してください:写真が静かに欠落することはありません。フォルダの場合: uv run --no-project --with pillow python -I $S/sheets.py OUT DIR(--cols、--rows、--tile でサイズ変更可能。1枚のシートの長辺は約2500px以下に抑えてください)。

  • シートが少ない場合: 自分で読んでください。
  • シートが多い場合: サブエージェントに分散し、各エージェントにシートの範囲を割り当ててください。ビジョンに優れたモデルを使用してください(テストでは、最小のモデルは甘すぎました)。プロンプトでは、タイルIDごとに1行のJSONL({"id": "0007", "verdict": ...})を要求し、サブエージェントにタイルごとに画像を自分で読むよう指示し、コードを書かないように指示してください。その後、python3 -I $S/check_verdicts.py manifest.json out/*.jsonl を実行します:不足、重複、捏造されたIDがある場合や、すべてのタイルに同じ回答があるシート(通常はサブエージェントが適当に処理したことを示す)をフラグします。そのようなシートは自分で確認してください。
  • ファイルを変更する操作では、2回実行し、一致した場合のみ実行し、変更する各タイルを確認してください。
  • 使用したモデル、エージェント数、おおよそのトークンをプロジェクトのREADMEに記録してください。

写真の内容による検索

プライベートなインデックスが写真ごとに1つの埋め込み(デフォルトはSigLIP 2)を保持しており、ローカルプレビューから構築されるため、iCloudからのダウンロードは発生しません。インデックスは ~/Library/Application Support/photo-skill/index(PHOTO_INDEX_DIR)に保存され、gitには決して含まれません。測定値:M4のMacでApple Photosライブラリから約28枚/秒(10万枚で約1時間、最初に約400MBのモデルダウンロードが必要)、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 で渡します。これにより、埋め込みに「11月の京都の写真」を問い合わせるよりも優れた結果が得られます。
  • キャプションのようにフレーズを作成(「ラーメンの写真」や「ラーメン」など)、キーワードではありません。複数の --text は平均化され、--minus は概念を遠ざけます。
  • Appleのメタデータと組み合わせる。 photo_meta.py は meta.jsonl にAppleのラベル、AIキャプション、地名、人物名、美的スコアを出力します。どこで や 誰が(「…神社」などの地名)には強いですが、見た目(キャプションは「建物の前に立つ人々のグループ」のように曖昧)には弱いです。それをgrepし、インデックス検索と和集合または積集合を取ってください。OCRフィールド(text)はテストしたmacOS 26では空でした。写真内のテキストを検索するには、MCPのテキスト検索を使用するか、オリジナルをダウンロードして自分で読んでください。
  • ほぼ重複: like を1つの例で実行し、スコアを読んでください。≥ 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)は、午前3:30にその日の写真を追加するlaunchdジョブをインストールします。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のダイアログで写真へのアクセス許可を求められます。ユーザーが許可する必要があります。 <scratch>/photokit download OUT_DIR < uuids.txt はUUIDのみを受け取り、iCloudを許可し、各パスを出力し、失敗はstderrに報告します。メッセージ(共有)アイテムはPhotoKitとosxphotosの両方から見えません:プレビューのみが利用可能です。

  • import ALBUM FILE... はPhotos.appのスクリプティングを完全に回避し(起動あたりの上限なし、背後でスタックするエラーダイアログなし)、file<TAB>localIdentifier を出力します。それをログに記録してください。軽くテスト済み:数千のファイルの場合は、最初に数百個を実行し、新しいスナップショットと照合して確認してください。
  • date は未テストです。set_dates.py が実行されるまではそちらを使用してください。

ライブラリの変更(依頼があった場合のみ)

  1. ライブラリの外部にコピーをステージングし(cp -c クローンは無料)、先に修正します:日付と Orientation、動画の Rotation にはexiftoolを使用。
  2. python3 -I $S/import.py --album NAME --log LOG.jsonl [--dry-run] FILES...(または標準入力からパス):1回の呼び出しで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 を確認してください。
  • 新しい Photos.sqlite の横にある古い -wal が再生される可能性があります。snapshot.sh はそのため先に古いファイルをクリアします。
  • /private/tmp のスクラッチパッドはセッション間で消去されます。長期の作業はプロジェクトフォルダに保存してください。

関連スキル