This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
PhotoMapAI is a local-first image browser for large photo collections. It uses CLIP embeddings to power semantic text/image search and builds a UMAP "semantic map" that clusters images by content. The backend is FastAPI; the frontend is vanilla ES6 modules (no framework) using Swiper.js and Plotly.js. All processing is local — nothing is sent to external services.
# Install for development (Python 3.10–3.13)
pip install -e .[testing,development]
npm install
# Run the server (entry point defined in pyproject.toml)
start_photomap # http://localhost:8050
# Tests
make test # runs npm test + pytest
pytest tests # backend only
pytest tests/backend/test_search.py::test_text_search # single test
npm test # frontend Jest only
NODE_OPTIONS='--experimental-vm-modules' jest tests/frontend/search.test.js # single JS test
# Linting / formatting (CI enforces both)
make lint # runs backend-lint + frontend-lint
ruff check photomap tests --fix
npm run lint:fix
npm run format # prettier write
npm run format:check # CI check
# Docs
make docs # mkdocs serve on :8000Ruff is configured for line-length 120, target py310, rules E/W/F/I/UP/B (see pyproject.toml). Jest runs in jsdom with experimental ESM (the project is "type": "module").
photomap_server.py— FastAPI app entry point. Wires up routers, mounts/staticand Jinja2 templates, and defines the top-level/route.start_photomapfrompyproject.tomlrunsmain()here.routers/— one router per API surface:album,search,umap,index,curation,filetree,upgrade. Routers are included inphotomap_server.py;curation_routeris mounted with an explicit/api/curationprefix while the others set their own prefixes.config.py— YAML-backed album config. Access via theget_config_manager()singleton (lru_cached).Albumis a Pydantic model that expands~in image paths. Config lives in a platformdirs user config directory.embeddings.py— CLIP embedding generation and persistence (.npz).imagetool.py— shared CLI entry point forindex_images,update_images,search_images,search_text,find_duplicate_images(all registered as scripts inpyproject.toml).metadata_extraction.py/metadata_formatting.py— pulls EXIF + generator metadata (InvokeAI) out of images and formats for the UI.
This is the area under active refactor (current branch: lstein/feature/refactor-invoke-metadata). InvokeAI writes several incompatible metadata schemas into PNG tEXt chunks; the parser must auto-detect and upgrade.
invokemetadata.pydefinesGenerationMetadataas a PydanticAnnotated[Union[…], Field(discriminator="metadata_version")]overGenerationMetadata2,GenerationMetadata3, andGenerationMetadata5.GenerationMetadataAdapter.parse()inspects fields likecanvas_v2_metadata,app_version, andmodel_weightsto inject the correctmetadata_versionwhen the source JSON predates the discriminator.invoke/holds the per-version schemas:invoke2metadata.py,invoke3metadata.py,invoke5metadata.py, pluscanvas2metadata.pyandcommon_metadata_elements.pyfor shared types.invoke_metadata_view.pyis the version-agnostic facade consumed byinvoke_formatter.py. When adding support for a new InvokeAI version, add a newinvokeNmetadata.py, extend the Union ininvokemetadata.py, teachparse()how to recognize legacy payloads that lack ametadata_versionfield, and extendInvokeMetadataView'sisinstancedispatch.invoke_formatter.py/exif_formatter.pyrender parsed metadata for the drawer UI;slide_summary.pyproduces the compact slideshow caption.invoke-DELETE/is a holdover from the refactor — leave it alone unless cleaning up.
static/javascript/— one ES6 module per feature. No build step; modules are served directly and imported frommain.js/index.js.state.jsis the centralized application state. Prefer extending it over adding new globals.events.jsowns global keyboard shortcuts; register new ones there rather than scattering listeners.localStorageis used for persisted user preferences,sessionStoragefor per-navigation state.templates/— Jinja2 templates rendered by FastAPI.
tests/backend/— pytest.conftest.py+fixtures.pyset up shared fixtures (test images intests/backend/test_images/). Use the FastAPITestClientfor router tests; seetest_search.py,test_albums.py,test_curation.pyas templates.tests/frontend/— Jest with jsdom.setup.jsprovides DOM fixtures. Seetests/frontend/README.mdfor setup notes.
From .github/copilot-instructions.md — the parts that actually affect how you write code here:
- Python: type hints on public functions,
pathlib.Path(notos.path) for file operations, f-strings, imports ordered stdlib → third-party → local (photomapis first-party to isort). Code must passruff check photomap tests. - Pinned quirk:
setuptools<67is intentional — avoids a deprecation warning from the CLIP dependency. Don't "fix" it. - New API endpoints: add/extend a router under
photomap/backend/routers/, use Pydantic models for request/response, include the router inphotomap_server.py, add atests/backend/test_<name>.py. - New frontend features: create a module in
static/javascript/, wire shared state throughstate.js, register shortcuts inevents.js, add a Jest test. - JavaScript: ES6 modules only,
const/let, must passnpm run lintandnpm run format:check.