# picvault **Repository Path**: spare_time/picvault ## Basic Information - **Project Name**: picvault - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-08 - **Last Updated**: 2026-10-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # PicVault PicVault is an MVP for local-first photo indexing and semantic search. It scans a directory on your machine, stores metadata in SQLite, builds local embeddings, and serves a browser UI at `http://127.0.0.1:3456`. ## MVP status The MVP supports local directory scanning, thumbnail generation, SQLite-backed photo metadata, path-based metadata filtering, semantic search with a local Chinese-CLIP model running in-process through ONNX Runtime, and an exact persistent `snapshot` vector backend (the default). Without the model PicVault still runs, but falls back to a deterministic development embedder whose results are unrelated to image content; the UI shows a warning in that case. OCR, face clustering, and ANN/USearch vector search are deferred from this MVP; search is an exact scan over the persisted vectors. ## Privacy - PicVault binds only to `127.0.0.1:3456` by default; LAN access is disabled. - Photos, metadata, thumbnails, vectors, and search queries remain on the local machine. - PicVault does not download models or send data to a cloud service. The model install script downloads the model only when you run it yourself. - API error handling does not log raw search queries. - Diagnostic output stays local on the service process; the app-data `logs/` directory is reserved for local log files and is never uploaded. ## Supported platforms PicVault targets macOS, Windows, and Linux/NAS environments. The default build requires libvips 8.10+ with HEIC/HEIF support. Follow the platform setup guide in [docs/native-dependencies.md](docs/native-dependencies.md). ## Run locally ```bash go run ./cmd/picvault ``` Open `http://127.0.0.1:3456` and manage your photo directories in the "照片目录" panel: click "选择文件夹" to pick a folder in the built-in directory browser (quick locations, subdirectory navigation, hidden-folder toggle), or expand "高级:手动输入路径" to type an absolute path directly. You can enable/disable or delete directories; deleting removes PicVault's index data only — photos on disk are never touched. The first start seeds `PICVAULT_LIBRARY_PATH` as the initial directory; once the page list exists, it is the single source of truth and the environment variable no longer syncs automatically: ```bash export PICVAULT_LIBRARY_PATH=/path/to/photos go run ./cmd/picvault ``` "扫描全部目录" queues a sequential scan over every enabled directory. Scans are incremental: unchanged photos are skipped, and a photo that fails to decode or embed is recorded as failed without stopping the scan. A directory that cannot be read is marked in error and the remaining directories still scan; an empty (possibly unmounted) directory is marked empty and keeps its existing index. After a scan completes, photos stored under the scanned directory that no longer exist are forgotten (database row, vector and cached thumbnails). A scan that is cancelled removes nothing. To expose PicVault on a LAN, set both an explicit non-local bind address and the opt-in flag. A non-local bind is rejected unless `PICVAULT_ALLOW_LAN=true` is set: ```bash export PICVAULT_BIND_ADDR=0.0.0.0:3456 export PICVAULT_ALLOW_LAN=true go run ./cmd/picvault ``` The directory-management write APIs (`POST/PATCH/DELETE /api/dirs` and `POST /api/library/scan`) are loopback-only by default: when the bind address is non-local they return 403 unless `PICVAULT_ENABLE_REMOTE_ADMIN=true` is set explicitly. PicVault has **no authentication**, so enabling remote administration lets anyone on the network rewrite the directory list — and a malicious directory entry points the indexer at any path the process can read. Only enable it on a trusted network, and prefer keeping administration on the machine that runs PicVault. Write APIs require `Content-Type: application/json` (the delete endpoint is exempt because it carries no body, but the scan is not). Example: ```bash curl -X POST -H 'Content-Type: application/json' http://127.0.0.1:3456/api/library/scan curl -X POST -H 'Content-Type: application/json' \ -d '{"path":"/path/to/photos"}' http://127.0.0.1:3456/api/dirs curl http://127.0.0.1:3456/api/dirs curl -X DELETE http://127.0.0.1:3456/api/dirs/1 ``` Vectors are kept in an exact in-memory index and persisted by the default `snapshot` backend, so restarts do not re-run the model over the library. The snapshot is an append-only binary file named after the embedder and vector dimension (`indexes/images--.vec`), so switching models never mixes vectors. Removing a photo appends a fixed-size tombstone record, and the file is compacted once stale records and tombstones dominate it. Set `PICVAULT_INDEX_PATH` to use an explicit file instead. Each vector is stored as a unit vector in half precision, in memory and on disk: about 1 KB per photo for the 512-dimension model, so 300,000 photos take about 310-350 MB and a search over them takes about 40 ms on an Apple M4. A file written by an earlier version (float32 records) is converted on first load and rewritten in the new format. If PicVault reports an incompatible snapshot, delete the named file and restart to rebuild it from the indexed photos. To keep vectors in memory only (rebuilt at every start): ```bash export PICVAULT_VECTOR_BACKEND=memory go run ./cmd/picvault ``` ## Run with Docker A multi-stage production image (no models or photos inside) is available for single-machine and NAS deployments via Docker Compose. See [docs/deployment.md](docs/deployment.md) for build commands, directory and permission setup, model installation on a data volume, and troubleshooting. ## Semantic search: install the local model Semantic search needs two things that PicVault never downloads by itself: the ONNX Runtime shared library (1.29 or newer) and the Chinese-CLIP ViT-B/16 ONNX export. Building PicVault does not require either; the library is loaded when the service starts. ### 1. Install ONNX Runtime - macOS: `brew install onnxruntime` (found at `/opt/homebrew/lib/libonnxruntime.dylib` or `/usr/local/lib/libonnxruntime.dylib`). - Linux / NAS: download the official release tarball (`onnxruntime-linux-x64-.tgz` or `onnxruntime-linux-aarch64-.tgz` from ) and unpack it to `/opt/onnxruntime`, or copy `lib/libonnxruntime.so*` next to the `picvault` binary. Distro packages are usually too old (Debian 13 ships 1.21; 1.29 or newer is required) and install to multiarch paths PicVault does not search. - Windows: download the official release zip (`onnxruntime-win-x64-.zip`) and put `lib\onnxruntime.dll` next to `picvault.exe`. PicVault looks for the library in `PICVAULT_ONNXRUNTIME_LIB` first, then next to the executable, then in the platform locations above (`/opt/homebrew/lib`, `/usr/local/lib` on macOS; `/opt/onnxruntime/lib`, `/usr/lib`, `/usr/local/lib` on Linux). ### 2. Install the model ```bash scripts/install-model.sh # macOS / Linux ``` ```powershell .\scripts\install-model.ps1 # Windows ``` The script prints the source ([zihuv/chinese-clip-vit-base-patch16-onnx](https://huggingface.co/zihuv/chinese-clip-vit-base-patch16-onnx), MIT license, pinned revision), the ~753 MB download size, and the destination, then asks for confirmation (`--yes` skips the prompt, `--dry-run` only prints the plan). It downloads `model_config.json`, `vocab.txt`, `text.onnx`, and `visual.onnx` into `/models/chinese-clip-vit-base-patch16` and verifies their sizes and checksums. Restart PicVault afterwards; existing photos are re-embedded in the background into a new model-specific vector file. ### 3. Optional: shrink the model to int8 The downloaded export is fp32. An int8 dynamically quantized copy of it costs roughly a third of the memory and runs two to three times faster, measured on macOS arm64 with ONNX Runtime 1.30.0: | | fp32 | int8 | | --- | --- | --- | | model files | 753 MB | 191 MB | | server resident memory | 941 MB | 336 MB | | per indexed photo | 198 ms | 76 ms | | per search query | 55 ms | 21 ms | Quantization happens once, offline, with Python. PicVault never runs Python and never quantizes anything itself; it only reads the directory the script writes. ```bash uv venv --python 3.12 /tmp/picvault-quant && . /tmp/picvault-quant/bin/activate uv pip install onnx onnxruntime # or: python3 -m venv ... && pip install onnx onnxruntime python scripts/quantize-model.py "$HOME/Library/Application Support/picvault/models/chinese-clip-vit-base-patch16" ``` That writes `…/models/chinese-clip-vit-base-patch16-int8` with `text-int8.onnx`, `visual-int8.onnx`, a copy of `vocab.txt`, and a `model_config.json` whose `model_id` carries an `-int8` suffix. Pass `--force` to overwrite an existing output directory. When `PICVAULT_MODEL_DIR` is unset, PicVault prefers the `-int8` directory if it holds a readable `model_config.json` and otherwise uses the fp32 one; it logs the directory it chose at startup, and `/api/library/status` reports the quantization as `embedderQuantization`. Vectors are stored per model ID, so switching variants starts from an empty vector file and the library is re-embedded in the background; both variants' vector files can coexist. #### Quality evidence On a local evaluation set of 16 CC0/public-domain Wikimedia Commons photos and their Chinese queries (`scripts/fetch-eval-set.py`), the two variants ranked the same: recall@1 was 9/9 for both over the nine pairs whose image was visually verified, the mean margin of the correct image over the best wrong one was 0.0655 fp32 against 0.0609 int8, image embeddings kept a mean cosine of 0.971 (minimum 0.940) against their fp32 counterparts and query embeddings 0.991, and the top-ranked image was identical for every query whose subject is actually present in the set. This is a small local check, not a published benchmark. Reproduce it with: ```bash PICVAULT_COMPARE_BASE_DIR="$HOME/Library/Application Support/picvault/models/chinese-clip-vit-base-patch16" \ PICVAULT_COMPARE_CANDIDATE_DIR="$HOME/Library/Application Support/picvault/models/chinese-clip-vit-base-patch16-int8" \ PICVAULT_EVAL_DIR="$HOME/.cache/picvault-eval" \ PICVAULT_ONNXRUNTIME_LIB=/opt/homebrew/lib/libonnxruntime.dylib \ go test ./internal/embed/onnx -run Compare -v ``` ### Configuration | Variable | Default | Meaning | | --- | --- | --- | | `PICVAULT_APPDATA_DIR` | `/picvault` | Application data root (`picvault.db`, `indexes/`, `thumbnails/`, `models/`, `logs/` live directly inside it) | | `PICVAULT_LIBRARY_PATH` | system Pictures directory | Seeded as the first photo directory on first start; after that the page list is the source of truth | | `PICVAULT_ENABLE_REMOTE_ADMIN` | `false` | Allows the directory-management write APIs when the bind address is non-loopback; requires `PICVAULT_ALLOW_LAN=true`. The app has no authentication — enable only on a trusted network | | `PICVAULT_EMBEDDER_BACKEND` | `auto` | `auto` uses the model when it and ONNX Runtime load, otherwise falls back to `deterministic` and reports why; `onnx` refuses to start without them; `deterministic` is for development | | `PICVAULT_MODEL_DIR` | `/models/chinese-clip-vit-base-patch16-int8` when it exists, otherwise `…/chinese-clip-vit-base-patch16` | Model directory | | `PICVAULT_ONNXRUNTIME_LIB` | platform search order above | ONNX Runtime shared library | | `PICVAULT_VECTOR_DIMENSION` | `8` | Deterministic embedder only; the model's dimension comes from `model_config.json` | `GET /api/library/status` reports `semanticSearch`, `embedder`, `embedderQuantization` (empty for the fp32 model), and, when the model is not in use, `embedderNote` with the reason. ### Memory profile The model runs inside the PicVault process with at most 2 intra-op threads. Startup only loads the runtime library (about 50 MB resident). The image model loads on the first indexed photo (about +370 MB) and is released after 2 minutes without indexing; the text model loads on the first search (about +520 MB) and is released after 10 minutes without searches. With both loaded expect roughly 0.55–0.95 GB. After a release the allocator may keep a few hundred MB of freed pages mapped for reuse by the next load (the OS reclaims them under memory pressure); repeated load/release cycles do not grow memory. The int8 variant above holds the same library in about a third of that: 336 MB resident with both sessions loaded. ## App data layout PicVault stores data in the operating system's user configuration directory under `picvault` (for example, `~/Library/Application Support/picvault` on macOS). Set `PICVAULT_APPDATA_DIR` to place the data root anywhere else — the directory you name is the data root itself (`picvault.db` sits directly inside it), which is how container deployments map a volume to `/data`. The application creates: ```text picvault/ ├── picvault.db ├── indexes/ │ └── images-chinese-clip-vit-base-patch16-512.vec ├── thumbnails/ │ ├── small/ │ └── medium/ ├── models/ │ ├── chinese-clip-vit-base-patch16/ │ └── chinese-clip-vit-base-patch16-int8/ (optional, preferred when present) └── logs/ ``` `picvault.db` contains the local photo metadata. The `indexes/` directory is used by the snapshot vector backend; `models/` holds the locally installed model. The UI can filter search results by the indexed file path metadata. ## Verification Run the full automated and manual verification checklist in [docs/verification.md](docs/verification.md).