Skip to content

Commit 3e091fc

Browse files
authored
Merge branch 'master' into chore/fix-deploy-workflows
2 parents 296bff2 + ac00ff4 commit 3e091fc

4 files changed

Lines changed: 247 additions & 36 deletions

File tree

.github/copilot-instructions.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# PhotoMapAI - GitHub Copilot Instructions
2+
3+
## Project Overview
4+
5+
PhotoMapAI is an AI-powered image browser and search tool that enables semantic search, clustering, and visualization of large photo collections. It uses the CLIP computer vision model for text and image-based search, and provides a unique "semantic map" that clusters and visualizes images by their content.
6+
7+
## Architecture
8+
9+
### Tech Stack
10+
- **Frontend**: HTML, CSS, JavaScript (ES6 modules), Swiper.js, Plotly.js
11+
- **Backend**: Python 3.10-3.13, FastAPI, Uvicorn
12+
- **AI/ML**: CLIP (clip-anytorch), PyTorch, scikit-learn, UMAP
13+
- **Image Processing**: Pillow, pillow-heif
14+
- **Testing**: pytest (Python), Jest (JavaScript)
15+
16+
### Project Structure
17+
```
18+
PhotoMapAI/
19+
├── photomap/
20+
│ ├── backend/ # FastAPI backend, routers, and business logic
21+
│ │ ├── routers/ # API endpoints (album, search, umap, index, etc.)
22+
│ │ ├── metadata_modules/ # Metadata extraction modules
23+
│ │ └── *.py # Core backend modules
24+
│ └── frontend/ # Frontend assets
25+
│ ├── static/ # JavaScript, CSS, images
26+
│ └── templates/ # Jinja2 HTML templates
27+
├── tests/
28+
│ ├── backend/ # pytest tests for Python backend
29+
│ └── frontend/ # Jest tests for JavaScript
30+
├── docs/ # MkDocs documentation
31+
└── docker/ # Docker configurations
32+
```
33+
34+
## Development Guidelines
35+
36+
### Code Style and Conventions
37+
38+
#### Python
39+
- Use type hints for function parameters and return values
40+
- Follow PEP 8 style guidelines
41+
- Use docstrings for modules, classes, and functions
42+
- Prefer f-strings for string formatting
43+
- Use pathlib.Path for file path operations instead of os.path
44+
- Import organization: standard library → third-party → local imports
45+
46+
#### JavaScript
47+
- Use ES6 modules with explicit imports/exports
48+
- Use const/let instead of var
49+
- Use descriptive function and variable names
50+
- Add comments for complex logic, especially in event handlers
51+
- Follow existing file structure: one module per file with clear responsibilities
52+
53+
### Testing
54+
55+
#### Python Tests (pytest)
56+
- Location: `tests/backend/`
57+
- Run with: `pytest tests` or `make test`
58+
- Use fixtures defined in `fixtures.py` for common test setup
59+
- Test naming: `test_<functionality>.py` and `test_<feature_name>()`
60+
- Always test API endpoints with the FastAPI test client
61+
62+
#### JavaScript Tests (Jest)
63+
- Location: `tests/frontend/`
64+
- Run with: `npm test` or `make test`
65+
- Use Jest with jsdom environment
66+
- Test DOM interactions and event handlers
67+
68+
### Building and Running
69+
70+
#### Development Setup
71+
```bash
72+
# Install development dependencies
73+
pip install -e .[testing,development]
74+
75+
# Install JavaScript dependencies
76+
npm install
77+
78+
# Run tests
79+
make test
80+
81+
# Build documentation
82+
make docs
83+
84+
# Build package
85+
make build
86+
```
87+
88+
#### Running the Application
89+
```bash
90+
# Start the PhotoMapAI server
91+
start_photomap
92+
93+
# Server runs at http://localhost:8050
94+
```
95+
96+
### API Structure
97+
98+
The backend uses FastAPI with modular routers:
99+
- `/api/albums/` - Album management and configuration
100+
- `/api/search/` - Image search (similarity, text, metadata)
101+
- `/api/umap/` - UMAP visualization data
102+
- `/api/index/` - Image indexing and embedding generation
103+
- `/api/curation/` - Image curation tools
104+
- `/api/filetree/` - File system navigation
105+
- `/api/upgrade/` - Application upgrade and version management
106+
107+
### Configuration Management
108+
109+
- Configuration stored in YAML files managed by `config.py`
110+
- Albums are defined with paths, embeddings, and metadata
111+
- Use `get_config_manager()` to access the singleton ConfigManager instance
112+
- Configuration includes album settings, UMAP parameters, and application preferences
113+
114+
### Important Patterns
115+
116+
#### Backend
117+
- Use FastAPI dependency injection for shared resources
118+
- Routers should be modular and focused on specific functionality
119+
- Use Pydantic models for request/response validation
120+
- Log important operations with Python's logging module
121+
- Handle file paths with pathlib.Path
122+
- Use the ConfigManager singleton pattern via `get_config_manager()` for accessing application configuration
123+
124+
#### Frontend
125+
- State management: Use the centralized `state.js` module for application state
126+
- Event handling: Register global keyboard shortcuts in `events.js`
127+
- Modular design: Each feature (UMAP, slideshow, metadata) has its own module
128+
- Use localStorage for persisting user preferences
129+
- Use sessionStorage for temporary state during navigation
130+
131+
### Dependencies and Security
132+
133+
- Keep dependencies minimal and well-maintained
134+
- Use `pillow-heif` for HEIC image support
135+
- Pin setuptools<67 to avoid CLIP deprecation warnings
136+
- All image processing is local; no data sent to external services
137+
138+
### Documentation
139+
140+
- Use MkDocs with Material theme for documentation
141+
- Documentation location: `docs/`
142+
- Build docs: `make docs`
143+
- Deploy docs: `make deploy-docs` (GitHub Pages)
144+
- Include developer documentation in `docs/developer/`
145+
146+
### Common Tasks
147+
148+
#### Adding a New API Endpoint
149+
1. Create or update a router in `photomap/backend/routers/`
150+
2. Use Pydantic models for request/response validation
151+
3. Add appropriate error handling
152+
4. Include the router in `photomap_server.py`
153+
5. Add tests in `tests/backend/`
154+
155+
#### Adding Frontend Features
156+
1. Create a new module in `photomap/frontend/static/javascript/`
157+
2. Export functions that other modules can import
158+
3. Update `events.js` if adding keyboard shortcuts
159+
4. Test with Jest in `tests/frontend/`
160+
5. Update state.js if managing new application state
161+
162+
#### Working with Images
163+
- Use PIL/Pillow for image operations
164+
- Generate thumbnails for performance
165+
- Support HEIC format via pillow-heif
166+
- Store embeddings in .npz format (NumPy)
167+
168+
### Performance Considerations
169+
170+
- Frontend: Lazy load images, use thumbnails for grid views
171+
- Backend: Cache embeddings, use async operations where appropriate
172+
- UMAP: Pre-compute and cache UMAP projections per album
173+
- Search: Use efficient similarity search with pre-computed embeddings
174+
175+
### Debugging
176+
177+
- Backend logs: Use Python logging module with appropriate levels
178+
- Frontend: Use browser DevTools console
179+
- Test specific functionality in isolation before integration
180+
- Check GitHub Actions for CI/CD test results
181+
182+
## Additional Resources
183+
184+
- [User Guide](https://lstein.github.io/PhotoMapAI/user-guide/basic-usage/)
185+
- [Developer Architecture Guide](https://lstein.github.io/PhotoMapAI/developer/architecture/)
186+
- [Installation Guide](https://lstein.github.io/PhotoMapAI/installation/)
187+
- [Contributing Guide](CONTRIBUTING.md)

photomap/frontend/static/css/spinners.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
left: 50%;
1717
top: 50%;
1818
transform: translate(-50%, -50%);
19-
z-index: 1000;
19+
z-index: 10000;
20+
pointer-events: none;
2021
}
2122

2223
#umapSpinner .umap-spinner {

photomap/frontend/static/javascript/umap.js

Lines changed: 56 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,50 +1256,72 @@ function proximityClusterOrder(clusterIndices, points, startIndex) {
12561256
async function handleClusterClick(clickedIndex) {
12571257
const clickedPoint = points.find((p) => p.index === clickedIndex);
12581258
if (!clickedPoint) return;
1259+
1260+
// Show spinner immediately to provide visual feedback
1261+
showUmapSpinner();
1262+
1263+
// Yield to the browser to allow spinner to render before heavy computation
1264+
await new Promise(resolve => setTimeout(resolve, 0));
1265+
1266+
try {
1267+
const clickedCluster = clickedPoint.cluster;
1268+
const clusterColor = getClusterColor(clickedCluster);
1269+
let clusterIndices = points
1270+
.filter((p) => p.cluster === clickedCluster)
1271+
.map((p) => p.index);
1272+
1273+
// Remove clickedFilename from the list
1274+
clusterIndices = clusterIndices.filter((fn) => fn !== clickedIndex);
1275+
1276+
// --- Greedy random walk order from clicked point ---
1277+
const sort_algorithm =
1278+
clusterIndices.length > randomWalkMaxSize
1279+
? proximityClusterOrder
1280+
: randomWalkClusterOrder;
1281+
const sortedClusterIndices = sort_algorithm(
1282+
[clickedIndex, ...clusterIndices],
1283+
points,
1284+
clickedIndex
1285+
);
12591286

1260-
const clickedCluster = clickedPoint.cluster;
1261-
const clusterColor = getClusterColor(clickedCluster);
1262-
let clusterIndices = points
1263-
.filter((p) => p.cluster === clickedCluster)
1264-
.map((p) => p.index);
1265-
1266-
// Remove clickedFilename from the list
1267-
clusterIndices = clusterIndices.filter((fn) => fn !== clickedIndex);
1268-
1269-
// --- Greedy random walk order from clicked point ---
1270-
const sort_algorithm =
1271-
clusterIndices.length > randomWalkMaxSize
1272-
? proximityClusterOrder
1273-
: randomWalkClusterOrder;
1274-
const sortedClusterIndices = sort_algorithm(
1275-
[clickedIndex, ...clusterIndices],
1276-
points,
1277-
clickedIndex
1278-
);
1279-
1280-
const clusterMembers = sortedClusterIndices.map((index) => ({
1281-
index: index,
1282-
cluster: clickedCluster === -1 ? "unclustered" : clickedCluster,
1283-
color: clusterColor,
1284-
}));
1287+
const clusterMembers = sortedClusterIndices.map((index) => ({
1288+
index: index,
1289+
cluster: clickedCluster === -1 ? "unclustered" : clickedCluster,
1290+
color: clusterColor,
1291+
}));
12851292

1286-
setSearchResults(clusterMembers, "cluster");
1293+
setSearchResults(clusterMembers, "cluster");
1294+
} finally {
1295+
// Always hide spinner, even if there's an error
1296+
hideUmapSpinner();
1297+
}
12871298
}
12881299

12891300
// Handle single image selection (navigate to clicked image)
12901301
async function handleImageClick(clickedIndex) {
12911302
const clickedPoint = points.find((p) => p.index === clickedIndex);
12921303
if (!clickedPoint) return;
1293-
1294-
// Clear any existing search selection
1295-
exitSearchMode();
12961304

1297-
// Navigate directly to the clicked image without entering search mode
1298-
slideState.navigateToIndex(clickedIndex, false);
1305+
// Show spinner immediately to provide visual feedback
1306+
showUmapSpinner();
1307+
1308+
// Yield to the browser to allow spinner to render before heavy computation
1309+
await new Promise(resolve => setTimeout(resolve, 0));
12991310

1300-
// Exit fullscreen mode if enabled
1301-
if (isFullscreen && state.umapExitFullscreenOnSelection) {
1302-
setTimeout(() => toggleFullscreen(false), 100); // slight delay to avoid flicker
1311+
try {
1312+
// Clear any existing search selection
1313+
exitSearchMode();
1314+
1315+
// Navigate directly to the clicked image without entering search mode
1316+
slideState.navigateToIndex(clickedIndex, false);
1317+
1318+
// Exit fullscreen mode if enabled
1319+
if (isFullscreen && state.umapExitFullscreenOnSelection) {
1320+
setTimeout(() => toggleFullscreen(false), 100); // slight delay to avoid flicker
1321+
}
1322+
} finally {
1323+
// Always hide spinner, even if there's an error
1324+
hideUmapSpinner();
13031325
}
13041326
}
13051327

photomap/frontend/templates/modules/umap-floating-window.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@
117117
left: 50%;
118118
top: 50%;
119119
transform: translate(-50%, -50%);
120-
z-index: 1000;
120+
z-index: 10000;
121+
pointer-events: none;
121122
"
122123
>
123124
<div class="umap-spinner"></div>

0 commit comments

Comments
 (0)