SpectatorDB
SpectatorDB is an embeddable, offline-first media-intelligence store for Python. It keeps each media file and its structured metadata and its vector embedding behind a single interface — the union that neither an object store nor a vector database gives you alone — with zero required runtime dependencies and no server to run.
It is designed for per-device autonomy on commodity hardware (e.g. a Raspberry Pi): each device owns its own data and works with no cloud, no network, and no AI model present. AI enrichment is optional and applied after a capture is stored, so a slow or missing model never costs you data.
Features
- One store for media + metadata + embeddings, backed by the local file system and SQLite.
- One-line folder import with EXIF/mtime capture times and SHA-256 content-hash deduplication.
- Composable queries by time range, media type, device id, and labels
(any-match), newest-first, with limit/offset, plus
count()for paging. - Semantic similarity search over embeddings, scoped to a single embedding model — pure-Python cosine, no extra dependencies.
- Store-first, enrich-later: insert immediately, attach labels/description/
embedding afterward via
update_enrichment. - Durable by design: atomic insert/delete with compensating actions,
reconcile()to sweep orphans, schema versioning with forward migrations, and UTC-normalized timestamps at the API boundary. - Pluggable backends via the
StorageandMetadataStoreABCs.
Privacy & guarantees
- Offline always. No network calls, no telemetry — nothing ever leaves the device. A test in the suite asserts the package opens no sockets.
- Zero required runtime dependencies. The core is stdlib-only. The optional
[exif]extra pulls in Pillow for richer photo formats; the core never imports it. - Two-folder portability. Your data is just a folder of media files plus one SQLite catalog — open, inspectable, and readable decades from now.
Requirements
- Python 3.11+ (runs on Raspberry Pi OS Bookworm)
Installation
$ python -m pip install spectator-db # zero-dependency core
$ python -m pip install "spectator-db[exif]" # + Pillow for richer EXIF/HEIC
To work on the library itself, clone the repository and install it in editable mode with the development tools:
$ git clone https://github.com/iot-spectator/spectator-db.git
$ cd spectator-db
$ python -m pip install -e .
$ python -m pip install -r requirements.txt # dev/test/docs tools
Quick start
import pathlib
from datetime import datetime, timezone
from spectatordb import SpectatorDB, MediaType, LocalStorage, SQLiteMetadataStore
# Compose a store from a file-storage backend and a metadata backend.
db = SpectatorDB(
storage=LocalStorage(pathlib.Path("./media")),
metadata_store=SQLiteMetadataStore(pathlib.Path("./spectator.db")),
)
# 1. Import a whole folder in one line. Capture times come from EXIF (falling
# back to file mtime), and duplicates are skipped by content hash.
ids = db.import_dir(pathlib.Path("~/Pictures").expanduser())
# 2. Or store a single capture. captured_at is optional — omit it and it is
# read from EXIF, then the file's mtime. Returns None if skip_duplicates
# skipped it.
record_id = db.insert(
pathlib.Path("/path/to/snapshot.jpg"),
media_type=MediaType.IMAGE,
device_id="pi-01",
labels=["person"],
skip_duplicates=True,
)
# 3. Enrich later (store-first, enrich-later). embedding and embedding_model
# must be set together.
db.update_enrichment(
record_id,
description="A person at the front door",
embedding=[0.10, 0.20, 0.30],
embedding_model="clip-vit-b32",
)
# 4. Query with composable filters, newest-first; count() pages without
# loading rows.
hits = db.query(media_type=MediaType.IMAGE, labels=["person"], limit=10)
total = db.count(media_type=MediaType.IMAGE, labels=["person"])
# 5. Semantic similarity search, scoped to one embedding model.
similar = db.search_similar([0.11, 0.19, 0.31], model="clip-vit-b32", limit=5)
# 6. Fix a wrong capture time or device after the fact.
db.update_metadata(record_id, captured_at=datetime(2025, 6, 15, tzinfo=timezone.utc))
# 7. Read a record and copy its file back out.
record = db.get(record_id)
db.retrieve(record_id, pathlib.Path("./out.jpg"))
Example: a personal photo library
examples/photos.py is a complete command-line tool built
on the public API — import a folder, tag it, search it, find look-alikes:
$ python examples/photos.py import ~/Pictures
imported 2153 file(s) from /home/you/Pictures
library now holds 2153 record(s)
$ python examples/photos.py list --type image --since 2026-01-01 --limit 3
7e281c56 2026-03-14 09:12 image 2.4 MB -
1e457344 2026-02-02 17:40 image 1.9 MB -
cd4f7678 2026-01-08 11:05 image 3.1 MB -
$ python examples/photos.py tag 7e281c56 --add beach --add sunset
$ python examples/photos.py list --label beach
$ python examples/photos.py similar 7e281c56
Every command except embed runs on the stdlib-only core. embed needs an
image decoder, so it requires the [exif] extra:
$ python -m pip install "spectator-db[exif]"
$ python examples/photos.py embed
The embedding it computes is a 64-dimension color-layout signature — a 4x4 grid of mean RGB plus a luma histogram. It matches on color and composition, not meaning: it will find your other beach photos, but it does not understand what a beach is. It exists so the similarity path is exercised end to end with no dependencies to speak of. Swapping in a real model such as CLIP means replacing one function; storage and search do not change.
Public API
The supported, semver-protected surface is exported from the spectatordb
package root:
from spectatordb import (
SpectatorDB, # the facade orchestrating storage + metadata
MediaRecord, # the stored-item data model
MediaType, # IMAGE | VIDEO
ReconcileReport, # result of SpectatorDB.reconcile()
UNSET, # sentinel for partial update_enrichment() updates
Storage, # file-storage backend ABC
LocalStorage, # local-filesystem Storage backend
SaveMode, # COPY | MOVE
MetadataStore, # metadata backend ABC
SQLiteMetadataStore, # SQLite MetadataStore backend (default)
)
SpectatorDB methods: insert, import_dir, exists, update_enrichment,
update_metadata, get, retrieve, query, count, search_similar,
delete, reconcile.
Concurrency
A SQLiteMetadataStore uses one WAL-mode connection per process and serializes
writes with a lock; reads are concurrent. It is thread-safe for the expected
low-write workload — use one instance per process.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file spectator_db-0.2.0.tar.gz.
File metadata
- Download URL: spectator_db-0.2.0.tar.gz
- Upload date:
- Size: 39.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b9c93f166f1fdfd026237e718916483f8cb28c24d5db5c70f871c1b79c91d09d
|
|
| MD5 |
4acd06c2ae10ada02a4b239ff81d9a4d
|
|
| BLAKE2b-256 |
f9c1d419937c7088f5854fb8be3868b0f9772341bd7216942ae2dbdbf9128a71
|
Provenance
The following attestation bundles were made for spectator_db-0.2.0.tar.gz:
Publisher:
pypi_publish.yml on iot-spectator/spectator-db
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
spectator_db-0.2.0.tar.gz -
Subject digest:
b9c93f166f1fdfd026237e718916483f8cb28c24d5db5c70f871c1b79c91d09d - Sigstore transparency entry: 2365333055
- Sigstore integration time:
-
Permalink:
iot-spectator/spectator-db@6c2b236580f2f3da9509f49685b6ba353cfa8f12 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/iot-spectator
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi_publish.yml@6c2b236580f2f3da9509f49685b6ba353cfa8f12 -
Trigger Event:
release
-
Statement type:
File details
Details for the file spectator_db-0.2.0-py3-none-any.whl.
File metadata
- Download URL: spectator_db-0.2.0-py3-none-any.whl
- Upload date:
- Size: 30.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
57522edc9dc2ee2f0ab0b195fca90f7dc25a50b11b4a29a56e9c0b2a090f6f36
|
|
| MD5 |
5e6570f9a6523106590193e6bf324811
|
|
| BLAKE2b-256 |
815ed34fa120731aa78eac38d11c0685fb1cc6531efe02650f613840fa68bff3
|
Provenance
The following attestation bundles were made for spectator_db-0.2.0-py3-none-any.whl:
Publisher:
pypi_publish.yml on iot-spectator/spectator-db
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
spectator_db-0.2.0-py3-none-any.whl -
Subject digest:
57522edc9dc2ee2f0ab0b195fca90f7dc25a50b11b4a29a56e9c0b2a090f6f36 - Sigstore transparency entry: 2365333103
- Sigstore integration time:
-
Permalink:
iot-spectator/spectator-db@6c2b236580f2f3da9509f49685b6ba353cfa8f12 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/iot-spectator
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi_publish.yml@6c2b236580f2f3da9509f49685b6ba353cfa8f12 -
Trigger Event:
release
-
Statement type: