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をgrep | 30秒 + ミリ秒/アイテム |
| 場所、ラベル、写真内テキスト、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)。遠くの群衆や後頭部は見逃すため、公開前に確認してください。
写真の表示
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
これにより、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が実行されるまではそちらを使用してください。
ライブラリの変更(依頼があった場合のみ)
- ライブラリの外部にコピーをステージングし(
cp -cクローンは無料)、先に修正します:日付とOrientation、動画のRotationにはexiftoolを使用。 python3 -I $S/import.py --album NAME --log LOG.jsonl [--dry-run] FILES...(または標準入力からパス):1回の呼び出しで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を確認してください。 - 新しい
Photos.sqliteの横にある古い-walが再生される可能性があります。snapshot.shはそのため先に古いファイルをクリアします。 /private/tmpのスクラッチパッドはセッション間で消去されます。長期の作業はプロジェクトフォルダに保存してください。