FastAPI + Angular single-page application for browsing, filtering, and managing photos.
python viewer.py
# Open http://localhost:5000This serves both the API and the pre-built Angular application on a single port.
For higher throughput (4 Uvicorn workers):
python viewer.py --productionRun the API server and Angular dev server separately:
# Terminal 1: API server
python viewer.py
# API available at http://localhost:5000
# Terminal 2: Angular dev server with hot reload
cd client && npx ng serve
# Open http://localhost:4200 (proxies API calls to :5000)Optional password protection via config:
{
"viewer": {
"password": "your-password-here"
}
}When set, users must authenticate before accessing the viewer. An optional edition_password grants access to person management and comparison mode.
For family NAS scenarios where each member has private photo directories. Enabled by adding a users section to scoring_config.json:
{
"users": {
"alice": {
"password_hash": "salt_hex:dk_hex",
"display_name": "Alice",
"role": "superadmin",
"directories": ["/volume1/Photos/Alice"]
},
"bob": {
"password_hash": "salt_hex:dk_hex",
"display_name": "Bob",
"role": "user",
"directories": ["/volume1/Photos/Bob"]
},
"shared_directories": [
"/volume1/Photos/Family",
"/volume1/Photos/Vacations"
]
}
}Users are created via CLI only (no registration UI):
python database.py --add-user alice --role superadmin --display-name "Alice"See Configuration for full reference.
| Role | View own + shared | Rate/favorite | Manage persons/faces | Trigger scans |
|---|---|---|---|---|
user |
yes | yes | no | no |
admin |
yes | yes | yes | no |
superadmin |
yes | yes | yes | yes |
Each user sees photos from their configured directories plus shared directories. Visibility is enforced across all endpoints: gallery, thumbnails, downloads, stats, filter options, and person pages.
In multi-user mode, star ratings, favorites, and rejected flags are stored per-user in the user_preferences table. Each user rates independently — Alice's favorites don't affect Bob's view.
To migrate existing single-user ratings:
python database.py --migrate-user-preferences --user alice| Filter | Options |
|---|---|
| Photo Type | Top Picks, Portraits, People in Scene, Landscapes, Architecture, Nature, Animals, Art & Statues, Black & White, Low Light, Silhouettes, Macro, Astrophotography, Street, Long Exposure, Aerial & Drone, Concerts |
| Quality Level | Good (6+), Great (7+), Excellent (8+), Best (9+) |
| Camera & Lens | Equipment-based filtering |
| Person | Filter by recognized person |
| Category | Filter by photo category |
| Category | Filters |
|---|---|
| Date | Start and end date |
| Scores | Aggregate, aesthetic, TOPIQ score, quality score |
| Extended Quality | Aesthetic IAA (artistic merit), Face Quality IQA, LIQE score |
| Face Metrics | Face quality, eye sharpness, face sharpness, face ratio, face confidence, face count |
| Composition | Composition score, power points, leading lines, isolation, composition pattern |
| Subject Saliency | Subject sharpness, subject prominence, subject placement, background separation |
| Technical | Sharpness, contrast, dynamic range, noise level |
| Color | Color score, saturation, luminance, histogram spread |
| Exposure | Exposure score |
| User Ratings | Star rating |
| Camera Settings | ISO, aperture (f-stop range slider), focal length (range slider) |
| Content | Tags, monochrome toggle |
Filter by SAMP-Net detected patterns:
- rule_of_thirds, golden_ratio, center, diagonal
- horizontal, vertical, symmetric, triangle
- curved, radial, vanishing_point, pattern, fill_frame
25+ sortable columns grouped by category:
| Group | Columns |
|---|---|
| General | Aggregate Score, Aesthetic, TOPIQ Score, Date Taken, Star Rating, Favorites, Rejected |
| Extended Quality | Aesthetic IAA, Face Quality IQA, LIQE Score |
| Face Metrics | Face Quality, Eye Sharpness, Face Sharpness, Face Ratio, Face Confidence, Face Count |
| Technical | Tech Sharpness, Contrast, Noise Level |
| Color | Color Score, Saturation |
| Exposure | Exposure Score, Mean Luminance, Histogram Spread, Dynamic Range |
| Composition | Composition Score, Power Point Score, Leading Lines, Isolation Bonus, Composition Pattern |
| Subject Saliency | Subject Sharpness, Subject Prominence, Subject Placement, Background Separation |
- Thumbnail with score badge
- Clickable tags for quick filtering
- Person avatars for recognized faces
- Category badge
- Click photos to select, Shift+Click for range selection
- Action bar appears with selection count and available actions
- Favorite — Mark all selected as favorite (clears rejected)
- Reject — Mark all selected as rejected (clears favorite and rating)
- Rate — Set star rating (1–5) for all selected, or clear rating
- Copy filenames — Copy selected filenames to clipboard
- Download — Download selected photos
- Clear selection with Escape or the Clear button
Bulk actions require edition mode. Double-click any photo to download it directly.
- Layout Mode - Switch between Grid (uniform cards) and Mosaic (justified rows preserving aspect ratios). Mosaic is desktop-only; mobile always uses grid.
- Thumbnail Size - Slider to adjust card/row height (120–400px, persisted in localStorage)
- Hide Details - Hide photo metadata on cards (grid mode only)
- Hide Tooltip - Disable the hover tooltip that shows photo details on desktop
- Hide Blinks - Filter out photos with detected blinks
- Best of Burst - Show only top-scored photo from each burst
- Infinite Scroll - Photos load as you scroll
Click the "Similar" button on any photo to choose a similarity mode:
- Visual (default) — pHash hamming distance (70%) + CLIP/SigLIP cosine similarity (30%). Falls back to CLIP-only when no pHash is available.
- Color — Histogram intersection (70%) + saturation distance (10%) + luminance distance (10%) + monochrome bonus (10%). Pre-filters by monochrome flag and saturation range.
- Person — Finds photos containing the same person(s). Uses
person_idwhen available (fast), otherwise falls back to face embedding cosine similarity.
Use the similarity threshold slider (0–90%) to control how strict the matching is (not shown in person mode). The panel supports infinite scroll for large result sets.
Active filters shown as removable chips with counts at top of gallery.
Dropdown shows persons with face thumbnails. Click to filter gallery.
Click person name to view all their photos at /person/<id>.
Access via header button or /persons:
| Action | How To |
|---|---|
| Merge | Select source person, click target, confirm |
| Delete | Click delete button on person card |
| Rename | Click person name to edit inline |
When viewer.features.show_scan_button is true and the user has superadmin role, a Scan button appears in the gallery header.
- Select directories to scan from the modal
- Scan runs as a background subprocess (
facet.py) - Only one scan at a time (global lock)
- Progress displayed in a terminal-style output area
This is useful when the viewer runs on the same machine that has GPU access for scoring.
Hybrid search combining CLIP/SigLIP embedding similarity (70%) with FTS5 BM25 text matching on captions and tags (30%). Type a query like "sunset over mountains" or "child playing in snow" and the viewer returns matching photos ranked by combined score.
- Requires stored
clip_embeddingdata (computed during scoring) - Uses sqlite-vec for KNN vector search when installed, falls back to in-memory NumPy
- FTS5 text search on AI captions/tags provides additional keyword matching (run
database.py --rebuild-ftsto enable) - Uses the same embedding model as the active VRAM profile (SigLIP 2 for 16gb/24gb, CLIP ViT-L-14 for legacy/8gb)
- Controlled by
viewer.features.show_semantic_search(default:true)
Organize photos into named albums. Access via the /albums route.
Create albums and add photos from the gallery using multi-select. Albums support:
- Name and description
- Custom cover photo
- Custom ordering
- Browse album contents at
/album/:albumId
Save a combination of filters (camera, tag, person, date range, score thresholds, etc.) as a smart album. Smart albums dynamically update as new photos match the saved filter criteria. The filter combination is stored as JSON in smart_filter_json.
| Endpoint | Description |
|---|---|
GET /api/albums |
List all albums |
POST /api/albums |
Create album |
GET /api/albums/{id} |
Get album details |
PUT /api/albums/{id} |
Update album (name, description, cover) |
DELETE /api/albums/{id} |
Delete album |
GET /api/albums/{id}/photos |
List photos in album (supports page, per_page, sort, sort_direction) |
POST /api/albums/{id}/photos |
Add photos to album |
DELETE /api/albums/{id}/photos |
Remove photos from album |
Controlled by viewer.features.show_albums (default: true).
Share albums with external users via tokenized links. No authentication required to view shared albums.
| Action | How To |
|---|---|
| Share | Open album, click "Share" button to generate a shareable link |
| Revoke | Click "Unshare" to invalidate the share token |
| View | Recipients open the link to browse the shared album at /shared/album/:id |
| Endpoint | Description |
|---|---|
POST /api/albums/{id}/share |
Generate share token for album |
DELETE /api/albums/{id}/share |
Revoke share token |
GET /api/shared/album/{id}?token= |
View shared album (no auth required) |
Get a detailed breakdown of a photo's scores with strengths, weaknesses, and improvement suggestions.
Available on all VRAM profiles. Analyzes stored metrics (aesthetic, composition, sharpness, face quality, etc.) and generates a structured explanation of why the photo scored the way it did.
Uses the configured VLM (Qwen3.5-2B or Qwen3.5-4B) to provide a richer, context-aware critique. Requires 16gb or 24gb VRAM profile and viewer.features.show_vlm_critique: true.
| Endpoint | Description |
|---|---|
GET /api/critique?path=<photo_path>&mode=rule |
Rule-based score breakdown |
GET /api/critique?path=<photo_path>&mode=vlm |
VLM-powered critique (requires GPU) |
Controlled by viewer.features.show_critique (default: true) and viewer.features.show_vlm_critique (default: false).
Get an AI-generated natural language caption for any photo. Captions are generated on first request and cached in the caption database column. Captions can be edited manually in edition mode via the photo detail page.
| Endpoint | Description |
|---|---|
GET /api/caption?path=<photo_path> |
Get or generate caption for a photo |
PUT /api/caption |
Update caption text (edition mode required) |
Also available via CLI for bulk generation and translation:
python facet.py --generate-captions # Generate captions for all uncaptioned photos
python facet.py --translate-captions # Translate captions to configured target languageCaption translation uses MarianMT (CPU, no GPU required). Configure the target language in scoring_config.json under translation.target_language (default: "fr"). Supported languages: French, German, Spanish, Italian.
Controlled by viewer.features.show_captions (default: true). Requires 16gb or 24gb VRAM profile for VLM-based captioning.
Browse photos taken on the same calendar date in previous years. A memories dialog shows a year-by-year retrospective of matching photos.
| Endpoint | Description |
|---|---|
GET /api/memories?date=YYYY-MM-DD |
Get photos taken on this date in previous years |
Controlled by viewer.features.show_memories (default: true).
Chronological photo browser with date-based navigation. Scroll through photos organized by date with a sidebar showing available years and months.
| Endpoint | Description |
|---|---|
GET /api/timeline?cursor=&limit=&direction= |
Paginated timeline photos with cursor-based navigation |
GET /api/timeline/dates?year=&month= |
Available dates for year/month navigation |
Access via the /timeline route. Controlled by viewer.features.show_timeline (default: true).
View photos on an interactive map based on GPS coordinates extracted from EXIF data. Uses Leaflet for map rendering with clustering at different zoom levels.
Extract GPS coordinates from existing photos:
python facet.py --extract-gps # Extract GPS lat/lng from EXIF into databaseGPS coordinates are also extracted automatically during scoring for new photos.
| Endpoint | Description |
|---|---|
GET /api/photos/map?bounds=&zoom=&limit= |
Photos within map bounds (clustered by zoom) |
GET /api/photos/map/count |
Total count of geotagged photos |
Access via the /map route. Controlled by viewer.features.show_map (default: true).
Curated photo diaporamas (slideshows) grouped by theme. Access via the /capsules route.
Capsules are auto-generated from your library using multiple algorithms:
- Journey — trips detected via GPS clustering, with reverse-geocoded destination names ("Journey to Rome — March 2025")
- Moments with [Person] — best photos of each recognized person
- Seasonal Palette — photos grouped by season + year
- Golden Collection — top 1% by aggregate score
- Color Story — visually similar groups via CLIP embedding clustering
- This Week, Years Ago — extended "On This Day" across ±3 days
- Location — geotagged photo clusters with place names
- Favorites — favorited photos grouped by year and season
- Dimension-based — auto-generated from camera, lens, category, composition pattern, focal length range, time of day, star rating, and cross-dimensional combos
Click any capsule card to start a slideshow. Features:
- Themed transitions — slide (journeys), zoom (portraits), kenburns (golden/seasonal), crossfade (default)
- Auto-chaining — when a capsule finishes, a transition card shows the next capsule before continuing
- Shuffle & resume — photos are shuffled for variety; resume position is tracked per capsule
- Adaptive grouping — portrait photos are grouped side-by-side based on viewport aspect ratio
- Save as album — save any capsule as a permanent album
Capsules rotate on a configurable schedule (default: 24 hours). Cover photos and seeded discovery capsules align to the same rotation period. The "Regenerate" button in the header forces an immediate refresh.
Location and journey capsules show place names (e.g., "Paris, France") instead of coordinates. This uses offline geocoding via the reverse_geocoder package — no API calls needed. Results are cached in the database.
Install: pip install reverse_geocoder
| Endpoint | Description |
|---|---|
GET /api/capsules |
Paginated capsule list (cached) |
GET /api/capsules/{id}/photos |
Photos for a specific capsule |
POST /api/capsules/{id}/save-album |
Save capsule as album (edition mode) |
See Configuration — Capsules for all settings.
Browse your photo library by directory structure. Access via the /folders route.
- Breadcrumb navigation to move up the directory tree
- Each folder shows a cover photo (highest-scoring image in that directory)
- Click a folder to descend into it, or click a photo to open it in the gallery
- Respects multi-user directory visibility in multi-user mode
Filter photos by geographic location using an interactive map picker:
- Click the location filter button to open the map dialog
- Click or drag on the map to set a center point
- Adjust the radius slider to control the search area
- Photos within the selected radius are filtered into the gallery
- Requires GPS coordinates (run
--extract-gpsif photos have EXIF GPS data)
Find person clusters that may be the same individual. Access via /merge-suggestions or from the Manage Persons page.
- Similarity threshold slider — adjust how similar two persons must look to be suggested as a merge (lower = more suggestions, higher = more conservative)
- One-click merge — review each suggestion and merge with a single click
- Batch merge — select multiple suggestions and merge them all at once
- Also available via CLI:
python facet.py --suggest-person-merges
Find groups of visually similar photos across your library for culling. Unlike burst detection (which groups by time), similar groups use CLIP/SigLIP embedding similarity to find photos that look alike regardless of when they were taken.
Access via the similarity tab in the burst culling component.
| Endpoint | Description |
|---|---|
GET /api/similar-groups?threshold=&page=&per_page= |
Paginated groups of visually similar photos |
Requires a non-empty edition_password in config (single-user) or admin/superadmin role (multi-user).
Click "Compare" button in gallery header.
- Side-by-side photo comparison
- Selection strategies dropdown
- Progress bar toward 50 comparisons
- Real-time statistics (A wins, B wins, ties)
- Category filter for focused comparison
| Key | Action |
|---|---|
A |
Select left photo as winner |
B |
Select right photo as winner |
T |
Mark as tie |
S |
Skip pair |
Escape |
Close category override modal |
| Strategy | Description |
|---|---|
uncertainty |
Similar scores (most informative) |
boundary |
6-8 score range (ambiguous zone) |
active |
Fewest comparisons (ensures coverage) |
random |
Random pairs (baseline) |
- Always visible below comparison
- Sliders for each weight metric
- Real-time score preview with delta
- "Suggest Weights" learns from comparisons
- "Reset" restores original weights
- Click edit button on photo's category badge
- Select target category
- Click "Analyze Filter Conflicts"
- Review why photo doesn't match
- Apply override to manually assign
The Stats page (/stats) provides analytics across 5 tabs. Use the category and date range selectors in the toolbar to filter all charts to a specific subset of your library.
| Tab | Description |
|---|---|
| Equipment | Camera bodies, lenses, and combos (top 20 each) |
| Shooting Settings | ISO, aperture, focal length, shutter speed distributions |
| Timeline | Photos over time |
| Categories | Category analytics, weight management, and score correlations |
| Correlations | Custom X/Y metric correlation charts with grouping |
Interactive dashboard with 4 sub-tabs:
| Sub-tab | Description |
|---|---|
| Breakdown | Photo counts per category, average scores, score distribution histograms |
| Weights | Radar chart comparison (up to 5 categories), weight heatmap, and weight editor (edition mode) |
| Correlations | Pearson correlation heatmap showing how each dimension influences the aggregate, click-to-detail view |
| Overlap | Filter overlap analysis showing which categories share matching photos |
Each chart has a toggleable ? help button explaining how to read it. A global help toggle in the sub-tab bar shows explanations for all sub-tabs.
Available in the Weights sub-tab when edition mode is active:
- Select a category from the dropdown
- Adjust the 12 weight sliders (should sum to 100%)
- Use "Normalize to 100" to auto-balance
- Expand the collapsible Modifiers section to adjust bonuses/penalties
- The Score Distribution Preview shows a live before/after histogram as you move sliders
- Click Save to update
scoring_config.json(creates a timestamped backup) - Click Recompute Scores (appears after save) to apply new weights to all photos in that category
All stats are user-aware in multi-user mode — each user sees analytics for their visible photos only.
| Key | Action |
|---|---|
Escape |
Close filter drawer or clear selections |
Enter |
Submit search |
Shift+Click |
Range-select photos between last selected and clicked |
Double-click |
Download photo |
{
"viewer": {
"display": {
"tags_per_photo": 4,
"card_width_px": 168,
"image_width_px": 160,
"image_jpeg_quality": 96
}
}
}{
"viewer": {
"pagination": {
"default_per_page": 64
}
}
}{
"viewer": {
"dropdowns": {
"max_cameras": 50,
"max_lenses": 50,
"max_persons": 50,
"max_tags": 20,
"min_photos_for_person": 10
}
}
}Set min_photos_for_person higher to hide persons with few photos from the filter dropdown.
{
"viewer": {
"quality_thresholds": {
"good": 6,
"great": 7,
"excellent": 8,
"best": 9
}
}
}{
"viewer": {
"defaults": {
"hide_blinks": true,
"hide_bursts": true,
"hide_duplicates": true,
"hide_details": true,
"hide_rejected": true,
"sort": "aggregate",
"sort_direction": "DESC",
"type": ""
},
"default_category": ""
}
}{
"viewer": {
"photo_types": {
"top_picks_min_score": 7,
"top_picks_min_face_ratio": 0.2,
"top_picks_weights": {
"aggregate_percent": 30,
"aesthetic_percent": 28,
"composition_percent": 18,
"face_quality_percent": 24
}
}
}
}Run these for optimal performance:
python database.py --migrate-tags # 10-50x faster tag queries
python database.py --refresh-stats # Precompute aggregations
python database.py --optimize # Defragment databasePrecomputed aggregations with 5-minute TTL:
- Total photo counts
- Camera/lens model counts
- Person counts
- Category and pattern counts
Check status:
python database.py --stats-infoFilter dropdowns load on-demand via API:
/api/filter_options/cameras/api/filter_options/lenses/api/filter_options/tags/api/filter_options/persons/api/filter_options/patterns/api/filter_options/categories/api/filter_options/apertures/api/filter_options/focal_lengths
Interactive API documentation is available at /api/docs (Swagger UI) and the OpenAPI schema at /api/openapi.json.
| Endpoint | Description |
|---|---|
GET /api/photos |
Paginated photo list with filters |
GET /api/photo |
Single photo details |
GET /api/type_counts |
Photo counts per type |
GET /api/similar_photos/{path} |
Similar photos (modes: visual, color, person) |
GET /api/search?q=&limit=&threshold= |
Semantic text-to-image search |
GET /api/critique?path=&mode= |
AI critique (rule-based or VLM) |
GET /api/config |
Viewer configuration |
| Endpoint | Description |
|---|---|
POST /api/auth/login |
Authenticate and receive token |
POST /api/auth/edition/login |
Unlock edition mode |
POST /api/auth/edition/logout |
Lock edition mode (drop privileges, stay authenticated) |
GET /api/auth/status |
Check authentication status |
| Endpoint | Description |
|---|---|
GET /thumbnail |
Photo thumbnail |
GET /face_thumbnail/{id} |
Face crop thumbnail |
GET /person_thumbnail/{id} |
Person representative thumbnail |
GET /image |
Full-resolution image |
| Endpoint | Description |
|---|---|
GET /api/filter_options/cameras |
Camera models with counts |
GET /api/filter_options/lenses |
Lens models with counts |
GET /api/filter_options/tags |
Tags with counts |
GET /api/filter_options/persons |
Persons with counts |
GET /api/filter_options/patterns |
Composition patterns |
GET /api/filter_options/categories |
Categories with counts |
GET /api/filter_options/apertures |
Distinct f-stop values with counts |
GET /api/filter_options/focal_lengths |
Distinct focal lengths with counts |
| Endpoint | Description |
|---|---|
POST /api/photos/batch_favorite |
Mark multiple photos as favorite |
POST /api/photos/batch_reject |
Mark multiple photos as rejected |
POST /api/photos/batch_rating |
Set star rating for multiple photos |
| Endpoint | Description |
|---|---|
GET /api/persons |
List all persons |
POST /api/persons/{id}/rename |
Rename a person |
POST /api/persons/merge |
Merge two persons (JSON body) |
POST /api/persons/merge/{source_id}/{target_id} |
Merge source person into target |
POST /api/persons/merge_batch |
Merge multiple persons at once |
POST /api/persons/{id}/delete |
Delete a person |
POST /api/persons/delete_batch |
Delete multiple persons at once |
| Endpoint | Description |
|---|---|
GET /api/albums |
List all albums |
POST /api/albums |
Create album |
GET /api/albums/{id} |
Get album details |
PUT /api/albums/{id} |
Update album |
DELETE /api/albums/{id} |
Delete album |
GET /api/albums/{id}/photos |
List photos in album (paginated) |
POST /api/albums/{id}/photos |
Add photos to album |
DELETE /api/albums/{id}/photos |
Remove photos from album |
POST /api/albums/{id}/share |
Generate share token |
DELETE /api/albums/{id}/share |
Revoke share token |
GET /api/shared/album/{id}?token= |
View shared album (no auth) |
| Endpoint | Description |
|---|---|
GET /api/memories?date= |
Photos taken on this date in previous years |
GET /api/memories/check |
Check if memories exist for a date |
GET /api/caption?path= |
Get or generate AI caption |
PUT /api/caption |
Update photo caption (edition mode) |
GET /api/timeline?cursor=&limit=&direction= |
Paginated timeline photos |
GET /api/timeline/dates?year=&month= |
Available dates for navigation |
GET /api/timeline/years |
Available years with photo counts |
GET /api/timeline/months |
Available months for a year |
GET /api/photos/map?bounds=&zoom=&limit= |
Geotagged photos within bounds |
GET /api/photos/map/count |
Count of geotagged photos |
| Endpoint | Description |
|---|---|
GET /api/stats/overview |
Overall scoring statistics summary |
GET /api/stats/score_distribution |
Score distribution histogram data |
GET /api/stats/top_cameras |
Top cameras by photo count |
GET /api/stats/categories |
Category counts and averages |
GET /api/stats/gear |
Camera/lens/combo counts |
GET /api/stats/settings |
Shooting settings distributions |
GET /api/stats/timeline |
Timeline data |
GET /api/stats/correlations |
Custom metric correlations |
GET /api/stats/categories/breakdown |
Per-category photo counts and score distributions |
GET /api/stats/categories/weights |
Category weights and modifiers from config |
GET /api/stats/categories/correlations |
Pearson r correlation per dimension per category |
GET /api/stats/categories/metrics?category=X |
Raw metric values for client-side preview |
GET /api/stats/categories/overlap |
Filter overlap analysis between categories |
POST /api/stats/categories/update |
Update category weights/modifiers (edition mode) |
POST /api/stats/categories/recompute |
Recompute scores for a category (edition mode) |
| Endpoint | Description |
|---|---|
GET /api/comparison/next_pair |
Get next photo pair for comparison |
POST /api/comparison/submit |
Submit comparison result |
POST /api/comparison/reset |
Reset comparison data |
GET /api/comparison/stats |
Comparison session statistics |
GET /api/comparison/history |
List past comparisons |
POST /api/comparison/edit |
Edit a comparison result |
POST /api/comparison/delete |
Delete a comparison |
GET /api/comparison/coverage |
Category coverage of comparisons |
GET /api/comparison/confidence |
Confidence metrics for learned scores |
GET /api/comparison/photo_metrics |
Raw metrics for photos |
GET /api/comparison/category_weights |
Category weights/filters |
GET /api/comparison/learned_weights |
Suggested weights from comparisons |
POST /api/comparison/preview_score |
Preview with custom weights |
POST /api/comparison/suggest_filters |
Analyze filter conflicts |
POST /api/comparison/override_category |
Override photo category |
POST /api/recalculate |
Recalculate scores with current weights |
| Endpoint | Description |
|---|---|
GET /api/burst-groups |
List burst groups for culling |
POST /api/burst-groups/select |
Select keepers from a burst group |
GET /api/similar-groups?threshold=&page=&per_page= |
Groups of visually similar photos |
POST /api/similar-groups/select |
Select keepers from a similar group |
GET /api/culling-groups |
Combined burst and similar groups |
POST /api/culling-groups/confirm |
Confirm culling selections |
| Endpoint | Description |
|---|---|
POST /api/scan/start |
Start a scoring scan (superadmin only) |
GET /api/scan/status |
Check scan progress |
GET /api/scan/directories |
List configured scan directories |
| Endpoint | Description |
|---|---|
GET /api/person/{id}/faces |
List faces for a person |
POST /api/person/{id}/avatar |
Set person avatar face |
GET /api/photo/faces |
List faces detected in a photo |
POST /api/face/{id}/assign |
Assign a face to a person |
POST /api/photo/assign_all_faces |
Assign all faces in a photo to a person |
POST /api/photo/unassign_person |
Unassign a person from a photo |
| Endpoint | Description |
|---|---|
POST /api/photo/set_rating |
Set star rating for a photo |
POST /api/photo/toggle_favorite |
Toggle favorite status |
POST /api/photo/toggle_rejected |
Toggle rejected status |
| Endpoint | Description |
|---|---|
POST /api/config/update_weights |
Update scoring weights |
GET /api/config/weight_snapshots |
List saved weight snapshots |
POST /api/config/save_snapshot |
Save current weights as snapshot |
POST /api/config/restore_weights |
Restore weights from snapshot |
| Endpoint | Description |
|---|---|
GET /api/merge_suggestions |
Suggested person merges based on face similarity |
| Endpoint | Description |
|---|---|
GET /api/folders |
List photo folder structure |
| Endpoint | Description |
|---|---|
GET /api/download/options |
Available download types for a photo (path, optional is_shared) |
GET /api/download |
Download a photo (path, type=original|darktable|raw, optional profile) |
Download types:
original— Serve the file as-is (JPG/HEIF) or rawpy-converted to JPEG (RAW files).darktable— Convert companion RAW with a named darktable profile (requiresprofileparam). Falls back to original if no companion RAW exists.raw— Serve the companion RAW file as-is (not available in shared albums).
The /api/download/options endpoint detects companion RAW files automatically and returns available options including configured darktable profiles. The viewer uses this to populate a per-photo download menu.
| Endpoint | Description |
|---|---|
GET /api/plugins |
List configured plugins |
POST /api/plugins/test-webhook |
Test a webhook plugin |
| Endpoint | Description |
|---|---|
GET /health |
Server health check |
GET /ready |
Server readiness check |
| Endpoint | Description |
|---|---|
GET /api/i18n/languages |
List available languages |
GET /api/i18n/{lang} |
Get translations for a language |
| Endpoint | Description |
|---|---|
GET /api/filter_options/location_name?lat=&lng= |
Reverse geocode coordinates to place name |
| Issue | Solution |
|---|---|
| Slow page load | Run --migrate-tags and --optimize |
| Filters not showing | Check --stats-info, run --refresh-stats |
| Person filter empty | Run --cluster-faces-incremental |
| Compare button missing | Set a non-empty edition_password (single-user) or use admin/superadmin role (multi-user) |
| Password not working | Check viewer.password (single-user) or verify password hash (multi-user) |
| User can't see photos | Check directories in their user config and shared_directories |
| Scan button missing | Requires superadmin role and viewer.features.show_scan_button: true |
| Search returns no results | Ensure photos have clip_embedding data (run scoring first) |
| VLM critique unavailable | Requires 16gb/24gb VRAM profile and viewer.features.show_vlm_critique: true |
| Map shows no photos | Run --extract-gps to populate GPS columns, ensure photos have EXIF GPS data |
| Captions not generating | Requires 16gb/24gb VRAM profile for VLM captioning |
| Timeline empty | Ensure photos have date_taken values |
| Port 5000 in use | Change port in viewer.py or kill the conflicting process |