josefchen commited on
Commit
91c562d
·
verified ·
1 Parent(s): 158edce

Publish the powered FlavourBench evidence explorer

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ data-powered/flavourbench-powered-space.json filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,41 +1,34 @@
1
  ---
2
  title: FlavourBench
3
- emoji: 🥘
4
  colorFrom: blue
5
- colorTo: yellow
6
  sdk: gradio
7
  sdk_version: 6.9.0
8
  app_file: app.py
9
  pinned: false
10
  license: other
11
- short_description: The executable culinary benchmark for frontier LLMs.
 
12
  ---
13
 
14
- # FlavourBench: Executable Culinary Evaluation of Frontier Language Models
15
 
16
- The executable culinary benchmark and evidence explorer for 20 frontier language-model endpoints,
17
- scored against answer keys computed before evaluation.
 
18
 
19
- [Paper](https://github.com/josefchen/flavourbench/blob/main/paper/build/flavourbench.pdf) ·
20
- [Dataset](https://huggingface.co/datasets/josefchen/flavourbench) ·
21
- [Source and reproduction](https://github.com/josefchen/flavourbench)
22
 
23
- The Space presents the complete 20-model, 32-task public release. Its primary table contains only
24
- FlavourBench Score, correct count, Wilson 95% interval, and parsed-answer count. Equal scores share
25
- a score rank. Epicure-assisted results remain available in the model and pair views as a secondary
26
- execution diagnostic. The Space does not call models or Epicure at runtime; the checked-in release
27
- JSON is the sole data source.
28
 
29
- ## Local launch
30
-
31
- ```bash
32
- pip install -r requirements.txt
33
- python app.py
34
- ```
35
-
36
- For a Hugging Face deployment, upload the contents of this directory as the Space repository. The
37
- release file under `data/` keeps the first launch self-contained. A later revision can load the
38
- same content-addressed table configs from the companion dataset repository.
39
 
 
40
  See the repository [rights boundary](https://github.com/josefchen/flavourbench/blob/main/LICENSES.md)
41
- before publishing or mirroring records.
 
1
  ---
2
  title: FlavourBench
3
+ emoji: 🍲
4
  colorFrom: blue
5
+ colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.9.0
8
  app_file: app.py
9
  pinned: false
10
  license: other
11
+ datasets:
12
+ - josefchen/flavourbench
13
  ---
14
 
15
+ # FlavourBench
16
 
17
+ An interactive explorer for the powered FlavourBench release: 20 frontier endpoints, 640
18
+ executable culinary decisions, 12,800 primary responses, 1,280 label-permuted repeats, and all 190
19
+ paired statistical comparisons.
20
 
21
+ The Space reads one content-addressed bundle and makes no provider calls. Use it to inspect:
 
 
22
 
23
+ - the FlavourBench Score, simultaneous 95% intervals, and statistical rank groups;
24
+ - family-level model profiles and label-permutation repeatability;
25
+ - exact prompts, candidate sets, frozen 56-portfolio score maps, and real model responses; and
26
+ - any paired model contrast with its bootstrap interval and Holm-adjusted result.
 
27
 
28
+ [Paper](https://github.com/josefchen/flavourbench/blob/main/paper/build/flavourbench.pdf) |
29
+ [Dataset](https://huggingface.co/datasets/josefchen/flavourbench) |
30
+ [Source](https://github.com/josefchen/flavourbench)
 
 
 
 
 
 
 
31
 
32
+ The dataset contains full response records. The Space carries compact excerpts for fast browsing.
33
  See the repository [rights boundary](https://github.com/josefchen/flavourbench/blob/main/LICENSES.md)
34
+ for component-level licensing.
app.py CHANGED
@@ -1,5 +1,6 @@
1
  from __future__ import annotations
2
 
 
3
  import html
4
  import json
5
  import os
@@ -10,36 +11,30 @@ import gradio as gr
10
  import pandas as pd
11
 
12
  HERE = Path(__file__).resolve().parent
13
- RELEASE_PATH = Path(
14
- os.environ.get("FLAVOURBENCH_RELEASE", HERE / "data" / "epicure-native-release.json")
 
 
 
15
  )
16
 
17
- BRAND = {
18
- "blue": "#1769AA",
19
- "gold": "#E6A11A",
20
- "teal": "#168C7A",
21
- "red": "#C75450",
22
- "charcoal": "#262B33",
23
- "paper": "#F7F8FA",
24
- }
25
 
26
  CSS = """
27
  :root {
28
- --fb-blue: #1769AA;
29
- --fb-blue-soft: #EAF3FA;
30
- --fb-gold: #E6A11A;
31
- --fb-teal: #168C7A;
32
- --fb-red: #C75450;
33
  --fb-ink: #262B33;
34
- --fb-muted: #66707D;
35
- --fb-paper: #F7F8FA;
36
  --fb-panel: #FFFFFF;
37
- --fb-rule: #DDE3EA;
38
  --fb-code: #EEF2F5;
39
  }
40
  .dark {
41
- --fb-blue-soft: #112B3E;
42
- --fb-ink: #ECF1F5;
43
  --fb-muted: #A7B2BC;
44
  --fb-paper: #11171D;
45
  --fb-panel: #182129;
@@ -49,194 +44,200 @@ CSS = """
49
  body, .gradio-container {
50
  background: var(--fb-paper) !important;
51
  color: var(--fb-ink) !important;
52
- font-family: "Geist", "Inter", "Avenir Next", system-ui, sans-serif !important;
 
 
 
 
 
 
 
 
 
53
  }
54
- .gradio-container { max-width: 1440px !important; }
55
- .fb-shell { max-width: 1320px; margin: 0 auto; }
56
  .fb-kicker {
57
- color: var(--fb-blue);
58
  font-family: "IBM Plex Mono", ui-monospace, monospace;
59
  font-size: 12px;
60
  font-weight: 700;
61
  letter-spacing: .12em;
62
  text-transform: uppercase;
63
  }
64
- .fb-hero {
65
- display: grid;
66
- grid-template-columns: minmax(0, 1.05fr) minmax(420px, .95fr);
67
- gap: 42px;
68
- padding: 54px 8px 38px;
69
- border-bottom: 1px solid var(--fb-rule);
70
- }
71
  .fb-hero h1 {
72
  color: var(--fb-ink);
73
- font-size: clamp(52px, 7vw, 92px);
74
- letter-spacing: -.065em;
75
- line-height: .92;
76
- margin: 14px 0 24px;
 
77
  }
78
  .fb-dek {
79
  color: var(--fb-muted);
80
- font-size: 20px;
81
- line-height: 1.5;
82
  margin: 0;
83
- max-width: 720px;
84
  }
85
- .fb-dek strong { color: var(--fb-ink); font-weight: 650; }
86
  .fb-stats {
87
- display: flex;
88
- flex-wrap: wrap;
89
- gap: 18px 30px;
90
- margin-top: 34px;
91
  }
92
- .fb-stat { min-width: 118px; }
93
  .fb-stat strong {
94
  color: var(--fb-ink);
95
  display: block;
96
- font-size: 29px;
97
  letter-spacing: -.04em;
98
  line-height: 1;
99
  }
100
  .fb-stat span {
101
  color: var(--fb-muted);
102
  display: block;
103
- font-size: 12px;
104
  margin-top: 7px;
105
  text-transform: uppercase;
106
- letter-spacing: .08em;
107
  }
108
  .fb-frontier {
109
  align-self: end;
110
  background: var(--fb-panel);
111
  border: 1px solid var(--fb-rule);
112
- border-top: 4px solid var(--fb-blue);
113
- padding: 22px 22px 17px;
 
114
  }
115
  .fb-frontier-head {
116
  align-items: baseline;
117
  display: flex;
118
  justify-content: space-between;
119
- margin-bottom: 16px;
120
  }
121
  .fb-frontier-head strong { font-size: 14px; }
122
  .fb-frontier-head span { color: var(--fb-muted); font-size: 12px; }
123
- .fb-rail-row {
124
  align-items: center;
125
  display: grid;
126
- grid-template-columns: 116px 1fr 34px;
127
  gap: 10px;
128
- margin: 10px 0;
129
  }
130
- .fb-rail-label {
131
  color: var(--fb-ink);
132
  font-size: 12px;
133
  overflow: hidden;
134
  text-overflow: ellipsis;
135
  white-space: nowrap;
136
  }
137
- .fb-rail {
138
- background: var(--fb-code);
139
- height: 12px;
140
- position: relative;
141
- }
142
- .fb-rail-base {
143
- background: var(--fb-blue);
144
- bottom: 0;
145
- height: 7px;
146
  left: 0;
147
  position: absolute;
 
 
148
  }
149
- .fb-rail-ci {
150
- border-left: 1px solid var(--fb-muted);
151
- border-right: 1px solid var(--fb-muted);
152
  border-top: 2px solid var(--fb-muted);
153
- height: 4px;
154
  position: absolute;
155
- top: 0;
156
  }
157
- .fb-rail-score {
158
- color: var(--fb-muted);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  font-family: "IBM Plex Mono", ui-monospace, monospace;
160
  font-size: 11px;
161
  text-align: right;
162
  }
163
- .fb-note {
164
- border-left: 3px solid var(--fb-gold);
165
  color: var(--fb-muted);
166
- font-size: 13px;
167
- line-height: 1.45;
168
- margin-top: 17px;
169
- padding-left: 12px;
170
  }
171
- .fb-section-title { margin: 30px 0 6px; }
172
- .fb-section-title h2 {
173
  color: var(--fb-ink);
174
- font-size: 31px;
175
  letter-spacing: -.035em;
176
- margin: 5px 0 2px;
177
  }
178
- .fb-section-title p { color: var(--fb-muted); margin: 0; }
179
- .fb-pair-status {
180
- align-items: stretch;
181
  display: grid;
182
- grid-template-columns: 1fr 1fr;
183
  gap: 12px;
184
  margin: 8px 0 16px;
185
  }
186
- .fb-condition {
187
  background: var(--fb-panel);
188
  border: 1px solid var(--fb-rule);
 
189
  padding: 16px;
190
  }
191
- .fb-condition.good { border-top: 4px solid var(--fb-teal); }
192
- .fb-condition.bad { border-top: 4px solid var(--fb-red); }
193
- .fb-condition.unknown { border-top: 4px solid var(--fb-muted); }
194
- .fb-condition small {
195
  color: var(--fb-muted);
196
  display: block;
197
  font-size: 11px;
198
- letter-spacing: .08em;
199
  text-transform: uppercase;
200
  }
201
- .fb-condition strong { display: block; font-size: 22px; margin: 7px 0 4px; }
202
- .fb-condition code, .fb-hash {
 
 
 
 
 
 
 
 
203
  background: var(--fb-code);
204
  color: var(--fb-muted);
205
  font-family: "IBM Plex Mono", ui-monospace, monospace;
206
  font-size: 11px;
207
  }
208
- .fb-method-grid {
209
  display: grid;
210
- grid-template-columns: 1.25fr .75fr;
211
  gap: 28px;
212
  }
213
- .fb-callout {
214
- background: var(--fb-blue-soft);
215
- border-left: 4px solid var(--fb-blue);
216
- padding: 20px;
217
- }
218
- .fb-callout h3 { margin-top: 0; }
219
  .fb-footer {
220
  border-top: 1px solid var(--fb-rule);
221
  color: var(--fb-muted);
222
  font-size: 12px;
223
- margin-top: 40px;
224
- padding: 20px 8px 36px;
225
  }
226
- .tabs { border-bottom: 1px solid var(--fb-rule) !important; }
227
- .tab-nav button { font-weight: 600 !important; }
228
- .tab-nav button.selected { color: var(--fb-blue) !important; }
229
- button.primary { background: var(--fb-blue) !important; border-color: var(--fb-blue) !important; }
230
  .gradio-dataframe, .block { border-radius: 8px !important; }
231
- @media (max-width: 920px) {
232
- .fb-hero, .fb-method-grid { grid-template-columns: 1fr; }
233
  .fb-hero { gap: 28px; padding-top: 34px; }
234
- .fb-frontier { min-width: 0; }
235
  }
236
- @media (max-width: 620px) {
237
- .fb-hero h1 { font-size: 52px; }
238
- .fb-pair-status { grid-template-columns: 1fr; }
239
- .fb-rail-row { grid-template-columns: 92px 1fr 30px; }
240
  }
241
  @media (prefers-reduced-motion: reduce) {
242
  *, *::before, *::after { animation: none !important; transition: none !important; }
@@ -244,93 +245,116 @@ button.primary { background: var(--fb-blue) !important; border-color: var(--fb-b
244
  """
245
 
246
 
247
- def _load_release() -> dict[str, Any]:
248
- if not RELEASE_PATH.is_file():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  raise FileNotFoundError(
250
- f"Release not found at {RELEASE_PATH}. Set FLAVOURBENCH_RELEASE to its path."
251
  )
252
- return json.loads(RELEASE_PATH.read_text(encoding="utf-8"))
 
 
 
 
 
 
 
 
 
253
 
254
 
255
- RELEASE = _load_release()
256
- MODELS = RELEASE["leaderboard"]["models"]
257
- TASKS = RELEASE["tasks"]
258
- MODEL_BY_NAME = {model["display_name"]: model for model in MODELS}
259
- TASK_BY_ID = {task["task_id"]: task for task in TASKS}
260
- OBSERVATION_INDEX = {
261
- (row["model_id"], row["task_id"], row["condition"]): row for row in RELEASE["observations"]
 
 
262
  }
 
 
 
263
 
264
 
265
- def _score_display_models() -> list[dict[str, Any]]:
266
- return sorted(
267
- MODELS,
268
- key=lambda model: (
269
- -float(model["conditions"]["epicure_off"]["accuracy_percent"]),
270
- str(model["display_name"]).casefold(),
271
- ),
272
- )
273
 
274
 
275
- DISPLAY_MODELS = _score_display_models()
276
- SCORE_RANKS: dict[str, int] = {}
277
- _previous_score: float | None = None
278
- _current_rank = 0
279
- for _position, _model in enumerate(DISPLAY_MODELS, start=1):
280
- _score = float(_model["conditions"]["epicure_off"]["accuracy_percent"])
281
- if _previous_score is None or _score != _previous_score:
282
- _current_rank = _position
283
- _previous_score = _score
284
- SCORE_RANKS[str(_model["model_id"])] = _current_rank
285
 
286
 
287
- def _short_name(display_name: str) -> str:
288
- return display_name.split(": ", 1)[-1].replace(" Pro", "")
 
 
 
 
 
289
 
290
 
291
  def _frontier_html() -> str:
292
  rows = []
293
- for model in DISPLAY_MODELS[:12]:
294
- condition = model["conditions"]["epicure_off"]
295
- score = float(condition["accuracy_percent"])
296
- lower, upper = (100 * float(value) for value in condition["wilson_95"])
 
 
297
  rows.append(
298
- "<div class='fb-rail-row'>"
299
- f"<div class='fb-rail-label' title='{html.escape(model['display_name'])}'>"
300
- f"{html.escape(_short_name(model['display_name']))}</div>"
301
- "<div class='fb-rail'>"
302
- f"<span class='fb-rail-ci' style='left:{lower:.3f}%;width:{upper - lower:.3f}%'></span>"
303
- f"<span class='fb-rail-base' style='width:{score:.3f}%'></span>"
304
  "</div>"
305
- f"<div class='fb-rail-score'>{score:.0f}</div>"
 
306
  "</div>"
307
  )
308
  return "".join(rows)
309
 
310
 
311
  def _hero_html() -> str:
312
- counts = RELEASE["counts"]
313
  return f"""
314
  <div class="fb-shell fb-hero">
315
  <section>
316
- <div class="fb-kicker">The executable culinary benchmark · 20 frontier endpoints</div>
317
- <h1>How well do frontier models reason about flavour?</h1>
318
- <p class="fb-dek">FlavourBench scores 20 current language-model endpoints against answer
319
- keys compiled by Epicure.
320
- <strong>Blue is the FlavourBench Score. Gray shows its Wilson 95% interval.</strong>
321
- Open any pair to inspect the prompt, answers, tool trace, and hashes.</p>
322
  <div class="fb-stats">
323
- <div class="fb-stat"><strong>{counts["models"]}</strong><span>models</span></div>
324
- <div class="fb-stat"><strong>{counts["tasks"]}</strong><span>tasks</span></div>
325
- <div class="fb-stat"><strong>{counts["models"] * counts["tasks"]}</strong><span>score cells</span></div>
326
- <div class="fb-stat"><strong>{counts["families"]}</strong><span>task families</span></div>
327
  </div>
328
  </section>
329
- <section class="fb-frontier" aria-label="FlavourBench Score with Wilson intervals">
330
- <div class="fb-frontier-head"><strong>FlavourBench Score</strong><span>Top 12 · score and Wilson 95%</span></div>
331
  {_frontier_html()}
332
- <div class="fb-note">One answer equals 3.125 points. The score is exact for these 32 tasks;
333
- overlapping intervals mean nearby rows are not a definitive general ordering.</div>
334
  </section>
335
  </div>
336
  """
@@ -339,20 +363,20 @@ def _hero_html() -> str:
339
  def _leaderboard_frame() -> pd.DataFrame:
340
  rows = []
341
  for model in DISPLAY_MODELS:
342
- off = model["conditions"]["epicure_off"]
343
- lower, upper = (100 * float(value) for value in off["wilson_95"])
344
  rows.append(
345
  {
346
- "Score rank": (
347
- SCORE_RANKS[str(model["model_id"])]
348
- if int(off["parseable_answers"]) > 0
349
- else "DNF"
 
 
 
 
350
  ),
351
- "Model": model["display_name"],
352
- "FlavourBench Score": f"{off['accuracy_percent']:.3g}%",
353
- "Correct": f"{off['correct']}/32",
354
- "Wilson 95%": f"{lower:.1f}% to {upper:.1f}%",
355
- "Parsed answers": f"{off['parseable_answers']}/32",
356
  }
357
  )
358
  return pd.DataFrame(rows)
@@ -360,127 +384,116 @@ def _leaderboard_frame() -> pd.DataFrame:
360
 
361
  def _model_detail(model_name: str) -> tuple[str, pd.DataFrame]:
362
  model = MODEL_BY_NAME[model_name]
363
- off = model["conditions"]["epicure_off"]
364
- on = model["conditions"]["epicure_on"]
365
  summary = f"""
366
- <div class="fb-pair-status">
367
- <div class="fb-condition good">
368
- <small>FlavourBench Score</small>
369
- <strong>{off["accuracy_percent"]:.3g}%</strong>
370
- <span>Wilson 95%: {off["wilson_95"][0] * 100:.1f}% to {off["wilson_95"][1] * 100:.1f}%</span>
371
- </div>
372
- <div class="fb-condition good">
373
- <small>Named-operation diagnostic</small>
374
- <strong>{on["accuracy_percent"]:.3g}%</strong>
375
- <span>Expected ceiling; never a ranking input</span>
376
- </div>
377
  </div>
378
  """
379
- family_rows = []
380
- for family in RELEASE["leaderboard"]["design"]["families"]:
381
- family_rows.append(
382
- {
383
- "Family": family.title(),
384
- "Model only": f"{off['family_accuracy'][family] * 100:.1f}%",
385
- "Model + Epicure": f"{on['family_accuracy'][family] * 100:.1f}%",
386
- "Change": (
387
- f"{(on['family_accuracy'][family] - off['family_accuracy'][family]) * 100:+.1f} pp"
388
- ),
389
- }
390
- )
391
- return summary, pd.DataFrame(family_rows)
392
-
393
-
394
- def _observation(model_id: str, task_id: str, condition: str) -> dict[str, Any]:
395
- return OBSERVATION_INDEX.get(
396
- (model_id, task_id, condition),
397
  {
398
- "answer_markdown": "No observation was recorded.",
399
- "correct": False,
400
- "parseable_normal_completion": False,
401
- "latency_ms": None,
402
- "response_artifact_sha256": "unavailable",
403
- "source_status": "unavailable",
404
- "tool_trace": [],
405
- },
406
  )
 
407
 
408
 
409
- def _status_card(label: str, row: dict[str, Any]) -> str:
410
- observed = bool(row.get("parseable_normal_completion"))
411
- if not observed:
412
- css_class = "unknown"
413
- status = "Unavailable"
414
- elif row.get("correct"):
415
- css_class = "good"
416
- status = "Correct"
417
- else:
418
- css_class = "bad"
419
- status = "Incorrect"
420
- latency = row.get("latency_ms")
421
- latency_text = f"{latency:,} ms" if isinstance(latency, int) else "not observed"
422
- artifact = html.escape(str(row.get("response_artifact_sha256", "unavailable"))[:16])
423
- return f"""
424
- <div class="fb-condition {css_class}">
425
- <small>{html.escape(label)}</small>
426
- <strong>{status}</strong>
427
- <span>Choice {html.escape(str(row.get("observed_choice") or "none"))} · {latency_text}</span><br>
428
- <code>{artifact}...</code>
429
- </div>
430
- """
431
-
432
-
433
- def _pair_detail(
434
- model_name: str, task_id: str
435
- ) -> tuple[str, str, dict[str, str], str, str, list[Any], str]:
436
  model = MODEL_BY_NAME[model_name]
 
437
  task = TASK_BY_ID[task_id]
438
- off = _observation(model["model_id"], task_id, "epicure_off")
439
- on = _observation(model["model_id"], task_id, "epicure_on")
440
- status = (
441
- "<div class='fb-pair-status'>"
442
- + _status_card("Model only", off)
443
- + _status_card("Model + Epicure", on)
444
- + "</div>"
 
 
 
 
 
 
 
 
 
 
 
 
445
  )
446
- reference = json.dumps(
447
  {
448
- "call": task["reference_tool_call"],
449
- "result": task["reference_tool_result"],
450
- "result_sha256": task["reference_tool_result_sha256"],
451
- },
452
- indent=2,
453
- ensure_ascii=False,
454
- )
 
 
 
 
 
 
455
  provenance = (
456
- f"Task family: `{task['family']}` \n"
457
- f"Scoring family: `{task['scoring_family']}` \n"
458
- f"Prompt SHA-256: `{task['prompt_sha256']}` \n"
459
- f"Release artifact: `{RELEASE['artifact_sha256']}`"
460
  )
 
 
 
461
  return (
462
  status,
463
- task["prompt"],
464
- task["choices"],
465
- str(off.get("answer_markdown") or "No answer recorded."),
466
- str(on.get("answer_markdown") or "No answer recorded."),
467
- on.get("tool_trace") or [],
468
- reference + "\n\n" + provenance,
469
  )
470
 
471
 
472
- def _task_label(task: dict[str, Any]) -> str:
473
- return f"{task['task_id']} · {task['family']}"
474
-
475
-
476
- MODEL_NAMES = list(MODEL_BY_NAME)
477
- TASK_LABEL_TO_ID = {_task_label(task): task["task_id"] for task in TASKS}
478
-
479
-
480
- def _pair_from_label(
481
- model_name: str, task_label: str
482
- ) -> tuple[str, str, dict[str, str], str, str, list[Any], str]:
483
- return _pair_detail(model_name, TASK_LABEL_TO_ID[task_label])
 
 
 
 
 
 
 
 
 
 
 
484
 
485
 
486
  theme = gr.themes.Base(
@@ -490,7 +503,7 @@ theme = gr.themes.Base(
490
  c200="#A9D0E9",
491
  c300="#75B3DA",
492
  c400="#4292C6",
493
- c500=BRAND["blue"],
494
  c600="#12588F",
495
  c700="#104873",
496
  c800="#103C5D",
@@ -501,26 +514,24 @@ theme = gr.themes.Base(
501
  font=gr.themes.GoogleFont("Geist", weights=(400, 500, 600, 700)),
502
  font_mono=gr.themes.GoogleFont("IBM Plex Mono", weights=(400, 600)),
503
  ).set(
504
- body_background_fill=BRAND["paper"],
505
  block_background_fill="#FFFFFF",
506
  block_border_width="1px",
507
  block_label_text_weight="600",
508
- button_primary_background_fill=BRAND["blue"],
509
  button_primary_background_fill_hover="#12588F",
510
  )
511
 
512
 
513
- with gr.Blocks(title="FlavourBench · Executable culinary benchmark") as demo:
514
  gr.HTML(_hero_html())
515
-
516
  with gr.Tabs():
517
  with gr.Tab("Leaderboard"):
518
  gr.HTML(
519
  """
520
- <div class="fb-section-title">
521
- <div class="fb-kicker">Automated exact-choice track</div>
522
- <h2>The complete public benchmark</h2>
523
- <p>Score rank follows only the FlavourBench Score on the fixed 32-task panel.</p>
524
  </div>
525
  """
526
  )
@@ -530,24 +541,19 @@ with gr.Blocks(title="FlavourBench · Executable culinary benchmark") as demo:
530
  wrap=True,
531
  show_search="filter",
532
  show_row_numbers=False,
533
- column_widths=[55, 280, 130, 85, 175, 155],
534
  )
535
  gr.Markdown(
536
- "**Reading the table.** The score is exact for this fixed panel. The Wilson interval "
537
- "is a descriptive sampling indicator, not proof about all culinary reasoning. "
538
- "Equal scores share a score rank, and leading intervals overlap, so nearby rows "
539
- "should be treated as a close score group. "
540
- "Parsed answers keeps answer-contract failures visible. Epicure-assisted results "
541
- "remain available in Model fingerprint and Pair Lens, but they do not affect rank."
542
  )
543
 
544
- with gr.Tab("Model fingerprint"):
545
  gr.HTML(
546
  """
547
- <div class="fb-section-title">
548
- <div class="fb-kicker">Family profile</div>
549
- <h2>Where does Epicure change the model?</h2>
550
- <p>Compare substitution, composition, cookability, and evidence tasks.</p>
551
  </div>
552
  """
553
  )
@@ -570,94 +576,98 @@ with gr.Blocks(title="FlavourBench · Executable culinary benchmark") as demo:
570
  outputs=[model_summary, family_table],
571
  )
572
 
573
- with gr.Tab("Pair Lens"):
574
  gr.HTML(
575
  """
576
- <div class="fb-section-title">
577
- <div class="fb-kicker">Model-task evidence</div>
578
- <h2>Open the score</h2>
579
- <p>Inspect both answers, the exact Epicure reference, the observed trace, and hashes.</p>
580
  </div>
581
  """
582
  )
583
  with gr.Row():
584
- pair_model = gr.Dropdown(
585
  choices=MODEL_NAMES,
586
  value=MODEL_NAMES[0],
587
  label="Model",
588
  filterable=True,
589
  scale=1,
590
  )
591
- pair_task = gr.Dropdown(
592
  choices=list(TASK_LABEL_TO_ID),
593
  value=next(iter(TASK_LABEL_TO_ID)),
594
  label="Task",
595
  filterable=True,
596
  scale=2,
597
  )
598
- inspect_button = gr.Button("Inspect pair", variant="primary", scale=0)
599
-
600
- initial_pair = _pair_from_label(MODEL_NAMES[0], next(iter(TASK_LABEL_TO_ID)))
601
- pair_status = gr.HTML(initial_pair[0])
602
- prompt = gr.Textbox(
603
- value=initial_pair[1],
604
- label="Exact prompt",
605
- lines=12,
606
  interactive=False,
 
 
607
  )
608
- choices = gr.JSON(value=initial_pair[2], label="Choices")
609
- with gr.Row():
610
- off_answer = gr.Markdown(value=initial_pair[3], label="Model only answer")
611
- on_answer = gr.Markdown(value=initial_pair[4], label="Model + Epicure answer")
612
- tool_trace = gr.JSON(value=initial_pair[5], label="Observed Epicure trace")
613
- reference = gr.Textbox(
614
- value=initial_pair[6],
615
- label="Reference operation and provenance",
616
- lines=16,
617
- interactive=False,
618
  )
619
- inspect_button.click(
620
- _pair_from_label,
621
- inputs=[pair_model, pair_task],
622
- outputs=[
623
- pair_status,
624
- prompt,
625
- choices,
626
- off_answer,
627
- on_answer,
628
- tool_trace,
629
- reference,
630
- ],
631
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
 
633
  with gr.Tab("Method and download"):
634
  gr.HTML(
635
  f"""
636
- <div class="fb-section-title">
637
- <div class="fb-kicker">Reproduce, cite, extend</div>
638
- <h2>One release, five public tables</h2>
639
- <p>The Space reads a content-addressed JSON release and makes no provider calls.</p>
640
  </div>
641
- <div class="fb-method-grid">
642
  <div>
643
  <h3>Scoring contract</h3>
644
- <p><strong>FlavourBench Score</strong> is model-only exact-choice accuracy over
645
- all 32 tasks and is the only ranking metric. Epicure compiles the reference
646
- answers; it is not a model row. The matched named-operation condition makes the
647
- same runtime output available to the endpoint, so its near-100% result is an
648
- expected execution ceiling, not a second benchmark score.</p>
649
- <p>Tasks cover substitution, composition, cookability, and evidence. Every expected
650
- answer is derived from a fixed read-only Epicure operation.</p>
651
- <h3>Public records</h3>
652
- <p>Downloadable configs cover models, tasks, observations, paired outcomes, and
653
- leaderboard rows. Response and result hashes connect every table.</p>
654
  </div>
655
- <aside class="fb-callout">
656
- <h3>Exact release</h3>
657
- <p><span class="fb-hash">{RELEASE["artifact_sha256"]}</span></p>
658
- <p>{RELEASE["counts"]["models"]} models · {RELEASE["counts"]["tasks"]} tasks ·
659
- {RELEASE["counts"]["assigned_arms"]:,} assigned arms</p>
660
- <p>Track: {html.escape(RELEASE["track"])}</p>
661
  </aside>
662
  </div>
663
  """
@@ -668,25 +678,18 @@ with gr.Blocks(title="FlavourBench · Executable culinary benchmark") as demo:
668
  git clone https://github.com/josefchen/flavourbench.git
669
  cd flavourbench
670
  pip install -e '.[dev]'
671
- python -I paper/reproduce_epicure_native.py \\
672
- --release paper/generated/epicure-native/epicure-native-release.json
673
  ```
674
 
675
- **Project:** [paper](https://github.com/josefchen/flavourbench/blob/main/paper/build/flavourbench.pdf)
676
- · [dataset](https://huggingface.co/datasets/josefchen/flavourbench)
677
- · [source](https://github.com/josefchen/flavourbench)
678
-
679
- **Rights note:** tasks and authored metadata are CC BY 4.0. Model responses and third-party
680
- materials retain the boundaries in
681
- [`LICENSES.md`](https://github.com/josefchen/flavourbench/blob/main/LICENSES.md). This explorer is a
682
- public research preview.
683
  """
684
  )
685
 
686
  gr.HTML(
687
  """
688
  <div class="fb-shell fb-footer">
689
- FlavourBench · Executable culinary evaluation · Public automated benchmark
690
  </div>
691
  """
692
  )
 
1
  from __future__ import annotations
2
 
3
+ import hashlib
4
  import html
5
  import json
6
  import os
 
11
  import pandas as pd
12
 
13
  HERE = Path(__file__).resolve().parent
14
+ BUNDLE_PATH = Path(
15
+ os.environ.get(
16
+ "FLAVOURBENCH_BUNDLE",
17
+ HERE / "data-powered" / "flavourbench-powered-space.json",
18
+ )
19
  )
20
 
21
+ BLUE = "#1769AA"
22
+ CHARCOAL = "#262B33"
 
 
 
 
 
 
23
 
24
  CSS = """
25
  :root {
26
+ --fb-accent: #1769AA;
27
+ --fb-accent-soft: #EAF3FA;
 
 
 
28
  --fb-ink: #262B33;
29
+ --fb-muted: #657180;
30
+ --fb-paper: #F6F8FA;
31
  --fb-panel: #FFFFFF;
32
+ --fb-rule: #D9E0E7;
33
  --fb-code: #EEF2F5;
34
  }
35
  .dark {
36
+ --fb-accent-soft: #102B3E;
37
+ --fb-ink: #EAF0F4;
38
  --fb-muted: #A7B2BC;
39
  --fb-paper: #11171D;
40
  --fb-panel: #182129;
 
44
  body, .gradio-container {
45
  background: var(--fb-paper) !important;
46
  color: var(--fb-ink) !important;
47
+ font-family: "Geist", "Avenir Next", system-ui, sans-serif !important;
48
+ }
49
+ .gradio-container { max-width: 1460px !important; }
50
+ .fb-shell { max-width: 1360px; margin: 0 auto; }
51
+ .fb-hero {
52
+ display: grid;
53
+ grid-template-columns: minmax(0, .9fr) minmax(520px, 1.1fr);
54
+ gap: 54px;
55
+ padding: 48px 8px 34px;
56
+ border-bottom: 1px solid var(--fb-rule);
57
  }
 
 
58
  .fb-kicker {
59
+ color: var(--fb-accent);
60
  font-family: "IBM Plex Mono", ui-monospace, monospace;
61
  font-size: 12px;
62
  font-weight: 700;
63
  letter-spacing: .12em;
64
  text-transform: uppercase;
65
  }
 
 
 
 
 
 
 
66
  .fb-hero h1 {
67
  color: var(--fb-ink);
68
+ font-size: clamp(46px, 6vw, 78px);
69
+ letter-spacing: -.06em;
70
+ line-height: .96;
71
+ margin: 14px 0 18px;
72
+ max-width: 760px;
73
  }
74
  .fb-dek {
75
  color: var(--fb-muted);
76
+ font-size: 19px;
77
+ line-height: 1.48;
78
  margin: 0;
79
+ max-width: 640px;
80
  }
 
81
  .fb-stats {
82
+ display: grid;
83
+ grid-template-columns: repeat(4, minmax(92px, 1fr));
84
+ gap: 22px;
85
+ margin-top: 31px;
86
  }
87
+ .fb-stat { border-top: 2px solid var(--fb-rule); padding-top: 11px; }
88
  .fb-stat strong {
89
  color: var(--fb-ink);
90
  display: block;
91
+ font-size: 26px;
92
  letter-spacing: -.04em;
93
  line-height: 1;
94
  }
95
  .fb-stat span {
96
  color: var(--fb-muted);
97
  display: block;
98
+ font-size: 11px;
99
  margin-top: 7px;
100
  text-transform: uppercase;
101
+ letter-spacing: .07em;
102
  }
103
  .fb-frontier {
104
  align-self: end;
105
  background: var(--fb-panel);
106
  border: 1px solid var(--fb-rule);
107
+ border-top: 4px solid var(--fb-accent);
108
+ border-radius: 8px;
109
+ padding: 20px 22px 16px;
110
  }
111
  .fb-frontier-head {
112
  align-items: baseline;
113
  display: flex;
114
  justify-content: space-between;
115
+ margin-bottom: 13px;
116
  }
117
  .fb-frontier-head strong { font-size: 14px; }
118
  .fb-frontier-head span { color: var(--fb-muted); font-size: 12px; }
119
+ .fb-forest-row {
120
  align-items: center;
121
  display: grid;
122
+ grid-template-columns: 150px 1fr 42px 38px;
123
  gap: 10px;
124
+ min-height: 26px;
125
  }
126
+ .fb-model {
127
  color: var(--fb-ink);
128
  font-size: 12px;
129
  overflow: hidden;
130
  text-overflow: ellipsis;
131
  white-space: nowrap;
132
  }
133
+ .fb-axis { height: 13px; position: relative; }
134
+ .fb-axis::before {
135
+ background: var(--fb-rule);
136
+ content: "";
137
+ height: 1px;
 
 
 
 
138
  left: 0;
139
  position: absolute;
140
+ right: 0;
141
+ top: 6px;
142
  }
143
+ .fb-ci {
 
 
144
  border-top: 2px solid var(--fb-muted);
145
+ height: 1px;
146
  position: absolute;
147
+ top: 5px;
148
  }
149
+ .fb-ci::before, .fb-ci::after {
150
+ background: var(--fb-muted);
151
+ content: "";
152
+ height: 7px;
153
+ position: absolute;
154
+ top: -4px;
155
+ width: 1px;
156
+ }
157
+ .fb-ci::before { left: 0; }
158
+ .fb-ci::after { right: 0; }
159
+ .fb-point {
160
+ background: var(--fb-accent);
161
+ height: 11px;
162
+ position: absolute;
163
+ top: 1px;
164
+ width: 3px;
165
+ }
166
+ .fb-number {
167
+ color: var(--fb-ink);
168
  font-family: "IBM Plex Mono", ui-monospace, monospace;
169
  font-size: 11px;
170
  text-align: right;
171
  }
172
+ .fb-group {
 
173
  color: var(--fb-muted);
174
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
175
+ font-size: 10px;
176
+ text-align: right;
 
177
  }
178
+ .fb-section { margin: 27px 0 8px; }
179
+ .fb-section h2 {
180
  color: var(--fb-ink);
181
+ font-size: 30px;
182
  letter-spacing: -.035em;
183
+ margin: 0 0 4px;
184
  }
185
+ .fb-section p { color: var(--fb-muted); margin: 0; max-width: 70ch; }
186
+ .fb-metric-grid {
 
187
  display: grid;
188
+ grid-template-columns: repeat(4, 1fr);
189
  gap: 12px;
190
  margin: 8px 0 16px;
191
  }
192
+ .fb-metric {
193
  background: var(--fb-panel);
194
  border: 1px solid var(--fb-rule);
195
+ border-radius: 8px;
196
  padding: 16px;
197
  }
198
+ .fb-metric small {
 
 
 
199
  color: var(--fb-muted);
200
  display: block;
201
  font-size: 11px;
202
+ letter-spacing: .05em;
203
  text-transform: uppercase;
204
  }
205
+ .fb-metric strong { color: var(--fb-ink); display: block; font-size: 23px; margin-top: 5px; }
206
+ .fb-evidence {
207
+ background: var(--fb-accent-soft);
208
+ border-left: 4px solid var(--fb-accent);
209
+ border-radius: 0 8px 8px 0;
210
+ color: var(--fb-ink);
211
+ line-height: 1.48;
212
+ padding: 17px 19px;
213
+ }
214
+ .fb-evidence code, .fb-hash {
215
  background: var(--fb-code);
216
  color: var(--fb-muted);
217
  font-family: "IBM Plex Mono", ui-monospace, monospace;
218
  font-size: 11px;
219
  }
220
+ .fb-method {
221
  display: grid;
222
+ grid-template-columns: 1.15fr .85fr;
223
  gap: 28px;
224
  }
 
 
 
 
 
 
225
  .fb-footer {
226
  border-top: 1px solid var(--fb-rule);
227
  color: var(--fb-muted);
228
  font-size: 12px;
229
+ margin-top: 36px;
230
+ padding: 18px 8px 26px;
231
  }
 
 
 
 
232
  .gradio-dataframe, .block { border-radius: 8px !important; }
233
+ @media (max-width: 980px) {
234
+ .fb-hero, .fb-method { grid-template-columns: 1fr; }
235
  .fb-hero { gap: 28px; padding-top: 34px; }
 
236
  }
237
+ @media (max-width: 700px) {
238
+ .fb-hero h1 { font-size: 48px; }
239
+ .fb-stats, .fb-metric-grid { grid-template-columns: repeat(2, 1fr); }
240
+ .fb-forest-row { grid-template-columns: 105px 1fr 36px 30px; }
241
  }
242
  @media (prefers-reduced-motion: reduce) {
243
  *, *::before, *::after { animation: none !important; transition: none !important; }
 
245
  """
246
 
247
 
248
+ class SpaceDataError(RuntimeError):
249
+ """The public explorer bundle is invalid."""
250
+
251
+
252
+ def _canonical(value: object) -> bytes:
253
+ return json.dumps(
254
+ value,
255
+ ensure_ascii=False,
256
+ separators=(",", ":"),
257
+ sort_keys=True,
258
+ allow_nan=False,
259
+ ).encode()
260
+
261
+
262
+ def _load_bundle() -> dict[str, Any]:
263
+ if BUNDLE_PATH.is_symlink() or not BUNDLE_PATH.is_file():
264
  raise FileNotFoundError(
265
+ f"Powered Space bundle not found at {BUNDLE_PATH}. Set FLAVOURBENCH_BUNDLE."
266
  )
267
+ value = json.loads(BUNDLE_PATH.read_text(encoding="utf-8"))
268
+ payload = dict(value)
269
+ recorded = str(payload.pop("artifact_sha256", ""))
270
+ if (
271
+ recorded != hashlib.sha256(_canonical(payload)).hexdigest()
272
+ or value.get("schema_version") != "flavourbench-powered-space-bundle-v1"
273
+ or value.get("status") != "final_complete"
274
+ ):
275
+ raise SpaceDataError("powered Space bundle failed verification")
276
+ return value
277
 
278
 
279
+ BUNDLE = _load_bundle()
280
+ MODELS = BUNDLE["models"]
281
+ TASKS = BUNDLE["tasks"]
282
+ PAIRWISE = BUNDLE["pairwise_comparisons"]
283
+ MODEL_BY_NAME = {str(row["model_name"]): row for row in MODELS}
284
+ MODEL_BY_ID = {str(row["model_id"]): row for row in MODELS}
285
+ TASK_BY_ID = {str(row["task_id"]): row for row in TASKS}
286
+ OBSERVATIONS = {
287
+ (str(row["model_id"]), str(row["task_id"])): row for row in BUNDLE["primary_observations"]
288
  }
289
+ PAIR_INDEX: dict[tuple[str, str], dict[str, Any]] = {}
290
+ for _row in PAIRWISE:
291
+ PAIR_INDEX[(str(_row["left_model_id"]), str(_row["right_model_id"]))] = _row
292
 
293
 
294
+ def _rank_key(row: dict[str, Any]) -> tuple[bool, int, str]:
295
+ rank = row.get("point_estimate_rank")
296
+ return rank is None, int(rank or 10_000), str(row["model_id"])
 
 
 
 
 
297
 
298
 
299
+ DISPLAY_MODELS = sorted(MODELS, key=_rank_key)
300
+ MODEL_NAMES = [str(row["model_name"]) for row in DISPLAY_MODELS]
301
+ TASK_LABEL_TO_ID = {
302
+ f"{row['task_id']} | {str(row['family']).replace('_', ' ')}": str(row["task_id"])
303
+ for row in TASKS
304
+ }
 
 
 
 
305
 
306
 
307
+ def _short(value: str) -> str:
308
+ return (
309
+ value.replace("GPT-5.6 ", "5.6 ")
310
+ .replace("Claude ", "")
311
+ .replace("DeepSeek ", "DS ")
312
+ .replace("Command ", "Cmd ")
313
+ )
314
 
315
 
316
  def _frontier_html() -> str:
317
  rows = []
318
+ for model in DISPLAY_MODELS[:10]:
319
+ score = float(model["flavourbench_score"])
320
+ lower, upper = (float(value) for value in model["score_simultaneous_95_ci"])
321
+ left = max(0.0, min(100.0, lower))
322
+ right = max(left, min(100.0, upper))
323
+ group = model.get("statistical_rank_group")
324
  rows.append(
325
+ "<div class='fb-forest-row'>"
326
+ f"<div class='fb-model' title='{html.escape(str(model['model_name']))}'>"
327
+ f"{html.escape(_short(str(model['model_name'])))}</div>"
328
+ "<div class='fb-axis'>"
329
+ f"<span class='fb-ci' style='left:{left:.3f}%;width:{right - left:.3f}%'></span>"
330
+ f"<span class='fb-point' style='left:{score:.3f}%'></span>"
331
  "</div>"
332
+ f"<div class='fb-number'>{score:.1f}</div>"
333
+ f"<div class='fb-group'>G{group if group is not None else '-'}</div>"
334
  "</div>"
335
  )
336
  return "".join(rows)
337
 
338
 
339
  def _hero_html() -> str:
340
+ inference = BUNDLE["analysis"]["inference"]
341
  return f"""
342
  <div class="fb-shell fb-hero">
343
  <section>
344
+ <div class="fb-kicker">Executable culinary evaluation</div>
345
+ <h1>640 decisions.<br>No model judge.</h1>
346
+ <p class="fb-dek">Executable score maps rank 20 frontier endpoints with shared-task
347
+ uncertainty and inspectable responses.</p>
 
 
348
  <div class="fb-stats">
349
+ <div class="fb-stat"><strong>{len(MODELS)}</strong><span>models</span></div>
350
+ <div class="fb-stat"><strong>{len(TASKS)}</strong><span>tasks</span></div>
351
+ <div class="fb-stat"><strong>{len(MODELS) * len(TASKS):,}</strong><span>primary cells</span></div>
352
+ <div class="fb-stat"><strong>{inference["pairwise_hypotheses"]}</strong><span>paired tests</span></div>
353
  </div>
354
  </section>
355
+ <section class="fb-frontier" aria-label="Score forest with simultaneous intervals">
356
+ <div class="fb-frontier-head"><strong>FlavourBench Score</strong><span>Top 10, simultaneous 95%</span></div>
357
  {_frontier_html()}
 
 
358
  </section>
359
  </div>
360
  """
 
363
  def _leaderboard_frame() -> pd.DataFrame:
364
  rows = []
365
  for model in DISPLAY_MODELS:
366
+ ci = model["score_simultaneous_95_ci"]
367
+ repeat = model.get("repeatability") or {}
368
  rows.append(
369
  {
370
+ "Rank": model.get("point_estimate_rank") or "DNF",
371
+ "Group": model.get("statistical_rank_group") or "DNF",
372
+ "Model": model["model_name"],
373
+ "Score": round(float(model["flavourbench_score"]), 2),
374
+ "Simultaneous 95%": f"{ci[0]:.2f} to {ci[1]:.2f}",
375
+ "Completed": f"{model['availability']['completed']}/640",
376
+ "Repeat Jaccard": (
377
+ round(float(repeat["mean_ingredient_set_jaccard"]), 3) if repeat else None
378
  ),
379
+ "Route": model["provider_name"],
 
 
 
 
380
  }
381
  )
382
  return pd.DataFrame(rows)
 
384
 
385
  def _model_detail(model_name: str) -> tuple[str, pd.DataFrame]:
386
  model = MODEL_BY_NAME[model_name]
387
+ repeat = model.get("repeatability") or {}
388
+ rank_interval = model.get("bootstrap_rank_95_interval") or [None, None]
389
  summary = f"""
390
+ <div class="fb-metric-grid">
391
+ <div class="fb-metric"><small>FlavourBench Score</small><strong>{model["flavourbench_score"]:.2f}</strong></div>
392
+ <div class="fb-metric"><small>Statistical group</small><strong>G{model.get("statistical_rank_group") or "-"}</strong></div>
393
+ <div class="fb-metric"><small>Bootstrap rank</small><strong>{rank_interval[0]}-{rank_interval[1]}</strong></div>
394
+ <div class="fb-metric"><small>Repeat Jaccard</small><strong>{float(repeat.get("mean_ingredient_set_jaccard", 0)):.3f}</strong></div>
 
 
 
 
 
 
395
  </div>
396
  """
397
+ family_rows = [
398
+ {
399
+ "Family": family.replace("_", " ").title(),
400
+ "Score": round(float(score), 3),
401
+ }
402
+ for family, score in model["family_scores"].items()
403
+ ]
404
+ chance = model["chance_comparison"]
405
+ family_rows.append(
 
 
 
 
 
 
 
 
 
406
  {
407
+ "Family": "Exact chance baseline",
408
+ "Score": round(float(chance["exact_chance_score"]), 3),
409
+ }
 
 
 
 
 
410
  )
411
+ return summary, pd.DataFrame(family_rows)
412
 
413
 
414
+ def _task_detail(
415
+ model_name: str, task_label: str
416
+ ) -> tuple[str, str, dict[str, str], pd.DataFrame, str, str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  model = MODEL_BY_NAME[model_name]
418
+ task_id = TASK_LABEL_TO_ID[task_label]
419
  task = TASK_BY_ID[task_id]
420
+ observation = OBSERVATIONS[(str(model["model_id"]), task_id)]
421
+ scoring = observation["scoring"]
422
+ observed = scoring.get("observed_selection")
423
+ optimum = str(task["optimal_selection"])
424
+ observed_ingredients = [task["choices"][label] for label in observed] if observed else []
425
+ optimum_ingredients = [task["choices"][label] for label in optimum]
426
+ status = f"""
427
+ <div class="fb-evidence">
428
+ <strong>{html.escape(model_name)}</strong> selected
429
+ <code>{html.escape(str(observed or "no valid selection"))}</code> and scored
430
+ <strong>{float(scoring["score"]):.2f}</strong>. The optimum is
431
+ <code>{html.escape(optimum)}</code>.
432
+ <br>Observed: {html.escape(", ".join(observed_ingredients) or "none")}
433
+ <br>Optimum: {html.escape(", ".join(optimum_ingredients))}
434
+ </div>
435
+ """
436
+ ranked = sorted(
437
+ task["selection_scores_bps"].items(),
438
+ key=lambda item: (-int(item[1]), str(item[0])),
439
  )
440
+ score_rows = [
441
  {
442
+ "Selection": selection,
443
+ "Ingredients": ", ".join(task["choices"][label] for label in selection),
444
+ "Score": int(score) / 100,
445
+ "Role": (
446
+ "model selection"
447
+ if selection == observed
448
+ else "optimum"
449
+ if selection == optimum
450
+ else ""
451
+ ),
452
+ }
453
+ for selection, score in ranked[:12]
454
+ ]
455
  provenance = (
456
+ f"Response SHA-256: `{observation['artifact_sha256']}` \n"
457
+ f"Actual model: `{observation.get('actual_model_id')}` \n"
458
+ f"Provider: `{observation.get('actual_provider')}` \n"
459
+ f"Prompt SHA-256: `{task['prompt_sha256']}`"
460
  )
461
+ answer = str(observation.get("answer_excerpt") or "No answer was recorded.")
462
+ if observation.get("answer_truncated"):
463
+ answer += "\n\n[Excerpt truncated. The full response is in the dataset.]"
464
  return (
465
  status,
466
+ str(task["prompt"]),
467
+ dict(task["choices"]),
468
+ pd.DataFrame(score_rows),
469
+ answer,
470
+ provenance,
 
471
  )
472
 
473
 
474
+ def _pair_detail(left_name: str, right_name: str) -> str:
475
+ left = str(MODEL_BY_NAME[left_name]["model_id"])
476
+ right = str(MODEL_BY_NAME[right_name]["model_id"])
477
+ if left == right:
478
+ return "<div class='fb-evidence'>Choose two different models.</div>"
479
+ row = PAIR_INDEX.get((left, right))
480
+ sign = 1.0
481
+ if row is None:
482
+ row = PAIR_INDEX[(right, left)]
483
+ sign = -1.0
484
+ difference = sign * float(row["mean_difference"])
485
+ interval = [sign * float(value) for value in row["bootstrap_95_ci"]]
486
+ interval.sort()
487
+ verdict = "distinguishable after Holm correction" if row["holm_significant"] else "not resolved"
488
+ return f"""
489
+ <div class="fb-evidence">
490
+ <strong>{html.escape(left_name)}</strong> minus <strong>{html.escape(right_name)}</strong>:
491
+ <strong>{difference:+.3f} points</strong> (bootstrap 95% {interval[0]:+.3f} to {interval[1]:+.3f}).
492
+ The comparison is <strong>{verdict}</strong> across all 190 tests.
493
+ <br>Holm p = <code>{float(row["holm_p"]):.4g}</code>, paired Cohen dz =
494
+ <code>{row.get("cohen_dz")}</code>.
495
+ </div>
496
+ """
497
 
498
 
499
  theme = gr.themes.Base(
 
503
  c200="#A9D0E9",
504
  c300="#75B3DA",
505
  c400="#4292C6",
506
+ c500=BLUE,
507
  c600="#12588F",
508
  c700="#104873",
509
  c800="#103C5D",
 
514
  font=gr.themes.GoogleFont("Geist", weights=(400, 500, 600, 700)),
515
  font_mono=gr.themes.GoogleFont("IBM Plex Mono", weights=(400, 600)),
516
  ).set(
517
+ body_background_fill="#F6F8FA",
518
  block_background_fill="#FFFFFF",
519
  block_border_width="1px",
520
  block_label_text_weight="600",
521
+ button_primary_background_fill=BLUE,
522
  button_primary_background_fill_hover="#12588F",
523
  )
524
 
525
 
526
+ with gr.Blocks(title="FlavourBench | Executable culinary evaluation") as demo:
527
  gr.HTML(_hero_html())
 
528
  with gr.Tabs():
529
  with gr.Tab("Leaderboard"):
530
  gr.HTML(
531
  """
532
+ <div class="fb-section">
533
+ <h2>The powered frontier panel</h2>
534
+ <p>Point ranks are shown beside statistical groups and simultaneous intervals.</p>
 
535
  </div>
536
  """
537
  )
 
541
  wrap=True,
542
  show_search="filter",
543
  show_row_numbers=False,
544
+ column_widths=[55, 55, 245, 80, 180, 110, 125, 120],
545
  )
546
  gr.Markdown(
547
+ "A point rank orders the observed scores. A statistical group keeps models together "
548
+ "when the shared-task evidence does not separate them after multiplicity control."
 
 
 
 
549
  )
550
 
551
+ with gr.Tab("Model profile"):
552
  gr.HTML(
553
  """
554
+ <div class="fb-section">
555
+ <h2>Family profile and repeatability</h2>
556
+ <p>Inspect where a model earns its score and whether its selection survives relabeling.</p>
 
557
  </div>
558
  """
559
  )
 
576
  outputs=[model_summary, family_table],
577
  )
578
 
579
+ with gr.Tab("Task lens"):
580
  gr.HTML(
581
  """
582
+ <div class="fb-section">
583
+ <h2>Open one scored decision</h2>
584
+ <p>Read the prompt, the model answer, and the top of the precomputed reward surface.</p>
 
585
  </div>
586
  """
587
  )
588
  with gr.Row():
589
+ task_model = gr.Dropdown(
590
  choices=MODEL_NAMES,
591
  value=MODEL_NAMES[0],
592
  label="Model",
593
  filterable=True,
594
  scale=1,
595
  )
596
+ task_selector = gr.Dropdown(
597
  choices=list(TASK_LABEL_TO_ID),
598
  value=next(iter(TASK_LABEL_TO_ID)),
599
  label="Task",
600
  filterable=True,
601
  scale=2,
602
  )
603
+ inspect_task = gr.Button("Inspect", variant="primary", scale=0)
604
+ initial = _task_detail(MODEL_NAMES[0], next(iter(TASK_LABEL_TO_ID)))
605
+ task_status = gr.HTML(initial[0])
606
+ prompt = gr.Textbox(value=initial[1], label="Exact prompt", lines=13, interactive=False)
607
+ choices = gr.JSON(value=initial[2], label="Candidates")
608
+ score_map = gr.Dataframe(
609
+ value=initial[3],
610
+ label="Top 12 of 56 frozen selections",
611
  interactive=False,
612
+ wrap=True,
613
+ show_row_numbers=False,
614
  )
615
+ answer = gr.Markdown(value=initial[4], label="Model response")
616
+ provenance = gr.Markdown(value=initial[5], label="Provenance")
617
+ inspect_task.click(
618
+ _task_detail,
619
+ inputs=[task_model, task_selector],
620
+ outputs=[task_status, prompt, choices, score_map, answer, provenance],
 
 
 
 
621
  )
622
+
623
+ with gr.Tab("Pairwise evidence"):
624
+ gr.HTML(
625
+ """
626
+ <div class="fb-section">
627
+ <h2>Is the score gap resolved?</h2>
628
+ <p>Query any paired contrast from the 190-test family.</p>
629
+ </div>
630
+ """
 
 
 
631
  )
632
+ with gr.Row():
633
+ left_model = gr.Dropdown(
634
+ choices=MODEL_NAMES,
635
+ value=MODEL_NAMES[0],
636
+ label="First model",
637
+ filterable=True,
638
+ )
639
+ right_model = gr.Dropdown(
640
+ choices=MODEL_NAMES,
641
+ value=MODEL_NAMES[1],
642
+ label="Second model",
643
+ filterable=True,
644
+ )
645
+ compare = gr.Button("Compare", variant="primary", scale=0)
646
+ pair_result = gr.HTML(_pair_detail(MODEL_NAMES[0], MODEL_NAMES[1]))
647
+ compare.click(_pair_detail, inputs=[left_model, right_model], outputs=pair_result)
648
 
649
  with gr.Tab("Method and download"):
650
  gr.HTML(
651
  f"""
652
+ <div class="fb-section">
653
+ <h2>One metric, complete evidence</h2>
654
+ <p>The Space makes no model or provider calls.</p>
 
655
  </div>
656
+ <div class="fb-method">
657
  <div>
658
  <h3>Scoring contract</h3>
659
+ <p>Every task exposes eight candidates and all 56 three-item scores. The
660
+ FlavourBench Score is the equal-family mean over 640 tasks. Invalid and failed
661
+ responses remain in the denominator at zero.</p>
662
+ <h3>Inference</h3>
663
+ <p>Results use 50,000 family-stratified shared-task bootstraps, simultaneous
664
+ score bands, 100,000 sign flips, Holm correction, exact-chance tests, and 64
665
+ label-permuted repeats per model.</p>
 
 
 
666
  </div>
667
+ <aside class="fb-evidence">
668
+ <strong>Exact release</strong><br>
669
+ <span class="fb-hash">{BUNDLE["release_artifact_sha256"]}</span><br><br>
670
+ 20 models<br>640 tasks<br>12,800 primary responses<br>1,280 repeats
 
 
671
  </aside>
672
  </div>
673
  """
 
678
  git clone https://github.com/josefchen/flavourbench.git
679
  cd flavourbench
680
  pip install -e '.[dev]'
681
+ make -C paper -f Makefile.powered analysis
682
+ make -C paper -f Makefile.powered arxiv
683
  ```
684
 
685
+ [Paper](https://github.com/josefchen/flavourbench/blob/main/paper/build/flavourbench.pdf) | [Dataset](https://huggingface.co/datasets/josefchen/flavourbench) | [Source](https://github.com/josefchen/flavourbench)
 
 
 
 
 
 
 
686
  """
687
  )
688
 
689
  gr.HTML(
690
  """
691
  <div class="fb-shell fb-footer">
692
+ FlavourBench | Executable culinary evaluation of frontier language models
693
  </div>
694
  """
695
  )
build_powered_space_bundle.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import hashlib
5
+ import json
6
+ import os
7
+ import tempfile
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ HERE = Path(__file__).resolve().parent
12
+ DEFAULT_DATASET = HERE.parent / "dataset" / "data-powered"
13
+ DEFAULT_OUTPUT = HERE / "data-powered" / "flavourbench-powered-space.json"
14
+
15
+
16
+ class PoweredSpaceBuildError(RuntimeError):
17
+ """The powered Space bundle failed verification."""
18
+
19
+
20
+ def _canonical(value: object) -> bytes:
21
+ return json.dumps(
22
+ value,
23
+ ensure_ascii=False,
24
+ separators=(",", ":"),
25
+ sort_keys=True,
26
+ allow_nan=False,
27
+ ).encode()
28
+
29
+
30
+ def _load(path: Path) -> dict[str, Any]:
31
+ if path.is_symlink() or not path.is_file():
32
+ raise PoweredSpaceBuildError(f"input is not a regular file: {path}")
33
+ value = json.loads(path.read_text(encoding="utf-8"))
34
+ if not isinstance(value, dict):
35
+ raise PoweredSpaceBuildError(f"input is not a JSON object: {path}")
36
+ return value
37
+
38
+
39
+ def _semantic_valid(document: dict[str, Any]) -> bool:
40
+ payload = dict(document)
41
+ recorded = str(payload.pop("artifact_sha256", ""))
42
+ return bool(recorded and recorded == hashlib.sha256(_canonical(payload)).hexdigest())
43
+
44
+
45
+ def _jsonl(path: Path, *, expected_sha256: str, expected_rows: int) -> list[dict[str, Any]]:
46
+ payload = path.read_bytes()
47
+ if hashlib.sha256(payload).hexdigest() != expected_sha256:
48
+ raise PoweredSpaceBuildError(f"dataset table hash failed: {path.name}")
49
+ rows = [json.loads(line) for line in payload.splitlines() if line]
50
+ if len(rows) != expected_rows or any(not isinstance(row, dict) for row in rows):
51
+ raise PoweredSpaceBuildError(f"dataset table cardinality failed: {path.name}")
52
+ return rows
53
+
54
+
55
+ def build_bundle(*, release: dict[str, Any], dataset_directory: Path) -> dict[str, Any]:
56
+ if not _semantic_valid(release) or release.get("status") != "final_complete":
57
+ raise PoweredSpaceBuildError("release is not the final powered release")
58
+ manifest = _load(dataset_directory / "DATA_MANIFEST.json")
59
+ if (
60
+ not _semantic_valid(manifest)
61
+ or manifest.get("release_artifact_sha256") != release["artifact_sha256"]
62
+ ):
63
+ raise PoweredSpaceBuildError("dataset manifest and release differ")
64
+ records = {str(row["name"]): row for row in manifest["files"]}
65
+
66
+ def rows(name: str) -> list[dict[str, Any]]:
67
+ record = records[name]
68
+ return _jsonl(
69
+ dataset_directory / name,
70
+ expected_sha256=str(record["sha256"]),
71
+ expected_rows=int(record["rows"]),
72
+ )
73
+
74
+ models = rows("models.jsonl")
75
+ tasks = rows("tasks.jsonl")
76
+ primary = rows("primary_observations.jsonl")
77
+ pairwise = rows("pairwise_comparisons.jsonl")
78
+ compact_observations = []
79
+ for row in primary:
80
+ generation = row.get("generation") or {}
81
+ answer = str(generation.get("answer_markdown") or "")
82
+ compact_observations.append(
83
+ {
84
+ "model_id": row["model_id"],
85
+ "task_id": row["task_id"],
86
+ "status": row["status"],
87
+ "scoring": row["scoring"],
88
+ "answer_excerpt": answer[:1600],
89
+ "answer_truncated": len(answer) > 1600,
90
+ "actual_model_id": generation.get("actual_model_id"),
91
+ "actual_provider": generation.get("actual_provider"),
92
+ "finish_reason": generation.get("finish_reason"),
93
+ "latency_ms": generation.get("latency_ms"),
94
+ "cost_micros": generation.get("cost_micros"),
95
+ "artifact_sha256": row["artifact_sha256"],
96
+ }
97
+ )
98
+ bundle: dict[str, Any] = {
99
+ "schema_version": "flavourbench-powered-space-bundle-v1",
100
+ "release_artifact_sha256": release["artifact_sha256"],
101
+ "dataset_manifest_sha256": manifest["artifact_sha256"],
102
+ "status": release["status"],
103
+ "benchmark": release["benchmark"],
104
+ "track": release["track"],
105
+ "analysis": release["analysis"],
106
+ "claim_boundary": release["claim_boundary"],
107
+ "models": models,
108
+ "tasks": tasks,
109
+ "primary_observations": compact_observations,
110
+ "pairwise_comparisons": pairwise,
111
+ }
112
+ bundle["artifact_sha256"] = hashlib.sha256(_canonical(bundle)).hexdigest()
113
+ return bundle
114
+
115
+
116
+ def _bytes(bundle: dict[str, Any]) -> bytes:
117
+ return (json.dumps(bundle, ensure_ascii=False, indent=2, sort_keys=True) + "\n").encode()
118
+
119
+
120
+ def _write_atomic(path: Path, payload: bytes) -> None:
121
+ path.parent.mkdir(parents=True, exist_ok=True)
122
+ descriptor, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
123
+ temporary_path = Path(temporary)
124
+ try:
125
+ with os.fdopen(descriptor, "wb") as handle:
126
+ handle.write(payload)
127
+ handle.flush()
128
+ os.fsync(handle.fileno())
129
+ os.replace(temporary_path, path)
130
+ finally:
131
+ temporary_path.unlink(missing_ok=True)
132
+
133
+
134
+ def main() -> None:
135
+ parser = argparse.ArgumentParser(description="Build the compact powered FlavourBench Space")
136
+ parser.add_argument("--release", type=Path, required=True)
137
+ parser.add_argument("--dataset-directory", type=Path, default=DEFAULT_DATASET)
138
+ parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
139
+ parser.add_argument("--check", action="store_true")
140
+ args = parser.parse_args()
141
+ bundle = build_bundle(release=_load(args.release), dataset_directory=args.dataset_directory)
142
+ payload = _bytes(bundle)
143
+ if args.check:
144
+ if not args.output.is_file() or args.output.read_bytes() != payload:
145
+ raise PoweredSpaceBuildError("generated Space bundle differs")
146
+ print(f"OK: Space bundle {bundle['artifact_sha256']}")
147
+ return
148
+ _write_atomic(args.output, payload)
149
+ print(f"Wrote {args.output} ({len(payload)} bytes)")
150
+
151
+
152
+ if __name__ == "__main__":
153
+ main()
data-powered/flavourbench-powered-space.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f143b60b4bf836636bf1618b3aea375befb810ce9c7551fa3d5362ab3218005f
3
+ size 11385494
data/epicure-native-release.json DELETED
The diff for this file is too large to render. See raw diff