Argus Quarry
Provenance-first acquisition of public-domain / CC0 images — the input
stage of the Argus suite. The quarry digs up raw material: it downloads images
from upstream archives and lands them, with full provenance and licensing,
into a folder the rest of the suite already consumes (DATASET_DIR →
/data/images).
Subjects are grouped into LoRA-training categories — identity (people),
wardrobe (garments), setting (scenes/environments) and concept
(styles/themes) — and everything lands sorted into <category>/<subject>/
subfolders. A category is independent of identity: a subject can be Mark Twain,
a red dress, a modern kitchen, or cyberpunk.
It is deliberately lean — acquisition + provenance, nothing more. Quality scoring, near-duplicate detection, faces, embeddings and captioning are owned downstream by argus-curator and argus-lens; quarry never re-implements them.
Want a UI? Quarry is CLI-only by design (see DESIGN.md §9). The suite's web frontend — argus-studio — surfaces the curation and captioning stages that consume quarry's output (e.g. its
/curateview scans the<category>/<subject>/tree quarry publishes).
argus-quarry (NEW) argus-curator (:8101) argus-lens (:8100) argus-studio
─ download ─┐ ─ scan + score ─┐ ─ caption ─┐ ─ web UI (:3000)
─ verify ─┤ images + ─ near-dup ─┤ manifest ─ buckets ─┤ dataset ─ /curate
─ provenance┤ provenance ─ face-cluster ─┤ ─ (ident/ ─┤ → LoRA ─ caption
─ SHA256 ─┴───────────► ─ select+export ─┴──────────► wardrobe)─┘
/data/images (DATASET_DIR) ─────────────────────────────────────────────────►
See DESIGN.md for the full rationale and phased plan.
Why it exists
- Provenance-first. Every image carries its source URL, landing page, licence and attribution. A record with no accepted licence (PD / CC0) is quarantined, never landed.
- Idempotent. Exact-dedup by SHA256 (
UNIQUEin the DB) and astatuslifecycle mean reruns resume partials and never duplicate bytes. - Bounded. A per-file resolution/size cap and a total-archive GB budget keep the pool predictable; the full-resolution URL is always retained for later re-fetch.
- Source-independent. Every downloader yields the same
SourceRecord, so the pipeline never learns which archive a file came from. - Category-sorted. Subjects carry a category (identity / wardrobe / setting /
concept) and land under
<category>/<subject>/, so one pool serves every LoRA workflow.
Install
pip install argus-quarry # library + downloaders
pip install "argus-quarry[cli]" # + the argus-quarry command
pip install "argus-quarry[phash]" # + opportunistic perceptual-hash metadata
pip install "argus-quarry[server]" # + the read-only provenance HTTP API (serve)
For development the suite uses uv (works on PEP 668 "externally managed" system Pythons):
make dev # uv venv + editable install (dev + cli extras)
# or, manually:
uv venv && uv pip install -e ".[dev,cli]"
Quickstart
# Inspect the curated subject seeds (all categories, or one)
argus-quarry subjects
argus-quarry subjects --category wardrobe
# Fetch from Wikimedia Commons into the raw pool, then publish a curator-ready,
# CC0/PD-only tree into $DATASET_DIR (symlinks by default). Omit --category to
# harvest every category (identity + wardrobe + setting + concept).
argus-quarry run --source commons --limit 20 --export --licence CC0,PD
argus-quarry run --category concept --limit 20 --export # just one category
# Or split the two stages
argus-quarry fetch --source commons --limit 20
argus-quarry export --dest ./data --licence CC0,PD # add --copy to avoid symlinks
# Inspect what you have
argus-quarry stats
argus-quarry list --category setting --licence CC0
argus-quarry verify # re-check files decode + match recorded SHA256
Installed into a
uvvenv? Prefix commands withuv run(e.g.uv run argus-quarry stats) orsource .venv/bin/activatefirst.
CLI
| Command | What it does |
|---|---|
run |
Fetch into the raw pool, then (optionally) publish — the compose entrypoint |
fetch |
Download candidates into the raw pool (no publish) |
export |
Publish a filtered <category>/<subject>/ tree into DATASET_DIR (symlink / --copy) |
list |
List landed photographs with provenance (filter by source / licence / category / subject) |
stats |
Counts by status / category / source / licence + raw-pool size |
verify |
Re-check landed files exist, decode, and match their recorded SHA256 |
subjects |
Show the subject seed(s) downloaders harvest around (filter by --category) |
serve |
Start the read-only provenance HTTP API on :8102 (needs the server extra) |
run, fetch, export and list all accept --category
(identity / wardrobe / setting / concept); with none given they span every category.
Layout produced
Quarry fetches into a raw pool it fully owns ($QUARRY_HOME, a sibling
side-car dir), then export publishes a clean tree into DATASET_DIR:
$QUARRY_HOME/ # side-car state — NEVER scanned by curator
├── images/
│ ├── identity/Albert_Einstein/… # the raw pool, sorted by <category>/<subject>/
│ ├── wardrobe/Red_dress/…
│ ├── setting/Modern_kitchen/…
│ └── concept/Cyberpunk/…
├── metadata/portraits.sqlite # provenance DB (subjects + photographs)
├── cache/ logs/
$DATASET_DIR/ # published, curator-ready view (via export)
├── identity/Albert_Einstein/… # symlinks (default) or copies into the pool
└── wardrobe/Red_dress/…
Provenance model
A single SQLite database (portraits.sqlite, WAL mode) with two tables:
subjects—name · category · wikidata_id · birth_year · death_year · occupation(the identity-only columns stayNULLfor wardrobe / setting / concept subjects)photographs—category · title · photographer · year · source · source_url · licence · attribution · width · height · file_size · filename · **sha256 (UNIQUE)** · phash · remote_url · status · downloaded_at
sha256 is the exact-dedup key (idempotent reruns); status
(pending → downloading → complete, plus duplicate / quarantined / failed)
tracks resumability. phash is recorded opportunistically and is informational
only — it never drives dedup here (that's argus-curator's job).
Configuration
Copy .env.example to .env. Key knobs:
| Env | Default | Meaning |
|---|---|---|
QUARRY_HOME |
./quarry |
Raw pool + DB + cache + logs (side-car dir) |
QUARRY_MAX_GB |
40 |
Total raw-pool ceiling; 0 = unlimited |
COMMONS_USER_AGENT |
descriptive default | Polite UA (Commons/LoC expect one) |
DATASET_DIR |
./data |
Published view export writes into |
QUARRY_PD12M_DIR / QUARRY_MEGALITH_DIR |
(unset) | Archived metadata for the archives (<root>/pd12m, <root>/megalith) |
QUARRY_ALLOW_FETCH |
(unset = off) | Let POST /archives/{name}/fetch land a slice from the server |
QUARRY_CORS_ORIGINS |
(unset) | Comma-separated browser origins for serve (same as --cors-origin). Required for a studio to POST slice fetches; replaces --cors's read-only * |
HTTP server
A tiny provenance API (FastAPI) over the raw pool. It is read-only apart from
one opt-in route, the archive slice fetch, which is refused unless
QUARRY_ALLOW_FETCH=1 (see Public-domain archives).
It powers the
argus-studio /gallery view, the same
way /curate talks to argus-curator on :8101.
pip install "argus-quarry[server]" # fastapi + uvicorn
argus-quarry serve --host 0.0.0.0 --port 8102 --cors
--cors allows any origin, read-only. The fetch route needs an explicit list,
because POST is never granted to *: pass --cors-origin http://localhost:3000
(repeatable) or set QUARRY_CORS_ORIGINS. A list replaces the wildcard, so name
every origin the studio is opened from.
The published image ships the server extra and serves by default on :8102:
docker run --rm -p 8102:8102 -v "$PWD/quarry:/data/quarry" \
-e QUARRY_HOME=/data/quarry ghcr.io/smk762/argus-quarry:latest
The entrypoint is still argus-quarry, so any acquisition subcommand works by
overriding the command (docker run ... ghcr.io/smk762/argus-quarry fetch …).
The pool root comes from $QUARRY_HOME (default ./quarry), exactly like every
other command — the compose service just sets QUARRY_HOME=/data/quarry.
Serving never mutates the provenance data — on any mount. The DB is opened
through a mode=ro SQLite URI, which still tracks a concurrently running fetch
through the WAL; the server never migrates, relocates, or writes a row, so
pointing it at an empty or unmigrated pool reports 503 rather than quietly
building one.
It is not wholly side-effect-free on a writable mount, though: mode=ro lets
SQLite create the -wal/-shm sidecars it uses to follow the WAL, and a
read-only connection can never checkpoint or unlink them, so those two files
persist beside the DB (reused across opens, not accumulated — two files, not
unbounded growth; issue #9). Mounting the pool :ro avoids them entirely:
docker run --rm -p 8102:8102 -v "$PWD/quarry:/data/quarry:ro" \
-e QUARRY_HOME=/data/quarry ghcr.io/smk762/argus-quarry:latest
A read-only directory also stops SQLite creating the -shm sidecar mode=ro
needs, so on that mount quarry falls back to immutable=1, which needs no
sidecars. immutable=1 cannot see a -wal, so it is used only when there is
none: a pool snapshotted mid-write, or left behind by a killed fetch, answers
503 instead of silently serving stale counts. Checkpoint it
(sqlite3 portraits.sqlite 'PRAGMA wal_checkpoint(TRUNCATE)'), or mount the pool
writable, and it serves normally.
The image runs as a non-root user (uid 10001), so a bind-mounted $QUARRY_HOME
must be readable by that uid to serve, and writable by it for the acquisition
subcommands (fetch, run) — e.g. chown -R 10001:10001 quarry/ on the host.
This keeps the read-only sidecars above, and anything the acquisition commands
land, owned by an unprivileged uid rather than root (issues #8/#9).
The read-only CLI commands (stats, list, export, verify without
--repair) open the pool the same way, so they work against a :ro mount too.
Liveness and readiness are separate. GET /health answers 200 whenever the
process is up — it never touches the DB — so a container mounted at an empty
QUARRY_HOME that is seeded later still comes up live. GET /ready opens the
pool and answers 503 with {"status": "unavailable", "service": "argus-quarry", "database": "..."} when it cannot be served (missing, unmigrated, or an
unreadable WAL); the database value is a single generic reason
("provenance database unavailable"), deliberately not a per-cause code, so it
never leaks a filesystem path. Point an orchestrator's readiness probe at
/ready to shed a container that is up but has no data to serve, and its
liveness probe at /health.
| Endpoint | Returns |
|---|---|
GET /health |
Liveness: 200 {status, service, version, quarry_home} while the process is up |
GET /ready |
Readiness: 200 {status: ready, service, database: ok}, or 503 {status: unavailable, service, database} when the pool cannot be served |
GET /stats |
Counts by status / category / source / licence + total_bytes (mirrors stats) |
GET /subjects?category= |
Distinct subjects with landed photo counts: {subjects: [{folder, category, photo_count}]} |
GET /photos?category=&subject=&licence=&source=&status=&limit=&offset= |
Paginated provenance rows: {total, offset, limit, photos: […]}; status defaults to complete (pass empty for all), licence accepts CSV (CC0,PD), limit ≤ 500 |
GET /photos/{id} |
One photograph with full provenance (404 if unknown) |
GET /thumb?id=&size=384 |
WEBP thumbnail rendered from the pooled file (size = longest edge, capped at 1024) |
Public-domain archives
Two large public-domain datasets can be searched and sliced without downloading their images. Quarry keeps their metadata locally (links, captions, sizes, licences) and lands only the slices you choose, through the same ingest pipeline as every other source (licence gate, SHA256 dedup, provenance).
| Archive | Items | Filter on | Images come from |
|---|---|---|---|
| PD12M | 12.4M | caption, institution (640), licence (CC0 / PDM), size, MIME type | the public s3://pd12m bucket |
| Megalith-10m | 9.58M | Florence-2 caption (8.94M), Flickr account, size (~9.4M) | Flickr (1024px) |
Megalith's links are a shuffled sample, so the Flickr account is its only built-in grouping. It also has no per-photo licence column, so each record states the dataset's licence filter as its licence label.
# 1. archive the metadata (~45 GB incl. PD12M's CLIP embeddings; resumable; no photos)
HF_TOKEN=hf_... uv run scripts/fetch_public_domain_metadata.py --root /media/photos/public_domain
# 2. point quarry at it and build the search index (DuckDB, under $QUARRY_HOME/cache/archives/)
export QUARRY_PD12M_DIR=/media/photos/public_domain/pd12m QUARRY_MEGALITH_DIR=/media/photos/public_domain/megalith
pip install "argus-quarry[cli,archives]"
argus-quarry archives index
argus-quarry archives search pd12m --q "harbour boats" --min-short-side 1024
# 3. land a slice saved from argus-studio's /gallery -> Sources (or written by hand)
argus-quarry archives fetch slice.json # --dry-run lists the URLs first
Tone, medium and year are read from each caption when the index is built, because neither dataset records them per image. They're cues, not ground truth: an item whose caption doesn't say is left out when you filter on them.
| Filter | Derived from | PD12M | Megalith |
|---|---|---|---|
tone = bw / color |
"black and white", "monochrome", "sepia" … versus colour words | 3.56M bw, 4.31M colour | 0.66M bw, 7.02M colour |
medium = photo, painting, drawing, print, map, text, object |
the caption's opening words ("The image shows a black and white photo of …") | ~5.1M labelled | photo unless the caption says otherwise (Megalith filtered out non-photos) |
year_min / year_max |
the first year or decade a caption mentions | 61k | 128k |
Neither archive carries capture dates (Megalith's archived EXIF has none), so the year filter only finds images whose caption states a year. Rebuild old indexes after upgrading: an index from an older quarry reports stale and asks for archives index.
Caption terms (q, exclude, --q, --exclude) are case-insensitive substrings. Every term in q must appear and none in exclude may; "quoted phrases" stay whole, and a leading - in q excludes a term (harbor boats -painting). An uncaptioned item doesn't mention anything, so an exclusion keeps it.
A slice is a query plus where it lands:
{"archive": "pd12m", "category": "setting", "subject": "Harbour_views", "limit": 200,
"query": {"q": "harbour boats", "collections": ["Rijksmuseum"], "min_short_side": 1024, "sort": "largest"}}
The server exposes the same thing over HTTP. Every route is GET except the fetch:
| Route | Returns |
|---|---|
GET /archives |
each archive's status (configured, present, indexed, items) and fetch_allowed |
GET /archives/{name}/facets |
collections, licences and MIME types with counts |
GET /archives/{name}/search?q=&exclude=&collection=&licence=&mime=&min_width=&min_height=&min_short_side=&orientation=&sized_only=&tone=&medium=&year_min=&year_max=&ids=&sort=&limit=&offset= |
{total, offset, limit, items}; sort is shuffle (fixed per index) or largest. Repeat collection or comma-join it with no space after the comma (institution names like Natural History Museum, Berlin keep theirs); medium must be one of photo, painting, drawing, print, map, text, object (422 otherwise) |
GET /archives/{name}/items/{id} |
one item |
GET /archives/{name}/thumb?id=&size= |
WEBP rendered in memory (PD12M), or a 307 to Flickr's own 500px rendition (Megalith) |
GET /archives/{name}/slice?<search filters>&category=&subject=&limit= |
{slice, matches, will_fetch, command, fetch_allowed} |
POST /archives/{name}/fetch (slice JSON body) |
NDJSON start / item / done or error frames; 403 unless QUARRY_ALLOW_FETCH=1, with the slice and CLI command in the body; 409 while another fetch runs |
Suite integration
Quarry ships a gallery profile in the suite's
argus-studio compose.yaml.
It's a run-to-completion job: fetch into the pool, publish into DATASET_DIR,
then let curator/lens (and the web UI's /curate view) consume the result.
docker compose --profile gallery up --build # fetch → pool → publish DATASET_DIR
docker compose --profile curator up --build # then curate the published images
The published tree symlinks back into
QUARRY_HOME/images. For those links to resolve inside the curator/lens containers, mountQUARRY_HOMEread-only there too, or runexport --copy.
Sources
| Source | Status |
|---|---|
| Wikimedia Commons | Phase 1 (implemented) |
| Library of Congress, Smithsonian, Rijksmuseum, LAC (Karsh allow-list) | Phase 2 |
| Europeana, Flickr Commons (strict per-record rights) | Phase 3 |
| PD12M, Megalith-10m | Archives (implemented): searched locally, landed by slice |
New sources register in downloaders/ behind a common Downloader contract, so
adding one never touches ingest, storage, or export.
Development
make lint # ruff
make test # pytest
make check # lint + test + build
Related projects
- argus-studio — the suite's Next.js web UI (captioning +
/curate). - argus-curator — training-suitability scoring, near-dup dedup, face clustering.
- argus-lens — intent-aware, multi-model captioning.
Licence
MIT — see LICENSE. Note: the MIT licence covers this software, not the images it downloads. Image licences are recorded per-record and enforced at ingest.
Metadata
Release files for argus-quarry 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| argus_quarry-0.3.1.tar.gz | 93.9 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| argus_quarry-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 160.3 kB
Release files / argus_quarry-0.3.1.tar.gz
| Download URL | argus_quarry-0.3.1.tar.gz |
|---|---|
| Size | 93.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
05287a938c90b772541afa6931650ba4adf42ef3b8716c2193e7b7ba37b3b597
|
|
BLAKE2b-256 checksum How to use checksums |
e45b88d1fa07553ef5ab8387d06b5545efa618c3da183a5f977bf6b486ef0387
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.
Transparency logRelease files / argus_quarry-0.3.1-py3-none-any.whl
| Download URL | argus_quarry-0.3.1-py3-none-any.whl |
|---|---|
| Size | 66.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
203c4a0043a08d5f1bfaa59b2b93cbcc65b272148c73807d2a0ba3f5429a16db
|
|
BLAKE2b-256 checksum How to use checksums |
66577f9f69bf1ea6c7fa19aa36117f1b823fb86030aba742fbf6a4b295239472
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.
Transparency logSecurity for argus-quarry 0.3.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for argus_quarry-0.3.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.