Skip to content

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.ThreadingHTTPServer from 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.py loads 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/ or checkpoints/.
  • 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:

  1. flatten transparency onto white,
  2. invert dark-mode captures (median grey level below 110),
  3. trim uniform margins,
  4. downscale anything over 1600 px on the long side,
  5. pad to a square with a small white margin,
  6. 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.