Reflect current state: Postgres+pgvector replaces SQLite, vision pipeline (YOLO, CLIP, InsightFace, OCR) is shipped, card-grid browse views for tags/colors/ratings/people, map view, duplicate detection, and semantic search are all live. Remove completed items from future features. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
230 lines
8.1 KiB
Markdown
230 lines
8.1 KiB
Markdown
# Mulita - Self-Hosted Photo Management Application
|
|
|
|
A self-hosted, Docker-deployed photo management application inspired by Lightroom's workflow. Mulita provides a fast, keyboard-driven interface to browse, organize, tag, and manage your photo library.
|
|
|
|
## Features
|
|
|
|
- **Photo Organization**: Browse photos in a timeline view with virtual scrolling for performance
|
|
- **Thumbnail Generation**: Automatic thumbnail generation for all photo formats including RAW
|
|
- **Metadata Extraction**: Full EXIF/XMP metadata extraction and GPS mapping
|
|
- **Keyboard Shortcuts**: Lightroom-style keyboard navigation and actions
|
|
- **File Support**: JPEG, PNG, RAW formats (CR2, CR3, NEF, ARW, etc.), HEIC/HEIF, and videos
|
|
- **Heaps**: Temporary collections for organizing photos
|
|
- **Tags & Ratings**: Organize with tags, star ratings, and color labels — each with a card-grid browse view that drills into a full Timeline detail
|
|
- **Dark Mode**: Photography-optimized dark interface
|
|
- **Vision Pipeline**: YOLO object detection, OCR text extraction, CLIP embeddings for semantic search, InsightFace face detection and clustering
|
|
- **People View**: Browse identified people as cards, click to see all photos of a person
|
|
- **Map View**: Browse GPS-tagged photos on an interactive Leaflet map
|
|
- **Duplicate Detection**: Perceptual hash-based duplicate grouping with best-pick UI
|
|
- **Semantic Search**: Natural-language photo search powered by CLIP embeddings
|
|
|
|
## Tech Stack
|
|
|
|
### Backend
|
|
- Python 3.12 with FastAPI
|
|
- PostgreSQL + pgvector with SQLAlchemy (async) and Alembic migrations
|
|
- Celery + Redis for background tasks
|
|
- pyvips for fast thumbnail generation
|
|
- ExifTool for metadata extraction
|
|
- ONNX Runtime for vision models (YOLO, CLIP, InsightFace)
|
|
|
|
### Frontend
|
|
- React 18 with TypeScript
|
|
- Vite for fast development
|
|
- TanStack Query for data fetching
|
|
- TanStack Virtual for virtualized scrolling
|
|
- Tailwind CSS for styling
|
|
- Zustand for state management
|
|
|
|
## Quick Start
|
|
|
|
### Prerequisites
|
|
- Docker and Docker Compose
|
|
|
|
### Setup (one variable)
|
|
|
|
1. Clone the repo:
|
|
```bash
|
|
git clone <repository-url>
|
|
cd muleimage
|
|
```
|
|
|
|
2. Copy the example env file and set **one** variable — the **host**
|
|
directory that contains your photo library. Whatever you point at
|
|
will become your library inside Mulita.
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
# then edit .env and set PHOTO_DIRS:
|
|
# macOS / Linux: PHOTO_DIRS=/Users/you/Pictures
|
|
# Network share: PHOTO_DIRS=/mnt/nas/photos
|
|
# Windows (WSL): PHOTO_DIRS=/mnt/c/Users/you/Pictures
|
|
```
|
|
|
|
3. Start the stack:
|
|
```bash
|
|
docker compose up -d
|
|
```
|
|
|
|
4. Open `http://localhost:3000`. On first boot Mulita will:
|
|
- Mount your `PHOTO_DIRS` at `/photos` inside the container
|
|
- Auto-create a source root called **Library** pointing at `/photos`
|
|
- Queue an initial scan, generate thumbnails, and start serving them
|
|
|
|
You don't need to touch `mulita.yml` or the API to get started.
|
|
|
|
### Configuration knobs
|
|
|
|
Everything is environment-driven. `PHOTO_DIRS` is the only required
|
|
value; the rest have sensible defaults documented in `.env.example`:
|
|
|
|
| Variable | Default | Notes |
|
|
|----------------------|---------|----------------------------------------------------|
|
|
| `PHOTO_DIRS` | — | **Required.** Host path mounted at `/photos`. |
|
|
| `FRONTEND_PORT` | `3000` | SPA host port. Bump if `3000` is taken. |
|
|
| `BACKEND_PORT` | `8001` | Direct backend port (debug only — frontend uses internal nginx proxy). |
|
|
| `REDIS_PORT` | `6379` | Redis host port (internal services don't need it). |
|
|
| `ALLOWED_ORIGINS` | `*` | Comma-separated CORS origins for direct backend access. Lock down for prod, e.g. `https://photos.example.com`. |
|
|
| `LOG_LEVEL` | `INFO` | Backend + worker log level. `DEBUG` for chasing scan issues. |
|
|
| `TZ` | `UTC` | Container timezone. Affects log timestamps and "added at". |
|
|
| `CELERYD_CONCURRENCY`| `4` | Parallel worker processes (scans, thumbs, metadata). Lower on a Pi, higher on a beefy host. |
|
|
|
|
### Accessing from another machine
|
|
|
|
The frontend talks to the backend through its bundled nginx, which
|
|
proxies `/api/` to the backend on the internal compose network. That
|
|
means requests are always **same-origin** as the page, so accessing
|
|
Mulita from another host works without any CORS dance:
|
|
|
|
```
|
|
http://<your-server-ip>:3000
|
|
```
|
|
|
|
If you want to put it behind a reverse proxy at e.g.
|
|
`https://photos.your.tld`, set `ALLOWED_ORIGINS` to that host so the
|
|
backend's direct port (`BACKEND_PORT`) also accepts cross-origin
|
|
requests if anything bypasses the proxy.
|
|
|
|
### How libraries are managed
|
|
|
|
Mulita is **config-driven**: the host directory you mount via
|
|
`PHOTO_DIRS` becomes your library, and the backend automatically
|
|
registers it as a source root on startup. There is no UI for adding
|
|
or removing source roots — to change what Mulita scans, edit `.env`
|
|
(or `docker-compose.yml` for multi-mount setups) and restart the
|
|
stack.
|
|
|
|
This keeps the model simple: **the docker mount IS the library**.
|
|
No two layers, no confusion about which view to use.
|
|
|
|
### Changing or adding libraries
|
|
|
|
To point at a different library:
|
|
1. Edit `PHOTO_DIRS` in `.env`
|
|
2. `docker compose down`
|
|
3. (Optional, for a clean slate) `docker volume rm muleimage_db_data muleimage_thumbs_data muleimage_proxies_data`
|
|
4. `docker compose up -d`
|
|
|
|
The new library shows up automatically. Without step 3 the old
|
|
library's metadata stays in the DB and you'll see a warning at
|
|
startup that the old source root's path is missing on disk —
|
|
that's a hint to clean up.
|
|
|
|
For multiple libraries, edit `docker-compose.yml` and add additional
|
|
mount lines:
|
|
|
|
```yaml
|
|
volumes:
|
|
- ${PHOTO_DIRS}:/photos:rw
|
|
- /Volumes/Archive:/archive:rw # additional library
|
|
```
|
|
|
|
Each mounted directory will need a corresponding source root row in
|
|
the DB; today that means `POST /api/v1/folders` via curl, or wait
|
|
for the multi-mount auto-registration that's on the roadmap.
|
|
|
|
### Read-only libraries
|
|
|
|
The default mount is `:rw` because file operations (rename, move,
|
|
empty discard pile) need to mutate the filesystem. If you want a
|
|
strict read-only library — pointing at a network share, an
|
|
authoritative archive, etc. — flip `:rw` to `:ro` in
|
|
`docker-compose.yml`. Mulita will keep working for browsing, rating,
|
|
color labels, picks, heaps, and the (soft) discard flag, but the
|
|
following will return an OS error:
|
|
|
|
- `PATCH /photos/{id}` with a new `filename` (rename)
|
|
- `POST /photos/move` (bulk move)
|
|
- `DELETE /discard/empty` (file unlinks)
|
|
|
|
**Heads up**: with `:rw`, Mulita has full write access to whatever
|
|
host directory you mount. Treat the same way you would Lightroom's
|
|
catalog folder.
|
|
|
|
## Architecture
|
|
|
|
The application consists of 5 Docker services:
|
|
|
|
- **frontend**: React SPA served by Nginx
|
|
- **backend**: FastAPI REST API
|
|
- **worker**: Celery workers for background tasks (thumbnails, metadata, vision pipeline)
|
|
- **redis**: Message broker for Celery
|
|
- **db**: PostgreSQL with pgvector extension (for CLIP/face embeddings)
|
|
|
|
## Keyboard Shortcuts
|
|
|
|
| Key | Action |
|
|
|-----|--------|
|
|
| `←` `→` `↑` `↓` | Navigate photos |
|
|
| `Space` | Quick preview |
|
|
| `Enter` | Open loupe view |
|
|
| `T` | Add to active heap |
|
|
| `1-5` | Set star rating |
|
|
| `Tab` | Toggle left sidebar |
|
|
| `I` | Toggle metadata panel |
|
|
| `G` | Grid view |
|
|
| `E` | Loupe view |
|
|
| `Delete` | Move to trash |
|
|
|
|
## Development
|
|
|
|
### Backend Development
|
|
```bash
|
|
cd backend
|
|
pip install -r requirements.txt
|
|
uvicorn app.main:app --reload
|
|
```
|
|
|
|
### Frontend Development
|
|
```bash
|
|
cd frontend
|
|
npm install
|
|
npm run dev
|
|
```
|
|
|
|
## Configuration
|
|
|
|
Source roots are managed by the UI / API (the database owns them). Edit
|
|
`mulita.yml` to configure operational settings only:
|
|
|
|
- Thumbnail sizes, quality, and format
|
|
- Scanner behaviour (watch, batch size, initial scan)
|
|
- Performance tuning (concurrency, cache TTLs, DB pool)
|
|
|
|
## Performance
|
|
|
|
- Handles 100,000+ photos efficiently
|
|
- Virtual scrolling for smooth timeline navigation
|
|
- Thumbnail generation at 10+ photos/second
|
|
- PostgreSQL full-text search with tsvector indexing
|
|
- pgvector for fast nearest-neighbor embedding search
|
|
|
|
## Future Features
|
|
|
|
- Smart albums (auto-populated by saved filters)
|
|
- Export presets
|
|
- Multi-user support
|
|
|
|
## License
|
|
|
|
MIT |