# 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 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://: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