Local (supported)

Artifex is designed to run locally. After the pipeline has been run (see README.md):

streamlit run app/main.py

The app listens on http://localhost:8501. Settings in .streamlit/config.toml:

Resource requirements

Resource Requirement
Python 3.10–3.12 (developed on 3.12)
RAM ≈1.1 GB resident for the analyser on CPU (DINOv2-B + reference index), measured on the development machine with an earlier 5,000-record index; the 10,084-record index adds ≈ 60 MB of embeddings; allow 2 GB for the app as a whole
GPU optional; CUDA is used automatically if available. Measured on the development machine (AMD Ryzen 7 5800HS, CPU only): ≈0.2 s per analysis after a ≈10 s one-time model load. Embedding extraction for the whole collection: ≈25–40 s on an RTX 3050 Laptop GPU (4 GB).
Disk ~0.4 GB model cache (models/hf), plus downloaded images and thumbnails (see the dataset card for the current size)

Upload handling

Streamlit keeps uploaded files in memory (its upload manager does not write them to disk). Artifex decodes the bytes in memory, never saves them, and holds at most the current analysis result in the user's session; closing the tab or pressing Remove image discards it. No cached uploads exist, so there is nothing to purge. If a future version caches uploads, it must provide a purge command and document it here.

In-browser edition (static website)

Deployed: https://sucksome-artifex.static.hf.space (Hugging Face Space SuckSome/artifex, static SDK, public), 22 October 2026, model artifex-0.1-e94d93bf34, dataset 0.1+5cc3c8d20c19 (redeployed the same day after the grouping correction). Verified after each upload: the isolation headers are served, the large files (redirected to Hugging Face's CDN) arrive with CORS headers, every page loads without script errors at 1440 px and 390 px in headless Chromium, and a held-out test image analysed on the live site gave the same region, period, tradition and abstention results as the Python application.

Content blockers. The first upload named the analysis page module analyse.js; EasyPrivacy (used by Opera's built-in blocker, uBlock Origin and others) blocks any file of that name, so the Analyse page failed in those browsers while passing in a plain headless browser. The module is now workspace.js, a test keeps site file names clear of common blocklist names, and the deployed site was re-checked in headless Chromium with all EasyList and EasyPrivacy rules that apply to first-party resources enforced (nothing blocked; analysis completed).

The hosted edition is a static website that runs the deployed model in the visitor's browser. There is no Artifex server: uploads are decoded and analysed on the visitor's device and never transmitted. It can be hosted anywhere that serves static files, including free static Hugging Face Spaces. (Hugging Face requires a paid plan for Docker Spaces; see the next section for the server-side alternative.)

python scripts/export_browser_model.py   # ONNX export of the encoder + comparison of weight formats (build/browser_model/)
python scripts/build_static_site.py      # site in build/static/ (web/ + exported data + vendored libraries + thumbnails)
python scripts/validate_browser.py       # end-to-end comparison with the Python application (headless Chromium)
python scripts/build_static_site.py --no-thumbnails   # include the comparison result in the site
python scripts/serve_static.py           # local preview at http://localhost:8600

What the site contains (≈ 490 MB):

Part Source Notes
model/dinov2b_fp16w.onnx (≈ 174 MB) export_browser_model.py DINOv2-B at the pinned commit, CLS + mean-patch pooling and L2 normalisation inside the graph; weights stored in float16, cast to float32 when the session is created. Downloaded on the first analysis and kept in the browser's Cache Storage.
data/embeddings.f16 (≈ 31 MB) deployed embeddings Reference embeddings in float16.
data/model.json heads.pkl, model_card.json Scaler means/scales, logistic-regression coefficients, temperatures, abstention thresholds, OOD threshold, test recall of the evaluated model.
data/records.json, vocab.json manifest, splits, configs Metadata shown on the site, in embedding order.
data/run/ latest evaluation run metrics.json, failures.json, report.md, predictions.csv, copied verbatim.
thumbnails/ (≈ 270 MB) CC0 thumbnails 384-px copies; record views link to the museum for full images.
vendor/ npm onnxruntime-web@1.30.0, plotly.js-cartesian-dist-min@4.1.1 Served from the site itself; the page requests nothing from third parties (on Hugging Face, large files are delivered through Hugging Face's own CDN).

Measured fidelity. Weight formats, compared on 300 held-out test images with the stored reference embeddings: float32 and float16-stored weights both give cosine ≥ 0.99997 and identical region and period predictions for every image; dynamic int8 quantisation (92 MB) destroys the representation (median cosine 0.13, 7 % region agreement) and is not used. End to end (validate_browser.py, 200 held-out test images, headless Chromium): the preprocessed model input is identical to Python's for every image (the JavaScript resize is a port of Pillow's fixed-point bicubic resampler), and region and period predictions, abstention decisions and result status agree for every image; the top-5 similar works agreed exactly for 99–100 % of images in the two builds measured (an ordering difference between near-tied works can come from the float16 reference embeddings). The exact figures of the deployed build are shown on the site's Methodology page (data/browser_validation.json).

Requirements. A browser with WebAssembly (all current browsers). The first analysis downloads ≈ 205 MB. ONNX Runtime holds the encoder weights in float32 (≈ 350 MB) plus activations, which may not fit on phones with little memory — the page then shows an error and no result. With the cross-origin-* headers set in the Space configuration, the page is cross-origin isolated and ONNX Runtime uses up to 4 threads; inside the Hugging Face page frame it falls back to one thread (slower, same results).

Server-side alternative: Docker (Hugging Face paid tier or any container host)

scripts/prepare_space.py assembles a Docker bundle of the Streamlit application in build/space/. Hugging Face refused to create a Docker Space on a free account ("requires a PRO subscription", October 2026), so this route is prepared and tested locally but not deployed:

python scripts/prepare_space.py --source-url https://huggingface.co/spaces/<user>/artifex/tree/main
hf auth login                      # write token
hf upload <user>/artifex build/space . --repo-type space

Hosted-mode settings (environment variables in the Dockerfile):

Variable Effect
ARTIFEX_HOST Privacy statements say uploads are analysed "on the server hosting this site (…)" instead of "on this machine"; the About page adds a note on the host's network path.
ARTIFEX_SOURCE_URL Footer links to the published source.
ARTIFEX_ROOT Project root inside the container.

The container binds 0.0.0.0:7860 (the local default stays localhost) and runs with --server.enableXsrfProtection=false: Streamlit's XSRF cookie is blocked inside the Hugging Face iframe, which breaks uploads. Artifex has no accounts or stored state for a forged request to act on.

Before publishing the bundle was tested by unpacking it, running it with the hosted settings on CPU only (CUDA_VISIBLE_DEVICES="", HF_HUB_OFFLINE=1) and analysing a test image: scores matched the GPU run to two decimals.

Checklist for any other host

  1. Host capacity. DINOv2-B needs ≈ 1.5 GB RAM with PyTorch, plus the reference index; Streamlit Community Cloud's ≈ 1 GB limit is not enough. DINOv2-S (dinov2s_letterbox_rgb) is a smaller fallback, but the deployed model must then be retrained and re-evaluated for it.
  2. Model download. Confirm the host permits downloading from the Hugging Face Hub, or bundle the pinned checkpoint.
  3. Images. All reference images are CC0 and may be redistributed; do not commit them to Git.
  4. Privacy. Review the host's request logging and temporary-file behaviour so that uploads are not persisted. Serve over HTTPS.
  5. Abuse limits. Keep the upload size and pixel limits; consider rate limiting at the proxy.
  6. Unavailable state. If the host cannot run inference, the analysis page shows a "model or dataset unavailable" state — never substitute precomputed or fake predictions.

No API keys or secrets are needed for any part of Artifex at runtime; publishing a Space needs the publisher's own Hugging Face write token, which stays on their machine.