CommunityImagegithub.com

kylefritz/photo-skill

Rechercher, visualiser, juger et organiser des photos dans la bibliothèque Apple Photos (ou tout dossier d'images) sur un Mac. Utilisez-le pour trouver une photo, une image, une vidéo, un selfie ou une capture d'écran.

Qu'est-ce que photo-skill ?

Ce skill transforme l'agent en un assistant photo avancé pour macOS, capable d'interagir avec la bibliothèque Apple Photos ou des dossiers d'images classiques. Il combine SQL, osxphotos, un index d'embeddings local (SigLIP 2) et des planches-contact générées automatiquement pour réduire une bibliothèque de 100 000 photos à quelques centaines de candidates que l'agent (ou ses sous-agents) peut juger visuellement. Une règle fondamentale est que le code trie et récupère, tandis que le modèle regarde et évalue : chaque photo reçoit un verdict individuel, vérifié pour éviter les erreurs. Le skill prend en charge les recherches par description (texte libre), par similarité visuelle, par métadonnées (dates, albums, personnes, lieux), la détection de doublons proches, l'import de fichiers avec correction des dates, et la modification discrète de la bibliothèque (masquer, favori, album) uniquement sur demande explicite. Il fonctionne avec Claude Code, Cursor et tout agent capable d'exécuter des commandes Bash et Python sur macOS, en respectant strictement la vie privée : les photos ne sont jamais partagées, les EXIF sont nettoyés avant toute sortie de la machine, et la base de données live n'est jamais touchée (seulement un clone APFS instantané).

Compatible avec✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/kylefritz/photo-skill/tree/HEAD

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

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Que fait photo-skill ?

Les scripts se trouvent dans scripts/ à côté de ce fichier ; écrivez-le $S ci-dessous, mais indiquez le chemin absolu dans chaque commande, car les variables shell ne sont pas transmises entre les appels Bash. La bibliothèque est ~/Pictures/Photos Library.photoslibrary sauf si PHOTOS_LIBRARY indique autre chose.

L'idée centrale : le code classe et récupère, le modèle regarde et juge. SQL, osxphotos et l'index d'embeddings réduisent une bibliothèque de 100 000 photos à quelques centaines de candidates ; les planches-contact les placent devant vous sous forme de grilles étiquetées ; vous (ou des sous-agents) jugez chaque vignette et écrivez un verdict par identifiant ; un script vérifie que chaque identifiant a reçu exactement une réponse.

Première fois sur un Mac

Exécutez sh $S/setup.sh vous-même (ajoutez --mcp pour le serveur MCP optionnel, --index pour la tâche d'indexation nocturne). Il vérifie uv, sqlite3, les outils en ligne de commande et la bibliothèque, initialise osxphotos et Pillow, et prouve l'accès complet au disque en prenant un instantané. L'accès complet au disque est la seule chose qu'une personne doit accorder manuellement : Réglages Système → Confidentialité et Sécurité → Accès complet au disque → activez l'application dans laquelle Claude s'exécute (Terminal, Ghostty, l'application Claude...), puis quittez et rouvrez-la.

Règles

  • Lecture seule sauf si le propriétaire demande une modification dans cette conversation (import, date, masquer, favori, album). La lecture seule exclut également osxphotos query --add-to-album et osxphotos import, batch-edit, timewarp, push-exif, sync, add-locations.
  • Ne jamais ouvrir la base Photos.sqlite en direct, même en lecture seule : SQLite écrit son fichier -shm. Interrogez un clone APFS créé par sh $S/snapshot.sh <dossier> (instantané, gratuit). Ne jamais écrire à l'intérieur du .photoslibrary.
  • Télécharger les originaux depuis iCloud est acceptable. Avec « Optimiser le stockage Mac », la plupart des originaux ne sont que dans iCloud (dans une bibliothèque d'environ 100 000 éléments, 1,4 % étaient sur le disque), mais chaque élément a un aperçu local. Les aperçus conviennent pour les planches-contact et les décisions oui/non, mais sont trop petits pour lire du texte ou des visages ; obtenez l'original avec photokit download (~1,7 s chacun).
  • Les photos sont privées. Travaillez dans le répertoire de travail temporaire de la session ou dans un dossier ignoré par git, ne les engagez jamais, et ne publiez pas les photos de quelqu'un (une page web, une PR, un téléchargement) sauf s'il le demande. Supprimez les EXIF (GPS !) de tout ce qui quitte la machine : réenregistrez via Pillow pour le faire.

Choisir l'outil

BesoinUtiliserVitesse
Comptages, dates, albums, personnes, corbeille/caché, boîtes GPS, scores Applesqlite3 sur l'instantané (reference.md contient les requêtes)instantané
Ce qu'il y a dans l'image (« un torii rouge »), plus comme celles-ci, quasi-doublonsphoto_index.sh search / like / query (ci-dessous)~15 s par appel
Les étiquettes Apple, la légende IA, les noms de lieux, les personnes nommées, le score esthétiquephoto_meta.py une fois → grep du JSONL30 s + ms/élément
Lieu, étiquette, texte dans la photo, rayon GPS depuis un processus résidentMCP apple-photos optionnel (setup.sh --mcp). Passez toujours library30-40 s au premier appel, puis <1 s
Les mêmes filtres depuis un shell, métadonnées JSONuvx --from osxphotos osxphotos query --library "$HOME/Pictures/Photos Library.photoslibrary" --mute ... --json~30 s par appel
Plusieurs recherches osxphotosun script : uv run --no-project --with osxphotos, osxphotos.PhotosDB(dbfile=LIB) chargé une fois30 s une fois
Regarder les photosidentifiants → uuid_sheets.sh → lire les planchessecondes
Fichiers en pleine résolutionphotokit download (réutilise un fichier déjà sur le disque)~1,7 s chacun depuis iCloud
Un dossier d'images simple, pas Apple Photossheets.py DOSSIER, et photo_index.sh build DOSSIER
Modifier la bibliothèqueimport.py, set_dates.py, set_flags.swift (ci-dessous)

Donnez toujours le chemin de la bibliothèque à osxphotos et au MCP (--library / "library" / PhotosDB(dbfile=...)). Sans cela, ils cherchent la « dernière bibliothèque ouverte » dans les préférences de Photos à l'intérieur de son conteneur d'application ; macOS bloque cette lecture derrière une invite « accéder aux données depuis d'autres applications » qui ne s'affiche jamais dans un terminal, et l'appel se bloque indéfiniment à 0 % de CPU. Pointer --library vers l'instantané fonctionne aussi, mais les étiquettes sont alors absentes et les chemins de fichiers sont erronés.

Quels éléments compter

  • ZTRASHEDSTATE = 0 toujours. ZKIND = 0 photos, 1 vidéos.
  • ZSAVEDASSETTYPE : 6 = la bibliothèque iCloud du propriétaire (son téléphone), 3 = importé, 4 = photo de quelqu'un d'autre dans un album partagé, 12 = Partagé avec vous depuis Messages. « Photos que j'ai prises » signifie 6 ou 3. Les éléments partagés apparaissent dans les plages de dates, alors mentionnez-les si vous les incluez.
  • Les éléments ZHIDDEN = 1 sont inclus dans les comptages sauf filtrage ; mentionnez-les s'ils apparaissent.
  • Dates : >= 'AAAA-MM-JJ' et < 'jour suivant' sur datetime(ZDATECREATED+978307200,'unixepoch','localtime'). between '…-20' and '…-22' supprime la plupart du 22. Même jour chaque année : strftime('%m-%d', …) = '10-31'. 'localtime' est le fuseau horaire de ce Mac ; ajoutez ZADDITIONALASSETATTRIBUTES.ZTIMEZONEOFFSET pour le fuseau où la photo a été prise.
  • Pas de localisation = -180.0, donc un filtre GPS supprime silencieusement ces éléments ; dites combien ont été exclus.
  • Aucun visage détecté : not exists (select 1 from ZDETECTEDFACE f where f.ZASSETFORFACE = a.Z_PK). Cela manque les foules lointaines et les têtes de dos, alors regardez quand même avant de publier quoi que ce soit.

Regarder les photos

  1. sh $S/snapshot.sh <zone_de_travail>/db
  2. Envoyez la requête d'identifiants directement dans le générateur de planches. Ne listez jamais les identifiants dans la conversation :
    sqlite3 <zone_de_travail>/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 <zone_de_travail>/planches
    

Cela écrit des grilles 6×5 de vignettes de 236 px étiquetées 0001, 0002… (les identifiants sont globaux, pas par planche, donc deux vignettes de planches différentes ne peuvent pas être confondues), un manifest.json (identifiant → fichier, plus tout élément ignoré et pourquoi) et missing.txt. Il utilise l'original quand il est sur le disque, sinon le plus grand aperçu local, et regarde aussi dans les dossiers d'albums partagés et Messages. Signalez un missing.txt non vide : les photos ne disparaissent jamais silencieusement. Pour un dossier : uv run --no-project --with pillow python -I $S/sheets.py SORTIE DOSSIER (--cols, --rows, --tile pour redimensionner ; une planche doit rester sous ~2 500 px sur son côté long).

  • Quelques planches : lisez-les vous-même.
  • Beaucoup de planches : répartissez-les entre sous-agents, chacun recevant un intervalle de planches. Utilisez un modèle bon en vision (en test, le plus petit modèle était trop indulgent). L'invite doit exiger une ligne JSONL par identifiant de vignette ({"id": "0007", "verdict": ...}) et dire au sous-agent de procéder vignette par vignette et de lire les images lui-même, sans écrire de code. Ensuite python3 -I $S/check_verdicts.py manifest.json out/*.jsonl : il échoue sur les identifiants manquants, en double ou inventés et signale les planches où chaque vignette a reçu la même réponse, ce qui signifie généralement que le sous-agent a survolé ; regardez cette planche vous-même.
  • Pour tout ce qui modifiera des fichiers, effectuez deux passages et n'agissez que là où ils sont d'accord, puis regardez chaque vignette que vous allez modifier.
  • Enregistrez le modèle, le nombre d'agents et les tokens approximatifs dans le README du projet.

Rechercher par ce qu'il y a dans l'image

Un index privé contient un embedding par photo (SigLIP 2 par défaut), construit à partir des aperçus locaux, donc rien n'est téléchargé depuis iCloud. Il se trouve dans ~/Library/Application Support/photo-skill/index (PHOTO_INDEX_DIR), jamais dans git. Mesuré : ~28 photos/s depuis une bibliothèque Photos sur un M4 (environ une heure pour 100 000, plus un téléchargement unique du modèle de ~400 Mo), ~70/s depuis un dossier de JPEG.

sh $S/photo_index.sh status
sh $S/photo_index.sh build                          # Apple Photos ; ajoute uniquement ce qui est nouveau, reprenable
sh $S/photo_index.sh build ~/Pictures/scans         # ou tout dossier (utilise son propre PHOTO_INDEX_DIR)
sh $S/photo_index.sh search "un torii rouge" 60 | cut -f1 | sh $S/uuid_sheets.sh <zone_de_travail>/resultats
sh $S/photo_index.sh like <zone_de_travail>/exemples.txt 60                 # les plus proches de tout identifiant exemple
sh $S/photo_index.sh query --text "un bol de ramen" --text "soupe de nouilles" --minus "un menu" --within <zone_de_travail>/voyage.txt --top 40

La sortie est identifiant<TAB>score, meilleur d'abord. L'index classe ; vous jugez. Lisez toujours la planche : une recherche pour « un distributeur automatique » renvoie aussi les réfrigérateurs de supérette et les distributeurs de billets.

  • Scores. Les cosinus texte→photo de SigLIP sont petits (0,10–0,20 pour une bonne correspondance) ; seul l'ordre compte, ne les seuillez jamais. Photo→photo avec le modèle par défaut : ≥ 0,97 même prise, ≥ 0,95 même moment (rafales, reprises), ~0,90 même endroit sous un angle différent, ~0,85 même type de scène. Remesurez après avoir changé le modèle.
  • Filtrez d'abord, puis classez. Les dates, lieux, albums, personnes et partagé-vs-personnel viennent de SQL sur l'instantané ; écrivez ces identifiants dans un fichier et passez --within. Cela bat la demande d'embedding pour « photos de Kyoto en novembre ».
  • Formulez comme une légende (« une photo d'un bol de ramen » ou « un bol de ramen »), pas comme des mots-clés. Plusieurs formulations --text sont moyennées ; --minus éloigne un concept.
  • Combinez avec les métadonnées Apple. photo_meta.py écrit meta.jsonl avec les propres étiquettes de Photos, légende IA, nom de lieu, personnes nommées et score esthétique. C'est fort sur où et qui (noms de lieux comme « … Sanctuaire »), faible sur à quoi ça ressemble (les légendes sont génériques : « un groupe de personnes debout devant un bâtiment »). Utilisez grep, puis union ou intersection avec une recherche d'index. Son champ OCR (text) est revenu vide sur macOS 26 lors des tests ; pour le texte dans les photos, utilisez la recherche textuelle du MCP ou téléchargez l'original et lisez-le vous-même.
  • Quasi-doublons : like avec un exemple et lisez les scores ; les paires ≥ 0,95 sont des reprises. Ne dédupliquez jamais par nom+taille (les copies sont renommées et réencodées).
  • Changer le modèle (PHOTO_INDEX_MODEL, tout checkpoint SigLIP/SigLIP 2) : l'index enregistre son modèle dans model.txt et refuse de mélanger deux modèles ; construisez le nouveau dans son propre PHOTO_INDEX_DIR. README.md compare les modèles.

Mises à jour nocturnes (optionnelles) : sh $S/photo_index.sh install-nightly (ou setup.sh --index) installe une tâche launchd qui ajoute les photos du jour à 3h30. Les tâches launchd n'ont pas l'accès complet au disque du terminal, donc la tâche démarre un petit lanceur (photo-index-runner, compilé à partir de index_runner.c) qui a besoin de sa propre autorisation : la commande ouvre Réglages Système à Accès complet au disque et révèle le fichier dans le Finder ; la personne clique sur +, le sélectionne, l'active. Vérifiez avec launchctl kickstart gui/$(id -u)/local.photo-skill.index, puis photo_index.sh status. « Operation not permitted » dans le journal signifie que l'autorisation manque. Réexécuter install-nightly réécrit la tâche mais conserve le lanceur, car le reconstruire perd l'autorisation.

Aide PhotoKit

Compilez une fois par session : swiftc -O $S/photokit.swift -o <zone_de_travail>/photokit. Sa première exécution demande l'accès à Photos dans une boîte de dialogue macOS que la personne devant le Mac doit autoriser. <zone_de_travail>/photokit download DOSSIER_SORTIE < uuids.txt prend les UUID bruts, autorise iCloud, imprime chaque chemin et signale les échecs sur stderr. Les éléments de Messages (Partagé avec vous) sont invisibles pour PhotoKit et osxphotos : leur aperçu est tout ce qui existe.

  • import ALBUM FICHIER... saute complètement le scripting de Photos.app (pas de limite par lancement, pas de boîte de dialogue d'erreur qui se bloque derrière) et imprime fichier<TAB>localIdentifier ; enregistrez-le. Légèrement testé : pour des milliers de fichiers, faites d'abord quelques centaines et vérifiez-les avec un instantané récent.
  • date n'est pas testé ; utilisez set_dates.py jusqu'à ce qu'il ait été exécuté.

Modifier la bibliothèque (uniquement sur demande)

  1. Staguez des copies en dehors de la bibliothèque (les clones cp -c sont gratuits) et corrigez-les d'abord : exiftool pour les dates et Orientation, Rotation pour les vidéos.
  2. python3 -I $S/import.py --album NOM --log LOG.jsonl [--dry-run] FICHIERS... (ou chemins sur stdin) : 50 fichiers par appel, redémarre Photos toutes les 1 500, s'arrête au premier lot court, reprenable depuis LOG. S'il s'arrête, Photos a une boîte de dialogue d'erreur ouverte : demandez à la personne de la fermer, puis réexécutez.
  3. Dates pour les fichiers sans date intégrée : python3 -I $S/set_dates.py < identifiant<TAB>AAAA-MM-JJ HH:MM:SS.
  4. Caché/Favori (AppleScript ne peut pas cacher) : swift $S/set_flags.swift < identifiant<TAB>cacher<TAB>fav. Un élément caché disparaît des fetchs, donc « still visible: 0 » signifie que ça a fonctionné.
  5. Vérifiez avec un instantané frais : chaque identifiant enregistré présent, aucun à la corbeille, comptes d'albums corrects.

Photos dans un film

Pour un diaporama, montage ou toute vidéo créée à partir de photos, utilisez le skill video-skill (github.com/kylefritz/video-skill ; s'il n'est pas installé, proposez de le cloner dans ~/.claude/skills/video-skill). La part de ce skill : trouver et juger les prises de vue, photokit download les originaux (les aperçus sont trop petits pour du 1080p), les réenregistrer sans EXIF, et regarder chacun en taille réelle pour les personnes avant qu'il n'entre dans un film. video-skill lit la vidéo de la même manière que ce skill lit les photos, donc ses planches d'images fonctionnent avec check_verdicts.py.

Pièges

  • Les dates Photos sont en secondes depuis le 2001-01-01 : ajoutez 978307200. Comparez les chaînes datetime(...) ; comparer un nombre à strftime('%s', ...) (texte) ne correspond silencieusement à rien.
  • ZCLOUDLOCALSTATE ne signifie pas que l'original est sur le disque ; vérifiez le fichier.
  • Les anciens appareils sans capteur d'orientation stockaient les portraits de travers ; corrigez avec la balise EXIF Orientation (sans perte).
  • Les coordonnées des visages dans Photos ont leur origine en bas à gauche : y = (1 - center_y) * height.
  • Ouvrir un placeholder Dropbox ou iCloud Drive bloque indéfiniment. Vérifiez d'abord st_blocks > 0.
  • Un fichier -wal obsolète à côté d'un Photos.sqlite frais est rejoué par-dessus ; snapshot.sh efface d'abord les anciens fichiers pour cette raison.
  • Les répertoires de travail /private/tmp sont effacés entre les sessions ; conservez les travaux longs dans un dossier de projet.

Skills associés