MolMini Frontend¶
Local web UI over a trained MolMini checkpoint: paste a structure screenshot, get a copyable SMILES with a depiction and computed properties.
pic2smiles export --checkpoint checkpoints/molmini_base_last.pt --output molmini.pt --half
pic2smiles serve --open
Design constraints¶
- No new dependencies. The server is
http.server.ThreadingHTTPServerfrom the standard library, and the page is plain HTML/CSS/JS with no build step. The frontend adds nothing to the project's install surface beyond what inference already needs (PyTorch, RDKit, Pillow). - The recognizer is optional.
webapp/engine.pyloads the predictor lazily. With no checkpoint the server still starts and the manual-SMILES workflow keeps working; the UI says why the recognizer is offline rather than silently degrading. - No writes. Uploaded images live in memory for the request only. Nothing is
written under
data/,reports/orcheckpoints/. - The model is loaded once, at startup by default, and inference is serialized behind a lock. Loading costs about two seconds, so paying it up front makes the first paste as fast as every later one.
Layout¶
| Path | Role |
|---|---|
src/pic2smiles/webapp/server.py |
Routing, JSON API, static files, CLI entrypoint |
src/pic2smiles/webapp/engine.py |
Checkpoint discovery, lazy predictor, screenshot preprocessing |
src/pic2smiles/webapp/properties.py |
RDKit canonicalization, identifiers, descriptors, declared-scope check, SVG |
src/pic2smiles/webapp/static/ |
index.html, styles.css, app.js |
HTTP API¶
| Method | Path | Body | Returns |
|---|---|---|---|
GET |
/api/status |
-- | app/python/RDKit versions and engine status |
POST |
/api/recognize |
{"image": "data:image/png;base64,...", "preprocess": true} |
recognition, engine, and analysis when a SMILES was produced |
POST |
/api/analyze |
{"smiles": "..."} |
analysis: canonical SMILES, InChI/InChIKey, properties, Lipinski, declared scope, SVG |
GET |
/healthz |
-- | {"ok": true} |
Failures carry a stable reason_code / error_code (checkpoint_missing,
torch_missing, import_failed, load_failed, inference_failed,
empty_prediction, invalid_smiles) plus an English detail string. The
frontend owns the user-facing wording and keys off the code.
available (checkpoint and PyTorch present) and loaded (weights actually in
memory) are reported separately; the UI shows a pending state until a real load
has succeeded.
Screenshot preprocessing¶
Applied by default, reported back to the UI, and switchable off in the page:
- flatten transparency onto white,
- invert dark-mode captures (median grey level below 110),
- trim uniform margins,
- downscale anything over 1600 px on the long side,
- pad to a square with a small white margin,
- upscale to the model's input size if smaller.
Step 5 is the one that carries weight. Training depictions are square RDKit
renders, and image_to_tensor resizes straight to 384x384 without preserving
aspect ratio, so a wide screenshot arrives stretched. Measured on a 1000x380
crop of a normal render:
| input | prediction | decode confidence |
|---|---|---|
| caffeine, preprocessed | Cn1c(=O)c2c(ncn2C)n(C)c1=O (correct) |
0.95 |
| caffeine, raw | CCCCCCCCCCCC(=O)N1CCC23CC4CC(C)(CC(C)(C4)C2(C)C1)C3=O |
0.68 |
| imatinib, preprocessed | correct | 0.95 |
| imatinib, raw | CCCCCCCCCCCCCCCCN1CCN(CCCNc2ccnc3cc(Cl)ccc23)CC1 |
0.78 |
The raw predictions are not near-misses; they are different molecules, returned with the kind of confidence that looks like success. This is the concrete case behind the model card's warning that the score is not a correctness signal, and the reason the UI puts the source-versus-re-render comparison front and centre.
Confidence¶
Prediction.score is the mean per-token log probability; the UI shows
exp(score) as a percentage, labelled as decode confidence. It is not
calibrated against correctness, and a high score is deliberately not shown
in a positive colour -- only a low one is shown as a warning.
On 12 DECIMER hand-drawn images, 11 predictions were wrong and every one of the 12 came back with a 0.65-0.93 score; the highest-scoring image (0.93) was wrong and the only correct one scored lower (0.85). See issue #5.
Declared scope¶
properties.check_declared_scope re-derives the model card's training scope
from the predicted molecule -- 4 to 48 heavy atoms, the twelve declared
elements, single fragment -- and the UI shows a warning when a prediction falls
outside it. Accuracy figures in reports/ do not transfer to those inputs.
Measured performance¶
M3, CPU, greedy decoding, exported molmini.pt (base, step 240,000):
| checkpoint load | 1.9 s |
| aspirin (13 heavy atoms) | ~110 ms |
| imatinib (37 heavy atoms) | ~190 ms |
MPS is available but slower here (390 ms on imatinib against 190 ms on CPU) and
produces identical output, so --device cpu is the default.