Spaces:
Running
Running
Rebuild passport as native HTML cards (light theme, no more alien dark-teal block)
Browse files- __pycache__/app.cpython-310.pyc +0 -0
- app.py +158 -6
__pycache__/app.cpython-310.pyc
CHANGED
|
Binary files a/__pycache__/app.cpython-310.pyc and b/__pycache__/app.cpython-310.pyc differ
|
|
|
app.py
CHANGED
|
@@ -647,7 +647,152 @@ def _sensory_profile(name):
|
|
| 647 |
out[axis] = float(np.mean(vals)) if vals else 0.0
|
| 648 |
return out
|
| 649 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 650 |
def render_passport(name):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 651 |
fig = plt.figure(figsize=(12, 10.5), facecolor=KAIKAKU_DARK)
|
| 652 |
fig.patch.set_facecolor(KAIKAKU_DARK)
|
| 653 |
gs = gridspec.GridSpec(4, 6, figure=fig,
|
|
@@ -1815,15 +1960,22 @@ Three sibling ingredient embeddings from [arXiv:2605.22391](https://arxiv.org/ab
|
|
| 1815 |
with gr.Tab("Inspect"):
|
| 1816 |
with gr.Tabs():
|
| 1817 |
with gr.Tab("Ingredient passport"):
|
| 1818 |
-
gr.Markdown("Single-page dossier for one ingredient.
|
| 1819 |
with gr.Row():
|
| 1820 |
pp_pick = gr.Dropdown(choices=ALL_INGREDIENTS, label="Ingredient", value="basil")
|
| 1821 |
pp_btn = gr.Button("Generate passport", variant="primary")
|
| 1822 |
-
|
| 1823 |
-
|
| 1824 |
-
|
| 1825 |
-
|
| 1826 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1827 |
|
| 1828 |
with gr.Tab("Mode wiki"):
|
| 1829 |
gr.Markdown("Click into any of the ~500 modes for a per-mode wiki page.")
|
|
|
|
| 647 |
out[axis] = float(np.mean(vals)) if vals else 0.0
|
| 648 |
return out
|
| 649 |
|
| 650 |
+
def render_passport_html(name):
|
| 651 |
+
"""Native HTML/CSS passport that lives inside the white Gradio page.
|
| 652 |
+
Returns (html_str, sensory_radar_png_figure)."""
|
| 653 |
+
if not name:
|
| 654 |
+
return "<p style='color:#888'>Pick an ingredient.</p>", None
|
| 655 |
+
pretty = name.replace("_", " ").title()
|
| 656 |
+
group = _NAME_TO_GROUP.get(name, "Other")
|
| 657 |
+
sens = _sensory_profile(name)
|
| 658 |
+
# Build sensory radar as a small standalone figure
|
| 659 |
+
fig, ax = plt.subplots(figsize=(4.5, 4.5), subplot_kw={"projection": "polar"})
|
| 660 |
+
fig.patch.set_facecolor("#ffffff")
|
| 661 |
+
ax.set_facecolor("#ffffff")
|
| 662 |
+
theta = np.linspace(0, 2*np.pi, len(PASSPORT_SENS), endpoint=False)
|
| 663 |
+
r = np.array([max(0.0, sens[a]) for a in PASSPORT_SENS])
|
| 664 |
+
theta_c = np.concatenate([theta, theta[:1]])
|
| 665 |
+
r_c = np.concatenate([r, r[:1]])
|
| 666 |
+
ax.plot(theta_c, r_c, color=KAIKAKU_ACCENT, lw=2.2)
|
| 667 |
+
ax.fill(theta_c, r_c, color=KAIKAKU_ACCENT, alpha=0.22)
|
| 668 |
+
ax.set_xticks(theta)
|
| 669 |
+
ax.set_xticklabels([a.title() for a in PASSPORT_SENS], color="#0f172a", fontsize=11, fontweight="bold")
|
| 670 |
+
ax.set_yticklabels([])
|
| 671 |
+
ax.set_ylim(0, max(0.5, float(r.max()) + 0.08))
|
| 672 |
+
ax.grid(color="#cbd5e1", alpha=0.7, lw=0.5)
|
| 673 |
+
ax.spines["polar"].set_color("#94a3b8")
|
| 674 |
+
plt.tight_layout()
|
| 675 |
+
radar_fig = fig
|
| 676 |
+
|
| 677 |
+
# Compute neighbours per sibling
|
| 678 |
+
nb_blocks = []
|
| 679 |
+
for sib in PASSPORT_SIBS:
|
| 680 |
+
m = MODELS[sib]
|
| 681 |
+
if name not in m.vocab:
|
| 682 |
+
rows_html = "<div style='color:#94a3b8;font-style:italic;padding:8px'>(not in vocab)</div>"
|
| 683 |
+
else:
|
| 684 |
+
q = _unit(m.E[m.vocab[name]])
|
| 685 |
+
items = []
|
| 686 |
+
for nb, sim in _topk(m, q, 5, exclude=[name]):
|
| 687 |
+
items.append(
|
| 688 |
+
f"<div style='display:flex;justify-content:space-between;padding:5px 0;"
|
| 689 |
+
f"border-bottom:1px solid #e2e8f0'>"
|
| 690 |
+
f"<span style='color:#0f172a;font-weight:500'>{nb.replace('_', ' ')}</span>"
|
| 691 |
+
f"<span style='color:{KAIKAKU_ACCENT};font-family:monospace;font-weight:600'>{sim:.3f}</span>"
|
| 692 |
+
f"</div>"
|
| 693 |
+
)
|
| 694 |
+
rows_html = "".join(items)
|
| 695 |
+
nb_blocks.append(
|
| 696 |
+
f"<div style='flex:1;min-width:0;background:#f8fafc;border:1px solid #e2e8f0;"
|
| 697 |
+
f"border-radius:10px;padding:14px 16px'>"
|
| 698 |
+
f"<div style='color:{KAIKAKU_ACCENT};font-family:monospace;font-size:0.78em;"
|
| 699 |
+
f"font-weight:700;margin-bottom:10px;letter-spacing:0.04em'>{sib.upper()} · NEAREST NEIGHBOURS</div>"
|
| 700 |
+
f"{rows_html}</div>"
|
| 701 |
+
)
|
| 702 |
+
|
| 703 |
+
# Cuisine affiliation bars
|
| 704 |
+
m_chem = MODELS["chem"]
|
| 705 |
+
cuisine_html = ""
|
| 706 |
+
if name in m_chem.vocab:
|
| 707 |
+
v = _unit(m_chem.E[m_chem.vocab[name]])
|
| 708 |
+
vals = []
|
| 709 |
+
for cu in _CUISINES:
|
| 710 |
+
key = f"cuisine:{cu}"
|
| 711 |
+
if key in m_chem.supervised_poles:
|
| 712 |
+
vals.append((cu.replace("_", " "), float(v @ _unit(m_chem.supervised_poles[key]))))
|
| 713 |
+
vmax = max((abs(v_) for _, v_ in vals), default=1.0) or 1.0
|
| 714 |
+
cuisine_rows = []
|
| 715 |
+
for label, val in vals:
|
| 716 |
+
pct = abs(val) / vmax * 100
|
| 717 |
+
bar_color = KAIKAKU_ACCENT if val >= 0 else "#f59e0b"
|
| 718 |
+
cuisine_rows.append(
|
| 719 |
+
f"<div style='display:grid;grid-template-columns:140px 1fr 60px;gap:10px;"
|
| 720 |
+
f"align-items:center;padding:5px 0'>"
|
| 721 |
+
f"<span style='color:#0f172a;font-weight:500;font-size:0.92em'>{label}</span>"
|
| 722 |
+
f"<div style='background:#e2e8f0;border-radius:6px;height:14px;position:relative;overflow:hidden'>"
|
| 723 |
+
f"<div style='background:{bar_color};height:100%;width:{pct:.1f}%;border-radius:6px'></div>"
|
| 724 |
+
f"</div>"
|
| 725 |
+
f"<span style='color:{KAIKAKU_ACCENT};font-family:monospace;text-align:right;"
|
| 726 |
+
f"font-weight:600;font-size:0.88em'>{val:+.3f}</span>"
|
| 727 |
+
f"</div>"
|
| 728 |
+
)
|
| 729 |
+
cuisine_html = "".join(cuisine_rows)
|
| 730 |
+
else:
|
| 731 |
+
cuisine_html = "<div style='color:#94a3b8;font-style:italic'>(not in chem vocab)</div>"
|
| 732 |
+
|
| 733 |
+
# Closest factor modes
|
| 734 |
+
scored = []
|
| 735 |
+
for sib in PASSPORT_SIBS:
|
| 736 |
+
m = MODELS[sib]
|
| 737 |
+
if name not in m.vocab: continue
|
| 738 |
+
v = _unit(m.E[m.vocab[name]])
|
| 739 |
+
for md in m.modes:
|
| 740 |
+
if md.kind != "factor": continue
|
| 741 |
+
scored.append((float(_unit(md.pole) @ v), sib, md))
|
| 742 |
+
scored.sort(key=lambda x: -x[0])
|
| 743 |
+
mode_rows = []
|
| 744 |
+
for sim, sib, md in scored[:3]:
|
| 745 |
+
members = ", ".join(n.replace("_", " ") for n in md.members[:6])
|
| 746 |
+
mode_rows.append(
|
| 747 |
+
f"<div style='background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;"
|
| 748 |
+
f"padding:14px 16px;margin-bottom:10px'>"
|
| 749 |
+
f"<div style='display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px'>"
|
| 750 |
+
f"<div><span style='color:{KAIKAKU_ACCENT};font-family:monospace;font-size:0.78em;"
|
| 751 |
+
f"font-weight:700;margin-right:10px;letter-spacing:0.04em'>{sib.upper()}</span>"
|
| 752 |
+
f"<span style='color:#0f172a;font-weight:600;font-size:1.05em'>{md.label}</span></div>"
|
| 753 |
+
f"<span style='color:{KAIKAKU_ACCENT};font-family:monospace;font-weight:600'>cos {sim:.3f}</span>"
|
| 754 |
+
f"</div>"
|
| 755 |
+
f"<div style='color:#475569;font-size:0.88em;line-height:1.4'>{members}</div>"
|
| 756 |
+
f"</div>"
|
| 757 |
+
)
|
| 758 |
+
|
| 759 |
+
html = f"""
|
| 760 |
+
<div style='max-width:1180px;margin:0 auto'>
|
| 761 |
+
<div style='border-bottom:3px solid {KAIKAKU_ACCENT};padding-bottom:8px;margin-bottom:18px'>
|
| 762 |
+
<div style='font-size:2.2em;font-weight:800;color:#0f172a;letter-spacing:-0.02em;line-height:1.0'>{pretty}</div>
|
| 763 |
+
<div style='color:{KAIKAKU_ACCENT};font-family:monospace;font-size:0.82em;font-weight:600;
|
| 764 |
+
margin-top:6px;letter-spacing:0.04em'>
|
| 765 |
+
INGREDIENT PASSPORT · FOOD GROUP: {group.upper()}
|
| 766 |
+
</div>
|
| 767 |
+
</div>
|
| 768 |
+
<div style='display:flex;gap:14px;margin-bottom:18px;flex-wrap:wrap'>{''.join(nb_blocks)}</div>
|
| 769 |
+
<div style='background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;
|
| 770 |
+
padding:14px 18px;margin-bottom:18px'>
|
| 771 |
+
<div style='color:{KAIKAKU_ACCENT};font-family:monospace;font-size:0.78em;
|
| 772 |
+
font-weight:700;margin-bottom:10px;letter-spacing:0.04em'>CUISINE AFFILIATION (chem)</div>
|
| 773 |
+
{cuisine_html}
|
| 774 |
+
</div>
|
| 775 |
+
<div>
|
| 776 |
+
<div style='color:{KAIKAKU_ACCENT};font-family:monospace;font-size:0.78em;
|
| 777 |
+
font-weight:700;margin-bottom:10px;letter-spacing:0.04em'>
|
| 778 |
+
CLOSEST EMERGENT FACTOR MODES (top 3 across siblings)
|
| 779 |
+
</div>
|
| 780 |
+
{''.join(mode_rows) if mode_rows else "<div style='color:#94a3b8'>(no modes)</div>"}
|
| 781 |
+
</div>
|
| 782 |
+
</div>
|
| 783 |
+
"""
|
| 784 |
+
return html, radar_fig
|
| 785 |
+
|
| 786 |
+
|
| 787 |
def render_passport(name):
|
| 788 |
+
"""Legacy entry point - delegate to the HTML version and discard the radar."""
|
| 789 |
+
html, _radar = render_passport_html(name)
|
| 790 |
+
# Return a placeholder matplotlib fig (small) since callers may expect one
|
| 791 |
+
fig, ax = plt.subplots(figsize=(0.1, 0.1))
|
| 792 |
+
ax.axis("off")
|
| 793 |
+
return fig
|
| 794 |
+
|
| 795 |
+
def _render_passport_dummy(name):
|
| 796 |
fig = plt.figure(figsize=(12, 10.5), facecolor=KAIKAKU_DARK)
|
| 797 |
fig.patch.set_facecolor(KAIKAKU_DARK)
|
| 798 |
gs = gridspec.GridSpec(4, 6, figure=fig,
|
|
|
|
| 1960 |
with gr.Tab("Inspect"):
|
| 1961 |
with gr.Tabs():
|
| 1962 |
with gr.Tab("Ingredient passport"):
|
| 1963 |
+
gr.Markdown("Single-page dossier for one ingredient.")
|
| 1964 |
with gr.Row():
|
| 1965 |
pp_pick = gr.Dropdown(choices=ALL_INGREDIENTS, label="Ingredient", value="basil")
|
| 1966 |
pp_btn = gr.Button("Generate passport", variant="primary")
|
| 1967 |
+
with gr.Row():
|
| 1968 |
+
with gr.Column(scale=3):
|
| 1969 |
+
pp_html = gr.HTML(value=render_passport_html("basil")[0])
|
| 1970 |
+
with gr.Column(scale=1):
|
| 1971 |
+
gr.Markdown(f"<div style='color:{KAIKAKU_ACCENT};font-family:monospace;"
|
| 1972 |
+
f"font-size:0.78em;font-weight:700;letter-spacing:0.04em'>SENSORY RADAR</div>")
|
| 1973 |
+
pp_radar = gr.Plot(value=render_passport_html("basil")[1], label="")
|
| 1974 |
+
def _passport_outputs(name):
|
| 1975 |
+
h, r = render_passport_html(name)
|
| 1976 |
+
return h, r
|
| 1977 |
+
pp_btn.click(_passport_outputs, inputs=[pp_pick], outputs=[pp_html, pp_radar], show_progress="minimal")
|
| 1978 |
+
pp_pick.change(_passport_outputs, inputs=[pp_pick], outputs=[pp_html, pp_radar], show_progress="minimal")
|
| 1979 |
|
| 1980 |
with gr.Tab("Mode wiki"):
|
| 1981 |
gr.Markdown("Click into any of the ~500 modes for a per-mode wiki page.")
|