Skip to content

Commit f70ec1e

Browse files
feat(demo): illustrative aroma-descriptor preview in the workbench (#73)
Show what an UNLOCKED aroma read looks like — descriptor bars (vanilla, fruity, minty, woody, ...) for ~16 famous flavor molecules — so the value is visible without faking a trained model. The descriptors are common-knowledge odor characters used illustratively; the card labels them "preview — illustrative ... not a live prediction" and keeps the "unlocks with your data" framing. /api/aroma returns the preview for known molecules, unavailable otherwise. Honest demonstration of the deferred aroma feature. Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
1 parent 5d4e935 commit f70ec1e

2 files changed

Lines changed: 72 additions & 2 deletions

File tree

training/app.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,57 @@ def api_suggest(qs: str = ""):
189189
return {"items": items}
190190

191191

192+
# --- Aroma PREVIEW (illustrative, NOT a trained model) -------------------------
193+
# Well-known odor characters for a handful of famous flavor molecules, so the demo can
194+
# SHOW what an unlocked aroma read looks like. These are common-knowledge descriptors
195+
# used as an illustration — the real model trains on a customer's expert-labeled odor
196+
# data (see docs/AROMA.md). Clearly flagged "preview" in the UI; never passed off as live.
197+
_AROMA_PREVIEW_RAW = [
198+
("O=Cc1ccc(O)c(OC)c1", [("vanilla", 0.95), ("sweet", 0.6), ("creamy", 0.4), ("woody", 0.25)]),
199+
("CC1=CCC(CC1)C(C)=C", [("citrus", 0.9), ("orange", 0.7), ("fresh", 0.5), ("terpene", 0.3)]),
200+
("CC(C)C1CCC(C)CC1O", [("minty", 0.9), ("cooling", 0.7), ("fresh", 0.5), ("herbal", 0.3)]),
201+
("C=CCc1ccc(O)c(OC)c1", [("clove", 0.9), ("spicy", 0.7), ("woody", 0.4), ("sweet", 0.3)]),
202+
("O=Cc1ccccc1", [("almond", 0.95), ("cherry", 0.5), ("sweet", 0.4)]),
203+
("O=C/C=C/c1ccccc1", [("cinnamon", 0.95), ("spicy", 0.6), ("sweet", 0.4), ("warm", 0.3)]),
204+
("CC(C)=CCCC(C)(O)C=C", [("floral", 0.85), ("lavender", 0.6), ("citrus", 0.4), ("woody", 0.25)]),
205+
("CCCC(=O)OCC", [("fruity", 0.9), ("pineapple", 0.7), ("sweet", 0.5)]),
206+
("CC(=O)C(C)=O", [("buttery", 0.95), ("creamy", 0.6)]),
207+
("CC(C)=CCC/C(C)=C/CO", [("rose", 0.85), ("floral", 0.7), ("citrus", 0.3)]),
208+
("CCOC(C)=O", [("solvent", 0.6), ("fruity", 0.5), ("sweet", 0.3)]),
209+
("O=Cc1ccco1", [("almond", 0.7), ("bready", 0.6), ("caramel", 0.5), ("sweet", 0.3)]),
210+
("Cc1ccc(C(C)C)cc1O", [("thyme", 0.85), ("herbal", 0.7), ("medicinal", 0.4), ("spicy", 0.3)]),
211+
("COc1ccccc1O", [("smoky", 0.85), ("medicinal", 0.6), ("woody", 0.5), ("spicy", 0.3)]),
212+
("C/C=C/c1ccc(OC)cc1", [("anise", 0.9), ("licorice", 0.7), ("sweet", 0.5)]),
213+
("CC(=O)CCc1ccc(O)cc1", [("raspberry", 0.9), ("jammy", 0.6), ("sweet", 0.5)]),
214+
]
215+
216+
217+
def _aroma_index():
218+
out = {}
219+
for smi, descs in _AROMA_PREVIEW_RAW:
220+
m = Chem.MolFromSmiles(smi)
221+
if m is not None:
222+
out[Chem.MolToInchiKey(m).split("-")[0]] = descs
223+
return out
224+
225+
226+
_AROMA_PREVIEW = _aroma_index()
227+
228+
229+
@app.post("/api/aroma")
230+
def api_aroma(q: Query):
231+
"""ILLUSTRATIVE aroma preview for a few well-known molecules — NOT a live model."""
232+
smi = _resolve(q.smiles)
233+
mol = Chem.MolFromSmiles(smi) if smi else None
234+
if mol is None:
235+
return {"available": False}
236+
descs = _AROMA_PREVIEW.get(Chem.MolToInchiKey(mol).split("-")[0])
237+
if not descs:
238+
return {"available": False}
239+
return {"available": True, "preview": True,
240+
"descriptors": [{"odor": n, "score": s} for n, s in descs]}
241+
242+
192243
@app.get("/", response_class=HTMLResponse)
193244
def home():
194245
return Path("workbench.html").read_text()

training/workbench.html

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@
7474
.aroma-lead b{color:var(--aroma)}
7575
.aroma-note{font-size:13px;color:var(--muted);margin:0;line-height:1.55;max-width:760px}
7676
.aroma-note b{color:var(--ink)}
77+
.aroma-prev-tag{font-size:11px;font-weight:650;text-transform:uppercase;letter-spacing:.05em;color:var(--aroma);margin:0 0 10px}
78+
.ameter{margin:8px 0}
79+
.arow{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:4px}
80+
.aname{font-weight:600;font-size:13px;text-transform:capitalize}
81+
.aval{font-family:var(--mono);font-size:12px;color:var(--muted)}
82+
.atrack{height:8px;background:#EDEFEC;border-radius:5px;overflow:hidden}
83+
.afill{height:100%;background:var(--aroma);border-radius:5px;transition:width .5s ease}
7784
.kv{display:grid;grid-template-columns:auto 1fr;gap:6px 16px;margin-bottom:10px}
7885
.kv .k{font-weight:600;font-size:13px}
7986
.kv .v{font-family:var(--mono);font-size:13px;color:var(--muted)}
@@ -166,6 +173,7 @@ <h2>Behavior &amp; safety</h2>
166173

167174
<div class="card aroma-card" id="aromaCard" style="display:none">
168175
<h2>Aroma</h2>
176+
<div id="aromaPreview" style="display:none;margin-bottom:14px"></div>
169177
<div class="aroma-lead">Aroma prediction unlocks with <b>your</b> data.</div>
170178
<p class="aroma-note">The public demo predicts <b>taste</b> from open data. A useful
171179
aroma model needs expert-labeled odor data — so we train it <b>on-premise, on your
@@ -217,14 +225,16 @@ <h2>Aroma</h2>
217225
go.disabled = true; go.textContent = 'Reading…';
218226
$('err').style.display='none';
219227
try{
220-
const [p, n, s, nm] = await Promise.all([
228+
const [p, n, s, nm, ar] = await Promise.all([
221229
post('/api/predict',{smiles:text}),
222230
post('/api/neighbors',{smiles:text, k:8}),
223231
post('/api/structure',{smiles:text}),
224-
post('/api/names',{smiles:text})
232+
post('/api/names',{smiles:text}),
233+
post('/api/aroma',{smiles:text})
225234
]);
226235
if(p.error){ throw new Error(p.error); }
227236
render(p, n.neighbors||[], (s&&s.svg)||null, nm||{});
237+
renderAroma(ar||{});
228238
}catch(e){
229239
$('results').style.display='none'; $('behaviorCard').style.display='none'; $('aromaCard').style.display='none'; $('footnote').style.display='none';
230240
$('err').textContent = e.message; $('err').style.display='block';
@@ -340,6 +350,15 @@ <h2>Aroma</h2>
340350
$('behavior').innerHTML = html;
341351
}
342352

353+
function renderAroma(ar){
354+
const box=$('aromaPreview');
355+
if(ar && ar.available && (ar.descriptors||[]).length){
356+
box.innerHTML='<div class="aroma-prev-tag">▸ preview — illustrative of the unlocked read (not a live prediction)</div>'+
357+
ar.descriptors.map(d=>{ const pct=Math.round(d.score*100); return `<div class="ameter"><div class="arow"><span class="aname">${d.odor}</span><span class="aval">${pct}%</span></div><div class="atrack"><div class="afill" style="width:${pct}%"></div></div></div>`; }).join('');
358+
box.style.display='block';
359+
} else { box.style.display='none'; }
360+
}
361+
343362
function chip(key, on, color, label){
344363
const style = on && color ? `style="background:${color}"` : '';
345364
return `<span class="chip ${on?'on':''}" ${style}>${label}</span>`;

0 commit comments

Comments
 (0)