File size: 12,107 Bytes
a23acdf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5dcf617
 
db295a6
5dcf617
 
 
4f4925d
 
 
 
 
a23acdf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5dcf617
a23acdf
 
 
 
db295a6
a23acdf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
<h2>Local (supported)</h2>
<p>Artifex is designed to run locally. After the pipeline has been run (see README.md):</p>
<pre><code class="language-bash">streamlit run app/main.py
</code></pre>
<p>The app listens on <a href="http://localhost:8501">http://localhost:8501</a>. Settings in <code>.streamlit/config.toml</code>:</p>
<ul>
<li><code>browser.gatherUsageStats = false</code> β€” Streamlit's usage telemetry is disabled.</li>
<li><code>server.maxUploadSize = 20</code> β€” upload limit in MB (the analyser also enforces <code>configs/preprocessing.yaml</code>).</li>
<li><code>client.showErrorDetails = false</code> β€” tracebacks (which can contain local paths) are not shown to visitors.</li>
</ul>
<h3>Resource requirements</h3>
<table>
<thead>
<tr>
<th>Resource</th>
<th>Requirement</th>
</tr>
</thead>
<tbody>
<tr>
<td>Python</td>
<td>3.10–3.12 (developed on 3.12)</td>
</tr>
<tr>
<td>RAM</td>
<td>β‰ˆ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</td>
</tr>
<tr>
<td>GPU</td>
<td>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).</td>
</tr>
<tr>
<td>Disk</td>
<td>~0.4 GB model cache (<code>models/hf</code>), plus downloaded images and thumbnails (see the dataset card for the current size)</td>
</tr>
</tbody>
</table>
<h3>Upload handling</h3>
<p>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 <em>Remove image</em> 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.</p>
<h2>In-browser edition (static website)</h2>
<p><strong>Deployed:</strong> <a href="https://sucksome-artifex.static.hf.space">https://sucksome-artifex.static.hf.space</a> (Hugging Face Space
<a href="https://huggingface.co/spaces/SuckSome/artifex"><code>SuckSome/artifex</code></a>, static SDK, public), 22 October 2026, model
<code>artifex-0.1-e94d93bf34</code>, dataset <code>0.1+5cc3c8d20c19</code> (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.</p>
<p><em>Content blockers.</em> The first upload named the analysis page module <code>analyse.js</code>; 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 <code>workspace.js</code>, 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).</p>
<p>The hosted edition is a static website that runs the deployed model <strong>in the visitor's browser</strong>. 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.)</p>
<pre><code class="language-bash">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
</code></pre>
<p>What the site contains (β‰ˆ 490 MB):</p>
<table>
<thead>
<tr>
<th>Part</th>
<th>Source</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>model/dinov2b_fp16w.onnx</code> (β‰ˆ 174 MB)</td>
<td><code>export_browser_model.py</code></td>
<td>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.</td>
</tr>
<tr>
<td><code>data/embeddings.f16</code> (β‰ˆ 31 MB)</td>
<td>deployed embeddings</td>
<td>Reference embeddings in float16.</td>
</tr>
<tr>
<td><code>data/model.json</code></td>
<td><code>heads.pkl</code>, <code>model_card.json</code></td>
<td>Scaler means/scales, logistic-regression coefficients, temperatures, abstention thresholds, OOD threshold, test recall of the evaluated model.</td>
</tr>
<tr>
<td><code>data/records.json</code>, <code>vocab.json</code></td>
<td>manifest, splits, configs</td>
<td>Metadata shown on the site, in embedding order.</td>
</tr>
<tr>
<td><code>data/run/</code></td>
<td>latest evaluation run</td>
<td><code>metrics.json</code>, <code>failures.json</code>, <code>report.md</code>, <code>predictions.csv</code>, copied verbatim.</td>
</tr>
<tr>
<td><code>thumbnails/</code> (β‰ˆ 270 MB)</td>
<td>CC0 thumbnails</td>
<td>384-px copies; record views link to the museum for full images.</td>
</tr>
<tr>
<td><code>vendor/</code></td>
<td>npm <code>onnxruntime-web@1.30.0</code>, <code>plotly.js-cartesian-dist-min@4.1.1</code></td>
<td>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).</td>
</tr>
</tbody>
</table>
<p><strong>Measured fidelity.</strong> 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 (<code>validate_browser.py</code>, 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 (<code>data/browser_validation.json</code>).</p>
<p><strong>Requirements.</strong> 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 <code>cross-origin-*</code> 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).</p>
<h2>Server-side alternative: Docker (Hugging Face paid tier or any container host)</h2>
<p><code>scripts/prepare_space.py</code> assembles a Docker bundle of the Streamlit application in <code>build/space/</code>. 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:</p>
<ul>
<li>code (<code>src/</code>, <code>app/</code>, <code>configs/</code>, <code>.streamlit/</code>), docs and screenshots;</li>
<li>the artefacts the website reads: manifest, splits, default-variant embeddings, deployed classifiers, the latest
  evaluation run (β‰ˆ 65 MB);</li>
<li>the CC0 thumbnails of all usable records, zipped (β‰ˆ 250 MB) and unpacked when the image is built. Full-size images, raw
  API responses, ablation embeddings and the model cache are <strong>not</strong> included; record detail views fall back to the
  384-px thumbnail;</li>
<li>a <code>Dockerfile</code> (Python 3.12, CPU-only PyTorch, exact package versions of the development environment because the
  classifiers are pickled scikit-learn objects) that downloads the pinned DINOv2 checkpoint at build time and then sets
  <code>HF_HUB_OFFLINE=1</code>, so the running app makes no calls to the Hub;</li>
<li>a <code>README.md</code> with the Space metadata followed by this project's README.</li>
</ul>
<pre><code class="language-bash">python scripts/prepare_space.py --source-url https://huggingface.co/spaces/&lt;user&gt;/artifex/tree/main
hf auth login                      # write token
hf upload &lt;user&gt;/artifex build/space . --repo-type space
</code></pre>
<p>Hosted-mode settings (environment variables in the Dockerfile):</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Effect</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ARTIFEX_HOST</code></td>
<td>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.</td>
</tr>
<tr>
<td><code>ARTIFEX_SOURCE_URL</code></td>
<td>Footer links to the published source.</td>
</tr>
<tr>
<td><code>ARTIFEX_ROOT</code></td>
<td>Project root inside the container.</td>
</tr>
</tbody>
</table>
<p>The container binds <code>0.0.0.0:7860</code> (the local default stays <code>localhost</code>) and runs with
<code>--server.enableXsrfProtection=false</code>: 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.</p>
<p>Before publishing the bundle was tested by unpacking it, running it with the hosted settings on CPU only
(<code>CUDA_VISIBLE_DEVICES=""</code>, <code>HF_HUB_OFFLINE=1</code>) and analysing a test image: scores matched the GPU run to two decimals.</p>
<h3>Checklist for any other host</h3>
<ol>
<li><strong>Host capacity.</strong> 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 (<code>dinov2s_letterbox_rgb</code>) is a smaller fallback, but the deployed model must
   then be retrained and re-evaluated for it.</li>
<li><strong>Model download.</strong> Confirm the host permits downloading from the Hugging Face Hub, or bundle the pinned checkpoint.</li>
<li><strong>Images.</strong> All reference images are CC0 and may be redistributed; do not commit them to Git.</li>
<li><strong>Privacy.</strong> Review the host's request logging and temporary-file behaviour so that uploads are not persisted. Serve
   over HTTPS.</li>
<li><strong>Abuse limits.</strong> Keep the upload size and pixel limits; consider rate limiting at the proxy.</li>
<li><strong>Unavailable state.</strong> If the host cannot run inference, the analysis page shows a "model or dataset unavailable"
   state β€” never substitute precomputed or fake predictions.</li>
</ol>
<p>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.</p>