O que photo-skill faz?
Os scripts ficam em scripts/ ao lado deste arquivo; escreva como $S abaixo, mas escreva o caminho absoluto em cada comando, porque as variáveis de shell não persistem entre chamadas Bash. A biblioteca é ~/Pictures/Photos Library.photoslibrary a menos que PHOTOS_LIBRARY diga o contrário.
A ideia central: o código ranqueia e busca, o modelo olha e julga. SQL, osxphotos e o índice de embeddings reduzem uma biblioteca de 100 mil fotos a algumas centenas de candidatos; contact sheets as colocam diante de você como grades rotuladas; você (ou subagentes) julga cada miniatura e escreve um veredito por id; um script verifica se cada id recebeu exatamente uma resposta.
Primeira vez em um Mac
Execute sh $S/setup.sh você mesmo (adicione --mcp para o servidor MCP opcional, --index para o trabalho de índice noturno). Ele verifica uv, sqlite3, as Ferramentas de Linha de Comando e a biblioteca, aquece osxphotos e Pillow, e prova o Acesso Total ao Disco tirando um snapshot. O Acesso Total ao Disco é a única coisa que só uma pessoa pode conceder: Ajustes do Sistema → Privacidade e Segurança → Acesso Total ao Disco → ative o aplicativo em que o Claude executa (Terminal, Ghostty, o aplicativo Claude...), depois saia e reabra.
Regras
- Somente leitura, a menos que o proprietário peça uma alteração nesta conversa (importar, data, ocultar, favoritar, álbum). Somente leitura também descarta
osxphotos query --add-to-albumeosxphotos import,batch-edit,timewarp,push-exif,sync,add-locations. - Nunca abra o
Photos.sqliteao vivo, nem somente leitura: SQLite escreve seu arquivo-shm. Consulte um clone APFS feito porsh $S/snapshot.sh <dir>(instantâneo, gratuito). Nunca escreva dentro do.photoslibrary. - Baixar originais do iCloud é permitido. Com "Otimizar Armazenamento do Mac", a maioria dos originais está apenas no iCloud (em uma biblioteca de ~100 mil itens, 1,4% estavam no disco), mas cada item tem uma prévia local. As prévias são boas para contact sheets e decisões sim/não, pequenas demais para ler texto ou rostos; baixe o original com
photokit download(~1,7 s cada). - Fotos são privadas. Trabalhe no scratchpad da sessão ou em uma pasta ignorada pelo git, nunca as comite e não publique as fotos de alguém (uma página web, um PR, um upload) a menos que eles peçam. Remova EXIF (GPS!) de qualquer coisa que saia da máquina: re-salvar pelo Pillow faz isso.
Escolha a ferramenta
| Necessidade | Use | Velocidade |
|---|---|---|
| Contagens, datas, álbuns, pessoas, lixeira/ocultos, caixas GPS, pontuações da Apple | sqlite3 no snapshot (reference.md tem as consultas) | instantâneo |
| O que está na foto ("um portão torii vermelho"), mais como estas, quase-duplicatas | photo_index.sh search / like / query (abaixo) | ~15 s por chamada |
| Rótulos da própria Apple, legenda IA, nomes de lugares, pessoas nomeadas, pontuação estética | photo_meta.py uma vez → filtre o JSONL | 30 s + ms/item |
| Lugar, rótulo, texto em foto, raio GPS a partir de um processo residente | MCP apple-photos opcional (setup.sh --mcp). Sempre passe library | 30-40 s na primeira chamada, depois <1 s |
| Os mesmos filtros do shell, metadados JSON | uvx --from osxphotos osxphotos query --library "$HOME/Pictures/Photos Library.photoslibrary" --mute ... --json | ~30 s por chamada |
| Muitas consultas osxphotos | um script: uv run --no-project --with osxphotos, carregue osxphotos.PhotosDB(dbfile=LIB) uma vez | 30 s uma vez |
| Olhar fotos | ids → uuid_sheets.sh → leia as planilhas | segundos |
| Arquivos em resolução total | photokit download (reutiliza um arquivo já no disco) | ~1,7 s cada do iCloud |
| Uma pasta simples de imagens, não Apple Photos | sheets.py DIR, e photo_index.sh build DIR | |
| Alterar a biblioteca | import.py, set_dates.py, set_flags.swift (abaixo) |
Sempre forneça o caminho da biblioteca para osxphotos e para o MCP (--library / "library" / PhotosDB(dbfile=...)). Sem ele, eles procuram a "última biblioteca aberta" nas preferências do Fotos dentro de seu contêiner do aplicativo; o macOS bloqueia essa leitura atrás de um prompt "acessar dados de outros aplicativos" que nunca aparece em um terminal, e a chamada trava para sempre com 0% de CPU. Apontar --library para o snapshot também funciona, mas então os rótulos estão faltando e os caminhos dos arquivos estão errados.
Quais itens contam
ZTRASHEDSTATE = 0sempre.ZKIND = 0fotos,1vídeos.ZSAVEDASSETTYPE: 6 = a biblioteca iCloud do proprietário (o celular dele), 3 = importado, 4 = foto de outra pessoa em um álbum compartilhado, 12 = Compartilhado com Você do Mensagens. "Fotos que tirei" significa 6 ou 3. Itens compartilhados aparecem em intervalos de data, então mencione se você os incluir.- Itens
ZHIDDEN = 1estão nas contagens a menos que filtrados; mencione-os se aparecerem. - Datas:
>= 'YYYY-MM-DD'e< 'próximo dia'emdatetime(ZDATECREATED+978307200,'unixepoch','localtime').between '…-20' and '…-22'perde a maior parte do dia 22. Mesmo dia todos os anos:strftime('%m-%d', …) = '10-31'.'localtime'é o fuso deste Mac; adicioneZADDITIONALASSETATTRIBUTES.ZTIMEZONEOFFSETpara o fuso onde foi tirada. - Nenhuma localização é
-180.0, então um filtro GPS silenciosamente descarta esses; diga quantos foram deixados de fora. - Nenhum rosto detectado:
not exists (select 1 from ZDETECTEDFACE f where f.ZASSETFORFACE = a.Z_PK). Ele perde multidões distantes e nucas, então ainda olhe antes de publicar algo.
Olhando fotos
sh $S/snapshot.sh <scratch>/db- Pipe a consulta de id diretamente no construtor de planilhas. Nunca imprima listas de ids na conversa:
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
Isso escreve grades 6×5 de miniaturas de 236 px rotuladas 0001, 0002… (os ids são globais, não por planilha, então as miniaturas de duas planilhas não podem ser confundidas), um manifest.json (id → arquivo, mais qualquer coisa ignorada e o motivo) e missing.txt. Ele usa o original quando está no disco, senão a maior prévia local, e também procura em pastas de álbuns compartilhados e Mensagens. Reporte um missing.txt não vazio: fotos nunca desaparecem silenciosamente. Para uma pasta: uv run --no-project --with pillow python -I $S/sheets.py OUT DIR (--cols, --rows, --tile para redimensionar; uma planilha deve ficar abaixo de ~2.500 px no lado maior).
- Poucas planilhas: leia você mesmo.
- Muitas planilhas: distribua para subagentes, cada um recebendo um intervalo de planilhas. Use um modelo bom em visão (em testes, o menor modelo era muito tolerante). O prompt deve exigir uma linha JSONL por id de miniatura (
{"id": "0007", "verdict": ...}) e dizer ao subagente para ir miniatura por miniatura e ler as imagens ele mesmo, não escrever código. Depoispython3 -I $S/check_verdicts.py manifest.json out/*.jsonl: ele falha em ids ausentes, duplicados ou inventados e sinaliza planilhas onde toda miniatura recebeu a mesma resposta, o que geralmente significa que o subagente passou por cima; olhe essa planilha você mesmo. - Para qualquer coisa que vá alterar arquivos, execute duas passagens e aja apenas onde elas concordam, depois olhe cada miniatura que você está prestes a alterar.
- Registre o modelo, o número de agentes e os tokens aproximados no README do projeto.
Pesquisa pelo que está na foto
Um índice privado mantém um embedding por foto (SigLIP 2 por padrão), construído a partir de prévias locais, então nada é baixado do iCloud. Ele fica em ~/Library/Application Support/photo-skill/index (PHOTO_INDEX_DIR), nunca no git. Medido: ~28 fotos/s de uma biblioteca do Fotos em um M4 (cerca de uma hora para 100k, mais um download de modelo de ~400 MB uma vez), ~70/s de uma pasta de JPEGs.
sh $S/photo_index.sh status
sh $S/photo_index.sh build # Apple Photos; adiciona apenas o que é novo, retomável
sh $S/photo_index.sh build ~/Pictures/scans # ou qualquer pasta (use seu próprio PHOTO_INDEX_DIR)
sh $S/photo_index.sh search "um portão torii vermelho" 60 | cut -f1 | sh $S/uuid_sheets.sh <scratch>/hits
sh $S/photo_index.sh like <scratch>/examples.txt 60 # mais próximo de qualquer id de exemplo
sh $S/photo_index.sh query --text "uma tigela de ramen" --text "sopa de macarrão" --minus "um cardápio" --within <scratch>/trip.txt --top 40
A saída é id<TAB>score, melhores primeiro. O índice ranqueia; você julga. Sempre leia a planilha: uma consulta por "uma máquina de venda automática" também retorna geladeiras de loja de conveniência e máquinas de bilhetes.
- Pontuações. Os cossenos texto→foto do SigLIP são pequenos (0,10–0,20 para uma boa correspondência); apenas a ordem importa, então nunca os limite. Foto→foto com o modelo padrão: ≥ 0,97 mesma foto, ≥ 0,95 mesmo momento (rajadas, retomadas), ~0,90 mesmo lugar em uma foto diferente, ~0,85 o mesmo tipo de cena. Remeça estes após alterar o modelo.
- Filtre primeiro, depois ranqueie. Datas, lugares, álbuns, pessoas e compartilhado vs. próprio vêm do SQL no snapshot; escreva esses ids em um arquivo e passe
--within. Isso é melhor do que pedir ao embedding por "fotos de Kyoto em novembro". - Formule como uma legenda ("uma foto de uma tigela de ramen" ou "uma tigela de ramen"), não palavras-chave. Várias formulações
--textsão calculadas em média;--minusafasta um conceito. - Combine com metadados da Apple.
photo_meta.pyescreve ummeta.jsonlcom os rótulos da própria Apple, legenda IA, nome do lugar, pessoas nomeadas e pontuação estética. É forte em onde e quem (nomes de lugares como "… Santuário"), fraco em como parece (legendas são genéricas: "um grupo de pessoas em pé na frente de um prédio"). Filtre e depois faça união ou interseção com uma pesquisa do índice. Seu campo OCR (text) retornou vazio no macOS 26 em testes; para texto em fotos, use a pesquisa de texto do MCP ou baixe o original e leia você mesmo. - Quase-duplicatas:
likecom um exemplo e leia as pontuações; pares ≥ 0,95 são retomadas. Nunca desduplique por nome+tamanho (cópias são renomeadas e recodificadas). - Alterar o modelo (
PHOTO_INDEX_MODEL, qualquer checkpoint SigLIP/SigLIP 2): o índice registra seu modelo emmodel.txte se recusa a misturar dois; construa o novo em seu próprioPHOTO_INDEX_DIR.README.mdcompara modelos.
Atualizações noturnas (opcional): sh $S/photo_index.sh install-nightly (ou setup.sh --index) instala um job launchd que adiciona as fotos do dia às 3:30. Jobs launchd não recebem o Acesso Total ao Disco do terminal, então o job inicia um pequeno lançador (photo-index-runner, construído a partir de index_runner.c) que precisa de sua própria concessão: o comando abre Ajustes do Sistema em Acesso Total ao Disco e revela o arquivo no Finder; a pessoa clica +, seleciona-o, ativa. Verifique com launchctl kickstart gui/$(id -u)/local.photo-skill.index, depois photo_index.sh status. "Operação não permitida" no log significa que a concessão está faltando. Reexecutar install-nightly reescreve o job, mas mantém o lançador, porque reconstruí-lo perde a concessão.
Auxiliar PhotoKit
Compile uma vez por sessão: swiftc -O $S/photokit.swift -o <scratch>/photokit. Sua primeira execução pede acesso ao Fotos em uma caixa de diálogo do macOS que a pessoa no Mac deve permitir. <scratch>/photokit download OUT_DIR < uuids.txt recebe UUIDs simples, permite iCloud, imprime cada caminho e reporta falhas em stderr. Itens do Mensagens (Compartilhado com Você) são invisíveis tanto para o PhotoKit quanto para o osxphotos: a prévia deles é tudo o que existe.
import ALBUM FILE...pula toda a scriptagem do aplicativo Fotos (sem limite por lançamento, sem caixa de diálogo de erro para ficar travado) e imprimefile<TAB>localIdentifier; registre isso. Levemente testado: para milhares de arquivos, faça algumas centenas primeiro e verifique-os com um snapshot novo.datenão foi testado; useset_dates.pyaté que tenha sido executado.
Alterando a biblioteca (apenas quando solicitado)
- Copie para fora da biblioteca (clones
cp -csão gratuitos) e corrija-as primeiro: exiftool para datas eOrientation,Rotationde vídeo. python3 -I $S/import.py --album NOME --log LOG.jsonl [--dry-run] ARQUIVOS...(ou caminhos no stdin): 50 arquivos por chamada, reinicia o Fotos a cada 1.500, para no primeiro lote curto, retomável a partir de LOG. Se parar, o Fotos tem uma caixa de diálogo de erro aberta: peça à pessoa para fechá-la e execute novamente.- Datas para arquivos sem nenhuma incorporada:
python3 -I $S/set_dates.py < id<TAB>YYYY-MM-DD HH:MM:SS. - Oculto/Favorito (AppleScript não pode ocultar):
swift $S/set_flags.swift < id<TAB>ocultar<TAB>fav. Um ativo oculto desaparece das buscas, então "ainda visível: 0" significa que funcionou. - Verifique com um snapshot novo: cada id registrado presente, nenhum na lixeira, contagens de álbum corretas.
Fotos em um filme
Para um slideshow, montagem ou qualquer vídeo feito de fotos, use o skill video-skill (github.com/kylefritz/video-skill; se não estiver instalado, ofereça-se para cloná-lo em ~/.claude/skills/video-skill). A parte deste skill: encontrar e julgar as fotos, photokit download os originais (prévias são pequenas demais para 1080p), re-salvá-los sem EXIF e olhar cada um em tamanho real para pessoas antes de colocá-lo em um filme. O video-skill lê vídeo da mesma forma que este skill lê fotos, então suas planilhas de quadro funcionam com check_verdicts.py.
Pegadinhas
- As datas das fotos são segundos desde 2001-01-01: adicione
978307200. Compare stringsdatetime(...); comparar um número astrftime('%s', ...)(texto) silenciosamente não corresponde a nada. ZCLOUDLOCALSTATEnão significa que o original está no disco; verifique o arquivo.- Câmeras antigas sem sensor de orientação armazenavam retratos de lado; corrija com a tag EXIF
Orientation(sem perdas). - Coordenadas de rosto no Fotos têm origem no canto inferior esquerdo:
y = (1 - center_y) * height. - Abrir um placeholder do Dropbox ou iCloud Drive trava para sempre. Verifique
st_blocks > 0primeiro. - Um
-waldesatualizado ao lado de umPhotos.sqlitenovo é reproduzido sobre ele;snapshot.shlimpa os arquivos antigos primeiro por esse motivo. - Scratchpads
/private/tmpsão limpos entre sessões; mantenha trabalhos longos em uma pasta de projeto.