Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 74 additions & 31 deletions training/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,8 +406,46 @@ def font(path, size):
f_pill = font(DJ + "DejaVuSans-Bold.ttf", 16)
f_lab = font(DJ + "DejaVuSans-Bold.ttf", 15)

W, H = 1200, 630
W = 1200
x = 508 # right column
ink, muted, cream, teal = (231, 237, 234), (148, 162, 169), (217, 171, 116), (43, 196, 196)

# ---- the full read: ALL SIX taste heads + every confident aroma head ----
taste_src = {"sweet": out.get("sweet"), "bitter": out.get("bitter"), "umami": out.get("umami"),
"sour": out.get("sour_predicted"), "salty": out.get("salty_predicted"),
"tasteless": out.get("tasteless")}
taste_bars = sorted(((t, float(v)) for t, v in taste_src.items() if isinstance(v, (int, float))),
key=lambda kv: -kv[1])
pa = P.predict_aroma(smi)
aroma_conf = [(d["odor"], d["score"]) for d in pa.get("descriptors", []) if d.get("confident")]
if aroma_conf:
aroma_bars = aroma_conf[:10]
aroma_label = "AROMA MODEL · confident odor heads"
else: # nothing cleared the 0.5 bar — show the strongest few, labelled honestly
aroma_bars = [(d["odor"], d["score"]) for d in pa.get("descriptors", [])[:3]]
aroma_label = "AROMA MODEL · top signals (none cleared 0.5)"

pill_items = ([(fl, cream) for fl in tags.get("flavors", [])[:3]]
+ [(t, _TASTE_RGB.get(t, teal)) for t in tags.get("tastes", [])]
+ [(a, teal) for a in tags.get("aromas", [])[:6]])

# ---- measure pill wrapping on a scratch canvas, then compute a height that fits everything ----
scratch = ImageDraw.Draw(Image.new("RGB", (10, 10)))
py = 306
px = x
for text, _c in pill_items:
w = scratch.textlength(text, font=f_pill)
if px + w + 22 > W - 40:
px, py = x, py + 40
px += w + 30
pills_bottom = py + 40
taste_label_y = pills_bottom + 18
taste_bars_y = taste_label_y + 26
aroma_label_y = taste_bars_y + len(taste_bars) * 28 + 16
aroma_bars_y = aroma_label_y + 26
content_bottom = aroma_bars_y + len(aroma_bars) * 28
H = max(560, content_bottom + 62)

img = Image.new("RGB", (W, H), (15, 19, 25))
dr = ImageDraw.Draw(img)
# corner aura
Expand All @@ -428,60 +466,65 @@ def font(path, size):
dr.text((112, 82), "taste & aroma from chemical structure", font=f_tag, fill=cream)
dr.line([40, 122, W - 40, 122], fill=(42, 50, 60), width=1)

# structure panel (white)
dr.rounded_rectangle([40, 150, 470, 520], radius=14, fill=(245, 247, 245))
d2 = rdMolDraw2D.MolDraw2DCairo(410, 350)
# structure panel (white) — grows with the card so it stays balanced
panel_bottom = min(H - 66, 620)
dr.rounded_rectangle([40, 150, 470, panel_bottom], radius=14, fill=(245, 247, 245))
struct_h = min(360, panel_bottom - 170)
d2 = rdMolDraw2D.MolDraw2DCairo(410, struct_h)
d2.drawOptions().padding = 0.12
d2.DrawMolecule(mol)
d2.FinishDrawing()
struct = Image.open(io.BytesIO(d2.GetDrawingText())).convert("RGBA")
img.paste(struct, (50, 160), struct)
img.paste(struct, (50, 150 + (panel_bottom - 150 - struct_h) // 2), struct)

# right column
x = 508
# right column — name / IUPAC / SMILES
dr.text((x, 158), name[:34], font=f_name, fill=ink)
if iupac and iupac.lower() != name.lower():
dr.text((x, 206), ("IUPAC " + iupac)[:64], font=f_body, fill=muted)
dr.text((x, 232), smi[:58], font=f_mono, fill=muted)

# "reads as" pills
# READS AS pills
dr.text((x, 280), "READS AS", font=f_lab, fill=muted)
px, py = x, 306
def pill(px, py, text, fg, border):
def pill(px, py, text, fg):
w = dr.textlength(text, font=f_pill)
if px + w + 22 > W - 40:
px, py = x, py + 40
dr.rounded_rectangle([px, py, px + w + 22, py + 30], radius=15, outline=border, width=2)
dr.rounded_rectangle([px, py, px + w + 22, py + 30], radius=15, outline=fg, width=2)
dr.text((px + 11, py + 6), text, font=f_pill, fill=fg)
return px + w + 30, py
for fl in tags.get("flavors", [])[:4]:
px, py = pill(px, py, fl, cream, cream)
for t in tags.get("tastes", [])[:4]:
c = _TASTE_RGB.get(t, teal)
px, py = pill(px, py, t, c, c)
for a in tags.get("aromas", [])[:5]:
px, py = pill(px, py, a, teal, teal)

# taste-probability bars (the numeric heads)
by = py + 58
dr.text((x, by - 26), "TASTE MODEL", font=f_lab, fill=muted)
bars = [(t, out.get(t)) for t in ("sweet", "bitter", "umami", "tasteless") if isinstance(out.get(t), (int, float))]
bars.sort(key=lambda kv: -kv[1])
for t, v in bars[:4]:
c = _TASTE_RGB.get(t, teal)
for text, c in pill_items:
px, py = pill(px, py, text, c)

def bar_row(by, label, v, c):
pct = int(round(v * 100))
dr.text((x, by), t, font=f_body, fill=ink)
dr.text((x, by), label, font=f_body, fill=ink)
dr.rounded_rectangle([x + 120, by + 4, x + 120 + 320, by + 16], radius=6, fill=(36, 44, 53))
dr.rounded_rectangle([x + 120, by + 4, x + 120 + int(320 * v), by + 16], radius=6, fill=c)
if v > 0:
dr.rounded_rectangle([x + 120, by + 4, x + 120 + int(320 * v), by + 16], radius=6, fill=c)
dr.text((x + 452, by), f"{pct}%", font=f_body, fill=muted)
by += 30

# TASTE MODEL — all six heads
dr.text((x, taste_label_y), "TASTE MODEL · all six heads", font=f_lab, fill=muted)
by = taste_bars_y
for t, v in taste_bars:
bar_row(by, t, v, _TASTE_RGB.get(t, teal))
by += 28

# AROMA MODEL — every confident odor head, each with its probability
dr.text((x, aroma_label_y), aroma_label, font=f_lab, fill=muted)
by = aroma_bars_y
for a, v in aroma_bars:
bar_row(by, a, v, teal)
by += 28

# footer
dr.line([40, 576, W - 40, 576], fill=(42, 50, 60), width=1)
fy = H - 54
dr.line([40, fy, W - 40, fy], fill=(42, 50, 60), width=1)
share = "flavormancer.echelonts.net/?q=" + (common or q or smi)
dr.text((40, 590), share, font=f_mono, fill=teal)
dr.text((40, fy + 14), share, font=f_mono, fill=teal)
tw = dr.textlength("before you pour", font=f_tag)
dr.text((W - 40 - tw, 588), "before you pour", font=f_tag, fill=cream)
dr.text((W - 40 - tw, fy + 12), "before you pour", font=f_tag, fill=cream)

buf = io.BytesIO()
img.save(buf, "PNG")
Expand Down
31 changes: 27 additions & 4 deletions training/workbench.html
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@
.bcell .bstruct{height:68px}.bcell .bstruct svg{max-width:100%;height:68px}
.bcell .bname{font-size:12px;font-weight:600;margin-top:5px;line-height:1.25;text-transform:capitalize;cursor:pointer}
.bcell .bname:hover{color:var(--aroma)}
.map-section{margin:22px 0 8px;border-top:1px solid #EEF0EC;padding-top:12px}
.map-section{margin:22px 0 8px}
.map-head{font-size:12px;font-weight:650;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);cursor:pointer}
.map-head .mapsub{font-weight:600;text-transform:none;letter-spacing:0;font-size:12px;color:var(--muted);opacity:.8}
.map-legend{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0 6px;font-size:12px}
Expand Down Expand Up @@ -373,6 +373,12 @@
.cmp-card:hover{border-color:var(--brand-2)}
.cmp-card img{display:block;width:100%;height:auto}
.cmp-card .empty{padding:20px}
.cmp-card.loading{display:flex;align-items:center;justify-content:center;min-height:300px}
.cmp-card.loading img{display:none}
.cmp-load{display:flex;flex-direction:column;align-items:center;gap:8px}
.cmp-load .loader-flask{width:96px;height:96px}
.cmp-load-cap{font-family:'Cinzel',Georgia,serif;font-size:12.5px;letter-spacing:.04em;color:var(--cream)}
.cmp-err{padding:26px 20px;color:var(--muted);font-size:13px;text-align:center}
.mix-ings{display:flex;flex-direction:column;gap:6px;margin-bottom:8px}
.mix-ing{display:flex;gap:10px;align-items:center;padding:8px;border:1px solid var(--line);border-radius:8px;background:var(--surface)}
.mix-ing:hover{background:var(--accent-soft)}
Expand Down Expand Up @@ -784,8 +790,8 @@ <h1>Flavormancer</h1>
<div id="mixResults"></div>
</div>

<label class="toggle"><input type="checkbox" id="cmpToggle"> <span id="cmpCaret"></span> Compare molecules — two reads side by side</label>
<div id="cmpPanel" style="display:none">
<label class="toggle"><input type="checkbox" id="cmpToggle" checked> <span id="cmpCaret"></span> Compare molecules — two reads side by side</label>
<div id="cmpPanel">
<p class="mix-explain">Enter two molecules to see their <b>flavor cards side by side</b> — compare
taste, aroma, and structure at a glance (great for a reformulation or a chirality pair).</p>
<div class="cmp-inputs">
Expand Down Expand Up @@ -2060,11 +2066,28 @@ <h4>Software &amp; type</h4>
// --- compare two molecules: side-by-side flavor cards (reuses /api/card) ---
$('cmpToggle').addEventListener('change', e=>{ $('cmpPanel').style.display=e.target.checked?'block':'none'; $('cmpCaret').textContent=e.target.checked?'▾':'▸'; });
attachRichSuggest($('cmpA')); attachRichSuggest($('cmpB'));
// compact flask loader (same spirit as the read modal) shown inside each box while the card renders
function cmpLoader(label){
return `<div class="cmp-load"><svg class="loader-flask" viewBox="0 0 140 140" aria-hidden="true">
<defs><linearGradient id="lg-cmp" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#7C5CBF"/><stop offset="1" stop-color="#2BC4C4"/></linearGradient></defs>
<circle cx="70" cy="70" r="62" fill="none" stroke="url(#lg-cmp)" stroke-width="3" opacity=".55"/>
<circle class="loader-ring" cx="70" cy="70" r="62" fill="none" stroke="url(#lg-cmp)" stroke-width="3" stroke-linecap="round" stroke-dasharray="80 320"/>
<path d="M58 44 h24 v14 l16 34 a6 6 0 0 1 -5.5 8.4 h-45 a6 6 0 0 1 -5.5 -8.4 l16 -34 z" fill="rgba(43,196,196,.10)" stroke="url(#lg-cmp)" stroke-width="3" stroke-linejoin="round"/>
<path d="M56 44 h28" stroke="url(#lg-cmp)" stroke-width="3.4" stroke-linecap="round"/>
<path d="M50.5 74 L89.5 74 L98 92 a6 6 0 0 1 -5.5 8.4 h-45 a6 6 0 0 1 -5.5 -8.4 Z" fill="url(#lg-cmp)" opacity=".72"/>
<circle class="bub b1" cx="64" cy="90" r="2.4" fill="#EAF6F4"/><circle class="bub b2" cx="73" cy="93" r="1.8" fill="#EAF6F4"/>
<circle class="bub b3" cx="77" cy="87" r="2.1" fill="#EAF6F4"/><circle class="bub b4" cx="68" cy="95" r="1.5" fill="#EAF6F4"/>
<g class="wisps" fill="none" stroke-linecap="round">
<path class="wisp w1" d="M69 72 C63 64 75 58 69 50 C63 43 77 36 70 28 C65 22 73 16 69 9" stroke="#D9AB74" stroke-width="2.4"/>
<path class="wisp w2" d="M63 71 C57 64 69 59 62 52 C56 46 66 40 62 33 C59 28 64 24 62 19" stroke="#2BC4C4" stroke-width="2"/>
<path class="wisp w3" d="M76 71 C82 64 70 59 77 52 C83 46 73 41 77 34 C79 30 75 26 77 22" stroke="#8A6BE0" stroke-width="2"/>
</g></svg><div class="cmp-load-cap">reading ${label}…</div></div>`;
}
function cmpRun(){
const a=$('cmpA').value.trim(), b=$('cmpB').value.trim();
if(!a||!b){ $('cmpResults').innerHTML='<div class="empty">Enter two molecules to compare.</div>'; return; }
$('cmpResults').innerHTML=[a,b].map(m=>{ const e=m.replace(/"/g,'&quot;');
return `<div class="cmp-card" data-q="${e}" title="Open the full read"><img loading="lazy" src="/api/card?q=${encodeURIComponent(m)}" alt="flavor card for ${e}" onerror="this.parentElement.textContent='No read for that molecule'"></div>`;
return `<div class="cmp-card loading" data-q="${e}" title="Open the full read">${cmpLoader(e)}<img src="/api/card?q=${encodeURIComponent(m)}" alt="flavor card for ${e}" onload="this.closest('.cmp-card').classList.remove('loading')" onerror="var c=this.closest('.cmp-card');c.classList.remove('loading');c.innerHTML='&lt;div class=cmp-err&gt;No read for that molecule&lt;/div&gt;'"></div>`;
}).join('');
}
$('cmpGo').addEventListener('click', cmpRun);
Expand Down
Loading