CommunityImagengithub.com

kylefritz/photo-skill

Busca, revisa, evalúa y organiza fotos en una biblioteca de Apple Photos (o cualquier carpeta de imágenes) en un Mac.

¿Qué es photo-skill?

Esta habilidad permite a un agente de IA interactuar de manera programática y segura con una biblioteca de Apple Photos en macOS. El flujo de trabajo central es que el código realiza búsquedas y recuperación de fotos (usando SQL, osxphotos y un índice de embeddings local para búsqueda por contenido visual), mientras que el modelo de lenguaje (o subagentes) examina y juzga los resultados presentados como hojas de contacto. Es útil para encontrar fotos por fecha, lugar, persona o contenido de la imagen; identificar duplicados o imágenes similares; y organizar la biblioteca (importar, cambiar fechas, ocultar o marcar como favorito) solo cuando el usuario lo solicita. Es compatible con macOS, requiere Apple Photos y se integra con herramientas de línea de comandos como sqlite3, osxphotos y scripts personalizados. Para funcionar correctamente, necesita permisos de 'Acceso total al disco' y acceso a la biblioteca de fotos, y utiliza instantáneas (snapshots) APFS para evitar modificar la base de datos en vivo.

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

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

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

¿Qué hace photo-skill?

Los scripts viven en scripts/ junto a este archivo; escríbelo como $S a continuación, pero escribe la ruta absoluta en cada comando, porque las variables de shell no persisten entre llamadas de Bash. La biblioteca es ~/Pictures/Photos Library.photoslibrary a menos que PHOTOS_LIBRARY indique lo contrario.

La idea central: el código clasifica y recupera, el modelo mira y juzga. SQL, osxphotos y el índice de embeddings reducen una biblioteca de 100k fotos a unos pocos cientos de candidatos; las hojas de contacto los presentan como cuadrículas etiquetadas; tú (o los subagentes) juzgas cada recuadro y escribes un veredicto por id; un script verifica que cada id haya recibido exactamente una respuesta.

Primera vez en un Mac

Ejecuta sh $S/setup.sh tú mismo (añade --mcp para el servidor MCP opcional, --index para el trabajo de indexación nocturno). Verifica uv, sqlite3, las Herramientas de Línea de Comandos y la biblioteca, prepara osxphotos y Pillow, y demuestra el Acceso Total al Disco tomando una instantánea. El Acceso Total al Disco es lo único que solo una persona puede conceder: Ajustes del Sistema → Privacidad y Seguridad → Acceso Total al Disco → activa la app en la que se ejecuta Claude (Terminal, Ghostty, la app de Claude...), luego sal y vuelve a abrirla.

Reglas

  • Solo lectura a menos que el propietario pida un cambio en esta conversación (importar, fecha, ocultar, favorito, álbum). Solo lectura también excluye osxphotos query --add-to-album y osxphotos import, batch-edit, timewarp, push-exif, sync, add-locations.
  • Nunca abras el Photos.sqlite en vivo, ni siquiera en modo lectura: SQLite escribe su archivo -shm. Consulta un clon APFS creado por sh $S/snapshot.sh <dir> (instantáneo, gratuito). Nunca escribas dentro del .photoslibrary.
  • Descargar originales de iCloud está bien. Con "Optimizar almacenamiento del Mac" la mayoría de los originales están solo en iCloud (en una biblioteca de ~100k elementos, el 1.4% estaba en disco), pero cada elemento tiene una vista previa local. Las vistas previas son suficientes para hojas de contacto y decisiones de sí/no, demasiado pequeñas para leer texto o caras; obtén el original con photokit download (~1.7 s cada uno).
  • Las fotos son privadas. Trabaja en el área de trabajo de la sesión o en una carpeta ignorada por git, nunca las confirmes, y no publiques las fotos de alguien (una página web, un PR, una subida) a menos que lo pidan. Elimina los EXIF (¡GPS!) de cualquier cosa que salga de la máquina: volver a guardar con Pillow lo hace.

Elegir la herramienta

NecesidadUsarVelocidad
Recuentos, fechas, álbumes, personas, papelera/ocultos, cajas GPS, puntuaciones de Applesqlite3 en la instantánea (reference.md tiene las consultas)instantáneo
Qué hay en la imagen ("una puerta torii roja"), más como estas, casi duplicadosphoto_index.sh search / like / query (abajo)~15 s por llamada
Etiquetas propias de Apple, título de IA, nombres de lugares, personas nombradas, puntuación estéticaphoto_meta.py una vez → filtra el JSONL30 s + ms/elemento
Lugar, etiqueta, texto en foto, radio GPS desde un proceso residenteMCP apple-photos opcional (setup.sh --mcp). Pasa siempre library30-40 s primera llamada, luego <1 s
Los mismos filtros desde un shell, metadatos JSONuvx --from osxphotos osxphotos query --library "$HOME/Pictures/Photos Library.photoslibrary" --mute ... --json~30 s por llamada
Muchas consultas de osxphotosun script: uv run --no-project --with osxphotos, osxphotos.PhotosDB(dbfile=LIB) cargado una vez30 s una vez
Mirar fotosids → uuid_sheets.sh → leer las hojassegundos
Archivos de resolución completaphotokit download (reutiliza un archivo ya en disco)~1.7 s cada uno desde iCloud
Una carpeta simple de imágenes, no Apple Photossheets.py DIR, y photo_index.sh build DIR
Cambiar la bibliotecaimport.py, set_dates.py, set_flags.swift (abajo)

Siempre pasa a osxphotos y al MCP la ruta de la biblioteca (--library / "library" / PhotosDB(dbfile=...)). Sin ella, buscan la "última biblioteca abierta" en las preferencias de Fotos dentro de su contenedor de app; macOS bloquea esa lectura detrás de un aviso de "acceder a datos de otras apps" que nunca se muestra en una terminal, y la llamada se cuelga para siempre al 0% de CPU. Apuntar --library a la instantánea también funciona, pero entonces faltan las etiquetas y las rutas de archivo son incorrectas.

Qué elementos cuentan

  • ZTRASHEDSTATE = 0 siempre. ZKIND = 0 fotos, 1 vídeos.
  • ZSAVEDASSETTYPE: 6 = la biblioteca de iCloud del propietario (su teléfono), 3 = importado, 4 = foto de otra persona en un álbum compartido, 12 = Compartido contigo desde Mensajes. "Fotos que tomé" significa 6 o 3. Los elementos compartidos aparecen en los rangos de fechas, así que menciónalos si los incluyes.
  • ZHIDDEN = 1 los elementos están en los recuentos a menos que se filtren; menciónalos si aparece alguno.
  • Fechas: >= 'YYYY-MM-DD' y < 'día siguiente' en datetime(ZDATECREATED+978307200,'unixepoch','localtime'). between '…-20' and '…-22' omite la mayor parte del día 22. Mismo día cada año: strftime('%m-%d', …) = '10-31'. 'localtime' es la zona horaria de este Mac; añade ZADDITIONALASSETATTRIBUTES.ZTIMEZONEOFFSET para la zona donde se tomó.
  • Sin ubicación es -180.0, por lo que un filtro GPS los descarta silenciosamente; di cuántos se omitieron.
  • Sin rostro detectado: not exists (select 1 from ZDETECTEDFACE f where f.ZASSETFORFACE = a.Z_PK). Omite multitudes lejanas y nucas, así que aún así mira antes de publicar cualquier cosa.

Mirar fotos

  1. sh $S/snapshot.sh <scratch>/db
  2. Conecta la consulta de ids directamente al constructor de hojas. Nunca imprimas listas de ids en la conversación:
    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
    

Esto escribe cuadrículas de 6×5 con recuadros de 236 px etiquetados 0001, 0002… (los ids son globales, no por hoja, por lo que los recuadros de dos hojas no se pueden confundir), un manifest.json (id → archivo, más cualquier cosa omitida y por qué) y missing.txt. Usa el original cuando está en disco, si no, la vista previa local más grande, y también busca en carpetas de álbumes compartidos y Mensajes. Informa de un missing.txt no vacío: las fotos nunca desaparecen silenciosamente. Para una carpeta: uv run --no-project --with pillow python -I $S/sheets.py OUT DIR (--cols, --rows, --tile para redimensionar; una hoja debe mantener menos de ~2500 px en su lado largo).

  • Algunas hojas: léelas tú mismo.
  • Muchas hojas: distribúyelas entre subagentes, a cada uno un rango de hojas. Usa un modelo que sea bueno en visión (en pruebas, el modelo más pequeño era demasiado indulgente). El prompt debe requerir una línea JSONL por id de recuadro ({"id": "0007", "verdict": ...}) y decirle al subagente que vaya recuadro por recuadro y lea las imágenes él mismo, que no escriba código. Luego python3 -I $S/check_verdicts.py manifest.json out/*.jsonl: falla en ids faltantes, duplicados o inventados y marca las hojas donde cada recuadro obtuvo la misma respuesta, lo que generalmente significa que el subagente hojeó; mira esa hoja tú mismo.
  • Para cualquier cosa que vaya a cambiar archivos, haz dos pasadas y actúa solo donde coincidan, luego mira cada recuadro que estés a punto de cambiar.
  • Registra el modelo, el número de agentes y los tokens aproximados en el README del proyecto.

Buscar por contenido de la imagen

Un índice privado contiene un embedding por foto (SigLIP 2 por defecto), construido a partir de vistas previas locales, por lo que no se descarga nada de iCloud. Vive en ~/Library/Application Support/photo-skill/index (PHOTO_INDEX_DIR), nunca en git. Medido: ~28 fotos/s desde una biblioteca de Fotos en un M4 (aproximadamente una hora por 100k, más una descarga de ~400 MB del modelo una vez), ~70/s desde una carpeta de JPEGs.

sh $S/photo_index.sh status
sh $S/photo_index.sh build                          # Apple Photos; añade solo lo nuevo, reanudable
sh $S/photo_index.sh build ~/Pictures/scans         # o cualquier carpeta (usa su propio PHOTO_INDEX_DIR)
sh $S/photo_index.sh search "una puerta torii roja" 60 | cut -f1 | sh $S/uuid_sheets.sh <scratch>/hits
sh $S/photo_index.sh like <scratch>/examples.txt 60                 # más cercano a cualquier id de ejemplo
sh $S/photo_index.sh query --text "un tazón de ramen" --text "sopa de fideos" --minus "un menú" --within <scratch>/trip.txt --top 40

La salida es id<TAB>puntuación, mejor primero. El índice clasifica; tú juzgas. Siempre lee la hoja: una consulta para "una máquina expendedora" también devuelve refrigeradores de tiendas de conveniencia y máquinas de billetes.

  • Puntuaciones. Los cosenos Texto→foto de SigLIP son pequeños (0.10–0.20 para una buena coincidencia); solo importa el orden, así que nunca los limites. Foto→foto con el modelo por defecto: ≥ 0.97 misma toma, ≥ 0.95 mismo momento (ráfagas, retomas), ~0.90 mismo lugar en una toma diferente, ~0.85 el mismo tipo de escena. Vuelve a medir estos después de cambiar el modelo.
  • Filtra primero, luego clasifica. Fechas, lugares, álbumes, personas y compartido-vs-propio vienen de SQL en la instantánea; escribe esos ids en un archivo y pasa --within. Eso es mejor que pedirle al embedding "fotos de Kioto en noviembre".
  • Frasea como un título ("una foto de un tazón de ramen" o "un tazón de ramen"), no palabras clave. Varios --text se promedian; --minus aleja un concepto.
  • Combínalo con los metadatos de Apple. photo_meta.py escribe meta.jsonl con las propias etiquetas de Fotos, título de IA, nombre del lugar, personas nombradas y puntuación estética. Es fuerte en dónde y quién (nombres de lugares como "… Santuario"), débil en cómo se ve (los títulos son genéricos: "un grupo de personas de pie frente a un edificio"). Fíltralo, luego únelo o interseca con una búsqueda en el índice. Su campo OCR (text) volvió vacío en macOS 26 en las pruebas; para texto en fotos usa la búsqueda de texto del MCP o descarga el original y léelo tú mismo.
  • Casi duplicados: like con un ejemplo y lee las puntuaciones; los pares ≥ 0.95 son retomas. Nunca des-dupliques por nombre+tamaño (las copias se renombran y recodifican).
  • Cambiar el modelo (PHOTO_INDEX_MODEL, cualquier checkpoint de SigLIP/SigLIP 2): el índice registra su modelo en model.txt y se niega a mezclar dos; construye el nuevo en su propio PHOTO_INDEX_DIR. README.md compara modelos.

Actualizaciones nocturnas (opcional): sh $S/photo_index.sh install-nightly (o setup.sh --index) instala un trabajo de launchd que añade las fotos del día a las 3:30. Los trabajos de launchd no obtienen el Acceso Total al Disco de la terminal, por lo que el trabajo inicia un pequeño lanzador (photo-index-runner, compilado desde index_runner.c) que necesita su propio permiso: el comando abre Ajustes del Sistema en Acceso Total al Disco y revela el archivo en Finder; la persona hace clic en +, lo elige, lo activa. Verifica con launchctl kickstart gui/$(id -u)/local.photo-skill.index, luego photo_index.sh status. "Operation not permitted" en el registro significa que falta el permiso. Volver a ejecutar install-nightly reescribe el trabajo pero conserva el lanzador, porque reconstruirlo pierde el permiso.

Ayudante PhotoKit

Compila una vez por sesión: swiftc -O $S/photokit.swift -o <scratch>/photokit. Su primera ejecución pide acceso a Fotos en un diálogo de macOS que la persona en el Mac debe permitir. <scratch>/photokit download OUT_DIR < uuids.txt toma UUIDs simples, permite iCloud, imprime cada ruta e informa fallos en stderr. Los elementos de Mensajes (Compartido contigo) son invisibles tanto para PhotoKit como para osxphotos: su vista previa es todo lo que hay.

  • import ALBUM FILE... omite por completo la creación de scripts de la app Fotos (sin límite por lanzamiento, sin diálogo de error que quede atascado) e imprime file<TAB>localIdentifier; registra eso. Poco probado: para miles de archivos, haz unos cientos primero y verifícalos contra una instantánea nueva.
  • date no está probado; usa set_dates.py hasta que se haya ejecutado.

Cambiar la biblioteca (solo cuando se solicite)

  1. Prepara copias fuera de la biblioteca (los clones cp -c son gratuitos) y arréglalas primero: exiftool para fechas y Orientation, Rotation de vídeo.
  2. python3 -I $S/import.py --album NAME --log LOG.jsonl [--dry-run] FILES... (o rutas en stdin): 50 archivos por llamada, reinicia Fotos cada 1500, se detiene en el primer lote corto, reanudable desde LOG. Si se detiene, Fotos tiene un diálogo de error abierto: pide a la persona que lo cierre y luego vuelve a ejecutar.
  3. Fechas para archivos sin ninguna incrustada: python3 -I $S/set_dates.py < id<TAB>YYYY-MM-DD HH:MM:SS.
  4. Oculto/Favorito (AppleScript no puede ocultar): swift $S/set_flags.swift < id<TAB>hide<TAB>fav. Un activo oculto desaparece de las búsquedas, así que "aún visible: 0" significa que funcionó.
  5. Verifica contra una instantánea nueva: cada id registrado presente, ninguno en la papelera, recuentos de álbumes correctos.

Fotos en una película

Para una presentación de diapositivas, montaje o cualquier vídeo hecho a partir de fotos, usa la habilidad video-skill (github.com/kylefritz/video-skill; si no está instalada, ofrécete a clonarla en ~/.claude/skills/video-skill). La parte de esta habilidad: encontrar y juzgar las tomas, photokit download los originales (las vistas previas son demasiado pequeñas para 1080p), volver a guardarlas sin EXIF y mirar cada una a tamaño completo para ver si hay personas antes de que entre en una película. video-skill lee el vídeo de la misma manera que esta habilidad lee las fotos, por lo que sus hojas de fotogramas funcionan con check_verdicts.py.

Trampas

  • Las fechas de Fotos son segundos desde 2001-01-01: suma 978307200. Compara cadenas datetime(...); comparar un número con strftime('%s', ...) (texto) no coincide silenciosamente con nada.
  • ZCLOUDLOCALSTATE no significa que el original esté en disco; verifica el archivo.
  • Las cámaras antiguas sin sensor de orientación almacenaban retratos de lado; corrige con la etiqueta EXIF Orientation (sin pérdida).
  • Las coordenadas de rostros en Fotos tienen su origen en la parte inferior izquierda: y = (1 - center_y) * height.
  • Abrir un marcador de posición de Dropbox o iCloud Drive se bloquea para siempre. Verifica st_blocks > 0 primero.
  • Un -wal obsoleto junto a un Photos.sqlite nuevo se reproduce sobre él; snapshot.sh borra los archivos antiguos primero por esa razón.
  • Las áreas de trabajo de /private/tmp se borran entre sesiones; mantén el trabajo de larga duración en una carpeta de proyecto.

Skills relacionados