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을 grep | 30초 + 항목당 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). 먼 군중이나 뒷머리는 놓칠 수 있으므로, 무언가를 게시하기 전에 여전히 확인하세요.
사진 보기
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
이 명령은 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가 실행될 때까지 이를 사용하세요.
라이브러리 변경 (요청 시에만)
- 라이브러리 외부에 복사본을 준비하고 (
cp -c클론은 무료) 먼저 수정: 날짜와Orientation, 비디오Rotation은 exiftool 사용. python3 -I $S/import.py --album NAME --log LOG.jsonl [--dry-run] FILES...(또는 stdin의 경로): 호출당 50개 파일, 1,500개마다 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. 숨겨진 자산은 가져오기에서 사라지므로 "여전히 표시됨: 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스크래치패드는 세션 사이에 지워집니다. 오래 실행되는 작업은 프로젝트 폴더에 보관하세요.