@struktur/benchmarks 2.6.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/LICENSE +110 -0
- package/README.md +108 -0
- package/dist/cache.d.ts +5 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +961 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/datasets/index.d.ts +5 -0
- package/dist/datasets/index.d.ts.map +1 -0
- package/dist/datasets/longdoc.d.ts +34 -0
- package/dist/datasets/longdoc.d.ts.map +1 -0
- package/dist/datasets/source.d.ts +48 -0
- package/dist/datasets/source.d.ts.map +1 -0
- package/dist/datasets/sroie.d.ts +14 -0
- package/dist/datasets/sroie.d.ts.map +1 -0
- package/dist/datasets/synthetic.d.ts +27 -0
- package/dist/datasets/synthetic.d.ts.map +1 -0
- package/dist/datasets.js +392 -0
- package/dist/datasets.js.map +1 -0
- package/dist/expose/gold-score.d.ts +15 -0
- package/dist/expose/gold-score.d.ts.map +1 -0
- package/dist/expose/gold.d.ts +8 -0
- package/dist/expose/gold.d.ts.map +1 -0
- package/dist/expose/instructions.d.ts +2 -0
- package/dist/expose/instructions.d.ts.map +1 -0
- package/dist/expose/metric.d.ts +7 -0
- package/dist/expose/metric.d.ts.map +1 -0
- package/dist/expose/schema.d.ts +304 -0
- package/dist/expose/schema.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +686 -0
- package/dist/index.js.map +1 -0
- package/dist/pricing.d.ts +15 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/report.d.ts +6 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/runner.d.ts +81 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/scoring/normalize.d.ts +20 -0
- package/dist/scoring/normalize.d.ts.map +1 -0
- package/dist/scoring/score.d.ts +39 -0
- package/dist/scoring/score.d.ts.map +1 -0
- package/dist/tracks.d.ts +16 -0
- package/dist/tracks.d.ts.map +1 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +40 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,961 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/config.ts
|
|
13
|
+
var config_exports = {};
|
|
14
|
+
__export(config_exports, {
|
|
15
|
+
BENCHMARK_MODEL: () => BENCHMARK_MODEL,
|
|
16
|
+
defaultDatasetDir: () => defaultDatasetDir,
|
|
17
|
+
defaultResultsDir: () => defaultResultsDir
|
|
18
|
+
});
|
|
19
|
+
var BENCHMARK_MODEL, defaultDatasetDir, defaultResultsDir;
|
|
20
|
+
var init_config = __esm({
|
|
21
|
+
"src/config.ts"() {
|
|
22
|
+
"use strict";
|
|
23
|
+
BENCHMARK_MODEL = "openrouter/deepseek/deepseek-v4-flash-vision-exp";
|
|
24
|
+
defaultDatasetDir = () => process.env.STRUKTUR_BENCHMARKS_DIR ?? `${process.env.HOME ?? "."}/.struktur/benchmarks`;
|
|
25
|
+
defaultResultsDir = () => `${defaultDatasetDir()}/results`;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// src/cli.ts
|
|
30
|
+
import { resolve } from "path";
|
|
31
|
+
|
|
32
|
+
// src/datasets/source.ts
|
|
33
|
+
init_config();
|
|
34
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
35
|
+
import path from "path";
|
|
36
|
+
var readCachedCases = async (file) => {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
39
|
+
} catch {
|
|
40
|
+
return void 0;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
async function loadDataset(dataset, options = {}) {
|
|
44
|
+
const base = path.join(options.cacheDir ?? defaultDatasetDir(), dataset.id, dataset.version);
|
|
45
|
+
const casesFile = path.join(base, "cases.json");
|
|
46
|
+
const rawDir = path.join(base, "raw");
|
|
47
|
+
const limit = options.limit;
|
|
48
|
+
const offset = options.offset ?? 0;
|
|
49
|
+
let cases = options.force ? void 0 : await readCachedCases(casesFile);
|
|
50
|
+
if (!cases) {
|
|
51
|
+
if (dataset.source.kind === "manual") {
|
|
52
|
+
const hint = dataset.source.hint ?? `download the dataset into ${rawDir}`;
|
|
53
|
+
throw new Error(`Dataset "${dataset.id}" requires manual download. ${hint}`);
|
|
54
|
+
}
|
|
55
|
+
await mkdir(rawDir, { recursive: true });
|
|
56
|
+
await dataset.download(rawDir, options.fetchImpl);
|
|
57
|
+
cases = await dataset.convert(rawDir);
|
|
58
|
+
await mkdir(base, { recursive: true });
|
|
59
|
+
await writeFile(casesFile, JSON.stringify(cases, null, 2), "utf8");
|
|
60
|
+
await writeFile(
|
|
61
|
+
path.join(base, "manifest.json"),
|
|
62
|
+
JSON.stringify(
|
|
63
|
+
{
|
|
64
|
+
id: dataset.id,
|
|
65
|
+
version: dataset.version,
|
|
66
|
+
license: dataset.license,
|
|
67
|
+
attribution: dataset.attribution,
|
|
68
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
69
|
+
count: cases.length
|
|
70
|
+
},
|
|
71
|
+
null,
|
|
72
|
+
2
|
|
73
|
+
),
|
|
74
|
+
"utf8"
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return cases.slice(offset, limit !== void 0 ? offset + limit : void 0);
|
|
78
|
+
}
|
|
79
|
+
var DEFAULT_HUB_BASE = "https://datasets-server.huggingface.co";
|
|
80
|
+
async function downloadHubRows(source, fetchImpl = fetch) {
|
|
81
|
+
const base = source.baseUrl ?? DEFAULT_HUB_BASE;
|
|
82
|
+
const repo = encodeURIComponent(source.repo);
|
|
83
|
+
const config = source.config ?? "default";
|
|
84
|
+
const split = source.split ?? "train";
|
|
85
|
+
const pageSize = 100;
|
|
86
|
+
const rows = [];
|
|
87
|
+
let offset = 0;
|
|
88
|
+
for (; ; ) {
|
|
89
|
+
const url = `${base}/rows?dataset=${repo}&config=${config}&split=${split}&offset=${offset}&length=${pageSize}`;
|
|
90
|
+
const res = await fetchImpl(url);
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
throw new Error(`Hugging Face datasets-server error ${res.status} for ${url}`);
|
|
93
|
+
}
|
|
94
|
+
const body = await res.json();
|
|
95
|
+
const page = body.rows ?? [];
|
|
96
|
+
if (page.length === 0) break;
|
|
97
|
+
rows.push(...page.map((r) => r.row));
|
|
98
|
+
if (page.length < pageSize) break;
|
|
99
|
+
offset += page.length;
|
|
100
|
+
}
|
|
101
|
+
return rows;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/datasets/sroie.ts
|
|
105
|
+
var REPO = "darentang/sroie";
|
|
106
|
+
var CONFIG = "sroie";
|
|
107
|
+
var LABELS = [
|
|
108
|
+
"O",
|
|
109
|
+
"B-COMPANY",
|
|
110
|
+
"I-COMPANY",
|
|
111
|
+
"B-DATE",
|
|
112
|
+
"I-DATE",
|
|
113
|
+
"B-ADDRESS",
|
|
114
|
+
"I-ADDRESS",
|
|
115
|
+
"B-TOTAL",
|
|
116
|
+
"I-TOTAL"
|
|
117
|
+
];
|
|
118
|
+
var SCHEMA = {
|
|
119
|
+
type: "object",
|
|
120
|
+
properties: {
|
|
121
|
+
company: { type: ["string", "null"] },
|
|
122
|
+
date: { type: ["string", "null"] },
|
|
123
|
+
address: { type: ["string", "null"] },
|
|
124
|
+
total: { type: ["string", "null"] }
|
|
125
|
+
},
|
|
126
|
+
additionalProperties: false
|
|
127
|
+
};
|
|
128
|
+
function reconstructEntities(words, tags) {
|
|
129
|
+
const out = {
|
|
130
|
+
company: null,
|
|
131
|
+
date: null,
|
|
132
|
+
address: null,
|
|
133
|
+
total: null
|
|
134
|
+
};
|
|
135
|
+
let current = null;
|
|
136
|
+
let buffer = [];
|
|
137
|
+
const flush = () => {
|
|
138
|
+
if (current && buffer.length > 0) {
|
|
139
|
+
out[current.toLowerCase()] = buffer.join(" ");
|
|
140
|
+
}
|
|
141
|
+
current = null;
|
|
142
|
+
buffer = [];
|
|
143
|
+
};
|
|
144
|
+
for (let i = 0; i < words.length; i++) {
|
|
145
|
+
const tag = LABELS[tags[i] ?? 0] ?? "O";
|
|
146
|
+
if (tag.startsWith("B-")) {
|
|
147
|
+
flush();
|
|
148
|
+
current = tag.slice(2);
|
|
149
|
+
buffer = [words[i] ?? ""];
|
|
150
|
+
} else if (tag.startsWith("I-") && current === tag.slice(2)) {
|
|
151
|
+
buffer.push(words[i] ?? "");
|
|
152
|
+
} else {
|
|
153
|
+
flush();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
flush();
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
var imageUrl = (imagePath) => `https://huggingface.co/datasets/${REPO}/resolve/main/${imagePath}`;
|
|
160
|
+
var imageArtifact = (id, url) => ({
|
|
161
|
+
id: `artifact-${id}`,
|
|
162
|
+
type: "image",
|
|
163
|
+
raw: async () => Buffer.from(""),
|
|
164
|
+
contents: [{ media: [{ type: "image", url }] }]
|
|
165
|
+
});
|
|
166
|
+
function sroieToCases(rows) {
|
|
167
|
+
const cases = [];
|
|
168
|
+
for (const row of rows) {
|
|
169
|
+
const words = row.words ?? [];
|
|
170
|
+
const tags = row.ner_tags ?? [];
|
|
171
|
+
const imagePath = row.image_path;
|
|
172
|
+
if (!imagePath) continue;
|
|
173
|
+
const gold = reconstructEntities(words, tags);
|
|
174
|
+
const id = row.id ?? imagePath;
|
|
175
|
+
const url = imageUrl(imagePath);
|
|
176
|
+
const text = words.join(" ");
|
|
177
|
+
cases.push({
|
|
178
|
+
id: `sroie-${id}`,
|
|
179
|
+
schema: SCHEMA,
|
|
180
|
+
gold,
|
|
181
|
+
artifacts: [imageArtifact(id, url)],
|
|
182
|
+
tracks: ["text", "text+embedded"],
|
|
183
|
+
artifactsByTrack: {
|
|
184
|
+
text: [
|
|
185
|
+
{
|
|
186
|
+
id: `artifact-${id}-text`,
|
|
187
|
+
type: "text",
|
|
188
|
+
raw: async () => Buffer.from(text, "utf8"),
|
|
189
|
+
contents: [{ text }]
|
|
190
|
+
}
|
|
191
|
+
],
|
|
192
|
+
"text+embedded": [imageArtifact(id, url)]
|
|
193
|
+
},
|
|
194
|
+
metrics: { default: "normalized" },
|
|
195
|
+
source: {
|
|
196
|
+
dataset: "sroie",
|
|
197
|
+
license: "ICDAR 2019 SROIE (research use)",
|
|
198
|
+
url: "https://github.com/zzzDavid/ICDAR-2019-SROIE",
|
|
199
|
+
attribution: "ICDAR 2019 SROIE (Huang et al., 2019)"
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return cases;
|
|
204
|
+
}
|
|
205
|
+
var download = async (rawDir, fetchImpl) => {
|
|
206
|
+
const { writeFile: writeFile4 } = await import("fs/promises");
|
|
207
|
+
const path4 = await import("path");
|
|
208
|
+
const all = [];
|
|
209
|
+
for (const split of ["train", "test"]) {
|
|
210
|
+
const rows = await downloadHubRows(
|
|
211
|
+
{ kind: "hub", repo: REPO, config: CONFIG, split },
|
|
212
|
+
fetchImpl
|
|
213
|
+
);
|
|
214
|
+
all.push(...rows);
|
|
215
|
+
}
|
|
216
|
+
await writeFile4(path4.join(rawDir, "rows.json"), JSON.stringify(all), "utf8");
|
|
217
|
+
};
|
|
218
|
+
var convert = async (rawDir) => {
|
|
219
|
+
const { readFile: readFile3 } = await import("fs/promises");
|
|
220
|
+
const path4 = await import("path");
|
|
221
|
+
const rows = JSON.parse(await readFile3(path4.join(rawDir, "rows.json"), "utf8"));
|
|
222
|
+
return sroieToCases(rows);
|
|
223
|
+
};
|
|
224
|
+
var sroie = {
|
|
225
|
+
id: "sroie",
|
|
226
|
+
version: "1",
|
|
227
|
+
source: { kind: "hub", repo: REPO, config: CONFIG },
|
|
228
|
+
license: "ICDAR 2019 SROIE (research use)",
|
|
229
|
+
attribution: "ICDAR 2019 SROIE (Huang et al., 2019)",
|
|
230
|
+
download,
|
|
231
|
+
convert
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// src/types.ts
|
|
235
|
+
var TRACKS = [
|
|
236
|
+
"text",
|
|
237
|
+
"text+embedded",
|
|
238
|
+
"text+screenshots",
|
|
239
|
+
"text+embedded+screenshots"
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
// src/index.ts
|
|
243
|
+
init_config();
|
|
244
|
+
|
|
245
|
+
// src/pricing.ts
|
|
246
|
+
var PRICING = {
|
|
247
|
+
// OpenRouter: $0.22/M input, $0.66/M output
|
|
248
|
+
"openrouter/deepseek/deepseek-v4-flash-vision-exp": { input: 0.22, output: 0.66 },
|
|
249
|
+
// OpenAI via OpenRouter: $0.15/M input, $0.60/M output
|
|
250
|
+
"openrouter/openai/gpt-4o-mini": { input: 0.15, output: 0.6 }
|
|
251
|
+
};
|
|
252
|
+
var envOverride = (() => {
|
|
253
|
+
const raw = process.env.STRUKTUR_BENCHMARK_PRICES;
|
|
254
|
+
if (!raw) return void 0;
|
|
255
|
+
try {
|
|
256
|
+
return JSON.parse(raw);
|
|
257
|
+
} catch {
|
|
258
|
+
return void 0;
|
|
259
|
+
}
|
|
260
|
+
})();
|
|
261
|
+
var normalizeModel = (model) => model.replace(/#.*$/, "");
|
|
262
|
+
function pricingFor(model) {
|
|
263
|
+
const key = normalizeModel(model);
|
|
264
|
+
return envOverride?.[key] ?? PRICING[key];
|
|
265
|
+
}
|
|
266
|
+
function estimateCostUsd(model, usage) {
|
|
267
|
+
const p = pricingFor(model);
|
|
268
|
+
if (!p) return 0;
|
|
269
|
+
return usage.inputTokens / 1e6 * p.input + usage.outputTokens / 1e6 * p.output;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/tracks.ts
|
|
273
|
+
import { parsePdf } from "@struktur/sdk";
|
|
274
|
+
var trackToPdfOptions = (track) => {
|
|
275
|
+
switch (track) {
|
|
276
|
+
case "text":
|
|
277
|
+
return { includeImages: false, screenshots: false };
|
|
278
|
+
case "text+embedded":
|
|
279
|
+
return { includeImages: true, screenshots: false };
|
|
280
|
+
case "text+screenshots":
|
|
281
|
+
return { includeImages: false, screenshots: true };
|
|
282
|
+
case "text+embedded+screenshots":
|
|
283
|
+
return { includeImages: true, screenshots: true };
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
async function materializeTrack(artifacts, track) {
|
|
287
|
+
return Promise.all(
|
|
288
|
+
artifacts.map(async (artifact) => {
|
|
289
|
+
if (artifact.type !== "pdf") return artifact;
|
|
290
|
+
const buffer = await artifact.raw();
|
|
291
|
+
return parsePdf(buffer, trackToPdfOptions(track));
|
|
292
|
+
})
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/scoring/normalize.ts
|
|
297
|
+
var EMPTY = /* @__PURE__ */ Symbol("struktur.benchmarks.empty");
|
|
298
|
+
var stripDiacritics = (s) => s.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
299
|
+
var normalizeString = (s) => stripDiacritics(s.toLowerCase()).replace(/\s+/g, " ").trim();
|
|
300
|
+
var normalizeAggressive = (s) => stripDiacritics(s.toLowerCase()).replace(/[^a-z0-9]+/g, "");
|
|
301
|
+
function normalizeValue(value, metric) {
|
|
302
|
+
if (value === null || value === void 0) return EMPTY;
|
|
303
|
+
if (metric === "exact") {
|
|
304
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
305
|
+
return value;
|
|
306
|
+
}
|
|
307
|
+
return JSON.stringify(value);
|
|
308
|
+
}
|
|
309
|
+
if (metric === "semantic") {
|
|
310
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
311
|
+
return normalizeAggressive(typeof value === "string" ? value : JSON.stringify(value));
|
|
312
|
+
}
|
|
313
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
314
|
+
return normalizeString(typeof value === "string" ? value : JSON.stringify(value));
|
|
315
|
+
}
|
|
316
|
+
var RELATIVE_TOLERANCE = 1e-6;
|
|
317
|
+
function valuesEqual(a, b, metric) {
|
|
318
|
+
const na = normalizeValue(a, metric);
|
|
319
|
+
const nb = normalizeValue(b, metric);
|
|
320
|
+
if (na === EMPTY || nb === EMPTY) return na === EMPTY && nb === EMPTY;
|
|
321
|
+
if (metric === "tolerance" && typeof na === "number" && typeof nb === "number") {
|
|
322
|
+
const scale = Math.max(1, Math.abs(na), Math.abs(nb));
|
|
323
|
+
return Math.abs(na - nb) <= RELATIVE_TOLERANCE * scale;
|
|
324
|
+
}
|
|
325
|
+
return na === nb;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/scoring/score.ts
|
|
329
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
330
|
+
"the",
|
|
331
|
+
"a",
|
|
332
|
+
"an",
|
|
333
|
+
"and",
|
|
334
|
+
"or",
|
|
335
|
+
"of",
|
|
336
|
+
"to",
|
|
337
|
+
"in",
|
|
338
|
+
"on",
|
|
339
|
+
"at",
|
|
340
|
+
"is",
|
|
341
|
+
"are",
|
|
342
|
+
"it",
|
|
343
|
+
"this",
|
|
344
|
+
"that",
|
|
345
|
+
"with",
|
|
346
|
+
"for",
|
|
347
|
+
"as",
|
|
348
|
+
"by",
|
|
349
|
+
"from",
|
|
350
|
+
"be",
|
|
351
|
+
"was",
|
|
352
|
+
"were",
|
|
353
|
+
"und",
|
|
354
|
+
"der",
|
|
355
|
+
"die",
|
|
356
|
+
"das",
|
|
357
|
+
"ein",
|
|
358
|
+
"eine",
|
|
359
|
+
"einen",
|
|
360
|
+
"dem",
|
|
361
|
+
"den",
|
|
362
|
+
"mit",
|
|
363
|
+
"von",
|
|
364
|
+
"zur",
|
|
365
|
+
"zum",
|
|
366
|
+
"auf",
|
|
367
|
+
"im",
|
|
368
|
+
"in",
|
|
369
|
+
"am",
|
|
370
|
+
"ist",
|
|
371
|
+
"sind",
|
|
372
|
+
"werden",
|
|
373
|
+
"wird",
|
|
374
|
+
"des",
|
|
375
|
+
"sich",
|
|
376
|
+
"auch",
|
|
377
|
+
"nicht",
|
|
378
|
+
"als",
|
|
379
|
+
"bei",
|
|
380
|
+
"f\xFCr",
|
|
381
|
+
"\xFCber",
|
|
382
|
+
"aus",
|
|
383
|
+
"nach",
|
|
384
|
+
"einer",
|
|
385
|
+
"einem",
|
|
386
|
+
"eine",
|
|
387
|
+
"zu",
|
|
388
|
+
"unter",
|
|
389
|
+
"an",
|
|
390
|
+
"sowie",
|
|
391
|
+
"durch",
|
|
392
|
+
"alle",
|
|
393
|
+
"dass",
|
|
394
|
+
"dieser",
|
|
395
|
+
"diese",
|
|
396
|
+
"hier",
|
|
397
|
+
"sein",
|
|
398
|
+
"ihr",
|
|
399
|
+
"wir",
|
|
400
|
+
"man"
|
|
401
|
+
]);
|
|
402
|
+
var tokenizeProse = (s) => {
|
|
403
|
+
if (typeof s !== "string") return [];
|
|
404
|
+
return Array.from(
|
|
405
|
+
new Set(
|
|
406
|
+
s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((w) => w.length >= 3 && !STOPWORDS.has(w))
|
|
407
|
+
)
|
|
408
|
+
);
|
|
409
|
+
};
|
|
410
|
+
function scoreProse(gold, pred) {
|
|
411
|
+
const s = empty();
|
|
412
|
+
const gt = tokenizeProse(String(gold ?? ""));
|
|
413
|
+
const pt = tokenizeProse(String(pred ?? ""));
|
|
414
|
+
const gs = new Set(gt);
|
|
415
|
+
const ps = new Set(pt);
|
|
416
|
+
const tp = gt.filter((w) => ps.has(w)).length;
|
|
417
|
+
const fp = pt.filter((w) => !gs.has(w)).length;
|
|
418
|
+
const fn = gt.filter((w) => !ps.has(w)).length;
|
|
419
|
+
s.tp = tp;
|
|
420
|
+
s.fp = fp;
|
|
421
|
+
s.fn = fn;
|
|
422
|
+
s.totalGold = gt.length;
|
|
423
|
+
s.totalPred = pt.length;
|
|
424
|
+
return finalize(s);
|
|
425
|
+
}
|
|
426
|
+
var empty = () => ({
|
|
427
|
+
tp: 0,
|
|
428
|
+
fp: 0,
|
|
429
|
+
fn: 0,
|
|
430
|
+
totalGold: 0,
|
|
431
|
+
totalPred: 0,
|
|
432
|
+
precision: 1,
|
|
433
|
+
recall: 1,
|
|
434
|
+
f1: 1,
|
|
435
|
+
exactMatch: true,
|
|
436
|
+
fieldErrors: []
|
|
437
|
+
});
|
|
438
|
+
var finalize = (s) => {
|
|
439
|
+
s.precision = s.tp + s.fp === 0 ? 1 : s.tp / (s.tp + s.fp);
|
|
440
|
+
s.recall = s.tp + s.fn === 0 ? 1 : s.tp / (s.tp + s.fn);
|
|
441
|
+
s.f1 = s.precision + s.recall === 0 ? 0 : 2 * s.precision * s.recall / (s.precision + s.recall);
|
|
442
|
+
s.exactMatch = s.fp === 0 && s.fn === 0;
|
|
443
|
+
return s;
|
|
444
|
+
};
|
|
445
|
+
var merge = (into, child) => {
|
|
446
|
+
into.tp += child.tp;
|
|
447
|
+
into.fp += child.fp;
|
|
448
|
+
into.fn += child.fn;
|
|
449
|
+
into.totalGold += child.totalGold;
|
|
450
|
+
into.totalPred += child.totalPred;
|
|
451
|
+
into.fieldErrors.push(...child.fieldErrors);
|
|
452
|
+
};
|
|
453
|
+
var isLeaf = (v) => v === null || v === void 0 || typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
454
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
455
|
+
var leafCount = (v) => {
|
|
456
|
+
if (Array.isArray(v)) return v.reduce((n, item) => n + leafCount(item), 0);
|
|
457
|
+
if (isObject(v)) {
|
|
458
|
+
return Object.values(v).reduce((n, item) => n + leafCount(item), 0);
|
|
459
|
+
}
|
|
460
|
+
return 1;
|
|
461
|
+
};
|
|
462
|
+
var canonPath = (path4) => path4.replace(/\[[^\]]*\]/g, "[]");
|
|
463
|
+
var metricFor = (spec, path4) => (
|
|
464
|
+
// field overrides are keyed by the leaf field name (e.g. "area"), robust to
|
|
465
|
+
// concrete array keys in the path.
|
|
466
|
+
spec.fields?.[path4.split(".").pop()] ?? spec.default ?? "normalized"
|
|
467
|
+
);
|
|
468
|
+
var alignmentFor = (spec, path4) => spec.arrays?.find((a) => canonPath(a.path) === canonPath(path4)) ?? spec.arrays?.find((a) => canonPath(path4).endsWith(canonPath(a.path)));
|
|
469
|
+
var isSetPath = (spec, path4) => (spec.sets ?? []).some(
|
|
470
|
+
(s) => canonPath(s) === canonPath(path4) || canonPath(path4).endsWith(canonPath(s))
|
|
471
|
+
);
|
|
472
|
+
function groupByKey(items, key) {
|
|
473
|
+
const map = /* @__PURE__ */ new Map();
|
|
474
|
+
const keys = Array.isArray(key) ? key : [key];
|
|
475
|
+
for (const item of items) {
|
|
476
|
+
const k = keys.map((kf) => isObject(item) ? item[kf] : void 0).map((v) => String(normalizeValue(v, "normalized"))).join("|");
|
|
477
|
+
const arr = map.get(k) ?? [];
|
|
478
|
+
arr.push(item);
|
|
479
|
+
map.set(k, arr);
|
|
480
|
+
}
|
|
481
|
+
return map;
|
|
482
|
+
}
|
|
483
|
+
function scoreLeaf(path4, gold, pred, spec) {
|
|
484
|
+
const metric = metricFor(spec, path4);
|
|
485
|
+
if (metric === "prose" && (typeof gold === "string" || typeof pred === "string")) {
|
|
486
|
+
return scoreProse(gold, pred);
|
|
487
|
+
}
|
|
488
|
+
const s = empty();
|
|
489
|
+
s.totalGold = isLeaf(gold) ? 1 : leafCount(gold);
|
|
490
|
+
s.totalPred = isLeaf(pred) ? 1 : leafCount(pred);
|
|
491
|
+
if (valuesEqual(gold, pred, metric)) {
|
|
492
|
+
s.tp = isLeaf(gold) ? 1 : leafCount(gold);
|
|
493
|
+
return finalize(s);
|
|
494
|
+
}
|
|
495
|
+
const goldEmpty = gold === null || gold === void 0;
|
|
496
|
+
const predEmpty = pred === null || pred === void 0;
|
|
497
|
+
if (goldEmpty) s.fp = s.totalPred;
|
|
498
|
+
else if (predEmpty) s.fn = s.totalGold;
|
|
499
|
+
else {
|
|
500
|
+
s.fn = s.totalGold;
|
|
501
|
+
s.fp = s.totalPred;
|
|
502
|
+
}
|
|
503
|
+
s.fieldErrors.push({ path: path4, gold, pred, metric });
|
|
504
|
+
return finalize(s);
|
|
505
|
+
}
|
|
506
|
+
function scoreIndexed(path4, g, p, spec) {
|
|
507
|
+
const s = empty();
|
|
508
|
+
const len = Math.max(g.length, p.length);
|
|
509
|
+
for (let i = 0; i < len; i++) {
|
|
510
|
+
merge(s, scoreAt(`${path4}[${i}]`, g[i] ?? null, p[i] ?? null, spec));
|
|
511
|
+
}
|
|
512
|
+
return finalize(s);
|
|
513
|
+
}
|
|
514
|
+
function scoreAligned(path4, g, p, key, spec) {
|
|
515
|
+
const s = empty();
|
|
516
|
+
const gm = groupByKey(g, key);
|
|
517
|
+
const pm = groupByKey(p, key);
|
|
518
|
+
let matchedAny = false;
|
|
519
|
+
for (const [k, gItems] of gm) {
|
|
520
|
+
const pItems = pm.get(k);
|
|
521
|
+
if (!pItems || pItems.length === 0) {
|
|
522
|
+
for (const gi of gItems) {
|
|
523
|
+
s.fn += leafCount(gi);
|
|
524
|
+
s.totalGold += leafCount(gi);
|
|
525
|
+
}
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
matchedAny = true;
|
|
529
|
+
const pairs = Math.min(gItems.length, pItems.length);
|
|
530
|
+
for (let i = 0; i < pairs; i++) {
|
|
531
|
+
merge(s, scoreAt(`${path4}[${String(k)}]`, gItems[i], pItems[i], spec));
|
|
532
|
+
}
|
|
533
|
+
for (let i = pairs; i < gItems.length; i++) {
|
|
534
|
+
s.fn += leafCount(gItems[i]);
|
|
535
|
+
s.totalGold += leafCount(gItems[i]);
|
|
536
|
+
}
|
|
537
|
+
for (let i = pairs; i < pItems.length; i++) {
|
|
538
|
+
s.fp += leafCount(pItems[i]);
|
|
539
|
+
s.totalPred += leafCount(pItems[i]);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
for (const [k, pItems] of pm) {
|
|
543
|
+
if (gm.has(k)) continue;
|
|
544
|
+
for (const pi of pItems) {
|
|
545
|
+
s.fp += leafCount(pi);
|
|
546
|
+
s.totalPred += leafCount(pi);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (!matchedAny && g.length > 0 && p.length > 0) {
|
|
550
|
+
return scoreIndexed(path4, g, p, spec);
|
|
551
|
+
}
|
|
552
|
+
return finalize(s);
|
|
553
|
+
}
|
|
554
|
+
function scoreSet(path4, g, p, spec) {
|
|
555
|
+
const s = empty();
|
|
556
|
+
const goldKeyed = /* @__PURE__ */ new Map();
|
|
557
|
+
const predKeyed = /* @__PURE__ */ new Map();
|
|
558
|
+
const metric = metricFor(spec, canonPath(path4));
|
|
559
|
+
const bump = (map, v) => {
|
|
560
|
+
const k = String(normalizeValue(v, metric));
|
|
561
|
+
map.set(k, (map.get(k) ?? 0) + 1);
|
|
562
|
+
};
|
|
563
|
+
for (const v of g) bump(goldKeyed, v);
|
|
564
|
+
for (const v of p) bump(predKeyed, v);
|
|
565
|
+
s.totalGold = g.length;
|
|
566
|
+
s.totalPred = p.length;
|
|
567
|
+
for (const [k, gc] of goldKeyed) {
|
|
568
|
+
const pc = predKeyed.get(k) ?? 0;
|
|
569
|
+
const common = Math.min(gc, pc);
|
|
570
|
+
s.tp += common;
|
|
571
|
+
s.fn += gc - common;
|
|
572
|
+
s.fp += pc - common;
|
|
573
|
+
if (common < gc) {
|
|
574
|
+
s.fieldErrors.push({
|
|
575
|
+
path: path4,
|
|
576
|
+
gold: g.find((v) => String(normalizeValue(v, metric)) === k),
|
|
577
|
+
pred: null,
|
|
578
|
+
metric
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
for (const [k, pc] of predKeyed) {
|
|
583
|
+
const gc = goldKeyed.get(k) ?? 0;
|
|
584
|
+
if (pc > gc) {
|
|
585
|
+
s.fieldErrors.push({
|
|
586
|
+
path: path4,
|
|
587
|
+
gold: null,
|
|
588
|
+
pred: p.find((v) => String(normalizeValue(v, metric)) === k),
|
|
589
|
+
metric
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return finalize(s);
|
|
594
|
+
}
|
|
595
|
+
function scoreAt(path4, gold, pred, spec) {
|
|
596
|
+
if (isLeaf(gold) || isLeaf(pred)) {
|
|
597
|
+
return scoreLeaf(path4, gold, pred, spec);
|
|
598
|
+
}
|
|
599
|
+
if (Array.isArray(gold) || Array.isArray(pred)) {
|
|
600
|
+
const g = Array.isArray(gold) ? gold : [gold];
|
|
601
|
+
const p = Array.isArray(pred) ? pred : [pred];
|
|
602
|
+
if (isSetPath(spec, path4)) {
|
|
603
|
+
return scoreSet(path4, g, p, spec);
|
|
604
|
+
}
|
|
605
|
+
const align = alignmentFor(spec, path4);
|
|
606
|
+
return align ? scoreAligned(path4, g, p, align.key, spec) : scoreIndexed(path4, g, p, spec);
|
|
607
|
+
}
|
|
608
|
+
const s = empty();
|
|
609
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(gold), ...Object.keys(pred)]);
|
|
610
|
+
for (const key of keys) {
|
|
611
|
+
const childPath = path4 === "" ? key : `${path4}.${key}`;
|
|
612
|
+
const gv = gold[key] ?? null;
|
|
613
|
+
const pv = pred[key] ?? null;
|
|
614
|
+
merge(s, scoreAt(childPath, gv, pv, spec));
|
|
615
|
+
}
|
|
616
|
+
return finalize(s);
|
|
617
|
+
}
|
|
618
|
+
function scoreData(gold, pred, spec = {}) {
|
|
619
|
+
return scoreAt("", gold, pred, spec);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/runner.ts
|
|
623
|
+
import { extract, resolveModel, toJsonSchema } from "@struktur/sdk";
|
|
624
|
+
import {
|
|
625
|
+
simple,
|
|
626
|
+
parallel,
|
|
627
|
+
sequential,
|
|
628
|
+
parallelAutoMerge,
|
|
629
|
+
sequentialAutoMerge,
|
|
630
|
+
doublePass,
|
|
631
|
+
doublePassAutoMerge,
|
|
632
|
+
agent
|
|
633
|
+
} from "@struktur/sdk";
|
|
634
|
+
|
|
635
|
+
// src/cache.ts
|
|
636
|
+
import { createHash } from "crypto";
|
|
637
|
+
import { mkdir as mkdir2, readFile as readFile2, rename, writeFile as writeFile2 } from "fs/promises";
|
|
638
|
+
import path2 from "path";
|
|
639
|
+
var stableStringify = (v) => {
|
|
640
|
+
if (v === null) return "null";
|
|
641
|
+
if (Array.isArray(v)) return `[${v.map(stableStringify).join(",")}]`;
|
|
642
|
+
if (typeof v === "object") {
|
|
643
|
+
const rec = v;
|
|
644
|
+
const keys = Object.keys(rec).sort();
|
|
645
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(rec[k])}`).join(",")}}`;
|
|
646
|
+
}
|
|
647
|
+
return JSON.stringify(v) ?? String(v);
|
|
648
|
+
};
|
|
649
|
+
var hashKey = (input) => createHash("sha256").update(stableStringify(input)).digest("hex").slice(0, 16);
|
|
650
|
+
async function cacheGet(dir, key) {
|
|
651
|
+
try {
|
|
652
|
+
const raw = await readFile2(path2.join(dir, `${key}.json`), "utf8");
|
|
653
|
+
return JSON.parse(raw);
|
|
654
|
+
} catch {
|
|
655
|
+
return void 0;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async function cacheSet(dir, key, value) {
|
|
659
|
+
const file = path2.join(dir, `${key}.json`);
|
|
660
|
+
await mkdir2(dir, { recursive: true });
|
|
661
|
+
const tmp = `${file}.tmp`;
|
|
662
|
+
await writeFile2(tmp, JSON.stringify(value, null, 2), "utf8");
|
|
663
|
+
await rename(tmp, file);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/runner.ts
|
|
667
|
+
init_config();
|
|
668
|
+
var builtins = {
|
|
669
|
+
simple: (model, _spec, instructions) => simple({ model, outputInstructions: instructions }),
|
|
670
|
+
parallel: (model, _spec, instructions) => parallel({ model, mergeModel: model, chunkSize: 1e4, outputInstructions: instructions }),
|
|
671
|
+
sequential: (model, _spec, instructions) => sequential({ model, chunkSize: 1e4, outputInstructions: instructions }),
|
|
672
|
+
parallelAutoMerge: (model, _spec, instructions) => parallelAutoMerge({
|
|
673
|
+
model,
|
|
674
|
+
dedupeModel: model,
|
|
675
|
+
chunkSize: 1e4,
|
|
676
|
+
outputInstructions: instructions
|
|
677
|
+
}),
|
|
678
|
+
sequentialAutoMerge: (model, _spec, instructions) => sequentialAutoMerge({
|
|
679
|
+
model,
|
|
680
|
+
dedupeModel: model,
|
|
681
|
+
chunkSize: 1e4,
|
|
682
|
+
outputInstructions: instructions
|
|
683
|
+
}),
|
|
684
|
+
doublePass: (model, _spec, instructions) => doublePass({ model, mergeModel: model, chunkSize: 1e4, outputInstructions: instructions }),
|
|
685
|
+
doublePassAutoMerge: (model, _spec, instructions) => doublePassAutoMerge({
|
|
686
|
+
model,
|
|
687
|
+
dedupeModel: model,
|
|
688
|
+
chunkSize: 1e4,
|
|
689
|
+
outputInstructions: instructions
|
|
690
|
+
}),
|
|
691
|
+
agent: (_model, modelSpec, instructions) => {
|
|
692
|
+
const [provider, ...rest] = modelSpec.split("/");
|
|
693
|
+
const modelId = rest.join("/");
|
|
694
|
+
if (!provider || !modelId)
|
|
695
|
+
throw new Error(`Agent requires 'provider/model'. Got: ${modelSpec}`);
|
|
696
|
+
return agent({
|
|
697
|
+
provider,
|
|
698
|
+
modelId,
|
|
699
|
+
maxSteps: 50,
|
|
700
|
+
maxIterations: 1,
|
|
701
|
+
vision: true,
|
|
702
|
+
outputInstructions: instructions
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
function resolveStrategy(entry, model, modelSpec) {
|
|
707
|
+
if (typeof entry === "string") {
|
|
708
|
+
const factory = builtins[entry];
|
|
709
|
+
if (!factory) {
|
|
710
|
+
throw new Error(
|
|
711
|
+
`Unknown builtin strategy: ${entry}. Available: ${Object.keys(builtins).join(", ")}`
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
return { name: entry, strategy: factory(model, modelSpec) };
|
|
715
|
+
}
|
|
716
|
+
if (typeof entry === "function") {
|
|
717
|
+
const s = entry(model, modelSpec);
|
|
718
|
+
return { name: s.name, strategy: s };
|
|
719
|
+
}
|
|
720
|
+
if ("strategy" in entry) {
|
|
721
|
+
const inner = entry.strategy;
|
|
722
|
+
if (typeof inner === "string") {
|
|
723
|
+
const factory = builtins[inner];
|
|
724
|
+
if (!factory) throw new Error(`Unknown builtin strategy: ${inner}`);
|
|
725
|
+
return {
|
|
726
|
+
name: entry.label ?? inner,
|
|
727
|
+
strategy: factory(model, modelSpec, entry.instructions)
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
const s = inner(model, modelSpec, entry.instructions);
|
|
731
|
+
return { name: entry.label ?? s.name, strategy: s };
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
name: entry.name,
|
|
735
|
+
strategy: entry
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
var emptyUsage = () => ({ inputTokens: 0, outputTokens: 0, totalTokens: 0 });
|
|
739
|
+
async function runBenchmark(options) {
|
|
740
|
+
const modelSpec = options.model ?? BENCHMARK_MODEL;
|
|
741
|
+
const model = await resolveModel(modelSpec);
|
|
742
|
+
const cacheDir = options.cacheDir ?? defaultResultsDir();
|
|
743
|
+
const useCache = options.cache ?? true;
|
|
744
|
+
const variant = options.variant ?? "";
|
|
745
|
+
const strats = options.strategies.map((s) => resolveStrategy(s, model, modelSpec));
|
|
746
|
+
const cells = [];
|
|
747
|
+
for (const c of options.cases) {
|
|
748
|
+
const tracks = c.tracks ?? [...TRACKS];
|
|
749
|
+
for (const track of tracks) {
|
|
750
|
+
for (const { name, strategy } of strats) {
|
|
751
|
+
const cacheKey = hashKey({
|
|
752
|
+
caseId: c.id,
|
|
753
|
+
track,
|
|
754
|
+
strategy: name,
|
|
755
|
+
model: modelSpec,
|
|
756
|
+
variant,
|
|
757
|
+
schema: toJsonSchema(c.schema),
|
|
758
|
+
gold: c.gold
|
|
759
|
+
});
|
|
760
|
+
const cached = useCache ? await cacheGet(cacheDir, cacheKey) : void 0;
|
|
761
|
+
if (cached) {
|
|
762
|
+
cached.cached = true;
|
|
763
|
+
if (cached.costUsd === void 0)
|
|
764
|
+
cached.costUsd = estimateCostUsd(modelSpec, cached.usage);
|
|
765
|
+
cells.push(cached);
|
|
766
|
+
options.onCell?.({ caseId: c.id, track, strategy: name, cached: true });
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
options.onCell?.({ caseId: c.id, track, strategy: name, cached: false });
|
|
770
|
+
const artifacts = c.artifactsByTrack?.[track] ?? await materializeTrack(c.artifacts, track);
|
|
771
|
+
const start = Date.now();
|
|
772
|
+
let data;
|
|
773
|
+
let usage = emptyUsage();
|
|
774
|
+
let valid = true;
|
|
775
|
+
let error;
|
|
776
|
+
try {
|
|
777
|
+
const result = await extract({ artifacts, schema: c.schema, strategy });
|
|
778
|
+
data = result.data;
|
|
779
|
+
usage = result.usage;
|
|
780
|
+
if (result.error) {
|
|
781
|
+
valid = false;
|
|
782
|
+
error = result.error.message;
|
|
783
|
+
}
|
|
784
|
+
} catch (e) {
|
|
785
|
+
valid = false;
|
|
786
|
+
error = e.message;
|
|
787
|
+
data = null;
|
|
788
|
+
}
|
|
789
|
+
const cell = {
|
|
790
|
+
caseId: c.id,
|
|
791
|
+
track,
|
|
792
|
+
strategy: name,
|
|
793
|
+
model: modelSpec,
|
|
794
|
+
cached: false,
|
|
795
|
+
valid,
|
|
796
|
+
error,
|
|
797
|
+
score: scoreData(c.gold, c.transform ? c.transform(data) : data, c.metrics),
|
|
798
|
+
usage,
|
|
799
|
+
latencyMs: Date.now() - start,
|
|
800
|
+
costUsd: estimateCostUsd(modelSpec, usage),
|
|
801
|
+
data
|
|
802
|
+
};
|
|
803
|
+
if (useCache) await cacheSet(cacheDir, cacheKey, cell);
|
|
804
|
+
cells.push(cell);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return buildReport(modelSpec, cells, variant);
|
|
809
|
+
}
|
|
810
|
+
function buildReport(model, cells, variant) {
|
|
811
|
+
const summary = /* @__PURE__ */ new Map();
|
|
812
|
+
for (const cell of cells) {
|
|
813
|
+
const key = `${cell.strategy}|${cell.track}`;
|
|
814
|
+
let row = summary.get(key);
|
|
815
|
+
if (!row) {
|
|
816
|
+
row = {
|
|
817
|
+
strategy: cell.strategy,
|
|
818
|
+
track: cell.track,
|
|
819
|
+
cases: 0,
|
|
820
|
+
meanF1: 0,
|
|
821
|
+
meanPrecision: 0,
|
|
822
|
+
meanRecall: 0,
|
|
823
|
+
validityRate: 0,
|
|
824
|
+
exactMatchRate: 0,
|
|
825
|
+
totalInputTokens: 0,
|
|
826
|
+
totalOutputTokens: 0,
|
|
827
|
+
totalCostUsd: 0,
|
|
828
|
+
meanCostUsd: 0,
|
|
829
|
+
totalLatencyMs: 0,
|
|
830
|
+
meanLatencyMs: 0
|
|
831
|
+
};
|
|
832
|
+
summary.set(key, row);
|
|
833
|
+
}
|
|
834
|
+
row.cases++;
|
|
835
|
+
row.meanF1 += cell.score.f1;
|
|
836
|
+
row.meanPrecision += cell.score.precision;
|
|
837
|
+
row.meanRecall += cell.score.recall;
|
|
838
|
+
if (cell.valid) row.validityRate++;
|
|
839
|
+
if (cell.score.exactMatch) row.exactMatchRate++;
|
|
840
|
+
row.totalInputTokens += cell.usage.inputTokens;
|
|
841
|
+
row.totalOutputTokens += cell.usage.outputTokens;
|
|
842
|
+
row.totalCostUsd += cell.costUsd ?? estimateCostUsd(model, cell.usage);
|
|
843
|
+
row.totalLatencyMs += cell.latencyMs;
|
|
844
|
+
row.meanLatencyMs += cell.latencyMs;
|
|
845
|
+
}
|
|
846
|
+
for (const row of summary.values()) {
|
|
847
|
+
row.meanF1 /= row.cases;
|
|
848
|
+
row.meanPrecision /= row.cases;
|
|
849
|
+
row.meanRecall /= row.cases;
|
|
850
|
+
row.validityRate /= row.cases;
|
|
851
|
+
row.exactMatchRate /= row.cases;
|
|
852
|
+
row.meanCostUsd = row.cases ? row.totalCostUsd / row.cases : 0;
|
|
853
|
+
row.meanLatencyMs = row.cases ? row.meanLatencyMs / row.cases : 0;
|
|
854
|
+
}
|
|
855
|
+
return {
|
|
856
|
+
model,
|
|
857
|
+
variant,
|
|
858
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
859
|
+
cells,
|
|
860
|
+
summary: [...summary.values()]
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// src/report.ts
|
|
865
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
866
|
+
import path3 from "path";
|
|
867
|
+
var pct = (v) => `${(v * 100).toFixed(1)}%`;
|
|
868
|
+
function toMarkdownTable(report) {
|
|
869
|
+
const lines = [];
|
|
870
|
+
lines.push(`# Benchmark report`);
|
|
871
|
+
lines.push(``);
|
|
872
|
+
lines.push(`- model: \`${report.model}\``);
|
|
873
|
+
if (report.variant) lines.push(`- variant: \`${report.variant}\``);
|
|
874
|
+
lines.push(`- generated: ${report.generatedAt}`);
|
|
875
|
+
lines.push(``);
|
|
876
|
+
lines.push(
|
|
877
|
+
`| strategy | track | cases | F1 | precision | recall | valid | exact | in tok | out tok | cost (USD) | ms/case | total ms |`
|
|
878
|
+
);
|
|
879
|
+
lines.push(`|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|`);
|
|
880
|
+
for (const row of report.summary) {
|
|
881
|
+
lines.push(
|
|
882
|
+
`| ${row.strategy} | ${row.track} | ${row.cases} | ${pct(row.meanF1)} | ${pct(row.meanPrecision)} | ${pct(row.meanRecall)} | ${pct(row.validityRate)} | ${pct(row.exactMatchRate)} | ${row.totalInputTokens} | ${row.totalOutputTokens} | ${row.totalCostUsd.toFixed(4)} | ${Math.round(row.meanLatencyMs)} | ${row.totalLatencyMs} |`
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
return lines.join("\n");
|
|
886
|
+
}
|
|
887
|
+
async function saveReport(report, file) {
|
|
888
|
+
await mkdir3(path3.dirname(file), { recursive: true });
|
|
889
|
+
await writeFile3(file, JSON.stringify(report, null, 2), "utf8");
|
|
890
|
+
const mdFile = file.replace(/\.json$/, "") + ".md";
|
|
891
|
+
await writeFile3(mdFile, toMarkdownTable(report), "utf8");
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/cli.ts
|
|
895
|
+
var args = process.argv.slice(2);
|
|
896
|
+
var cmd = args[0];
|
|
897
|
+
async function main() {
|
|
898
|
+
switch (cmd) {
|
|
899
|
+
case "ls": {
|
|
900
|
+
const { readdir } = await import("fs/promises");
|
|
901
|
+
const { defaultDatasetDir: defaultDatasetDir2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
902
|
+
const dir = defaultDatasetDir2();
|
|
903
|
+
try {
|
|
904
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
905
|
+
for (const e of entries) {
|
|
906
|
+
if (e.isDirectory()) console.log(` ${e.name}`);
|
|
907
|
+
}
|
|
908
|
+
} catch {
|
|
909
|
+
console.log(`Cache dir ${dir} is empty or missing.`);
|
|
910
|
+
}
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
case "fetch": {
|
|
914
|
+
const all = args.includes("--all");
|
|
915
|
+
const name = args[1];
|
|
916
|
+
const datasets = { sroie };
|
|
917
|
+
if (all) {
|
|
918
|
+
for (const [key, ds] of Object.entries(datasets)) {
|
|
919
|
+
console.log(`Fetching ${key}...`);
|
|
920
|
+
await loadDataset(ds, { force: true });
|
|
921
|
+
console.log(` done.`);
|
|
922
|
+
}
|
|
923
|
+
} else if (name && name in datasets) {
|
|
924
|
+
console.log(`Fetching ${name}...`);
|
|
925
|
+
await loadDataset(datasets[name], { force: true });
|
|
926
|
+
console.log(` done.`);
|
|
927
|
+
} else {
|
|
928
|
+
console.log("Usage: struktur-benchmarks fetch <dataset> | --all");
|
|
929
|
+
console.log(`Available: ${Object.keys(datasets).join(", ")}`);
|
|
930
|
+
}
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
case "run": {
|
|
934
|
+
const modulePath = args[1];
|
|
935
|
+
if (!modulePath) {
|
|
936
|
+
console.log("Usage: struktur-benchmarks run <module.js>");
|
|
937
|
+
console.log(" Module must export: { cases, strategies }");
|
|
938
|
+
process.exit(1);
|
|
939
|
+
}
|
|
940
|
+
const mod = await import(resolve(modulePath));
|
|
941
|
+
const report = await runBenchmark({
|
|
942
|
+
cases: mod.cases,
|
|
943
|
+
strategies: mod.strategies ?? ["simple"]
|
|
944
|
+
});
|
|
945
|
+
console.log(toMarkdownTable(report));
|
|
946
|
+
await saveReport(report, `benchmark-report-${Date.now()}.json`);
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
default:
|
|
950
|
+
console.log("Usage: struktur-benchmarks <ls|fetch|run>");
|
|
951
|
+
console.log(" ls List cached datasets");
|
|
952
|
+
console.log(" fetch Download a dataset");
|
|
953
|
+
console.log(" run Run a module");
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
main().catch((e) => {
|
|
958
|
+
console.error(e);
|
|
959
|
+
process.exit(1);
|
|
960
|
+
});
|
|
961
|
+
//# sourceMappingURL=cli.js.map
|