CommunityBildgithub.com

kylefritz/photo-skill

Fotos in einer Apple Photos Bibliothek (oder einem beliebigen Bildordner) auf dem Mac finden, betrachten, bewerten und organisieren.

Was ist photo-skill?

Dieses Skill ermöglicht es KI-Agenten (wie Claude Code), auf einem Mac mit der Apple Photos Bibliothek zu interagieren, ohne die Privatsphäre des Nutzers zu gefährden. Der Kernansatz ist „Code bewertet und ruft ab, das Modell schaut und beurteilt“: SQL-Abfragen und ein lokaler Embedding-Index (SigLIP/SigLIP 2) reduzieren eine Bibliothek von über 100.000 Fotos auf eine handverlesene Auswahl, die dann als Kontaktbögen (gerasterte Vorschaubilder) dargestellt wird. Der Agent (oder Unteragenten) beurteilt jedes Bild einzeln, was eine effiziente und skalierbare Bildanalyse ermöglicht. Das Skill unterstützt verschiedene Operationen, darunter die Suche nach Bildinhalten („ein rotes Torii-Tor“), das Erkennen von nahen Duplikaten, das Durchsuchen von Gesichtern, Texten in Fotos und Metadaten wie Daten, Orten und Bewertungen. Es arbeitet mit sqlite3, osxphotos, einem optionalen MCP-Server und einer Reihe von Hilfsskripten zusammen. Änderungen an der Bibliothek (Importieren, Datumsänderungen, Ausblenden/Markieren) werden nur auf expliziten Wunsch des Benutzers durchgeführt, und Fotos verlassen den Computer nie ohne Entfernung von EXIF-Daten (GPS). Es ist für macOS optimiert und erfordert Vollzugriff auf die Festplatte sowie einmalige Berechtigungen für FotoKit.

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

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

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Was macht photo-skill?

Die Skripte liegen in scripts/ neben dieser Datei; nachfolgend als $S bezeichnet, aber schreibe den absoluten Pfad in jedem Befehl aus, da Shell-Variablen nicht zwischen Bash-Aufrufen übertragen werden. Die Bibliothek ist ~/Pictures/Photos Library.photoslibrary, sofern PHOTOS_LIBRARY nichts anderes angibt.

Die Kernidee: Code bewertet und ruft ab, das Modell schaut und beurteilt. SQL, osxphotos und der Embedding-Index reduzieren eine Bibliothek mit 100.000 Fotos auf ein paar hundert Kandidaten; Kontaktbögen legen sie dir als beschriftete Raster vor; du (oder Unteragenten) beurteilen jede Kachel und schreiben ein Urteil pro ID; ein Skript prüft, dass jede ID genau eine Antwort erhalten hat.

Erstmalig auf einem Mac

Führe selbst sh $S/setup.sh aus (füge --mcp für den optionalen MCP-Server hinzu, --index für den nächtlichen Index-Job). Es prüft uv, sqlite3, die Command Line Tools und die Bibliothek, wärmt osxphotos und Pillow auf und beweist den Vollzugriff auf die Festplatte durch die Erstellung eines Schnappschusses. Der Vollzugriff auf die Festplatte ist das Einzige, was nur eine Person gewähren kann: Systemeinstellungen → Datenschutz & Sicherheit → Vollzugriff auf die Festplatte → schalte die App ein, in der Claude läuft (Terminal, Ghostty, die Claude-App...), beende sie dann und öffne sie erneut.

Regeln

  • Nur lesend, es sei denn, der Besitzer bittet in diesem Gespräch um eine Änderung (importieren, Datum, ausblenden, favorisieren, Album). Schreibgeschützt schließt auch osxphotos query --add-to-album und osxphotos import, batch-edit, timewarp, push-exif, sync, add-locations aus.
  • Öffne niemals die live Photos.sqlite, nicht einmal schreibgeschützt: SQLite schreibt seine -shm-Datei. Führe eine Abfrage auf einem APFS-Klon durch, der mit sh $S/snapshot.sh <verz> erstellt wurde (sofort, kostenlos). Schreibe niemals in das .photoslibrary.
  • Das Herunterladen von Originalen aus iCloud ist in Ordnung. Bei „Mac-Speicher optimieren“ sind die meisten Originale nur in iCloud (in einer Bibliothek mit ~100.000 Elementen waren 1,4 % auf der Festplatte), aber jedes Element hat eine lokale Vorschau. Vorschauen sind für Kontaktbögen und Ja/Nein-Entscheidungen geeignet, aber zu klein zum Lesen von Text oder Gesichtern; lade das Original mit photokit download herunter (~1,7 s pro Stück).
  • Fotos sind privat. Arbeite im Sitzungs-Scratchpad oder in einem gitignorierten Ordner, übertrage sie niemals und veröffentliche keine Fotos von Personen (Webseite, PR, Upload), es sei denn, sie bitten darum. Entferne EXIF (GPS!) von allem, was den Rechner verlässt: erneutes Speichern durch Pillow erledigt das.

Wähle das Werkzeug

BedarfVerwendeGeschwindigkeit
Zählungen, Daten, Alben, Personen, Papierkorb/ausgeblendet, GPS-Boxen, Apple-Bewertungensqlite3 auf dem Schnappschuss (reference.md enthält die Abfragen)sofort
Was ist auf dem Bild („ein rotes Torii-Tor“), mehr davon, nahe Duplikatephoto_index.sh search / like / query (unten)~15 s pro Aufruf
Apples eigene Labels, KI-Bildunterschrift, Ortsnamen, benannte Personen, ästhetische Punktzahlphoto_meta.py einmal → grep das JSONL30 s + ms/Element
Ort, Label, Text-im-Foto, GPS-Radius von einem residenten Prozessoptionaler apple-photos MCP (setup.sh --mcp). Übergib immer library30-40 s erster Aufruf, dann <1 s
Dieselben Filter von einer Shell, JSON-Metadatenuvx --from osxphotos osxphotos query --library "$HOME/Pictures/Photos Library.photoslibrary" --mute ... --json~30 s pro Aufruf
Viele osxphotos-Abfragenein Skript: uv run --no-project --with osxphotos, osxphotos.PhotosDB(dbfile=LIB) einmal geladen30 s einmal
Fotos ansehenIDs → uuid_sheets.sh → die Blätter lesenSekunden
Dateien in voller Auflösungphotokit download (verwendet eine bereits auf der Festplatte befindliche Datei erneut)~1,7 s pro Stück aus iCloud
Ein einfacher Bildordner, nicht Apple Photossheets.py VERZ, und photo_index.sh build VERZ
Die Bibliothek ändernimport.py, set_dates.py, set_flags.swift (unten)

Gib osxphotos und dem MCP immer den Bibliothekspfad an (--library / "library" / PhotosDB(dbfile=...)). Ohne diesen suchen sie die „zuletzt geöffnete Bibliothek“ in den Einstellungen von Photos innerhalb des App-Containers; macOS blockiert diesen Lesevorgang hinter einer Aufforderung „auf Daten von anderen Apps zugreifen“, die in einem Terminal nie erscheint, und der Aufruf hängt bei 0 % CPU. Wenn --library auf den Schnappschuss zeigt, funktioniert es auch, aber dann fehlen Labels und die Dateipfade sind falsch.

Welche Elemente zählen

  • ZTRASHEDSTATE = 0 immer. ZKIND = 0 Fotos, 1 Videos.
  • ZSAVEDASSETTYPE: 6 = die iCloud-Bibliothek des Besitzers (sein Telefon), 3 = importiert, 4 = das Foto einer anderen Person in einem geteilten Album, 12 = „Mit dir geteilt“ aus Nachrichten. „Fotos, die ich gemacht habe“ bedeutet 6 oder 3. Geteilte Elemente erscheinen in Datumsbereichen, erwähne es also, wenn du sie einschließt.
  • ZHIDDEN = 1 Elemente sind in Zählungen enthalten, es sei denn, sie werden herausgefiltert; erwähne sie, wenn welche auftauchen.
  • Daten: >= 'YYYY-MM-DD' und < 'nächster Tag' auf datetime(ZDATECREATED+978307200,'unixepoch','localtime'). between '…-20' and '…-22' lässt den größten Teil des 22. weg. Gleicher Tag jedes Jahr: strftime('%m-%d', …) = '10-31'. 'localtime' ist die Zone dieses Macs; füge ZADDITIONALASSETATTRIBUTES.ZTIMEZONEOFFSET für die Zone hinzu, in der das Foto aufgenommen wurde.
  • Kein Standort ist -180.0, daher verwirft ein GPS-Filter diese stillschweigend; gib an, wie viele ausgelassen wurden.
  • Kein erkanntes Gesicht: not exists (select 1 from ZDETECTEDFACE f where f.ZASSETFORFACE = a.Z_PK). Es übersieht entfernte Menschenmengen und Hinterköpfe, schau also trotzdem nach, bevor du etwas veröffentlichst.

Fotos ansehen

  1. sh $S/snapshot.sh <scratch>/db
  2. Leite die ID-Abfrage direkt in den Blattersteller. Gib niemals ID-Listen in die Unterhaltung aus:
    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
    

Das schreibt 6×5-Raster aus 236-Px-Kacheln, beschriftet mit 0001, 0002… (IDs sind global, nicht pro Blatt, daher können Kacheln von zwei Blättern nicht verwechselt werden), eine manifest.json (ID → Datei, plus was übersprungen wurde und warum) und missing.txt. Es verwendet das Original, wenn es auf der Festplatte ist, andernfalls die größte lokale Vorschau, und sucht auch in Ordnern für geteilte Alben und Nachrichten. Melde ein nicht-leeres missing.txt: Fotos verschwinden nie stillschweigend. Für einen Ordner: uv run --no-project --with pillow python -I $S/sheets.py OUT VERZ (--cols, --rows, --tile zum Ändern der Größe; ein Blatt sollte auf seiner langen Seite unter ~2.500 px bleiben).

  • Ein paar Blätter: lies sie selbst.
  • Viele Blätter: verteile sie an Unteragenten, denen jeweils ein Bereich von Blättern zugewiesen wird. Verwende ein Modell, das gut im Sehen ist (in Tests war das kleinste Modell zu nachsichtig). Der Prompt muss eine JSONL-Zeile pro Kachel-ID erfordern ({"id": "0007", "verdict": ...}) und dem Unteragenten sagen, er solle Kachel für Kachel vorgehen und die Bilder selbst lesen, keinen Code schreiben. Führe dann python3 -I $S/check_verdicts.py manifest.json out/*.jsonl aus: es schlägt fehl bei fehlenden, doppelten oder erfundenen IDs und markiert Blätter, bei denen jede Kachel dieselbe Antwort erhalten hat, was normalerweise bedeutet, dass der Unteragent überflogen hat; sieh dir dieses Blatt selbst an.
  • Für alles, was Dateien ändert, führe zwei Durchläufe durch und handle nur dort, wo sie übereinstimmen, dann sieh dir jede Kachel an, die du ändern willst.
  • Notiere das Modell, die Anzahl der Agenten und die ungefähren Token im README des Projekts.

Suche nach dem, was auf dem Bild ist

Ein privater Index enthält ein Embedding pro Foto (standardmäßig SigLIP 2), erstellt aus lokalen Vorschauen, sodass nichts aus iCloud heruntergeladen wird. Er befindet sich in ~/Library/Application Support/photo-skill/index (PHOTO_INDEX_DIR), niemals in git. Gemessen: ~28 Fotos/s aus einer Photos-Bibliothek auf einem M4 (etwa eine Stunde pro 100.000, plus einmaliger Download eines ~400 MB großen Modells), ~70/s aus einem Ordner mit JPEGs.

sh $S/photo_index.sh status
sh $S/photo_index.sh build                          # Apple Photos; fügt nur Neues hinzu, fortsetzbar
sh $S/photo_index.sh build ~/Pictures/scans         # oder ein beliebiger Ordner (verwendet eigenes 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                 # nächste zu einer beliebigen Beispiel-ID
sh $S/photo_index.sh query --text "a bowl of ramen" --text "noodle soup" --minus "a menu" --within <scratch>/trip.txt --top 40

Ausgabe ist id<TAB>score, beste zuerst. Der Index bewertet; du beurteilst. Lies immer das Blatt: Eine Abfrage nach „einem Getränkeautomaten“ gibt auch Kühlschränke und Fahrkartenautomaten im Convenience-Store zurück.

  • Bewertungen. Text→Foto-Cosinus-Werte von SigLIP sind klein (0,10–0,20 für eine gute Übereinstimmung); nur die Reihenfolge zählt, setze also niemals einen Schwellenwert. Foto→Foto mit dem Standardmodell: ≥ 0,97 gleiche Aufnahme, ≥ 0,95 gleicher Moment (Serien, Wiederholungen), ~0,90 gleicher Ort in einer anderen Aufnahme, ~0,85 die gleiche Art von Szene. Miss diese Werte nach einem Modellwechsel neu.
  • Erst filtern, dann bewerten. Daten, Orte, Alben, Personen und geteilt vs. eigen stammen aus SQL auf dem Schnappschuss; schreibe diese IDs in eine Datei und übergib --within. Das ist besser, als das Embedding nach „Fotos aus Kyoto im November“ zu fragen.
  • Formuliere wie eine Bildunterschrift („ein Foto einer Schüssel Ramen“ oder „eine Schüssel Ramen“), nicht wie Schlüsselwörter. Mehrere --text-Formulierungen werden gemittelt; --minus drückt ein Konzept weg.
  • Kombiniere mit Apples Metadaten. photo_meta.py schreibt meta.jsonl mit Apples eigenen Labels, KI-Bildunterschrift, Ortsnamen, benannten Personen und ästhetischer Punktzahl. Es ist stark bei wo und wer (Ortsnamen wie „…-Schrein“), schwach bei wie es aussieht (Bildunterschriften sind allgemein: „eine Gruppe von Menschen, die vor einem Gebäude stehen“). Durchsuche es, vereinige es dann oder schneide es mit einer Indexsuche. Sein OCR-Feld (text) war in Tests auf macOS 26 leer; für Text in Fotos verwende die Textsuche des MCP oder lade das Original herunter und lies es selbst.
  • Nahe Duplikate: like mit einem Beispiel und lies die Bewertungen; ≥ 0,95 Paare sind Wiederholungen. Dedupliziere niemals nach Name+Größe (Kopien werden umbenannt und neu codiert).
  • Modell wechseln (PHOTO_INDEX_MODEL, jeder SigLIP/SigLIP 2-Checkpoint): Der Index zeichnet sein Modell in model.txt auf und weigert sich, zwei zu mischen; erstelle das neue in seinem eigenen PHOTO_INDEX_DIR. README.md vergleicht Modelle.

Nächtliche Updates (optional): sh $S/photo_index.sh install-nightly (oder setup.sh --index) installiert einen launchd-Job, der die Fotos des Tages um 3:30 Uhr hinzufügt. launchd-Jobs erhalten nicht den Vollzugriff auf die Festplatte des Terminals, daher startet der Job einen winzigen Starter (photo-index-runner, erstellt aus index_runner.c), der seine eigene Berechtigung benötigt: Der Befehl öffnet die Systemeinstellungen bei Vollzugriff auf die Festplatte und zeigt die Datei im Finder an; die Person klickt auf +, wählt sie aus und schaltet sie ein. Überprüfe mit launchctl kickstart gui/$(id -u)/local.photo-skill.index, dann photo_index.sh status. „Operation not permitted“ im Log bedeutet, dass die Berechtigung fehlt. Die erneute Ausführung von install-nightly überschreibt den Job, behält aber den Starter, da ein Neuerstellen die Berechtigung verliert.

PhotoKit-Helfer

Einmal pro Sitzung kompilieren: swiftc -O $S/photokit.swift -o <scratch>/photokit. Sein erster Lauf fordert den Zugriff auf Fotos in einem macOS-Dialog an, den die Person am Mac erlauben muss. <scratch>/photokit download OUT_DIR < uuids.txt akzeptiert reine UUIDs, erlaubt iCloud, gibt jeden Pfad aus und meldet Fehler auf stderr. Nachrichten-Elemente („Mit dir geteilt“) sind für PhotoKit und osxphotos gleichermaßen unsichtbar: Ihre Vorschau ist alles, was es gibt.

  • import ALBUM FILE... überspringt die Photos.app-Skripting vollständig (keine Obergrenze pro Aufruf, kein Fehlerdialog, der hängen bleibt) und gibt file<TAB>localIdentifier aus; protokolliere das. Leicht getestet: Bei Tausenden von Dateien zuerst ein paar hundert machen und anhand eines neuen Schnappschusses überprüfen.
  • date ist ungetestet; verwende set_dates.py, bis es ausgeführt wurde.

Die Bibliothek ändern (nur auf Anfrage)

  1. Kopiere Stufen außerhalb der Bibliothek (cp -c-Klone sind kostenlos) und korrigiere sie zuerst: exiftool für Daten und Orientation, Video Rotation.
  2. python3 -I $S/import.py --album NAME --log LOG.jsonl [--dry-run] FILES... (oder Pfade auf stdin): 50 Dateien pro Aufruf, starte Photos alle 1.500 neu, stoppe bei der ersten kurzen Charge, fortsetzbar ab LOG. Wenn es anhält, hat Photos einen offenen Fehlerdialog: Bitte die Person, ihn zu schließen, dann erneut ausführen.
  3. Daten für Dateien ohne eingebettete Daten: python3 -I $S/set_dates.py < id<TAB>YYYY-MM-DD HH:MM:SS.
  4. Ausgeblendet/Favorit (AppleScript kann nicht ausblenden): swift $S/set_flags.swift < id<TAB>hide<TAB>fav. Ein ausgeblendetes Asset verschwindet aus Abfragen, also bedeutet „noch sichtbar: 0“, dass es funktioniert hat.
  5. Überprüfe anhand eines neuen Schnappschusses: Jede protokollierte ID vorhanden, keine gelöscht, Album-Anzahl stimmt.

Fotos in einen Film

Für eine Diashow, Montage oder jedes Video, das aus Fotos erstellt wird, verwende das video-skill-Skill (github.com/kylefritz/video-skill; falls es nicht installiert ist, biete an, es in ~/.claude/skills/video-skill zu klonen). Der Teil dieses Skills: Finde und beurteile die Aufnahmen, lade die Originale mit photokit download herunter (Vorschauen sind für 1080p zu klein), speichere sie ohne EXIF neu und betrachte jedes in voller Größe auf Personen, bevor es in einen Film kommt. video-skill liest Videos genauso, wie dieses Skill Fotos liest, daher funktionieren seine Frame-Blätter mit check_verdicts.py.

Fallstricke

  • Fotos-Daten sind Sekunden seit dem 01.01.2001: addiere 978307200. Vergleiche datetime(...)-Zeichenketten; der Vergleich einer Zahl mit strftime('%s', ...) (Text) stimmt stillschweigend mit nichts überein.
  • ZCLOUDLOCALSTATE bedeutet nicht, dass das Original auf der Festplatte ist; überprüfe die Datei.
  • Alte Kameras ohne Orientierungssensor speicherten Porträts seitwärts; korrigiere mit dem EXIF Orientation-Tag (verlustfrei).
  • Gesichtskoordinaten in Photos haben ihren Ursprung unten links: y = (1 - center_y) * height.
  • Das Öffnen eines Dropbox- oder iCloud Drive-Platzhalters blockiert für immer. Überprüfe zuerst st_blocks > 0.
  • Eine veraltete -wal neben einer frischen Photos.sqlite wird darüber abgespielt; snapshot.sh löscht aus diesem Grund zuerst die alten Dateien.
  • /private/tmp-Scratchpads werden zwischen Sitzungen gelöscht; behalte länger laufende Arbeiten in einem Projektordner.

Verwandte Skills