"""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}", )) fig.update_layout( title=dict(text="Pairwise cosine within the basket", font=dict(size=14)), height=420, margin=dict(l=80, r=20, t=50, b=80), paper_bgcolor="#ffffff", plot_bgcolor="#ffffff", ) return fig def supervised_slerp_multi(sibling, basket, directions, theta, k): m = MODELS[sibling] v = _basket_centroid(m, basket) if v is None: return [] d = _stack_directions(m, directions, use_factor_pole=False) if d is None: return [[n, f"{s:.4f}"] for n, s in _topk(m, v, k, basket)] q = _slerp(v, d, theta) return [[n, f"{s:.4f}"] for n, s in _topk(m, q, k, basket)] def emergent_slerp_multi(sibling, basket, mode_labels, theta, k): 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) if v is None: return [] d = _stack_directions(m, mode_ids, use_factor_pole=True) if d is None: return [[n, f"{s:.4f}"] for n, s in _topk(m, v, k, basket)] q = _slerp(v, d, theta) return [[n, f"{s:.4f}"] for n, s in _topk(m, q, k, basket)] def arithmetic(sibling, positives, negatives, k): 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 [[n, f"{s:.4f}"] for n, s in _topk(m, q, k, (positives or []) + (negatives or []))] def browse_modes(sibling, kind_filter, query): 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, directions, theta, k): out = [] for sib in ["cooc","core","chem"]: m = MODELS[sib] v = _basket_centroid(m, basket) if v is None: out.append([]); continue 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(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([[n, f"{s:.4f}"] for n, s in hits]) return out[0], out[1], out[2] def _umap_coords(sibling, three_d): """Lift the 2D UMAP into 3D by appending the embedding's third principal axis if requested.""" base = UMAP[sibling] # (1790, 2) if not three_d: return base, None # Compute a third dim via simple PCA on the underlying embedding m = MODELS[sibling] E = m.E - m.E.mean(axis=0, keepdims=True) # First three PCs U, S, Vt = np.linalg.svd(E, full_matrices=False) pc1 = (E @ Vt[0]); pc1 = (pc1 - pc1.mean()) / (pc1.std() + 1e-9) # Combine base 2D with pc1 scaled to the same range scale = (base.max() - base.min()) * 0.25 z = pc1 * scale return base, z.astype(np.float32) def umap_view(sibling, basket, show_neighbours, k, three_d=False): coords2, z = _umap_coords(sibling, three_d) m = MODELS[sibling] name_to_idx = m.vocab by_group = {} for i, fg in enumerate(FOOD_GROUPS): by_group.setdefault(fg, []).append(i) order = ["Other"] + [g for g in FG_COLORS if g != "Other"] fig = go.Figure() def add_scatter(name, idxs, marker, text, hover, mode="markers"): if three_d: fig.add_trace(go.Scatter3d( x=coords2[idxs,0], y=coords2[idxs,1], z=z[idxs], mode=mode, name=name, marker=marker, text=text, hovertemplate=hover, textfont=dict(size=10), )) else: fig.add_trace(go.Scatter( x=coords2[idxs,0], y=coords2[idxs,1], mode=mode, name=name, marker=marker, text=text, hovertemplate=hover, textfont=dict(size=10), )) for fg in order: if fg not in by_group: continue idxs = by_group[fg] marker = dict( size=4 if not three_d else 3, color=FG_COLORS.get(fg, "#888888"), opacity=0.35 if fg == "Other" else 0.7, line=dict(width=0), ) add_scatter(fg, idxs, marker, [NAMES_BY_IDX[i] for i in idxs], "%{text}
group: " + fg + "") if basket: bi = [name_to_idx[b] for b in basket if b in name_to_idx] if bi: marker = dict( size=16 if not three_d else 8, color="#e30613", symbol="star" if not three_d else "diamond", line=dict(color="white", width=2), ) add_scatter("Basket", bi, marker, [NAMES_BY_IDX[i] for i in bi], "%{text}", mode="markers+text") if show_neighbours: centroid = _basket_centroid(m, basket) if centroid is not None: nb_pairs = _topk(m, centroid, k=int(k), exclude=basket) nb_idxs = [name_to_idx[n] for n, _ in nb_pairs if n in name_to_idx] if nb_idxs: marker = dict( size=10 if not three_d else 6, color="#ff8800", symbol="circle", line=dict(color="white", width=1), ) add_scatter(f"Top-{k} neighbours", nb_idxs, marker, [NAMES_BY_IDX[i] for i in nb_idxs], "%{text} (neighbour)", mode="markers+text") title_suffix = " (3D, PCA z-axis)" if three_d else "" fig.update_layout( title=dict(text=f"UMAP of Epicure-{sibling.capitalize()}{title_suffix}", font=dict(size=15)), height=650, legend=dict(orientation="v", x=1.02, y=1, font=dict(size=11), bgcolor="rgba(255,255,255,0.8)"), margin=dict(l=40, r=160, t=60, b=40), paper_bgcolor="#ffffff", plot_bgcolor="#ffffff", ) if not three_d: fig.update_xaxes(showgrid=True, gridcolor="#eee", zeroline=False, title="UMAP 1") fig.update_yaxes(showgrid=True, gridcolor="#eee", zeroline=False, title="UMAP 2") else: fig.update_layout(scene=dict( xaxis_title="UMAP 1", yaxis_title="UMAP 2", zaxis_title="PC1 (z)", bgcolor="#ffffff", )) return fig # ===== fridge parser ===== _LINE_SPLIT = re.compile(r"[\n;]") _BRACKET = re.compile(r"\([^)]*\)") _QTY = (r"(?:\d+(?:[\.,/]\d+)?|" r"a|an|one|two|three|four|five|six|seven|eight|nine|ten|half|quarter)") _UNIT = (r"(?:cups?|tbsp\.?|tablespoons?|tsp\.?|teaspoons?|" r"oz\.?|ounces?|lbs?\.?|pounds?|grams?|kgs?|kilos?|" r"ml|liters?|litres?|cloves?|bunches?|sprigs?|pinch(?:es)?|" r"slices?|pieces?|cans?|packets?|sticks?|leaves?|stalks?|heads?|inch(?:es)?|" r"splash(?:es)?|dash(?:es)?|drops?|handfuls?|large|small|medium)") _LEADING_QTY = re.compile(rf"^\s*{_QTY}\s+(?:{_UNIT}\b\s*)?(?:of\s+)?", re.IGNORECASE) _LEADING_UNIT_ONLY = re.compile(rf"^\s*{_UNIT}\b\s*(?:of\s+)?", re.IGNORECASE) _JUICE_OF = re.compile(rf"^\s*(?:juice|zest)\s+(?:of\s+)?(?:{_QTY}\s+)?", re.IGNORECASE) _LEADING_PREP = re.compile( r"^\s*(?:fresh|dried|cooked|frozen|raw|ripe|firm|boneless|skinless|smoked|low[- ]fat)\s+", re.IGNORECASE, ) _TRAILING_PREP = re.compile( r"\s*,\s*(?:chopped|minced|diced|sliced|grated|crushed|whole|ground|peeled|" r"to taste|optional|finely|coarsely|cubed|shredded|julienned|halved|quartered|warmed|" r"toasted|roasted|bruised|melted|softened|cooked|drained|rinsed|patted dry|trimmed|" r"deveined|seeded|stemmed|crumbled).*$", re.IGNORECASE, ) _KNOWN_PLURALS = { "tortillas":"tortilla","thighs":"thigh","leaves":"leaf","onions":"onion", "potatoes":"potato","tomatoes":"tomato","cloves":"clove", } def _clean_line(line): s = line.strip().lower() s = _BRACKET.sub(" ", s) if "juice" in s or "zest" in s: s = _JUICE_OF.sub("", s) s = _TRAILING_PREP.sub("", s) s = _LEADING_QTY.sub("", s) s = _LEADING_UNIT_ONLY.sub("", s) s = _LEADING_PREP.sub("", s) s = _LEADING_PREP.sub("", s) tokens = [_KNOWN_PLURALS.get(t, t) for t in s.split()] s = " ".join(tokens) return re.sub(r"\s+", " ", s).strip() def _fuzzy_lookup(cleaned, vocab, vocab_sp, min_score): if not cleaned: return None, 0.0 candidates = [] for scorer in (fuzz_scorers.token_set_ratio, fuzz_scorers.WRatio, fuzz_scorers.partial_ratio): hits = fuzz_process.extract(cleaned, vocab_sp, scorer=scorer, score_cutoff=min_score, limit=10) for _name_sp, score, idx in hits: candidates.append((vocab[idx], float(score))) if not candidates: return None, 0.0 cleaned_tokens = set(cleaned.split()) def rank_key(c): name, score = c nt = set(name.replace("_"," ").split()) return (-score, 0 if nt.issubset(cleaned_tokens) else 1, -len(name)) candidates.sort(key=rank_key) return candidates[0] def parse_fridge(raw_text, sibling, min_score=70): if not raw_text or not raw_text.strip(): return [], [] vocab = list(MODELS[sibling].vocab.keys()) vocab_sp = [v.replace("_"," ") for v in vocab] rows, matched = [], [] for line in _LINE_SPLIT.split(raw_text): if not line.strip(): continue cleaned = _clean_line(line) if not cleaned: rows.append([line.strip(), "(empty)", 0.0, ""]); continue match, score = _fuzzy_lookup(cleaned, vocab, vocab_sp, int(min_score)) if match is None: tokens = cleaned.split() if len(tokens) > 1: match, score = _fuzzy_lookup(" ".join(tokens[:-1]), vocab, vocab_sp, int(min_score)) if match is None: rows.append([line.strip(), "(no match)", 0.0, cleaned]); continue rows.append([line.strip(), match, round(score, 1), cleaned]) matched.append(match) seen, dedup = set(), [] for n in matched: if n not in seen: seen.add(n); dedup.append(n) return rows, dedup # ===== UI ===== THEME = gr.themes.Soft( primary_hue="red", secondary_hue="orange", neutral_hue="slate", font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"], ) # Precompute the initial UMAP for the default sibling+basket so the tab is not empty on first open. _INITIAL_UMAP = umap_view("chem", ["chicken","lemon","garlic"], True, 8, three_d=False) _INITIAL_HEATMAP = _basket_heatmap(MODELS["chem"], ["chicken","lemon","garlic"]) with gr.Blocks(title="Epicure Explorer", theme=THEME, css=""" .gradio-container {max-width: 1280px !important;} footer {visibility: hidden;} h1 {margin-bottom: 0.2em;} .subtitle {color: #666; font-size: 0.95em; margin-top: 0;} """) as demo: gr.Markdown( """# Epicure Explorer

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()