@wendellresearch-ai/benchwarmers 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -4
- package/USAGE.md +8 -1
- package/VERSION +1 -1
- package/cli.py +4 -1
- package/metrics/aliases.py +24 -3
- package/metrics/endpoints.py +36 -4
- package/metrics/model_roles.py +3 -1
- package/metrics/route.py +3 -2
- package/metrics/wrong.py +3 -1
- package/package.json +1 -1
- package/scripts/check.py +2 -0
- package/scripts/onboard.py +12 -1
- package/scripts/test_no_owui.py +5 -2
- package/scripts/test_serve_edit.py +4 -4
- package/scripts/test_server_json.py +22 -2
- package/serve.py +286 -29
- package/suite/README.md +33 -0
- package/suite/pace.py +8 -4
- package/suite/run.py +144 -16
- package/suite/tasks/vision-bars/fixtures/bars.png +0 -0
- package/suite/tasks/vision-bars/task.json +40 -0
- package/suite/tasks/vision-invoice/fixtures/invoice.png +0 -0
- package/suite/tasks/vision-invoice/task.json +40 -0
- package/suite/tasks/vision-label/fixtures/label.png +0 -0
- package/suite/tasks/vision-label/task.json +52 -0
- package/suite/tasks/vision-receipt/fixtures/receipt.png +0 -0
- package/suite/tasks/vision-receipt/task.json +44 -0
- package/suite/tasks/vision-screen/fixtures/screen.png +0 -0
- package/suite/tasks/vision-screen/task.json +48 -0
- package/suite/tasks/vision-shapes/fixtures/shapes.png +0 -0
- package/suite/tasks/vision-shapes/task.json +40 -0
- package/suite/tasks/vision-table/fixtures/table.png +0 -0
- package/suite/tasks/vision-table/task.json +40 -0
- package/suite/test_vision_tasks.py +201 -0
- package/suite/vision_keys.py +74 -0
- package/suite/vision_sample.py +232 -0
- package/suite/vision_tasks.py +338 -0
- package/ui/bench.js +87 -10
package/suite/run.py
CHANGED
|
@@ -146,6 +146,50 @@ def available_models(cache: dict | None = None,
|
|
|
146
146
|
return models
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
def images_only() -> set[str]:
|
|
150
|
+
"""Aliases whose role is vision or image (model_roles). No text shape
|
|
151
|
+
would ever seat them, so the text and code tasks are not theirs: their
|
|
152
|
+
answers only spent calls and put a reader of images on the code charts.
|
|
153
|
+
Routing still sees them (available_models), where a vision model sits
|
|
154
|
+
for images."""
|
|
155
|
+
import model_roles
|
|
156
|
+
return {a for a, r in model_roles.load()["models"].items() if r in ("vision", "image")}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def sees_images(alias: str) -> bool:
|
|
160
|
+
"""Whether the catalog says `alias` takes images, by the gateway's rule
|
|
161
|
+
(gateway.cannot_see): its input types, the Vision / OCR override in its
|
|
162
|
+
model popup, or the capability seen working. One the catalog knows
|
|
163
|
+
nothing about is not asked: a model that cannot see answers anyway,
|
|
164
|
+
describing a picture it never got, and that would score as reading badly."""
|
|
165
|
+
import catalog
|
|
166
|
+
import endpoints
|
|
167
|
+
if endpoints.entry_of(alias).get("api") == "responses": # the client sends it no images yet
|
|
168
|
+
return False
|
|
169
|
+
return bool(catalog.info(alias).get("vision"))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def applies(spec: dict, model: str, skip: set[str] | None = None, seeing: dict | None = None) -> bool:
|
|
173
|
+
"""Whether `spec` is a task for `model`: a vision task (one with images)
|
|
174
|
+
for a model that takes them, any other for one that is not vision or
|
|
175
|
+
image only. So a vision model's suite is the vision tasks, a general one
|
|
176
|
+
that takes images gets both, and a text-only one never sees a picture.
|
|
177
|
+
`skip` (images_only()) and `seeing` (alias -> sees_images) save asking
|
|
178
|
+
again in a loop."""
|
|
179
|
+
if spec.get("images"):
|
|
180
|
+
if seeing is not None and model in seeing:
|
|
181
|
+
return seeing[model]
|
|
182
|
+
return sees_images(model)
|
|
183
|
+
return model not in (images_only() if skip is None else skip)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def tasks_for(model: str, tasks: list[dict]) -> list[dict]:
|
|
187
|
+
skip, seeing = images_only(), {}
|
|
188
|
+
if any(t.get("images") for t in tasks):
|
|
189
|
+
seeing[model] = sees_images(model)
|
|
190
|
+
return [t for t in tasks if applies(t, model, skip, seeing)]
|
|
191
|
+
|
|
192
|
+
|
|
149
193
|
def _registered_names() -> dict[str, str]:
|
|
150
194
|
"""alias -> the model name the endpoint knows it by; {} with no yaml yet."""
|
|
151
195
|
import endpoints
|
|
@@ -227,9 +271,9 @@ def next_free(tasks_dir: pathlib.Path) -> str | None:
|
|
|
227
271
|
is picked up on the next run rather than after a full cycle.
|
|
228
272
|
"""
|
|
229
273
|
best, best_age = None, None
|
|
230
|
-
held = ad_hoc_models()
|
|
274
|
+
held, specs = ad_hoc_models(), load_tasks(None)
|
|
231
275
|
for model in free_models():
|
|
232
|
-
if model in held:
|
|
276
|
+
if model in held or not tasks_for(model, specs):
|
|
233
277
|
continue
|
|
234
278
|
outputs = list(tasks_dir.glob(f"*/output/{model}.md"))
|
|
235
279
|
if not outputs:
|
|
@@ -267,17 +311,23 @@ def next_model(tasks_dir: pathlib.Path) -> str | None:
|
|
|
267
311
|
A gap is a worse blind spot than a stale number: an old score is
|
|
268
312
|
still evidence, a missing one is none.
|
|
269
313
|
3. oldest, so the set refreshes on its own
|
|
314
|
+
|
|
315
|
+
Complete means every task that is the model's (tasks_for): a vision
|
|
316
|
+
model is complete on the vision tasks alone.
|
|
270
317
|
"""
|
|
271
|
-
|
|
318
|
+
specs = load_tasks(None)
|
|
272
319
|
free = set(free_models())
|
|
273
320
|
best, best_key = None, None
|
|
274
321
|
for model in available_models():
|
|
275
322
|
if model in free:
|
|
276
323
|
continue
|
|
277
|
-
|
|
324
|
+
mine = tasks_for(model, specs)
|
|
325
|
+
if not mine:
|
|
326
|
+
continue
|
|
327
|
+
outputs = [p for p in (t["dir"] / "output" / f"{model}.md" for t in mine) if p.exists()]
|
|
278
328
|
if not outputs:
|
|
279
329
|
return model # never scored: go first
|
|
280
|
-
key = (len(outputs) >=
|
|
330
|
+
key = (len(outputs) >= len(mine), # False (incomplete) sorts first
|
|
281
331
|
max(o.stat().st_mtime for o in outputs))
|
|
282
332
|
if best_key is None or key < best_key:
|
|
283
333
|
best, best_key = model, key
|
|
@@ -314,7 +364,44 @@ def pick_tasks(tasks: list[dict], model: str, n: int = PER_RUN) -> list[dict]:
|
|
|
314
364
|
return sorted(tasks, key=lambda s: answer(s).stat().st_mtime)[:n]
|
|
315
365
|
|
|
316
366
|
|
|
367
|
+
SHIPPED = HERE / "tasks"
|
|
368
|
+
SHIPPED_PREFIX = "vision-"
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def seed_shipped() -> None:
|
|
372
|
+
"""Copy the vision tasks this checkout ships (suite/tasks/vision-*: made
|
|
373
|
+
images with their answer keys, suite/vision_tasks.py) into the data
|
|
374
|
+
folder, and keep their prompt, key and images current there. The saved
|
|
375
|
+
answers (output/) are the install's and are never touched. Other tasks
|
|
376
|
+
are the install's own and are not shipped this way."""
|
|
377
|
+
if SHIPPED.resolve() == TASKS.resolve():
|
|
378
|
+
return
|
|
379
|
+
for task in SHIPPED.glob(f"{SHIPPED_PREFIX}*/task.json"):
|
|
380
|
+
for src in [task, *sorted((task.parent / "fixtures").glob("*"))]:
|
|
381
|
+
dst = TASKS / src.relative_to(SHIPPED)
|
|
382
|
+
if not dst.exists() or dst.read_bytes() != src.read_bytes():
|
|
383
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
384
|
+
shutil.copyfile(src, dst)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
SETTINGS = DATA / "config" / "suite.json"
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def vision_held() -> str:
|
|
391
|
+
"""The day the vision tasks start, while it is still ahead: `vision_from`
|
|
392
|
+
(YYYY-MM-DD) in config/suite.json, else "". Until then no run picks them
|
|
393
|
+
(scheduled, onboarding, pace); `--task` still runs one. A hold, set at a
|
|
394
|
+
release so the first vision runs wait for a quiet hub."""
|
|
395
|
+
try:
|
|
396
|
+
day = str(json.loads(SETTINGS.read_text(encoding="utf-8")).get("vision_from") or "")
|
|
397
|
+
except (OSError, ValueError, AttributeError):
|
|
398
|
+
return ""
|
|
399
|
+
return day if day > dt.date.today().isoformat() else ""
|
|
400
|
+
|
|
401
|
+
|
|
317
402
|
def load_tasks(only: str | None) -> list[dict]:
|
|
403
|
+
seed_shipped()
|
|
404
|
+
held = "" if only else vision_held()
|
|
318
405
|
tasks = []
|
|
319
406
|
for path in sorted(TASKS.glob("*/task.json")):
|
|
320
407
|
spec = json.loads(path.read_text(encoding="utf-8"))
|
|
@@ -325,6 +412,8 @@ def load_tasks(only: str | None) -> list[dict]:
|
|
|
325
412
|
# A mined task is a draft until someone has read it (suite/mine.py).
|
|
326
413
|
if spec.get("draft") and not only:
|
|
327
414
|
continue
|
|
415
|
+
if held and spec.get("images"):
|
|
416
|
+
continue
|
|
328
417
|
tasks.append(spec)
|
|
329
418
|
if only and not tasks:
|
|
330
419
|
sys.exit(f"no task called {only!r}")
|
|
@@ -348,6 +437,23 @@ def build_prompt(spec: dict) -> str:
|
|
|
348
437
|
return "\n".join(parts)
|
|
349
438
|
|
|
350
439
|
|
|
440
|
+
MIME = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp"}
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def image_urls(spec: dict) -> list[str]:
|
|
444
|
+
"""A vision task's pictures (`images`, files in its fixtures/) as data:
|
|
445
|
+
URLs, the way the gateway passes a request's images on."""
|
|
446
|
+
import base64
|
|
447
|
+
urls = []
|
|
448
|
+
for name in spec.get("images", []):
|
|
449
|
+
path = spec["dir"] / "fixtures" / name
|
|
450
|
+
if not path.exists():
|
|
451
|
+
raise MissingAttachment(f"{spec['id']}: image not found: fixtures/{name}")
|
|
452
|
+
mime = MIME.get(path.suffix.lower(), "image/png")
|
|
453
|
+
urls.append(f"data:{mime};base64,{base64.b64encode(path.read_bytes()).decode('ascii')}")
|
|
454
|
+
return urls
|
|
455
|
+
|
|
456
|
+
|
|
351
457
|
# A model that was never allowed to answer has not answered badly, and a
|
|
352
458
|
# service that fell over has not answered at all. Reporting either as 0/N is a
|
|
353
459
|
# lie about the model — and reporting them as the SAME thing is a second lie,
|
|
@@ -810,12 +916,20 @@ def _child_env() -> dict:
|
|
|
810
916
|
|
|
811
917
|
|
|
812
918
|
def ask_once(binary: str, model: str, prompt: str,
|
|
813
|
-
timeout: int) -> tuple[str, float]:
|
|
919
|
+
timeout: int, images: list[str] | None = None) -> tuple[str, float]:
|
|
814
920
|
# BENCH_CLIENT=builtin: the same call without the `llm` subprocess, giving
|
|
815
921
|
# the same text for the same failure (metrics/client.py).
|
|
816
922
|
if str(ROOT / "metrics") not in sys.path:
|
|
817
923
|
sys.path.insert(0, str(ROOT / "metrics"))
|
|
818
924
|
import client
|
|
925
|
+
if images:
|
|
926
|
+
# Pictures go as the API's own image parts, through the built-in
|
|
927
|
+
# client whichever is chosen: `llm -m` reads a prompt, not parts.
|
|
928
|
+
import formats
|
|
929
|
+
content = [formats.text_part(prompt), *(formats.image(u) for u in images)]
|
|
930
|
+
res = client.chat(model, "", timeout=timeout, messages=[{"role": "user", "content": content}],
|
|
931
|
+
purpose="vision task")
|
|
932
|
+
return client.output(res, timeout), res["seconds"]
|
|
819
933
|
if client.backend(model) == "builtin":
|
|
820
934
|
res = client.chat(model, prompt, timeout=timeout)
|
|
821
935
|
return client.output(res, timeout), res["seconds"]
|
|
@@ -842,7 +956,7 @@ def ask_once(binary: str, model: str, prompt: str,
|
|
|
842
956
|
|
|
843
957
|
|
|
844
958
|
def ask(binary: str, model: str, prompt: str, timeout: int,
|
|
845
|
-
attempts: int = 3) -> tuple[str, float, int]:
|
|
959
|
+
attempts: int = 3, images: list[str] | None = None) -> tuple[str, float, int]:
|
|
846
960
|
"""Ask, retrying UPSTREAM FAILURES only.
|
|
847
961
|
|
|
848
962
|
A rate limit or an overloaded service is a fact about the afternoon, not
|
|
@@ -853,7 +967,7 @@ def ask(binary: str, model: str, prompt: str, timeout: int,
|
|
|
853
967
|
"""
|
|
854
968
|
total = 0.0
|
|
855
969
|
for attempt in range(1, attempts + 1):
|
|
856
|
-
body, seconds = ask_once(binary, model, prompt, timeout)
|
|
970
|
+
body, seconds = ask_once(binary, model, prompt, timeout, images)
|
|
857
971
|
total += seconds
|
|
858
972
|
kind = (outcome(body) or ("answered", ""))[0]
|
|
859
973
|
if kind != "error" or attempt == attempts:
|
|
@@ -895,7 +1009,7 @@ def main() -> int:
|
|
|
895
1009
|
if chosen is None:
|
|
896
1010
|
sys.exit("no :free model registered")
|
|
897
1011
|
args.models = [chosen]
|
|
898
|
-
tasks = pick_tasks(tasks, chosen)
|
|
1012
|
+
tasks = pick_tasks(tasks_for(chosen, tasks), chosen)
|
|
899
1013
|
print(f"--next-free chose {chosen}: {len(tasks)} tasks "
|
|
900
1014
|
f"(one model per run; the free tier is 50 calls a day, shared)")
|
|
901
1015
|
|
|
@@ -904,10 +1018,10 @@ def main() -> int:
|
|
|
904
1018
|
if chosen is None:
|
|
905
1019
|
sys.exit("no paid model registered")
|
|
906
1020
|
args.models = [chosen]
|
|
907
|
-
|
|
908
|
-
have =
|
|
909
|
-
tasks = pick_tasks(
|
|
910
|
-
print(f"--next chose {chosen} ({have}/{
|
|
1021
|
+
mine = tasks_for(chosen, tasks)
|
|
1022
|
+
have = sum((t["dir"] / "output" / f"{chosen}.md").exists() for t in mine)
|
|
1023
|
+
tasks = pick_tasks(mine, chosen)
|
|
1024
|
+
print(f"--next chose {chosen} ({have}/{len(mine)} tasks scored; "
|
|
911
1025
|
f"{len(tasks)} this run)")
|
|
912
1026
|
|
|
913
1027
|
models = args.models or [m for m in available_models()
|
|
@@ -919,10 +1033,20 @@ def main() -> int:
|
|
|
919
1033
|
ready = []
|
|
920
1034
|
for spec in tasks:
|
|
921
1035
|
try:
|
|
922
|
-
ready.append((spec, build_prompt(spec)))
|
|
1036
|
+
ready.append((spec, build_prompt(spec), image_urls(spec)))
|
|
923
1037
|
except MissingAttachment as exc:
|
|
924
1038
|
print(f"skipped {exc}")
|
|
925
|
-
tasks, prompts = [s for s, _ in ready], {s["id"]: p for s, p in ready}
|
|
1039
|
+
tasks, prompts = [s for s, _, _ in ready], {s["id"]: p for s, p, _ in ready}
|
|
1040
|
+
pictures = {s["id"]: u for s, _, u in ready}
|
|
1041
|
+
# Each pair only where the task is the model's (applies): pictures to a
|
|
1042
|
+
# model that takes them, text and code to one that is not images only.
|
|
1043
|
+
skip = images_only()
|
|
1044
|
+
seeing = {m: sees_images(m) for m in models} if any(pictures.values()) else {}
|
|
1045
|
+
pairs = {(s["id"], m) for s in tasks for m in models if applies(s, m, skip, seeing)}
|
|
1046
|
+
left = len(tasks) * len(models) - len(pairs)
|
|
1047
|
+
if left:
|
|
1048
|
+
print(f"{left} task/model pair(s) left out: vision tasks go to models that take images,"
|
|
1049
|
+
" text and code tasks to models that are not vision or image only")
|
|
926
1050
|
|
|
927
1051
|
if args.dry_run:
|
|
928
1052
|
for spec in tasks:
|
|
@@ -939,16 +1063,20 @@ def main() -> int:
|
|
|
939
1063
|
|
|
940
1064
|
for spec in tasks:
|
|
941
1065
|
prompt = prompts[spec["id"]]
|
|
1066
|
+
if not any((spec["id"], m) in pairs for m in models):
|
|
1067
|
+
continue
|
|
942
1068
|
print(f"\n## {spec['id']} — {spec['title']}")
|
|
943
1069
|
print(f" {len(prompt):,} chars, {len(spec.get('must_find', []))} findings\n")
|
|
944
1070
|
for model in models:
|
|
1071
|
+
if (spec["id"], model) not in pairs:
|
|
1072
|
+
continue
|
|
945
1073
|
# N attempts at the SAME question. The spread is the point: a
|
|
946
1074
|
# single sample is a measurement of one afternoon, and this suite
|
|
947
1075
|
# has been reporting those as though they were properties.
|
|
948
1076
|
takes = []
|
|
949
1077
|
for _ in range(max(1, args.repeat)):
|
|
950
1078
|
body, seconds, attempts = ask(binary, model, prompt,
|
|
951
|
-
args.timeout)
|
|
1079
|
+
args.timeout, images=pictures[spec["id"]])
|
|
952
1080
|
takes.append((score(spec, body), seconds, attempts, body))
|
|
953
1081
|
if args.repeat > 1:
|
|
954
1082
|
time.sleep(2)
|
|
Binary file
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "vision-bars",
|
|
3
|
+
"title": "Read a bar chart",
|
|
4
|
+
"shape": "vision",
|
|
5
|
+
"images": [
|
|
6
|
+
"bars.png"
|
|
7
|
+
],
|
|
8
|
+
"prompt": "Read the attached image and answer in exactly these lines, one per line, nothing else:\n\ntallest: <team>\nshortest: <team>\nweb: <Web's value>\nover_30: <how many teams are above 30, not counting 30 itself>\ntarget: <the target line's value>\n\nIf the image does not show something, write `not shown` for it. Do not guess.",
|
|
9
|
+
"min_answer_chars": 20,
|
|
10
|
+
"must_find": [
|
|
11
|
+
{
|
|
12
|
+
"regex": "(?m)^[^\\w\\n]*tallest[^\\w\\n]*:[ \\t*_`\\\"']*billing",
|
|
13
|
+
"label": "tallest: Billing"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"regex": "(?m)^[^\\w\\n]*shortest[^\\w\\n]*:[ \\t*_`\\\"']*data",
|
|
17
|
+
"label": "shortest: Data"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"regex": "(?m)^[^\\w\\n]*web[^\\w\\n]*:[ \\t*_`\\\"']*35\\b",
|
|
21
|
+
"label": "web: 35"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"regex": "(?m)^[^\\w\\n]*over[ _]30[^\\w\\n]*:[ \\t*_`\\\"']*2\\b",
|
|
25
|
+
"label": "over_30: 2"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"regex": "(?m)^[^\\w\\n]*target[^\\w\\n]*:[ \\t*_`\\\"']*(not shown|not visible|not present|not listed|not given|not stated|not on the|not in the|not provided|not included|none shown|n/?a\\b|none\\b|no such|does not show|doesn't show|cannot be (?:determined|answered|seen)|can't be (?:determined|answered|seen)|unanswerable)",
|
|
29
|
+
"label": "target: not shown"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"must_not_claim": [
|
|
33
|
+
{
|
|
34
|
+
"regex": "(?m)^[^\\w\\n]*target[^\\w\\n]*:[ \\t*_`\\\"']*\\d",
|
|
35
|
+
"label": "target: a value (the chart has no target line)"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"why": "Values read against gridlines every 5, labelled every 10; one count asks for care at the boundary.",
|
|
39
|
+
"made_by": "suite/vision_tasks.py"
|
|
40
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "vision-invoice",
|
|
3
|
+
"title": "Read an invoice turned on its side",
|
|
4
|
+
"shape": "vision",
|
|
5
|
+
"images": [
|
|
6
|
+
"invoice.png"
|
|
7
|
+
],
|
|
8
|
+
"prompt": "Read the attached image and answer in exactly these lines, one per line, nothing else:\n\ninvoice: <invoice number>\ndue: <YYYY-MM-DD>\namount_due: <amount>\nbill_to: <company>\npo_number: <purchase order number>\n\nIf the image does not show something, write `not shown` for it. Do not guess.",
|
|
9
|
+
"min_answer_chars": 20,
|
|
10
|
+
"must_find": [
|
|
11
|
+
{
|
|
12
|
+
"regex": "(?m)^[^\\w\\n]*invoice[^\\w\\n]*:[ \\t*_`\\\"']*inv-2026-0417\\b",
|
|
13
|
+
"label": "invoice: INV-2026-0417"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"regex": "(?m)^[^\\w\\n]*due[^\\w\\n]*:[ \\t*_`\\\"']*2026-05-02\\b",
|
|
17
|
+
"label": "due: 2026-05-02"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"regex": "(?m)^[^\\w\\n]*amount[ _]due[^\\w\\n]*:[ \\t*_`\\\"']*(eur|€)?\\s*1,?284\\.50\\b",
|
|
21
|
+
"label": "amount_due: EUR 1,284.50"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"regex": "(?m)^[^\\w\\n]*bill[ _]to[^\\w\\n]*:[ \\t*_`\\\"']*kestrel labs",
|
|
25
|
+
"label": "bill_to: Kestrel Labs BV"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"regex": "(?m)^[^\\w\\n]*po[ _]number[^\\w\\n]*:[ \\t*_`\\\"']*(not shown|not visible|not present|not listed|not given|not stated|not on the|not in the|not provided|not included|none shown|n/?a\\b|none\\b|no such|does not show|doesn't show|cannot be (?:determined|answered|seen)|can't be (?:determined|answered|seen)|unanswerable)",
|
|
29
|
+
"label": "po_number: not shown"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"must_not_claim": [
|
|
33
|
+
{
|
|
34
|
+
"regex": "(?m)^[^\\w\\n]*po[ _]number[^\\w\\n]*:[ \\t*_`\\\"']*\\d",
|
|
35
|
+
"label": "po_number: a value (the invoice has none)"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"why": "A page a scanner or a phone delivered sideways; two dates close together, only one asked for.",
|
|
39
|
+
"made_by": "suite/vision_tasks.py"
|
|
40
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "vision-label",
|
|
3
|
+
"title": "Read a graded card's label",
|
|
4
|
+
"shape": "vision",
|
|
5
|
+
"images": [
|
|
6
|
+
"label.png"
|
|
7
|
+
],
|
|
8
|
+
"prompt": "Read the attached image and answer in exactly these lines, one per line, nothing else:\n\ngrader: <company>\ngrade: <the number>\nyear: <year>\nnumber: <card number>\nplayer: <name>\ncert: <certification number>\nsurface: <surface subgrade>\nautograph: <is the card signed: yes or no>\n\nIf the image does not show something, write `not shown` for it. Do not guess.",
|
|
9
|
+
"min_answer_chars": 20,
|
|
10
|
+
"must_find": [
|
|
11
|
+
{
|
|
12
|
+
"regex": "(?m)^[^\\w\\n]*grader[^\\w\\n]*:[ \\t*_`\\\"']*northline",
|
|
13
|
+
"label": "grader: Northline"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"regex": "(?m)^[^\\w\\n]*grade[^\\w\\n]*:[ \\t*_`\\\"']*(mint\\s*)?9\\b(?!\\.)",
|
|
17
|
+
"label": "grade: 9"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"regex": "(?m)^[^\\w\\n]*year[^\\w\\n]*:[ \\t*_`\\\"']*2019\\b",
|
|
21
|
+
"label": "year: 2019"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"regex": "(?m)^[^\\w\\n]*number[^\\w\\n]*:[ \\t*_`\\\"']*#?\\s*148\\b",
|
|
25
|
+
"label": "number: 148"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"regex": "(?m)^[^\\w\\n]*player[^\\w\\n]*:[ \\t*_`\\\"']*maren okafor",
|
|
29
|
+
"label": "player: Maren Okafor"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"regex": "(?m)^[^\\w\\n]*cert[^\\w\\n]*:[ \\t*_`\\\"']*#?\\s*40731958\\b",
|
|
33
|
+
"label": "cert: 40731958"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"regex": "(?m)^[^\\w\\n]*surface[^\\w\\n]*:[ \\t*_`\\\"']*8\\.5\\b",
|
|
37
|
+
"label": "surface: 8.5"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"regex": "(?m)^[^\\w\\n]*autograph[^\\w\\n]*:[ \\t*_`\\\"']*(no\\b|not shown|not visible|not present|not listed|not given|not stated|not on the|not in the|not provided|not included|none shown|n/?a\\b|none\\b|no such|does not show|doesn't show|cannot be (?:determined|answered|seen)|can't be (?:determined|answered|seen)|unanswerable)",
|
|
41
|
+
"label": "autograph: no, or not shown"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"must_not_claim": [
|
|
45
|
+
{
|
|
46
|
+
"regex": "(?m)^[^\\w\\n]*autograph[^\\w\\n]*:[ \\t*_`\\\"']*(yes|signed|auto)",
|
|
47
|
+
"label": "autograph: yes (the label says nothing of one)"
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"why": "A label read field by field; the cert number must be exact to the digit.",
|
|
51
|
+
"made_by": "suite/vision_tasks.py"
|
|
52
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "vision-receipt",
|
|
3
|
+
"title": "Read a receipt",
|
|
4
|
+
"shape": "vision",
|
|
5
|
+
"images": [
|
|
6
|
+
"receipt.png"
|
|
7
|
+
],
|
|
8
|
+
"prompt": "Read the attached image and answer in exactly these lines, one per line, nothing else:\n\nstore: <name>\ndate: <YYYY-MM-DD>\nitems: <how many line items were bought>\npriciest: <the most expensive item>\ntotal: <amount paid>\ntip: <tip amount>\n\nIf the image does not show something, write `not shown` for it. Do not guess.",
|
|
9
|
+
"min_answer_chars": 20,
|
|
10
|
+
"must_find": [
|
|
11
|
+
{
|
|
12
|
+
"regex": "(?m)^[^\\w\\n]*store[^\\w\\n]*:[ \\t*_`\\\"']*harbor lane",
|
|
13
|
+
"label": "store: Harbor Lane Grocery"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"regex": "(?m)^[^\\w\\n]*date[^\\w\\n]*:[ \\t*_`\\\"']*2026-03-14\\b",
|
|
17
|
+
"label": "date: 2026-03-14"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"regex": "(?m)^[^\\w\\n]*items[^\\w\\n]*:[ \\t*_`\\\"']*4\\b",
|
|
21
|
+
"label": "items: 4"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"regex": "(?m)^[^\\w\\n]*priciest[^\\w\\n]*:[ \\t*_`\\\"']*cheddar",
|
|
25
|
+
"label": "priciest: Cheddar 200g"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"regex": "(?m)^[^\\w\\n]*total[^\\w\\n]*:[ \\t*_`\\\"']*\\$?\\s*22\\.17\\b",
|
|
29
|
+
"label": "total: 22.17"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"regex": "(?m)^[^\\w\\n]*tip[^\\w\\n]*:[ \\t*_`\\\"']*(not shown|not visible|not present|not listed|not given|not stated|not on the|not in the|not provided|not included|none shown|n/?a\\b|none\\b|no such|does not show|doesn't show|cannot be (?:determined|answered|seen)|can't be (?:determined|answered|seen)|unanswerable)",
|
|
33
|
+
"label": "tip: not shown"
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"must_not_claim": [
|
|
37
|
+
{
|
|
38
|
+
"regex": "(?m)^[^\\w\\n]*tip[^\\w\\n]*:[ \\t*_`\\\"']*\\$?\\s*\\d",
|
|
39
|
+
"label": "tip: a value (the receipt shows none)"
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"why": "Printed text, a date written differently from how it is asked for, a count and a maximum.",
|
|
43
|
+
"made_by": "suite/vision_tasks.py"
|
|
44
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "vision-screen",
|
|
3
|
+
"title": "Read a settings screen",
|
|
4
|
+
"shape": "vision",
|
|
5
|
+
"images": [
|
|
6
|
+
"screen.png"
|
|
7
|
+
],
|
|
8
|
+
"prompt": "Read the attached image and answer in exactly these lines, one per line, nothing else:\n\ntoggles_on: <every switch that is on>\nconflict: <the option chosen>\ndisabled: <the button that cannot be pressed>\nerror_code: <code>\naccount: <the signed-in email>\n\nIf the image does not show something, write `not shown` for it. Do not guess.",
|
|
9
|
+
"min_answer_chars": 20,
|
|
10
|
+
"must_find": [
|
|
11
|
+
{
|
|
12
|
+
"regex": "(?m)^[^\\w\\n]*toggles[ _]on[^\\w\\n]*:[ \\t*_`\\\"']*.*back up photos",
|
|
13
|
+
"label": "toggles_on: Back up photos"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"regex": "(?m)^[^\\w\\n]*toggles[ _]on[^\\w\\n]*:[ \\t*_`\\\"']*.*notify on conflicts",
|
|
17
|
+
"label": "toggles_on: Notify on conflicts"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"regex": "(?m)^[^\\w\\n]*conflict[^\\w\\n]*:[ \\t*_`\\\"']*keep both",
|
|
21
|
+
"label": "conflict: Keep both"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"regex": "(?m)^[^\\w\\n]*disabled[^\\w\\n]*:[ \\t*_`\\\"']*sync now",
|
|
25
|
+
"label": "disabled: Sync now"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"regex": "(?m)^[^\\w\\n]*error[ _]code[^\\w\\n]*:[ \\t*_`\\\"']*e-?213\\b",
|
|
29
|
+
"label": "error_code: E-213"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"regex": "(?m)^[^\\w\\n]*account[^\\w\\n]*:[ \\t*_`\\\"']*(not shown|not visible|not present|not listed|not given|not stated|not on the|not in the|not provided|not included|none shown|n/?a\\b|none\\b|no such|does not show|doesn't show|cannot be (?:determined|answered|seen)|can't be (?:determined|answered|seen)|unanswerable)",
|
|
33
|
+
"label": "account: not shown"
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"must_not_claim": [
|
|
37
|
+
{
|
|
38
|
+
"regex": "(?m)^[^\\w\\n]*toggles[ _]on[^\\w\\n]*:[ \\t*_`\\\"']*.*(mobile data|battery)",
|
|
39
|
+
"label": "toggles_on: a switch that is off"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"regex": "(?m)^[^\\w\\n]*account[^\\w\\n]*:[ \\t*_`\\\"']*\\S+@\\S+",
|
|
43
|
+
"label": "account: an email (the screen shows none)"
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"why": "A screen's state: which switches are on, which option is picked, which button is greyed out.",
|
|
47
|
+
"made_by": "suite/vision_tasks.py"
|
|
48
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "vision-shapes",
|
|
3
|
+
"title": "Count shapes and say where one is",
|
|
4
|
+
"shape": "vision",
|
|
5
|
+
"images": [
|
|
6
|
+
"shapes.png"
|
|
7
|
+
],
|
|
8
|
+
"prompt": "Read the attached image and answer in exactly these lines, one per line, nothing else:\n\nblue_circles: <how many>\nred_squares: <how many>\ngreen_triangles: <how many>\ntop_right: <the shape in the top-right corner, with its colour>\npurple_shapes: <how many>\n\nIf the image does not show something, write `not shown` for it. Do not guess.",
|
|
9
|
+
"min_answer_chars": 20,
|
|
10
|
+
"must_find": [
|
|
11
|
+
{
|
|
12
|
+
"regex": "(?m)^[^\\w\\n]*blue[ _]circles[^\\w\\n]*:[ \\t*_`\\\"']*6\\b",
|
|
13
|
+
"label": "blue_circles: 6"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"regex": "(?m)^[^\\w\\n]*red[ _]squares[^\\w\\n]*:[ \\t*_`\\\"']*3\\b",
|
|
17
|
+
"label": "red_squares: 3"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"regex": "(?m)^[^\\w\\n]*green[ _]triangles[^\\w\\n]*:[ \\t*_`\\\"']*2\\b",
|
|
21
|
+
"label": "green_triangles: 2"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"regex": "(?m)^[^\\w\\n]*top[ _]right[^\\w\\n]*:[ \\t*_`\\\"']*.*star",
|
|
25
|
+
"label": "top_right: a yellow star"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"regex": "(?m)^[^\\w\\n]*purple[ _]shapes[^\\w\\n]*:[ \\t*_`\\\"']*(0\\b|none\\b|zero\\b|no\\b|not shown|not visible|not present|not listed|not given|not stated|not on the|not in the|not provided|not included|none shown|n/?a\\b|none\\b|no such|does not show|doesn't show|cannot be (?:determined|answered|seen)|can't be (?:determined|answered|seen)|unanswerable)",
|
|
29
|
+
"label": "purple_shapes: 0"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"must_not_claim": [
|
|
33
|
+
{
|
|
34
|
+
"regex": "(?m)^[^\\w\\n]*purple[ _]shapes[^\\w\\n]*:[ \\t*_`\\\"']*[1-9]",
|
|
35
|
+
"label": "purple_shapes: a count above 0 (there are none)"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"why": "Counting to six without losing one, and a corner told apart from its neighbours.",
|
|
39
|
+
"made_by": "suite/vision_tasks.py"
|
|
40
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "vision-table",
|
|
3
|
+
"title": "Read a table",
|
|
4
|
+
"shape": "vision",
|
|
5
|
+
"images": [
|
|
6
|
+
"table.png"
|
|
7
|
+
],
|
|
8
|
+
"prompt": "Read the attached image and answer in exactly these lines, one per line, nothing else:\n\nsouth_q3: <number>\nwest_q1: <number>\ncentral_q3: <number>\nq4_highest: <the region with the most units in Q4>\nnortheast_q2: <number>\n\nIf the image does not show something, write `not shown` for it. Do not guess.",
|
|
9
|
+
"min_answer_chars": 20,
|
|
10
|
+
"must_find": [
|
|
11
|
+
{
|
|
12
|
+
"regex": "(?m)^[^\\w\\n]*south[ _]q3[^\\w\\n]*:[ \\t*_`\\\"']*1,?120\\b",
|
|
13
|
+
"label": "south_q3: 1,120"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"regex": "(?m)^[^\\w\\n]*west[ _]q1[^\\w\\n]*:[ \\t*_`\\\"']*870\\b",
|
|
17
|
+
"label": "west_q1: 870"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"regex": "(?m)^[^\\w\\n]*central[ _]q3[^\\w\\n]*:[ \\t*_`\\\"']*1,?098\\b",
|
|
21
|
+
"label": "central_q3: 1,098"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"regex": "(?m)^[^\\w\\n]*q4[ _]highest[^\\w\\n]*:[ \\t*_`\\\"']*east",
|
|
25
|
+
"label": "q4_highest: East"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"regex": "(?m)^[^\\w\\n]*northeast[ _]q2[^\\w\\n]*:[ \\t*_`\\\"']*(not shown|not visible|not present|not listed|not given|not stated|not on the|not in the|not provided|not included|none shown|n/?a\\b|none\\b|no such|does not show|doesn't show|cannot be (?:determined|answered|seen)|can't be (?:determined|answered|seen)|unanswerable)",
|
|
29
|
+
"label": "northeast_q2: not shown"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"must_not_claim": [
|
|
33
|
+
{
|
|
34
|
+
"regex": "(?m)^[^\\w\\n]*northeast[ _]q2[^\\w\\n]*:[ \\t*_`\\\"']*\\d",
|
|
35
|
+
"label": "northeast_q2: a value (there is no Northeast row)"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"why": "Cells found by row and column; Central's Q3 dips, so a value from the wrong column shows.",
|
|
39
|
+
"made_by": "suite/vision_tasks.py"
|
|
40
|
+
}
|