Spaces:
Running
Running
| """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() | |