"""Epicure Explorer: chef-facing operators over the three sibling embeddings."""
from __future__ import annotations
import os
import re
import sys
import json
import numpy as np
import gradio as gr
import plotly.graph_objects as go
try:
from epicure import Epicure
except ImportError:
from huggingface_hub import hf_hub_download
epicure_py = hf_hub_download("Kaikaku/epicure-cooc", "epicure.py")
sys.path.insert(0, os.path.dirname(epicure_py))
from epicure import Epicure
from rapidfuzz import process as fuzz_process, fuzz as fuzz_scorers
MODELS = {
"cooc": Epicure.from_pretrained("Kaikaku/epicure-cooc"),
"core": Epicure.from_pretrained("Kaikaku/epicure-core"),
"chem": Epicure.from_pretrained("Kaikaku/epicure-chem"),
}
ALL_INGREDIENTS = sorted(MODELS["cooc"].vocab.keys())
_HERE = os.path.dirname(os.path.abspath(__file__))
UMAP = np.load(os.path.join(_HERE, "umap_2d.npz"))
_lab = json.load(open(os.path.join(_HERE, "ingredient_labels.json")))
NAMES_BY_IDX = _lab["names"]
FOOD_GROUPS = _lab["food_groups"]
FG_COLORS = {
"Vegetable": "#2ca02c",
"Fruit": "#e377c2",
"Grain": "#bcbd22",
"Dairy": "#17becf",
"Spice": "#d62728",
"Pantry": "#ff7f0e",
"Beverage": "#9467bd",
"Other": "#cccccc",
}
SIBLING_BLURBS = {
"cooc": "**Cooc** walks recipe co-occurrence only. Neighbours are recipe companions: ingredients that *get cooked with* the seed.",
"core": "**Core** blends typed FlavorDB compound walks with injected I-I walks at ii_repeat=10. Concentrated geometry (PR=94), tightest emergent modes.",
"chem": "**Chem** walks typed FlavorDB compound metapaths only (ii_repeat=0). Neighbours are flavour-profile peers: ingredients that *share aroma chemistry* with the seed.",
}
# ===== math helpers =====
def _unit(v, eps=1e-9):
n = np.linalg.norm(v); return v / max(n, eps)
def _basket_centroid(m, names):
valid = [n for n in (names or []) if n in m.vocab]
if not valid: return None
return _unit(m.E[[m.vocab[n] for n in valid]].mean(axis=0))
def _stack_directions(m, keys, use_factor_pole=False):
poles = []
for k in keys or []:
if use_factor_pole:
for mode in m.modes:
if mode.mode_id == k:
poles.append(_unit(mode.pole)); break
else:
if k in m.supervised_poles:
poles.append(_unit(m.supervised_poles[k]))
if not poles: return None
return _unit(np.stack(poles, axis=0).sum(axis=0))
def _topk(m, q, k, exclude):
sims = m.E @ q
for n in exclude or []:
if n in m.vocab: sims[m.vocab[n]] = -np.inf
order = np.argsort(-sims)
return [(m.itos[int(i)], float(sims[i])) for i in order[:k]]
def _supervised_choices(sibling):
return sorted(MODELS[sibling].supervised_poles.keys())
def _factor_mode_choices(sibling):
return [(f"{m.label} ({m.mode_id})", m.mode_id) for m in MODELS[sibling].modes if m.kind == "factor"]
def _slerp(v, d, theta_deg):
d_perp = d - (d @ v) * v
n = np.linalg.norm(d_perp)
if n < 1e-9: return v
d_perp = d_perp / n
th = np.deg2rad(float(theta_deg))
return _unit(np.cos(th)*v + np.sin(th)*d_perp)
# ===== tab handlers =====
def basket_pairings(sibling, basket, k):
m = MODELS[sibling]
centroid = _basket_centroid(m, basket)
if centroid is None:
return [], [], None
nb = _topk(m, centroid, k, exclude=basket or [])
scored = [(mode.mode_id, mode.label, mode.kind, float(_unit(mode.pole) @ centroid)) for mode in m.modes]
scored.sort(key=lambda x: -x[3])
heatmap = _basket_heatmap(m, basket)
return (
[[name, f"{sim:.4f}"] for name, sim in nb],
[[mid, label, kind, f"{sim:.4f}"] for mid, label, kind, sim in scored[:k]],
heatmap,
)
def _basket_heatmap(m, basket):
valid = [n for n in (basket or []) if n in m.vocab]
if len(valid) < 2:
# Empty figure with a hint
fig = go.Figure()
fig.add_annotation(text="Add 2+ ingredients to see pairwise cosines",
showarrow=False, xref="paper", yref="paper", x=0.5, y=0.5,
font=dict(size=14, color="#888"))
fig.update_layout(height=420, plot_bgcolor="#fafafa", paper_bgcolor="#fafafa")
fig.update_xaxes(visible=False); fig.update_yaxes(visible=False)
return fig
idxs = [m.vocab[n] for n in valid]
sub = m.E[idxs]
sim = sub @ sub.T
fig = go.Figure(go.Heatmap(
z=sim, x=valid, y=valid,
colorscale="Viridis", zmin=-0.2, zmax=1.0,
colorbar=dict(title="cos"),
hovertemplate="%{y} <> %{x}
cos = %{z:.3f}
group: " + fg + "
Chef-facing operators over three sibling ingredient embeddings (Cooc / Core / Chem) from arXiv:2605.22391. 1,790 canonical ingredients across 7 languages, 300-D Metapath2Vec embeddings, controlled chemistry-vs-recipe-context spectrum.
""" ) sibling = gr.Radio(choices=["cooc","core","chem"], value="chem", label="Sibling embedding") sibling_help = gr.Markdown(SIBLING_BLURBS["chem"]) sibling.change(lambda s: SIBLING_BLURBS[s], inputs=sibling, outputs=sibling_help) # Shared state for cross-tab routing (e.g. Parse fridge -> Basket) shared_basket = gr.State([]) # ---------- Tab 1: Basket pairings + heatmap ---------- with gr.Tab("Basket pairings"): gr.Markdown( "Pick one or more ingredients. Tool averages their unit vectors and returns nearest neighbours " "plus closest modes of that centroid. The heatmap shows whether the basket is coherent " "(bright off-diagonals) or scattered." ) basket = gr.Dropdown( choices=ALL_INGREDIENTS, value=["chicken","lemon","garlic"], label="Ingredient basket (pick 1+)", multiselect=True, max_choices=10, ) k_pair = gr.Slider(1, 15, value=8, step=1, label="K") pair_btn = gr.Button("Find pairings", variant="primary") with gr.Row(): nb_table = gr.Dataframe(headers=["Neighbour","Cosine"], label="Top-K nearest neighbours", interactive=False) mode_table = gr.Dataframe(headers=["Mode id","Label","Kind","Cosine"], label="Closest modes", interactive=False) heatmap_plot = gr.Plot(value=_INITIAL_HEATMAP, label="Pairwise cosine within the basket") pair_btn.click( basket_pairings, inputs=[sibling, basket, k_pair], outputs=[nb_table, mode_table, heatmap_plot], show_progress="full", ) gr.Examples( examples=[ ["chem", ["chicken","lemon","garlic"], 8], ["core", ["miso","ginger","sesame_oil"], 8], ["chem", ["tomato","basil","mozzarella_cheese"], 8], ["cooc", ["chocolate","strawberry","cream"], 8], ["chem", ["cumin","coriander","turmeric"], 8], ["core", ["soy_sauce","ginger","scallion"], 8], ["chem", ["red_wine","beef","rosemary"], 8], ["core", ["coconut_milk","lemongrass","fish_sauce"], 8], ], inputs=[sibling, basket, k_pair], label="Try one of these baskets", ) # ---------- Tab 2: Supervised SLERP ---------- with gr.Tab("Supervised SLERP"): gr.Markdown( "Rotate the seed basket toward one or more supervised direction poles (cuisine, food group, " "NOVA, sensory, USDA macros). Multiple directions are summed before rotation." ) sup_basket = gr.Dropdown( choices=ALL_INGREDIENTS, value=["rice"], label="Seed basket (pick 1+)", multiselect=True, max_choices=10, ) sup_dirs = gr.Dropdown( choices=_supervised_choices("chem"), value=["cuisine:South_Asian"], label="Supervised directions (pick 1+; summed)", multiselect=True, max_choices=5, ) sup_theta = gr.Slider(0, 90, value=30, step=5, label="Rotation angle (deg)") sup_k = gr.Slider(1, 15, value=8, step=1, label="K") sup_btn = gr.Button("Rotate", variant="primary") sup_table = gr.Dataframe(headers=["Ingredient","Cosine"], label="Top-K rotated-query neighbours") sup_btn.click(supervised_slerp_multi, inputs=[sibling, sup_basket, sup_dirs, sup_theta, sup_k], outputs=sup_table, show_progress="full") sibling.change(lambda s: gr.Dropdown(choices=_supervised_choices(s), value=[]), inputs=sibling, outputs=sup_dirs) gr.Examples( examples=[ ["chem", ["rice"], ["cuisine:South_Asian"], 30, 8], ["chem", ["corn"], ["cuisine:Latin_American"], 30, 8], ["core", ["chicken"], ["cuisine:Mediterranean"], 45, 8], ["core", ["tomato","basil"], ["cuisine:Southeast_Asian"], 45, 8], ["chem", ["beef"], ["cuisine:East_Asian"], 60, 8], ["cooc", ["chocolate"], ["cuisine:Latin_American"], 30, 8], ], inputs=[sibling, sup_basket, sup_dirs, sup_theta, sup_k], label="Try one of these rotations", ) # ---------- Tab 3: Emergent SLERP ---------- with gr.Tab("Emergent SLERP"): gr.Markdown( "Rotate the seed basket toward one or more emergent factor-mode poles discovered " "by multi-seed-stable FastICA + GMM." ) em_basket = gr.Dropdown( choices=ALL_INGREDIENTS, value=["chocolate"], label="Seed basket (pick 1+)", multiselect=True, max_choices=10, ) factor_opts = _factor_mode_choices("chem") em_modes = gr.Dropdown( choices=[label for label, _ in factor_opts], value=[factor_opts[0][0]] if factor_opts else [], label="Factor modes (pick 1+; summed)", multiselect=True, max_choices=5, ) em_theta = gr.Slider(0, 90, value=30, step=5, label="Rotation angle (deg)") em_k = gr.Slider(1, 15, value=8, step=1, label="K") em_btn = gr.Button("Rotate", variant="primary") em_table = gr.Dataframe(headers=["Ingredient","Cosine"], label="Top-K rotated-query neighbours") em_btn.click(emergent_slerp_multi, inputs=[sibling, em_basket, em_modes, em_theta, em_k], outputs=em_table, show_progress="full") sibling.change(lambda s: gr.Dropdown(choices=[label for label, _ in _factor_mode_choices(s)], value=[]), inputs=sibling, outputs=em_modes) # ---------- Tab 4: Arithmetic ---------- with gr.Tab("Arithmetic"): gr.Markdown( "Mikolov-style vector arithmetic: `centroid(positives) - centroid(negatives)`, " "then top-K nearest neighbours. The killer demo is `miso - salt` on Core." ) pos_box = gr.Dropdown(choices=ALL_INGREDIENTS, value=["miso"], label="Positives", multiselect=True, max_choices=10) neg_box = gr.Dropdown(choices=ALL_INGREDIENTS, value=["salt"], label="Negatives", multiselect=True, max_choices=10) ar_k = gr.Slider(1, 15, value=8, step=1, label="K") ar_btn = gr.Button("Compute", variant="primary") ar_table = gr.Dataframe(headers=["Ingredient","Cosine"], label="Top-K nearest to result vector") ar_btn.click(arithmetic, inputs=[sibling, pos_box, neg_box, ar_k], outputs=ar_table, show_progress="full") gr.Examples( examples=[ ["core", ["miso"], ["salt"], 8], ["core", ["chicken","tofu"], ["beef"], 8], ["cooc", ["basil","cumin"], ["parsley"], 8], ["chem", ["chocolate"], ["sugar"], 8], ["chem", ["wine"], ["beer"], 8], ["core", ["bread"], ["flour"], 8], ["core", ["coffee"], ["milk"], 8], ["chem", ["mozzarella_cheese"], ["milk"], 8], ], inputs=[sibling, pos_box, neg_box, ar_k], label="Try one of these arithmetic queries", ) # ---------- Tab 5: Mode atlas ---------- with gr.Tab("Mode atlas"): gr.Markdown( "Browse the GMM mode atlas of the selected sibling. Cooc 150 modes / Core 193 / Chem 200. " "`factor` = emergent FastICA modes; `continuous` = quartile partitions of NOVA/sensory/USDA; " "`binary` = food-group buckets." ) atlas_kind = gr.Radio(choices=["all","factor","continuous","binary"], value="all", label="Mode kind") atlas_search = gr.Textbox(label="Search labels / properties", placeholder="e.g. South Asian, baking, fiber", value="") atlas_btn = gr.Button("Browse modes", variant="primary") atlas_table = gr.Dataframe( headers=["mode_id","kind","property","label","n_members","top members"], label="Modes (sorted by kind, then size descending)", wrap=True, interactive=False, ) atlas_btn.click(browse_modes, inputs=[sibling, atlas_kind, atlas_search], outputs=atlas_table, show_progress="full") # ---------- Tab 6: Compare siblings ---------- with gr.Tab("Compare siblings"): gr.Markdown( "Same query, three siblings, side by side. The spectrum-of-models thesis visible in one screen." ) cmp_basket = gr.Dropdown(choices=ALL_INGREDIENTS, value=["chicken"], label="Seed basket", multiselect=True, max_choices=10) cmp_dirs = gr.Dropdown( choices=_supervised_choices("chem"), value=[], label="Optional directions (leave empty for pure pairings)", multiselect=True, max_choices=5, ) cmp_theta = gr.Slider(0, 90, value=30, step=5, label="Rotation angle (deg)") cmp_k = gr.Slider(1, 15, value=8, step=1, label="K") cmp_btn = gr.Button("Compare across siblings", variant="primary") with gr.Row(): cmp_cooc = gr.Dataframe(headers=["Cooc neighbour","Cosine"], label="Cooc (recipe-context)") cmp_core = gr.Dataframe(headers=["Core neighbour","Cosine"], label="Core (blended)") cmp_chem = gr.Dataframe(headers=["Chem neighbour","Cosine"], label="Chem (chemistry)") cmp_btn.click(compare_siblings, inputs=[cmp_basket, cmp_dirs, cmp_theta, cmp_k], outputs=[cmp_cooc, cmp_core, cmp_chem], show_progress="full") gr.Examples( examples=[ [["chicken"], [], 0, 8], [["basil"], [], 0, 8], [["miso"], [], 0, 8], [["rice"], ["cuisine:South_Asian"], 30, 8], [["corn"], ["cuisine:Latin_American"], 30, 8], [["chicken","onion"], ["cuisine:Mediterranean"], 45, 8], ], inputs=[cmp_basket, cmp_dirs, cmp_theta, cmp_k], label="Try one of these side-by-side comparisons", ) # ---------- Tab 7: UMAP visualisation ---------- with gr.Tab("UMAP visualisation"): gr.Markdown( "2-D UMAP projection of the 1,790-ingredient embedding (cosine metric, n_neighbors=30, min_dist=0.03 " "-- paper Figure 1 hyperparameters). Points coloured by food group. Add ingredients to the basket " "to highlight them as red stars; their nearest neighbours appear as orange circles. " "Toggle 3D for a perspective view (third axis is PC1 of the embedding)." ) with gr.Row(): umap_basket = gr.Dropdown( choices=ALL_INGREDIENTS, value=["chicken","lemon","garlic"], label="Highlight these ingredients", multiselect=True, max_choices=10, ) with gr.Row(): umap_show_nb = gr.Checkbox(value=True, label="Show top-K neighbours of basket centroid") umap_3d = gr.Checkbox(value=False, label="3-D perspective (UMAP + PC1)") umap_k = gr.Slider(1, 20, value=10, step=1, label="K neighbours") umap_btn = gr.Button("Update plot", variant="primary") umap_plot = gr.Plot(value=_INITIAL_UMAP, label="UMAP") umap_btn.click(umap_view, inputs=[sibling, umap_basket, umap_show_nb, umap_k, umap_3d], outputs=umap_plot, show_progress="full") # Auto-refresh on sibling change sibling.change(umap_view, inputs=[sibling, umap_basket, umap_show_nb, umap_k, umap_3d], outputs=umap_plot) gr.Markdown("*Tip: scroll-zoom and box-zoom are enabled. Double-click to reset. Click a legend item to hide that food group.*") # ---------- Tab 8: Parse my fridge ---------- with gr.Tab("Parse my fridge"): gr.Markdown( "Paste a free-text ingredient list. Tool strips quantities and prep notes, then fuzzy-matches " "each line to canonical vocab. Hit **Send to Basket** to route the matched set into the Basket-pairings tab." ) fridge_text = gr.Textbox( label="Free-text ingredients (one per line or semicolon-separated)", lines=8, value=( "2 boneless chicken thighs\n" "1 cup coconut milk\n" "1 tbsp fish sauce (or soy sauce)\n" "fresh lemongrass, bruised\n" "3 cloves garlic, minced\n" "1 inch fresh ginger\n" "juice of one lime\n" "salt to taste" ), ) fridge_min = gr.Slider(40, 100, value=70, step=5, label="Min match score (rapidfuzz)") with gr.Row(): fridge_btn = gr.Button("Parse and match", variant="primary") fridge_send = gr.Button("Send matched to Basket tab", variant="secondary") fridge_table = gr.Dataframe( headers=["Input line", "Canonical match", "Score", "Cleaned"], label="Parsed matches", interactive=False, ) fridge_matched = gr.Textbox(label="Matched ingredients", interactive=False) def _parse(txt, sib, mn): rows, matches = parse_fridge(txt, sib, int(mn)) return rows, ", ".join(matches), matches fridge_btn.click( _parse, inputs=[fridge_text, sibling, fridge_min], outputs=[fridge_table, fridge_matched, shared_basket], show_progress="full", ) def _send_to_basket(matches): return gr.Dropdown(value=matches[:10] if matches else []) fridge_send.click(_send_to_basket, inputs=[shared_basket], outputs=[basket]) gr.Markdown( """--- **Cite:** Radzikowski and Chen, 2026, *Epicure: Navigating the Emergent Geometry of Food Ingredient Embeddings*, [arXiv:2605.22391](https://arxiv.org/abs/2605.22391). Artefacts: [epicure-cooc](https://huggingface.co/Kaikaku/epicure-cooc) | [epicure-core](https://huggingface.co/Kaikaku/epicure-core) | [epicure-chem](https://huggingface.co/Kaikaku/epicure-chem) | [corpus dataset](https://huggingface.co/datasets/Kaikaku/epicure-corpus-resources) """ ) if __name__ == "__main__": demo.launch()