"""Epicure Explorer: chef-facing operators over the three sibling embeddings. Six tabs: - Basket pairings: pick 1+ ingredients, get neighbours and closest modes of the basket centroid. - Supervised SLERP: rotate a multi-ingredient seed toward one or more supervised poles. - Emergent SLERP: rotate a seed toward one or more emergent factor-mode poles. - Arithmetic: Mikolov-style 'centroid(positives) - centroid(negatives)' nearest neighbours. - Mode atlas: browse all GMM modes per sibling with kind filter and label search. - Compare siblings: run the same query across cooc/core/chem in three columns. All three siblings (Cooc, Core, Chem) load on startup from public HF model repos. Paper: https://arxiv.org/abs/2605.22391 """ from __future__ import annotations import os import sys import numpy as np import gradio as gr 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 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()) # ===== math helpers ===== def _unit(v: np.ndarray, eps: float = 1e-9) -> np.ndarray: n = np.linalg.norm(v); return v / max(n, eps) def _basket_centroid(m: Epicure, names: list[str]) -> np.ndarray | None: valid = [n for n in (names or []) if n in m.vocab] if not valid: return None idxs = [m.vocab[n] for n in valid] return _unit(m.E[idxs].mean(axis=0)) def _stack_directions(m: Epicure, keys: list[str], use_factor_pole: bool = False) -> np.ndarray | None: 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: Epicure, q: np.ndarray, k: int, exclude: list[str]) -> list[tuple[str, float]]: sims = m.E @ q for name in exclude or []: if name in m.vocab: sims[m.vocab[name]] = -np.inf order = np.argsort(-sims) return [(m.itos[int(i)], float(sims[i])) for i in order[:k]] def _supervised_choices(sibling: str) -> list[str]: return sorted(MODELS[sibling].supervised_poles.keys()) def _factor_mode_choices(sibling: str) -> list[tuple[str, str]]: return [(f"{m.label} ({m.mode_id})", m.mode_id) for m in MODELS[sibling].modes if m.kind == "factor"] def _slerp(m: Epicure, v: np.ndarray, d: np.ndarray, theta_deg: float) -> np.ndarray: d_perp = d - (d @ v) * v n_perp = np.linalg.norm(d_perp) if n_perp < 1e-9: return v d_perp = d_perp / n_perp th = np.deg2rad(float(theta_deg)) return _unit(np.cos(th) * v + np.sin(th) * d_perp) # ===== tab handlers ===== def basket_pairings(sibling: str, basket: list[str], k: int): m = MODELS[sibling] centroid = _basket_centroid(m, basket) if centroid is None: return [], [] nb = _topk(m, centroid, k=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]) return ( [[name, f"{sim:.4f}"] for name, sim in nb], [[mid, label, kind, f"{sim:.4f}"] for mid, label, kind, sim in scored[:k]], ) def supervised_slerp_multi(sibling: str, basket: list[str], directions: list[str], theta: float, k: int): m = MODELS[sibling] v = _basket_centroid(m, basket) d = _stack_directions(m, directions, use_factor_pole=False) if v is None: return [] if d is None: return [[n, f"{s:.4f}"] for n, s in _topk(m, v, k, basket)] q = _slerp(m, v, d, theta) return [[name, f"{sim:.4f}"] for name, sim in _topk(m, q, k, basket)] def emergent_slerp_multi(sibling: str, basket: list[str], mode_labels: list[str], theta: float, k: int): m = MODELS[sibling] label_to_id = {f"{mode.label} ({mode.mode_id})": mode.mode_id for mode in m.modes if mode.kind == "factor"} mode_ids = [label_to_id[lab] for lab in (mode_labels or []) if lab in label_to_id] v = _basket_centroid(m, basket) d = _stack_directions(m, mode_ids, use_factor_pole=True) if v is None: return [] if d is None: return [[n, f"{s:.4f}"] for n, s in _topk(m, v, k, basket)] q = _slerp(m, v, d, theta) return [[name, f"{sim:.4f}"] for name, sim in _topk(m, q, k, basket)] def arithmetic(sibling: str, positives: list[str], negatives: list[str], k: int): m = MODELS[sibling] pos = _basket_centroid(m, positives) if pos is None: return [] neg = _basket_centroid(m, negatives) if negatives else None q = _unit(pos - neg) if neg is not None else pos return [[name, f"{sim:.4f}"] for name, sim in _topk(m, q, k, (positives or []) + (negatives or []))] def browse_modes(sibling: str, kind_filter: str, query: str): m = MODELS[sibling] rows = [] q = (query or "").strip().lower() for mode in m.modes: if kind_filter != "all" and mode.kind != kind_filter: continue if q and q not in mode.label.lower() and q not in mode.property.lower(): continue rows.append([ mode.mode_id, mode.kind, mode.property, mode.label, mode.n_members, ", ".join(mode.members[:12]), ]) rows.sort(key=lambda r: (r[1], -r[4])) return rows def compare_siblings(basket: list[str], directions: list[str], theta: float, k: int): out = [] for sib in ["cooc", "core", "chem"]: m = MODELS[sib] v = _basket_centroid(m, basket) if v is None: out.append([]); continue # Direction set can use any pole key; we intersect with this sibling's supervised_poles valid_dirs = [d for d in (directions or []) if d in m.supervised_poles] if valid_dirs: d_vec = _stack_directions(m, valid_dirs) q = _slerp(m, v, d_vec, theta) if d_vec is not None else v else: q = v hits = _topk(m, q, k=k, exclude=basket) out.append([[name, f"{sim:.4f}"] for name, sim in hits]) return out[0], out[1], out[2] # ===== UI ===== with gr.Blocks(title="Epicure Explorer") as demo: gr.Markdown( """# Epicure Explorer Chef-facing operators over the three Epicure sibling embeddings (Cooc, Core, Chem), from [arXiv:2605.22391](https://arxiv.org/abs/2605.22391). - **Cooc** walks recipe co-occurrence only. Neighbours are recipe companions. - **Core** blends typed FlavorDB compound walks with injected I-I walks. Concentrated geometry, tightest modes. - **Chem** walks typed FlavorDB compound metapaths only. Strongest supervised-direction recovery; neighbours are flavour-profile peers. Pick a sibling, then explore. Each tab has a few worked examples just below the form: click any row to populate the inputs. """ ) sibling = gr.Radio(choices=["cooc", "core", "chem"], value="chem", label="Sibling embedding") # ---------- Tab 1: Basket pairings ---------- with gr.Tab("Basket pairings"): gr.Markdown( "Pick one or more ingredients. The tool averages their unit vectors and returns nearest " "neighbours plus closest modes of that centroid. Useful for 'what should I add to what I have'." ) 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) pair_btn.click(basket_pairings, inputs=[sibling, basket, k_pair], outputs=[nb_table, mode_table]) 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 (possibly multi-ingredient) seed toward one or more supervised direction poles. " "Multiple directions are summed and L2-normalised before rotation, matching the paper's " "multi-constraint queries (e.g. 'chicken + processed + Western_Atlantic')." ) 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 before rotation)", 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) 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. Stack mode targets to combine culinary axes." ) 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 before rotation)", 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) 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( "Classic Mikolov-style vector arithmetic: `centroid(positives) - centroid(negatives)`, " "then top-K nearest neighbours. The killer demo is `miso - salt` on Core (returns the " "Japanese fermented-umami pantry minus the salty component): mirin, kombu, wakame, sake, dashi." ) pos_box = gr.Dropdown( choices=ALL_INGREDIENTS, value=["miso"], label="Positives (added)", multiselect=True, max_choices=10, ) neg_box = gr.Dropdown( choices=ALL_INGREDIENTS, value=["salt"], label="Negatives (subtracted)", 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) 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 browser ---------- with gr.Tab("Mode atlas"): gr.Markdown( "Browse the GMM mode atlas of the selected sibling. Cooc has 150 modes across 41 properties; " "Core 193 / 44; Chem 200 / 43. `factor` modes are the emergent FastICA factor poles; " "`continuous` modes are quartile partitions of NOVA / sensory / USDA scores; " "`binary` modes are food-group buckets. Search by label or property substring." ) 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) # ---------- Tab 6: Compare siblings ---------- with gr.Tab("Compare siblings"): gr.Markdown( "Run the same query across all three siblings in one shot. This is the spectrum-of-models " "view the paper is built around: Cooc shows recipe companions, Chem shows chemistry peers, " "Core sits in between. Leave the direction empty for pure basket pairings." ) cmp_basket = gr.Dropdown( choices=ALL_INGREDIENTS, value=["chicken"], label="Seed basket (pick 1+)", multiselect=True, max_choices=10, ) cmp_dirs = gr.Dropdown( choices=_supervised_choices("chem"), value=[], label="Optional: supervised 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; ignored if no directions)") 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], ) 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", ) 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). Models: [epicure-cooc](https://huggingface.co/Kaikaku/epicure-cooc) | [epicure-core](https://huggingface.co/Kaikaku/epicure-core) | [epicure-chem](https://huggingface.co/Kaikaku/epicure-chem). Dataset: [epicure-corpus-resources](https://huggingface.co/datasets/Kaikaku/epicure-corpus-resources). """ ) if __name__ == "__main__": demo.launch()