cbrowser 18.76.0 → 18.78.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/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/audit-tools.js +32 -2
- package/dist/mcp-tools/base/audit-tools.js.map +1 -1
- package/dist/visual/attention-ai-layer.d.ts +93 -0
- package/dist/visual/attention-ai-layer.d.ts.map +1 -0
- package/dist/visual/attention-ai-layer.js +233 -0
- package/dist/visual/attention-ai-layer.js.map +1 -0
- package/dist/visual/fixation-corpus.d.ts +132 -0
- package/dist/visual/fixation-corpus.d.ts.map +1 -0
- package/dist/visual/fixation-corpus.js +212 -0
- package/dist/visual/fixation-corpus.js.map +1 -0
- package/dist/visual/saliency-metrics.d.ts +111 -0
- package/dist/visual/saliency-metrics.d.ts.map +1 -0
- package/dist/visual/saliency-metrics.js +225 -0
- package/dist/visual/saliency-metrics.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI attention layer — persona-aware semantic reading of the rendered page.
|
|
3
|
+
*
|
|
4
|
+
* WHAT THE EXISTING TWO LAYERS CANNOT SEE
|
|
5
|
+
*
|
|
6
|
+
* Layer 1 (bottom-up saliency) knows where colour and texture pop. Layer 2
|
|
7
|
+
* (DOM semantics) knows element TYPE — CTA, heading, nav — times persona
|
|
8
|
+
* priority, plus keyword overlap with the goal. Neither knows what the content
|
|
9
|
+
* MEANS to this particular person. "Start free trial" and "Contact enterprise
|
|
10
|
+
* sales" are both CTAs of identical type and similar contrast; to a
|
|
11
|
+
* price-anxious first-timer they are not remotely the same object. Nor do the
|
|
12
|
+
* first two layers read text baked into images, judge compositional hierarchy
|
|
13
|
+
* (size, whitespace, alignment), or model reading order.
|
|
14
|
+
*
|
|
15
|
+
* That is the headroom this layer addresses: meaning, weighted by what this
|
|
16
|
+
* persona values.
|
|
17
|
+
*
|
|
18
|
+
* WHY IT IS A SEPARATE LAYER AND NOT A BLEND
|
|
19
|
+
*
|
|
20
|
+
* The deliberate design choice, and the one worth defending. Layers 1 and 2 are
|
|
21
|
+
* deterministic — the same screenshot yields the same map, which is what makes
|
|
22
|
+
* visual baselines and regression comparisons meaningful. An LLM's opinion is
|
|
23
|
+
* not deterministic, and silently averaging it into the same `saliency` number
|
|
24
|
+
* would make every downstream metric unreproducible while looking unchanged.
|
|
25
|
+
* This codebase has repeatedly shipped exactly that shape: a value quietly
|
|
26
|
+
* folded in under a name that already meant something else.
|
|
27
|
+
*
|
|
28
|
+
* So the AI regions are returned ALONGSIDE, named, with the model's own
|
|
29
|
+
* reasoning attached, and a caller opts in explicitly. A blended convenience
|
|
30
|
+
* field exists but says so in `attentionModel`.
|
|
31
|
+
*
|
|
32
|
+
* WHAT WE CANNOT CURRENTLY CLAIM
|
|
33
|
+
*
|
|
34
|
+
* There is no eye-tracking ground truth in this repository, so there is no
|
|
35
|
+
* measurement that says this layer makes the map more accurate — only that it
|
|
36
|
+
* makes it different. It is offered as an interpretive aid, and its output is
|
|
37
|
+
* labelled as a model's judgement rather than a measurement. Validating it
|
|
38
|
+
* needs a fixation dataset; until then, treat confident agreement between this
|
|
39
|
+
* layer and the other two as the useful signal, and disagreement as a question,
|
|
40
|
+
* not an answer.
|
|
41
|
+
*
|
|
42
|
+
* @copyright 2026 Alexandria Eden alexandria.shai.eden@gmail.com https://cbrowser.ai
|
|
43
|
+
* @license MIT
|
|
44
|
+
*/
|
|
45
|
+
import { createHash } from "crypto";
|
|
46
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync } from "fs";
|
|
47
|
+
import { join } from "path";
|
|
48
|
+
import { homedir } from "os";
|
|
49
|
+
const CACHE_DIR = join(process.env.CBROWSER_DATA_DIR || join(homedir(), ".cbrowser"), "ai-attention-cache");
|
|
50
|
+
/**
|
|
51
|
+
* Cache key covers everything that can change the answer: the exact image, the
|
|
52
|
+
* persona, the goal, the values and the model. An LLM layer is only usable in a
|
|
53
|
+
* regression context if repeating a run repeats the answer — and it makes the
|
|
54
|
+
* layer nearly free on re-analysis of an unchanged page.
|
|
55
|
+
*/
|
|
56
|
+
function cacheKey(imageBytes, persona, goal, values, model) {
|
|
57
|
+
const h = createHash("sha256");
|
|
58
|
+
h.update(imageBytes);
|
|
59
|
+
h.update(`|${persona}|${goal ?? ""}|${model}|`);
|
|
60
|
+
h.update(JSON.stringify(values ?? {}));
|
|
61
|
+
return h.digest("hex").slice(0, 32);
|
|
62
|
+
}
|
|
63
|
+
function readCache(key) {
|
|
64
|
+
try {
|
|
65
|
+
const p = join(CACHE_DIR, `${key}.json`);
|
|
66
|
+
if (!existsSync(p))
|
|
67
|
+
return null;
|
|
68
|
+
return { ...JSON.parse(readFileSync(p, "utf8")), cached: true };
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function writeCache(key, value) {
|
|
75
|
+
try {
|
|
76
|
+
if (!existsSync(CACHE_DIR))
|
|
77
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
78
|
+
writeFileSync(join(CACHE_DIR, `${key}.json`), JSON.stringify(value));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
/* cache is an optimisation; never fail the analysis for it */
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Describe persona values in words the model can act on. */
|
|
85
|
+
function describeValues(values) {
|
|
86
|
+
if (!values)
|
|
87
|
+
return "No specific motivational profile supplied.";
|
|
88
|
+
const notable = Object.entries(values)
|
|
89
|
+
.filter(([, v]) => typeof v === "number" && (v >= 0.7 || v <= 0.3))
|
|
90
|
+
.map(([k, v]) => `${k}=${v.toFixed(2)} (${v >= 0.7 ? "high" : "low"})`);
|
|
91
|
+
return notable.length ? notable.join(", ") : "Values are mid-range across the board.";
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Ask the model where this persona looks and why.
|
|
95
|
+
*
|
|
96
|
+
* Returns `unavailable` rather than throwing when there is no key or the call
|
|
97
|
+
* fails: this layer is additive, and an attention map without it is the same
|
|
98
|
+
* map the product shipped before it existed.
|
|
99
|
+
*/
|
|
100
|
+
export async function analyzeAttentionWithAI(screenshotPath, persona, options = {}) {
|
|
101
|
+
const { getAnthropicApiKey, getAnthropicModel, isApiKeyConfigured } = await import("../cognitive/index.js");
|
|
102
|
+
if (!isApiKeyConfigured()) {
|
|
103
|
+
return {
|
|
104
|
+
regions: [],
|
|
105
|
+
narrative: "",
|
|
106
|
+
model: "",
|
|
107
|
+
cached: false,
|
|
108
|
+
unavailable: "No Anthropic API key configured. The AI attention layer is opt-in and keyed; the visual and semantic layers are unaffected.",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
let bytes;
|
|
112
|
+
try {
|
|
113
|
+
bytes = readFileSync(screenshotPath);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return { regions: [], narrative: "", model: "", cached: false, unavailable: `Could not read ${screenshotPath}` };
|
|
117
|
+
}
|
|
118
|
+
const model = getAnthropicModel();
|
|
119
|
+
const key = cacheKey(bytes, persona, options.goal, options.values, model);
|
|
120
|
+
const hit = readCache(key);
|
|
121
|
+
if (hit)
|
|
122
|
+
return hit;
|
|
123
|
+
const dims = options.imageWidth && options.imageHeight
|
|
124
|
+
? `The image is ${options.imageWidth}x${options.imageHeight} pixels.`
|
|
125
|
+
: "Coordinates must be in the image's own pixel space.";
|
|
126
|
+
const system = `You model visual attention for a specific person looking at a web page. You are not describing what is on the page — you are predicting where THIS person's eyes go, and why, given who they are.
|
|
127
|
+
|
|
128
|
+
Return ONLY a JSON object, no prose outside it:
|
|
129
|
+
{"regions":[{"rect":{"x":0,"y":0,"width":0,"height":0},"weight":0.0,"label":"","reasoning":""}],"narrative":""}
|
|
130
|
+
|
|
131
|
+
Rules:
|
|
132
|
+
- ${dims} Coordinates are top-left origin.
|
|
133
|
+
- weight is 0-1: how strongly this person attends, relative to everything else on the page.
|
|
134
|
+
- Return at most 8 regions. Fewer is better than padding.
|
|
135
|
+
- reasoning must name something about THIS PERSON, not the page in general. "Large red button" is a description; "a price-anxious first-timer checks cost before anything else, so the pricing link pulls first" is a prediction.
|
|
136
|
+
- Attend to what the DOM cannot express: what the copy MEANS to them, text inside images, compositional hierarchy, and what they will deliberately SKIP.
|
|
137
|
+
- narrative: 2-4 sentences on how this person reads this page.`;
|
|
138
|
+
const goalLine = options.goal
|
|
139
|
+
? `They are trying to: ${options.goal}. Goal-relevant content should dominate.`
|
|
140
|
+
: `They have no specific task — this is undirected first-contact browsing.`;
|
|
141
|
+
const user = `Person: ${persona}
|
|
142
|
+
${options.personaDescription ? `Who they are: ${options.personaDescription}` : ""}
|
|
143
|
+
Motivational profile: ${describeValues(options.values)}
|
|
144
|
+
${goalLine}
|
|
145
|
+
|
|
146
|
+
Where do their eyes go on this page, and why?`;
|
|
147
|
+
try {
|
|
148
|
+
const { default: Anthropic } = await import("@anthropic-ai/sdk");
|
|
149
|
+
const client = new Anthropic({ apiKey: getAnthropicApiKey() });
|
|
150
|
+
const response = await client.messages.create({
|
|
151
|
+
model,
|
|
152
|
+
max_tokens: 1500,
|
|
153
|
+
system,
|
|
154
|
+
messages: [
|
|
155
|
+
{
|
|
156
|
+
role: "user",
|
|
157
|
+
content: [
|
|
158
|
+
{
|
|
159
|
+
type: "image",
|
|
160
|
+
source: { type: "base64", media_type: "image/png", data: bytes.toString("base64") },
|
|
161
|
+
},
|
|
162
|
+
{ type: "text", text: user },
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
});
|
|
167
|
+
const text = response.content[0]?.type === "text" ? response.content[0].text : "";
|
|
168
|
+
const match = text.match(/\{[\s\S]*\}/);
|
|
169
|
+
if (!match) {
|
|
170
|
+
return { regions: [], narrative: "", model, cached: false, unavailable: "Model returned no parseable JSON" };
|
|
171
|
+
}
|
|
172
|
+
const parsed = JSON.parse(match[0]);
|
|
173
|
+
const result = {
|
|
174
|
+
// Clamp rather than trust: a hallucinated weight of 12 would silently
|
|
175
|
+
// dominate any blend it is fed into.
|
|
176
|
+
regions: (parsed.regions ?? []).slice(0, 8).map((r) => ({
|
|
177
|
+
rect: {
|
|
178
|
+
x: Math.max(0, Math.round(r.rect?.x ?? 0)),
|
|
179
|
+
y: Math.max(0, Math.round(r.rect?.y ?? 0)),
|
|
180
|
+
width: Math.max(1, Math.round(r.rect?.width ?? 1)),
|
|
181
|
+
height: Math.max(1, Math.round(r.rect?.height ?? 1)),
|
|
182
|
+
},
|
|
183
|
+
weight: Math.min(1, Math.max(0, Number(r.weight) || 0)),
|
|
184
|
+
label: String(r.label ?? "").slice(0, 120),
|
|
185
|
+
reasoning: String(r.reasoning ?? "").slice(0, 400),
|
|
186
|
+
})),
|
|
187
|
+
narrative: String(parsed.narrative ?? "").slice(0, 1200),
|
|
188
|
+
model,
|
|
189
|
+
cached: false,
|
|
190
|
+
};
|
|
191
|
+
writeCache(key, result);
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
return {
|
|
196
|
+
regions: [],
|
|
197
|
+
narrative: "",
|
|
198
|
+
model,
|
|
199
|
+
cached: false,
|
|
200
|
+
unavailable: `AI attention layer failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Rasterise AI regions onto the analysis grid so they can be compared with, or
|
|
206
|
+
* blended into, the existing saliency cells.
|
|
207
|
+
*
|
|
208
|
+
* Kept separate from the fetch so a caller can display the regions and their
|
|
209
|
+
* reasoning WITHOUT blending — which is the recommended use, since the blend
|
|
210
|
+
* makes the map non-deterministic.
|
|
211
|
+
*/
|
|
212
|
+
export function aiRegionsToGrid(regions, rows, cols, imageWidth, imageHeight) {
|
|
213
|
+
const grid = new Float64Array(rows * cols);
|
|
214
|
+
if (regions.length === 0 || imageWidth <= 0 || imageHeight <= 0)
|
|
215
|
+
return grid;
|
|
216
|
+
for (const region of regions) {
|
|
217
|
+
const c0 = Math.max(0, Math.floor((region.rect.x / imageWidth) * cols));
|
|
218
|
+
const c1 = Math.min(cols - 1, Math.ceil(((region.rect.x + region.rect.width) / imageWidth) * cols) - 1);
|
|
219
|
+
const r0 = Math.max(0, Math.floor((region.rect.y / imageHeight) * rows));
|
|
220
|
+
const r1 = Math.min(rows - 1, Math.ceil(((region.rect.y + region.rect.height) / imageHeight) * rows) - 1);
|
|
221
|
+
for (let r = r0; r <= r1; r++) {
|
|
222
|
+
for (let c = c0; c <= c1; c++) {
|
|
223
|
+
const i = r * cols + c;
|
|
224
|
+
// Max, not sum: overlapping regions should not compound into a hotspot
|
|
225
|
+
// that neither region claimed on its own.
|
|
226
|
+
if (region.weight > grid[i])
|
|
227
|
+
grid[i] = region.weight;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return grid;
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=attention-ai-layer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attention-ai-layer.js","sourceRoot":"","sources":["../../src/visual/attention-ai-layer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixation corpora — human eye-tracking ground truth, in one shape.
|
|
3
|
+
*
|
|
4
|
+
* WHAT A CORPUS IS FOR HERE
|
|
5
|
+
*
|
|
6
|
+
* Three distinct uses, and conflating them is how saliency work goes wrong:
|
|
7
|
+
*
|
|
8
|
+
* 1. VALIDATION — score the shipped model against human fixations. This is
|
|
9
|
+
* what makes an accuracy claim sayable out loud. Study 1 did this against
|
|
10
|
+
* UEyes and reported AUC-Judd 0.663 / NSS 0.544 / CC 0.216.
|
|
11
|
+
* 2. CALIBRATION — tune model parameters (channel weights, surround radius,
|
|
12
|
+
* cell size) to maximise those metrics. Legitimate, but it spends the
|
|
13
|
+
* corpus: a model tuned on a set can no longer be honestly validated on
|
|
14
|
+
* that same set, which is why `split` exists below.
|
|
15
|
+
* 3. PRIORS — derive empirical structure from the corpus and bake it into the
|
|
16
|
+
* model. A centre-bias prior alone lifts AUC substantially on almost any
|
|
17
|
+
* dataset, because humans fixate the middle regardless of content.
|
|
18
|
+
*
|
|
19
|
+
* The interface is deliberately corpus-agnostic. UEyes is what is on disk
|
|
20
|
+
* today; MIT1003, CAT2000 and SALICON have the same shape (images + fixation
|
|
21
|
+
* points + density maps) and should not need new plumbing.
|
|
22
|
+
*
|
|
23
|
+
* WHAT UEYES GIVES THAT MOST CORPORA DO NOT
|
|
24
|
+
*
|
|
25
|
+
* - It is WEB PAGES (1980 of them), not natural scenes. Saliency models tuned on
|
|
26
|
+
* photographs transfer poorly to interfaces, where text and affordances
|
|
27
|
+
* dominate.
|
|
28
|
+
* - Fixation maps at 1s / 3s / 7s separately. Attention at first impression is
|
|
29
|
+
* not attention after consideration, and cbrowser's personas differ precisely
|
|
30
|
+
* on patience — an impatient persona should be scored against 1s, a thorough
|
|
31
|
+
* one against 7s. Scoring every persona against one horizon throws that away.
|
|
32
|
+
* - Per-fixation logs with duration and saccade magnitude/direction, so scanpath
|
|
33
|
+
* ORDER is available, not just aggregate density.
|
|
34
|
+
*
|
|
35
|
+
* @copyright 2026 Alexandria Eden alexandria.shai.eden@gmail.com https://cbrowser.ai
|
|
36
|
+
* @license MIT
|
|
37
|
+
*/
|
|
38
|
+
import type { Fixation, SaliencyGrid } from "./saliency-metrics.js";
|
|
39
|
+
/** Viewing durations UEyes separates. Others may expose only one. */
|
|
40
|
+
export type FixationHorizon = "1s" | "3s" | "7s";
|
|
41
|
+
export interface CorpusStimulus {
|
|
42
|
+
/** Stable id — the image filename without extension. */
|
|
43
|
+
id: string;
|
|
44
|
+
/** Absolute path to the stimulus image. */
|
|
45
|
+
imagePath: string;
|
|
46
|
+
/** Ground-truth fixation density map per horizon, when the corpus has them. */
|
|
47
|
+
fixationMaps: Partial<Record<FixationHorizon, string>>;
|
|
48
|
+
}
|
|
49
|
+
export interface CorpusInfo {
|
|
50
|
+
name: string;
|
|
51
|
+
root: string;
|
|
52
|
+
stimuli: CorpusStimulus[];
|
|
53
|
+
horizons: FixationHorizon[];
|
|
54
|
+
/** Absent when the corpus ships only density maps and no raw logs. */
|
|
55
|
+
fixationLogDir?: string;
|
|
56
|
+
citation: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* One participant fixation as recorded by the tracker.
|
|
60
|
+
*
|
|
61
|
+
* Coordinates are NORMALIZED 0-1 in the source logs and stay that way here.
|
|
62
|
+
* Converting to pixels requires the stimulus dimensions, and doing it at load
|
|
63
|
+
* time — before the caller says which grid they are scoring against — is how
|
|
64
|
+
* coordinate-space bugs get baked in. `fixationsToGrid` does the conversion at
|
|
65
|
+
* the point the target size is actually known.
|
|
66
|
+
*/
|
|
67
|
+
export interface RawFixation {
|
|
68
|
+
/** 0-1, left to right. */
|
|
69
|
+
xNorm: number;
|
|
70
|
+
/** 0-1, top to bottom. */
|
|
71
|
+
yNorm: number;
|
|
72
|
+
/** Dwell in seconds. */
|
|
73
|
+
duration: number;
|
|
74
|
+
/** Seconds since stimulus onset — this is what makes horizon filtering possible. */
|
|
75
|
+
startTime: number;
|
|
76
|
+
participantId: string;
|
|
77
|
+
stimulusId: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Discover a UEyes-layout corpus.
|
|
81
|
+
*
|
|
82
|
+
* Returns null rather than throwing when the directory is absent: the corpus is
|
|
83
|
+
* 13GB and is not, and should not be, a dependency of the shipped package.
|
|
84
|
+
* Anything that needs it degrades to "not available" rather than failing.
|
|
85
|
+
*/
|
|
86
|
+
export declare function loadUEyesCorpus(root: string): CorpusInfo | null;
|
|
87
|
+
/**
|
|
88
|
+
* Parse one Gazepoint-format tracker log.
|
|
89
|
+
*
|
|
90
|
+
* The columns that matter: MEDIA_NAME (which stimulus), FPOGX/FPOGY (gaze point
|
|
91
|
+
* of regard, normalized), FPOGD (duration), FPOGS (start time), FPOGV (valid).
|
|
92
|
+
*
|
|
93
|
+
* INVALID SAMPLES ARE DROPPED, not clamped. FPOGV=0 means the tracker lost the
|
|
94
|
+
* eye — a blink, a look away, a calibration slip. Those rows still carry
|
|
95
|
+
* coordinates, and treating them as fixations manufactures ground truth from
|
|
96
|
+
* noise. Off-screen coordinates are dropped for the same reason.
|
|
97
|
+
*/
|
|
98
|
+
export declare function parseGazepointLog(logPath: string): RawFixation[];
|
|
99
|
+
/**
|
|
100
|
+
* Keep only fixations that occurred within `seconds` of stimulus onset.
|
|
101
|
+
*
|
|
102
|
+
* This is what lets a persona be scored against the horizon that matches it: an
|
|
103
|
+
* impatient persona compared to 1s of human viewing, a thorough one to 7s.
|
|
104
|
+
* Comparing every persona to the same aggregate map discards the one dimension
|
|
105
|
+
* this corpus was built to expose.
|
|
106
|
+
*/
|
|
107
|
+
export declare function filterByHorizon(fixations: RawFixation[], seconds: number): RawFixation[];
|
|
108
|
+
/** Convert normalized fixations to a grid's pixel space. */
|
|
109
|
+
export declare function toGridFixations(fixations: RawFixation[], gridWidth: number, gridHeight: number): Fixation[];
|
|
110
|
+
/**
|
|
111
|
+
* Build a fixation density map by accumulating Gaussian blobs.
|
|
112
|
+
*
|
|
113
|
+
* Ground-truth maps ship with UEyes, but a corpus that provides only raw logs
|
|
114
|
+
* needs this to score CC at all. Sigma is in grid cells; the literature
|
|
115
|
+
* convention is roughly one degree of visual angle, and for a 32x32 analysis
|
|
116
|
+
* grid that lands near 1.
|
|
117
|
+
*/
|
|
118
|
+
export declare function fixationsToGrid(fixations: Fixation[], width: number, height: number, sigma?: number): SaliencyGrid;
|
|
119
|
+
/**
|
|
120
|
+
* Deterministic train/test split.
|
|
121
|
+
*
|
|
122
|
+
* Calibrating on a corpus and then reporting accuracy on the same corpus is
|
|
123
|
+
* measuring how well the parameters were fitted, not how well the model
|
|
124
|
+
* generalises — and it is the single easiest way to publish a number that does
|
|
125
|
+
* not survive review. The split is by stimulus id hash so it is stable across
|
|
126
|
+
* runs and machines without storing a manifest.
|
|
127
|
+
*/
|
|
128
|
+
export declare function splitCorpus(corpus: CorpusInfo, testFraction?: number): {
|
|
129
|
+
train: CorpusStimulus[];
|
|
130
|
+
test: CorpusStimulus[];
|
|
131
|
+
};
|
|
132
|
+
//# sourceMappingURL=fixation-corpus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixation-corpus.d.ts","sourceRoot":"","sources":["../../src/visual/fixation-corpus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAEpE,qEAAqE;AACrE,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAEjD,MAAM,WAAW,cAAc;IAC7B,wDAAwD;IACxD,EAAE,EAAE,MAAM,CAAC;IACX,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,WAAW;IAC1B,0BAA0B;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,0BAA0B;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,wBAAwB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAgC/D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,EAAE,CA+ChE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,EAAE,CAExF;AAED,4DAA4D;AAC5D,wBAAgB,eAAe,CAC7B,SAAS,EAAE,WAAW,EAAE,EACxB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,QAAQ,EAAE,CAMZ;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,QAAQ,EAAE,EACrB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,KAAK,SAAM,GACV,YAAY,CAoBd;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,UAAU,EAClB,YAAY,SAAM,GACjB;IAAE,KAAK,EAAE,cAAc,EAAE,CAAC;IAAC,IAAI,EAAE,cAAc,EAAE,CAAA;CAAE,CAarD"}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixation corpora — human eye-tracking ground truth, in one shape.
|
|
3
|
+
*
|
|
4
|
+
* WHAT A CORPUS IS FOR HERE
|
|
5
|
+
*
|
|
6
|
+
* Three distinct uses, and conflating them is how saliency work goes wrong:
|
|
7
|
+
*
|
|
8
|
+
* 1. VALIDATION — score the shipped model against human fixations. This is
|
|
9
|
+
* what makes an accuracy claim sayable out loud. Study 1 did this against
|
|
10
|
+
* UEyes and reported AUC-Judd 0.663 / NSS 0.544 / CC 0.216.
|
|
11
|
+
* 2. CALIBRATION — tune model parameters (channel weights, surround radius,
|
|
12
|
+
* cell size) to maximise those metrics. Legitimate, but it spends the
|
|
13
|
+
* corpus: a model tuned on a set can no longer be honestly validated on
|
|
14
|
+
* that same set, which is why `split` exists below.
|
|
15
|
+
* 3. PRIORS — derive empirical structure from the corpus and bake it into the
|
|
16
|
+
* model. A centre-bias prior alone lifts AUC substantially on almost any
|
|
17
|
+
* dataset, because humans fixate the middle regardless of content.
|
|
18
|
+
*
|
|
19
|
+
* The interface is deliberately corpus-agnostic. UEyes is what is on disk
|
|
20
|
+
* today; MIT1003, CAT2000 and SALICON have the same shape (images + fixation
|
|
21
|
+
* points + density maps) and should not need new plumbing.
|
|
22
|
+
*
|
|
23
|
+
* WHAT UEYES GIVES THAT MOST CORPORA DO NOT
|
|
24
|
+
*
|
|
25
|
+
* - It is WEB PAGES (1980 of them), not natural scenes. Saliency models tuned on
|
|
26
|
+
* photographs transfer poorly to interfaces, where text and affordances
|
|
27
|
+
* dominate.
|
|
28
|
+
* - Fixation maps at 1s / 3s / 7s separately. Attention at first impression is
|
|
29
|
+
* not attention after consideration, and cbrowser's personas differ precisely
|
|
30
|
+
* on patience — an impatient persona should be scored against 1s, a thorough
|
|
31
|
+
* one against 7s. Scoring every persona against one horizon throws that away.
|
|
32
|
+
* - Per-fixation logs with duration and saccade magnitude/direction, so scanpath
|
|
33
|
+
* ORDER is available, not just aggregate density.
|
|
34
|
+
*
|
|
35
|
+
* @copyright 2026 Alexandria Eden alexandria.shai.eden@gmail.com https://cbrowser.ai
|
|
36
|
+
* @license MIT
|
|
37
|
+
*/
|
|
38
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
39
|
+
import { join, basename, extname } from "path";
|
|
40
|
+
/**
|
|
41
|
+
* Discover a UEyes-layout corpus.
|
|
42
|
+
*
|
|
43
|
+
* Returns null rather than throwing when the directory is absent: the corpus is
|
|
44
|
+
* 13GB and is not, and should not be, a dependency of the shipped package.
|
|
45
|
+
* Anything that needs it degrades to "not available" rather than failing.
|
|
46
|
+
*/
|
|
47
|
+
export function loadUEyesCorpus(root) {
|
|
48
|
+
const imagesDir = join(root, "images");
|
|
49
|
+
if (!existsSync(imagesDir))
|
|
50
|
+
return null;
|
|
51
|
+
const horizons = [];
|
|
52
|
+
for (const h of ["1s", "3s", "7s"]) {
|
|
53
|
+
if (existsSync(join(root, "saliency_maps", `fixmaps_${h}`)))
|
|
54
|
+
horizons.push(h);
|
|
55
|
+
}
|
|
56
|
+
const stimuli = [];
|
|
57
|
+
for (const file of readdirSync(imagesDir)) {
|
|
58
|
+
const ext = extname(file).toLowerCase();
|
|
59
|
+
if (ext !== ".jpg" && ext !== ".png" && ext !== ".jpeg")
|
|
60
|
+
continue;
|
|
61
|
+
const id = basename(file, ext);
|
|
62
|
+
const fixationMaps = {};
|
|
63
|
+
for (const h of horizons) {
|
|
64
|
+
const candidate = join(root, "saliency_maps", `fixmaps_${h}`, file);
|
|
65
|
+
if (existsSync(candidate))
|
|
66
|
+
fixationMaps[h] = candidate;
|
|
67
|
+
}
|
|
68
|
+
stimuli.push({ id, imagePath: join(imagesDir, file), fixationMaps });
|
|
69
|
+
}
|
|
70
|
+
const logDir = join(root, "eyetracker_logs");
|
|
71
|
+
return {
|
|
72
|
+
name: "UEyes",
|
|
73
|
+
root,
|
|
74
|
+
stimuli: stimuli.sort((a, b) => a.id.localeCompare(b.id)),
|
|
75
|
+
horizons,
|
|
76
|
+
fixationLogDir: existsSync(logDir) ? logDir : undefined,
|
|
77
|
+
citation: "Jiang et al., UEyes: Understanding Visual Saliency across User Interface Types. CHI 2023.",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Parse one Gazepoint-format tracker log.
|
|
82
|
+
*
|
|
83
|
+
* The columns that matter: MEDIA_NAME (which stimulus), FPOGX/FPOGY (gaze point
|
|
84
|
+
* of regard, normalized), FPOGD (duration), FPOGS (start time), FPOGV (valid).
|
|
85
|
+
*
|
|
86
|
+
* INVALID SAMPLES ARE DROPPED, not clamped. FPOGV=0 means the tracker lost the
|
|
87
|
+
* eye — a blink, a look away, a calibration slip. Those rows still carry
|
|
88
|
+
* coordinates, and treating them as fixations manufactures ground truth from
|
|
89
|
+
* noise. Off-screen coordinates are dropped for the same reason.
|
|
90
|
+
*/
|
|
91
|
+
export function parseGazepointLog(logPath) {
|
|
92
|
+
const text = readFileSync(logPath, "utf8");
|
|
93
|
+
const lines = text.split("\n").filter((l) => l.trim().length > 0);
|
|
94
|
+
if (lines.length < 2)
|
|
95
|
+
return [];
|
|
96
|
+
const header = lines[0].split(",").map((h) => h.trim());
|
|
97
|
+
const col = (name) => header.findIndex((h) => h === name || h.startsWith(`${name}(`));
|
|
98
|
+
const iMedia = col("MEDIA_NAME");
|
|
99
|
+
const iX = col("FPOGX");
|
|
100
|
+
const iY = col("FPOGY");
|
|
101
|
+
const iD = col("FPOGD");
|
|
102
|
+
const iS = col("FPOGS");
|
|
103
|
+
const iV = col("FPOGV");
|
|
104
|
+
const iId = col("FPOGID");
|
|
105
|
+
if (iX < 0 || iY < 0)
|
|
106
|
+
return [];
|
|
107
|
+
const participantId = basename(logPath).replace(/_fixations\.csv$/i, "");
|
|
108
|
+
const out = [];
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
for (let i = 1; i < lines.length; i++) {
|
|
111
|
+
const parts = lines[i].split(",");
|
|
112
|
+
if (iV >= 0 && parts[iV]?.trim() !== "1")
|
|
113
|
+
continue; // tracker lost the eye
|
|
114
|
+
const xNorm = Number(parts[iX]);
|
|
115
|
+
const yNorm = Number(parts[iY]);
|
|
116
|
+
if (!Number.isFinite(xNorm) || !Number.isFinite(yNorm))
|
|
117
|
+
continue;
|
|
118
|
+
if (xNorm < 0 || xNorm > 1 || yNorm < 0 || yNorm > 1)
|
|
119
|
+
continue; // off-stimulus
|
|
120
|
+
// One row per SAMPLE, many samples per fixation. Deduplicate on FPOGID so a
|
|
121
|
+
// long fixation counts once — otherwise dwell time silently becomes a vote
|
|
122
|
+
// multiplier and every metric is weighted by how long the tracker sampled.
|
|
123
|
+
const fixId = iId >= 0 ? parts[iId]?.trim() : undefined;
|
|
124
|
+
const media = iMedia >= 0 ? (parts[iMedia] ?? "").trim() : "";
|
|
125
|
+
const key = `${media}|${fixId ?? `${i}`}`;
|
|
126
|
+
if (fixId && seen.has(key))
|
|
127
|
+
continue;
|
|
128
|
+
if (fixId)
|
|
129
|
+
seen.add(key);
|
|
130
|
+
out.push({
|
|
131
|
+
xNorm,
|
|
132
|
+
yNorm,
|
|
133
|
+
duration: iD >= 0 ? Number(parts[iD]) || 0 : 0,
|
|
134
|
+
startTime: iS >= 0 ? Number(parts[iS]) || 0 : 0,
|
|
135
|
+
participantId,
|
|
136
|
+
stimulusId: media ? basename(media, extname(media)) : "",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Keep only fixations that occurred within `seconds` of stimulus onset.
|
|
143
|
+
*
|
|
144
|
+
* This is what lets a persona be scored against the horizon that matches it: an
|
|
145
|
+
* impatient persona compared to 1s of human viewing, a thorough one to 7s.
|
|
146
|
+
* Comparing every persona to the same aggregate map discards the one dimension
|
|
147
|
+
* this corpus was built to expose.
|
|
148
|
+
*/
|
|
149
|
+
export function filterByHorizon(fixations, seconds) {
|
|
150
|
+
return fixations.filter((f) => f.startTime <= seconds);
|
|
151
|
+
}
|
|
152
|
+
/** Convert normalized fixations to a grid's pixel space. */
|
|
153
|
+
export function toGridFixations(fixations, gridWidth, gridHeight) {
|
|
154
|
+
return fixations.map((f) => ({
|
|
155
|
+
x: f.xNorm * gridWidth,
|
|
156
|
+
y: f.yNorm * gridHeight,
|
|
157
|
+
duration: f.duration * 1000,
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Build a fixation density map by accumulating Gaussian blobs.
|
|
162
|
+
*
|
|
163
|
+
* Ground-truth maps ship with UEyes, but a corpus that provides only raw logs
|
|
164
|
+
* needs this to score CC at all. Sigma is in grid cells; the literature
|
|
165
|
+
* convention is roughly one degree of visual angle, and for a 32x32 analysis
|
|
166
|
+
* grid that lands near 1.
|
|
167
|
+
*/
|
|
168
|
+
export function fixationsToGrid(fixations, width, height, sigma = 1.0) {
|
|
169
|
+
const values = new Float64Array(width * height);
|
|
170
|
+
const radius = Math.max(1, Math.ceil(sigma * 3));
|
|
171
|
+
const twoSigmaSq = 2 * sigma * sigma;
|
|
172
|
+
for (const f of fixations) {
|
|
173
|
+
const cx = f.x;
|
|
174
|
+
const cy = f.y;
|
|
175
|
+
const x0 = Math.max(0, Math.floor(cx - radius));
|
|
176
|
+
const x1 = Math.min(width - 1, Math.ceil(cx + radius));
|
|
177
|
+
const y0 = Math.max(0, Math.floor(cy - radius));
|
|
178
|
+
const y1 = Math.min(height - 1, Math.ceil(cy + radius));
|
|
179
|
+
for (let y = y0; y <= y1; y++) {
|
|
180
|
+
for (let x = x0; x <= x1; x++) {
|
|
181
|
+
const d2 = (x + 0.5 - cx) ** 2 + (y + 0.5 - cy) ** 2;
|
|
182
|
+
values[y * width + x] += Math.exp(-d2 / twoSigmaSq);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { values, width, height };
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Deterministic train/test split.
|
|
190
|
+
*
|
|
191
|
+
* Calibrating on a corpus and then reporting accuracy on the same corpus is
|
|
192
|
+
* measuring how well the parameters were fitted, not how well the model
|
|
193
|
+
* generalises — and it is the single easiest way to publish a number that does
|
|
194
|
+
* not survive review. The split is by stimulus id hash so it is stable across
|
|
195
|
+
* runs and machines without storing a manifest.
|
|
196
|
+
*/
|
|
197
|
+
export function splitCorpus(corpus, testFraction = 0.3) {
|
|
198
|
+
const hash = (s) => {
|
|
199
|
+
let h = 2166136261;
|
|
200
|
+
for (let i = 0; i < s.length; i++) {
|
|
201
|
+
h ^= s.charCodeAt(i);
|
|
202
|
+
h = Math.imul(h, 16777619);
|
|
203
|
+
}
|
|
204
|
+
return (h >>> 0) / 4294967296;
|
|
205
|
+
};
|
|
206
|
+
const train = [];
|
|
207
|
+
const test = [];
|
|
208
|
+
for (const s of corpus.stimuli)
|
|
209
|
+
(hash(s.id) < testFraction ? test : train).push(s);
|
|
210
|
+
return { train, test };
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=fixation-corpus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixation-corpus.js","sourceRoot":"","sources":["../../src/visual/fixation-corpus.ts"],"names":[],"mappings":"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"}
|