Community이미지github.com

kylefritz/photo-skill

Apple Photos 라이브러리(또는 모든 이미지 폴더)에서 사진을 찾고, 보고, 평가하고, 정리합니다.

photo-skill란 무엇인가요?

이 스킬은 macOS에서 Apple Photos 라이브러리나 일반 이미지 폴더를 다루기 위한 강력한 도구입니다. 사진 라이브러리에 대해 SQL, osxphotos 및 임베딩 인덱스를 사용하여 수만 장의 사진 중에서 관련 후보를 빠르게 추려내고, 연락처 시트(contact sheet) 형태로 사용자에게 제시합니다. 사용자(또는 하위 에이전트)는 각 사진을 직접 보고 판단(verdict)을 내리며, 스크립트가 모든 ID에 대해 정확히 하나의 답변이 있는지 확인합니다. 주요 기능으로는 사진 속 내용 검색(온디바이스 임베딩 인덱스 기반), 중복 사진 찾기, 날짜/장소/인물별 필터링, 사진 가져오기/날짜 설정/숨김/즐겨찾기 등 라이브러리 변경, iCloud 원본 다운로드 등이 있습니다. 특히 **읽기 전용**이 기본 원칙이며, 사용자가 명시적으로 요청할 때만 라이브러리를 변경합니다. 또한 `Photos.sqlite`를 직접 열지 않고 APFS 스냅샷을 사용하며, 모든 사진은 비공개로 처리되어야 합니다. 이 스킬은 Claude Code, Cursor, Codex 등 다양한 AI 에이전트 플랫폼에서 사용할 수 있으며, 사진 관련 작업을 안전하고 효율적으로 수행하도록 설계되었습니다.

지원 대상✓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 호출 간에 전달되지 않습니다). 라이브러리 경로는 PHOTOS_LIBRARY 환경 변수가 설정되지 않은 경우 ~/Pictures/Photos Library.photoslibrary입니다.

핵심 아이디어: 코드는 순위를 매기고 가져오며, 모델은 보고 판단합니다. SQL, osxphotos 및 임베딩 인덱스는 10만 장의 사진 라이브러리를 수백 개의 후보로 좁히고, 연락처 시트는 이 후보들을 레이블이 있는 그리드로 제시합니다. 사용자(또는 하위 에이전트)는 모든 타일을 판단하고 ID별로 평결을 작성합니다. 스크립트는 모든 ID가 정확히 하나의 답변을 받았는지 확인합니다.

Mac에서 처음 사용할 때

직접 sh $S/setup.sh를 실행하세요 (--mcp는 선택적 MCP 서버용, --index는 야간 인덱스 작업용). 이 스크립트는 uv, sqlite3, 커맨드 라인 도구 및 라이브러리를 확인하고, osxphotos와 Pillow를 워밍업하며, 스냅샷을 생성하여 Full Disk Access 권한을 증명합니다. Full Disk Access는 오직 사람만이 부여할 수 있는 권한입니다: 시스템 설정 → 개인정보 보호 및 보안 → 전체 디스크 접근 → Claude가 실행되는 앱(Terminal, 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%만이 디스크에 있었습니다). 하지만 모든 항목에는 로컬 미리보기가 있습니다. 미리보기는 연락처 시트와 예/아니오 판단에는 충분하지만, 텍스트나 얼굴을 읽기에는 너무 작습니다. 원본은 photokit download 명령으로 가져오세요 (항목당 약 1.7초).
  • 사진은 비공개입니다. 세션 스크래치패드나 gitignored 폴더에서 작업하고, 절대 커밋하지 마세요. 요청하지 않는 한 다른 사람의 사진을 게시하지 마세요 (웹 페이지, PR, 업로드). 기계를 떠나는 모든 것에서 EXIF(GPS!)를 제거하세요: Pillow를 통해 다시 저장하면 됩니다.

도구 선택

필요사용속도
개수, 날짜, 앨범, 사람, 휴지통/숨김, GPS 범위, Apple 점수스냅샷에서 sqlite3 (reference.md에 쿼리 있음)즉시
사진 속 내용 ("빨간 도리이 문"), 이와 비슷한 사진, 중복 사진photo_index.sh search / like / query (아래 설명)호출당 ~15초
Apple 자체 레이블, AI 캡션, 장소 이름, 이름이 있는 사람, 미적 점수photo_meta.py 한 번 실행 → JSONL을 grep30초 + 항목당 ms
장소, 레이블, 사진 속 텍스트, 상주 프로세스의 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는 "다른 앱의 데이터에 접근" 프롬프트 뒤에서 이 읽기를 차단하며, 터미널에는 이 프롬프트가 표시되지 않아 호출이 CPU 0%에서 영원히 멈춥니다. --library를 스냅샷으로 지정해도 작동하지만, 레이블이 누락되고 파일 경로가 잘못됩니다.

항목 기준

  • ZTRASHEDSTATE = 0 항상 적용. ZKIND = 0 사진, 1 비디오.
  • ZSAVEDASSETTYPE: 6 = 소유자의 iCloud 라이브러리 (소유자의 휴대폰), 3 = 가져온 항목, 4 = 공유 앨범에 있는 다른 사람의 사진, 12 = Messages에서 나와 공유한 항목. "내가 찍은 사진"은 6 또는 3을 의미합니다. 공유 항목은 날짜 범위에 나타나므로 포함하는 경우 이를 알리세요.
  • ZHIDDEN = 1 항목은 필터링되지 않는 한 개수에 포함됩니다. 발견되면 언급하세요.
  • 날짜: datetime(ZDATECREATED+978307200,'unixepoch','localtime')에 대해 >= 'YYYY-MM-DD' 및 < '다음 날'. 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
    

이 명령은 236px 타일의 6×5 그리드를 0001, 0002… 레이블로 작성합니다 (ID는 전역적이므로 두 시트의 타일이 혼동되지 않음). manifest.json (ID → 파일, 건너뛴 항목과 이유 포함) 및 missing.txt도 생성합니다. 디스크에 원본이 있으면 원본을 사용하고, 그렇지 않으면 가장 큰 로컬 미리보기를 사용하며, 공유 앨범 및 Messages 폴더도 확인합니다. 비어 있지 않은 missing.txt를 보고하세요: 사진이 조용히 사라지지 않습니다. 폴더의 경우: uv run --no-project --with pillow python -I $S/sheets.py OUT DIR (--cols, --rows, --tile로 크기 조정, 한 시트의 긴 쪽은 약 2,500px 이하로 유지).

  • 소수의 시트: 직접 읽으세요.
  • 많은 시트: 각각 시트 범위가 주어진 하위 에이전트로 분산하세요. 비전에 능숙한 모델을 사용하세요 (테스트에서 가장 작은 모델은 너무 관대했습니다). 프롬프트는 타일 ID당 하나의 JSONL 줄을 요구해야 합니다 ({"id": "0007", "verdict": ...}). 하위 에이전트에게 타일별로 이동하여 이미지를 직접 읽도록 지시하고, 코드를 작성하지 말라고 알리세요. 그런 다음 python3 -I $S/check_verdicts.py manifest.json out/*.jsonl을 실행하세요: 누락, 중복 또는 조작된 ID가 있으면 실패하고, 모든 타일이 동일한 답변을 받은 시트에 플래그를 지정합니다 (이는 일반적으로 하위 에이전트가 대충 훑어보았음을 의미하며, 해당 시트를 직접 확인하세요).
  • 파일을 변경하는 모든 작업에 대해 두 번의 패스를 실행하고 동의하는 항목에만 작업을 수행한 다음, 변경하려는 모든 타일을 확인하세요.
  • 모델, 에이전트 수 및 대략적인 토큰을 프로젝트의 README에 기록하세요.

사진 속 내용 검색

비공개 인덱스는 사진당 하나의 임베딩을 보유합니다 (기본값 SigLIP 2). 로컬 미리보기로 구축되므로 iCloud에서 아무것도 다운로드하지 않습니다. 인덱스는 ~/Library/Application Support/photo-skill/index (PHOTO_INDEX_DIR)에 있으며, 절대 git에 저장되지 않습니다. 측정 결과: M4의 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>점수이며, 가장 좋은 것이 먼저 나옵니다. 인덱스는 순위를 매깁니다. 사용자가 판단합니다. 항상 시트를 읽으세요: "자판기" 쿼리는 편의점 냉장고와 티켓 판매기도 반환합니다.

  • 점수. SigLIP의 텍스트→사진 코사인 유사도는 작습니다 (좋은 일치의 경우 0.10–0.20). 순서만 중요하므로 절대 임계값을 설정하지 마세요. 기본 모델의 사진→사진: ≥ 0.97 같은 샷, ≥ 0.95 같은 순간 (버스트, 재촬영), ~0.90 다른 각도에서 같은 장소, ~0.85 같은 종류의 장면. 모델을 변경한 후 이를 다시 측정하세요.
  • 먼저 필터링한 다음 순위를 매기세요. 날짜, 장소, 앨범, 사람 및 공유/자체 여부는 스냅샷의 SQL에서 가져옵니다. 해당 ID를 파일에 쓰고 --within을 전달하세요. 이는 "11월 교토 사진"을 임베딩에 묻는 것보다 훨씬 효과적입니다.
  • 캡션처럼 구문을 작성하세요 ("라면 한 그릇의 사진" 또는 "라면 한 그릇"), 키워드가 아닙니다. 여러 --text 구문은 평균화됩니다. --minus는 개념을 밀어냅니다.
  • Apple의 메타데이터와 결합하세요. photo_meta.py는 Photos 자체 레이블, AI 캡션, 장소 이름, 이름이 있는 사람 및 미적 점수가 포함된 meta.jsonl을 작성합니다. 어디서와 누가 (장소 이름, 예: "… 신사")에는 강력하지만, 어떻게 생겼는지에는 약합니다 (캡션은 일반적: "건물 앞에 서 있는 사람들"). 이를 grep한 다음 인덱스 검색과 합집합 또는 교집합을 수행하세요. 텍스트 인식 필드(text)는 macOS 26에서 테스트했을 때 비어 있었습니다. 사진 속 텍스트를 보려면 MCP의 텍스트 검색을 사용하거나 원본을 다운로드하여 직접 읽으세요.
  • 중복 사진: 하나의 예제로 like를 실행하고 점수를 읽으세요. ≥ 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 작업은 터미널의 Full Disk Access 권한을 받지 못하므로, 작업은 자체 권한이 필요한 작은 실행기(photo-index-runner, index_runner.c에서 빌드)를 시작합니다. 명령은 시스템 설정의 Full Disk Access를 열고 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. 첫 번째 실행 시 Mac의 대화 상자에서 Photos 접근 권한을 요청하며, Mac 사용자가 허용해야 합니다. <scratch>/photokit download OUT_DIR < uuids.txt는 UUID만 사용하고 iCloud를 허용하며 각 경로를 출력하고 stderr에 실패를 보고합니다. Messages (나와 공유한 항목) 항목은 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... (또는 stdin의 경로): 호출당 50개 파일, 1,500개마다 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. 숨겨진 자산은 가져오기에서 사라지므로 "여전히 표시됨: 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 스크래치패드는 세션 사이에 지워집니다. 오래 실행되는 작업은 프로젝트 폴더에 보관하세요.

관련 스킬