Skip to content

Commit 4ca21fc

Browse files
committed
refactor: prune screenshot diff guidance output
1 parent b5440fa commit 4ca21fc

9 files changed

Lines changed: 81 additions & 231 deletions

File tree

skills/agent-device/references/verification.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ agent-device diff screenshot --baseline ./baseline.png --out /tmp/diff.png --ove
5959

6060
- Text output includes ranked changed regions with screen-space rectangles, shape, size, density, average color, and luminance. JSON also includes normalized bounds.
6161
- The diff PNG uses a light grayscale current-screen context with changed pixels tinted red and changed regions outlined.
62-
- Install `tesseract` when you want `diff screenshot` to add best-effort OCR text deltas, movement clusters, added/removed text candidates, and bbox size-change hints. OCR improves the text/JSON descriptions only; it does not change the pixel comparison or the diff PNG.
62+
- Install `tesseract` when you want `diff screenshot` to add best-effort OCR text deltas, movement clusters, and bbox size-change hints. OCR improves the text/JSON descriptions only; it does not change the pixel comparison or the diff PNG.
6363
- When OCR is available, `diff screenshot` also reports best-effort non-text visual deltas by masking OCR text boxes out of the pixel diff and clustering the remaining residuals. Treat these as hints for icons, controls, and separators, not semantic icon recognition.
6464
- Add `--overlay-refs` to `diff screenshot` when you also want a separate current-screen overlay guide. The raw screenshot is still used for pixel comparison; the overlay guide is only context for non-text controls, icons, and tappable regions. When overlay refs intersect changed regions, the output lists the best current-screen ref matches under the affected region.
6565

src/utils/__tests__/output.test.ts

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -706,27 +706,11 @@ test('formatScreenshotDiffText renders mismatch with pixel counts without color'
706706
possibleTextMetricMismatch: true,
707707
},
708708
],
709-
addedText: [
710-
{
711-
text: 'Accessibility',
712-
rect: { x: 220, y: 2520, width: 220, height: 40 },
713-
confidence: 93,
714-
},
715-
],
716-
removedText: [
717-
{
718-
text: 'VPN',
719-
rect: { x: 120, y: 2100, width: 90, height: 36 },
720-
confidence: 96,
721-
},
722-
],
723709
movementClusters: [
724710
{
725711
texts: ['Wi-Fi', 'Bluetooth'],
726-
averageDelta: { x: 10, y: 12 },
727712
xRange: { min: 10, max: 12 },
728713
yRange: { min: 10, max: 14 },
729-
confidence: 90,
730714
},
731715
],
732716
},
@@ -739,6 +723,13 @@ test('formatScreenshotDiffText renders mismatch with pixel counts without color'
739723
rect: { x: 80, y: 318, width: 30, height: 30 },
740724
nearestText: 'Wi-Fi',
741725
},
726+
{
727+
index: 2,
728+
regionIndex: 1,
729+
slot: 'separator',
730+
likelyKind: 'separator',
731+
rect: { x: 90, y: 360, width: 120, height: 2 },
732+
},
742733
],
743734
}),
744735
);
@@ -752,10 +743,8 @@ test('formatScreenshotDiffText renders mismatch with pixel counts without color'
752743
text,
753744
/text movement cluster: "Wi-Fi", "Bluetooth" dx=\+10\.\.\+12px dy=\+10\.\.\+14px/,
754745
);
755-
assert.match(text, /added text candidates: "Accessibility" at x=220,y=2520/);
756-
assert.match(text, /removed text candidates: "VPN" at x=120,y=2100/);
757-
assert.match(text, /non-text controls\/boundaries: icon near "Wi-Fi" r1/);
758-
assert.match(text, /largest changed region: r1 top-left 70% of diff, brighter/);
746+
assert.match(text, /non-text controls: icon near "Wi-Fi" r1/);
747+
assert.match(text, /non-text boundaries: separator r1/);
759748
assert.match(text, /Changed regions:/);
760749
assert.match(text, /1\. top-left x=10 y=20 100x40, 70% of diff, change=brighter/);
761750
assert.match(
@@ -775,9 +764,10 @@ test('formatScreenshotDiffText renders mismatch with pixel counts without color'
775764
text,
776765
/1 \| "Wi-Fi" \| \+10,\+12 \| \+10,0 \| x=120,y=320,w=60,h=22 \| x=130,y=332,w=70,h=22 \| 94 \| ocr-bbox-size-change/,
777766
);
778-
assert.match(text, /Non-text visual deltas \(showing 1\/1; px\):/);
767+
assert.match(text, /Non-text visual deltas \(showing 2\/2; px\):/);
779768
assert.match(text, /item \| region \| slot \| kind \| bboxCurrent \| nearestText/);
780769
assert.match(text, /1 \| r1 \| leading \| icon \| x=80,y=318,w=30,h=30 \| "Wi-Fi"/);
770+
assert.match(text, /2 \| r1 \| separator \| separator \| x=90,y=360,w=120,h=2 \| -/);
781771
assert.equal(text.includes('\x1b['), false);
782772
});
783773

src/utils/__tests__/screenshot-diff-non-text.test.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,15 @@ test('summarizeNonTextDiffDeltas masks OCR text and reports leading icon residua
4747
provider: 'tesseract',
4848
baselineBlocks: 1,
4949
currentBlocks: 1,
50-
baselineBlocksRaw: [
50+
baselineBlocksRaw: [],
51+
currentBlocksRaw: [
5152
{
5253
text: 'Wi-Fi',
5354
confidence: 90,
5455
rect: { x: 68, y: 28, width: 60, height: 24 },
5556
normalizedRect: { x: 30.91, y: 23.33, width: 27.27, height: 20 },
5657
},
5758
],
58-
currentBlocksRaw: [],
5959
matches: [],
6060
},
6161
});
@@ -68,6 +68,40 @@ test('summarizeNonTextDiffDeltas masks OCR text and reports leading icon residua
6868
assert.equal(deltas[0]?.nearestText, 'Wi-Fi');
6969
});
7070

71+
test('summarizeNonTextDiffDeltas uses overlapping baseline text when current OCR misses a row', () => {
72+
const width = 220;
73+
const height = 120;
74+
const diffMask = new Uint8Array(width * height);
75+
paintMaskRect(diffMask, width, { x: 20, y: 30, width: 20, height: 20 });
76+
77+
const deltas = summarizeNonTextDiffDeltas({
78+
diffMask,
79+
width,
80+
height,
81+
regions: [],
82+
ocr: {
83+
provider: 'tesseract',
84+
baselineBlocks: 1,
85+
currentBlocks: 0,
86+
baselineBlocksRaw: [
87+
{
88+
text: 'Wi-Fi',
89+
confidence: 90,
90+
rect: { x: 68, y: 28, width: 60, height: 24 },
91+
normalizedRect: { x: 30.91, y: 23.33, width: 27.27, height: 20 },
92+
},
93+
],
94+
currentBlocksRaw: [],
95+
matches: [],
96+
},
97+
});
98+
99+
assert.equal(deltas.length, 1);
100+
assert.equal(deltas[0]?.slot, 'leading');
101+
assert.equal(deltas[0]?.likelyKind, 'icon');
102+
assert.equal(deltas[0]?.nearestText, 'Wi-Fi');
103+
});
104+
71105
test('summarizeNonTextDiffDeltas omits broad background residuals', () => {
72106
const width = 220;
73107
const height = 120;

src/utils/__tests__/screenshot-diff-ocr.test.ts

Lines changed: 0 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
parseTesseractTsv,
99
summarizeScreenshotOcr,
1010
summarizeOcrMovementClusters,
11-
summarizeOcrTextChanges,
1211
} from '../screenshot-diff-ocr.ts';
1312

1413
test('parseTesseractTsv groups word rows into text line blocks', () => {
@@ -71,67 +70,6 @@ test('matchOcrBlocks reports movement and OCR bbox size change', () => {
7170
assert.equal(matches[0]?.possibleTextMetricMismatch, true);
7271
});
7372

74-
test('summarizeOcrTextChanges normalizes noisy OCR labels before reporting candidates', () => {
75-
const changes = summarizeOcrTextChanges(
76-
[
77-
{
78-
text: 'Airplane Mode',
79-
confidence: 95,
80-
rect: { x: 100, y: 200, width: 80, height: 20 },
81-
normalizedRect: { x: 25, y: 25, width: 20, height: 2.5 },
82-
},
83-
{
84-
text: '2) Personal Hotspot',
85-
confidence: 90,
86-
rect: { x: 100, y: 260, width: 160, height: 20 },
87-
normalizedRect: { x: 25, y: 32.5, width: 40, height: 2.5 },
88-
},
89-
{
90-
text: 'Removed Row',
91-
confidence: 91,
92-
rect: { x: 100, y: 320, width: 140, height: 20 },
93-
normalizedRect: { x: 25, y: 40, width: 35, height: 2.5 },
94-
},
95-
{
96-
text: '4:44',
97-
confidence: 96,
98-
rect: { x: 10, y: 20, width: 60, height: 20 },
99-
normalizedRect: { x: 2.5, y: 2.5, width: 15, height: 2.5 },
100-
},
101-
],
102-
[
103-
{
104-
text: 'Airplane Mode @e~',
105-
confidence: 91,
106-
rect: { x: 120, y: 210, width: 120, height: 20 },
107-
normalizedRect: { x: 30, y: 26.25, width: 30, height: 2.5 },
108-
},
109-
{
110-
text: 'Personal Hotspot',
111-
confidence: 92,
112-
rect: { x: 120, y: 270, width: 140, height: 20 },
113-
normalizedRect: { x: 30, y: 33.75, width: 35, height: 2.5 },
114-
},
115-
{
116-
text: 'Added Row',
117-
confidence: 93,
118-
rect: { x: 120, y: 340, width: 110, height: 20 },
119-
normalizedRect: { x: 30, y: 42.5, width: 27.5, height: 2.5 },
120-
},
121-
],
122-
800,
123-
);
124-
125-
assert.deepEqual(
126-
changes.addedText.map((change) => change.text),
127-
['Added Row'],
128-
);
129-
assert.deepEqual(
130-
changes.removedText.map((change) => change.text),
131-
['Removed Row'],
132-
);
133-
});
134-
13573
test('summarizeOcrMovementClusters groups repeated x-axis text movement', () => {
13674
const clusters = summarizeOcrMovementClusters([
13775
{

src/utils/output.ts

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -336,43 +336,23 @@ function formatScreenshotDiffHints(data: ScreenshotDiffResult): string[] {
336336
);
337337
}
338338

339-
const addedText = data.ocr?.addedText ?? [];
340-
if (addedText.length > 0) {
341-
hints.push(`added text candidates: ${formatTextChanges(addedText)}`);
342-
}
343-
344-
const removedText = data.ocr?.removedText ?? [];
345-
if (removedText.length > 0) {
346-
hints.push(`removed text candidates: ${formatTextChanges(removedText)}`);
347-
}
348-
349339
const controlDeltas = (data.nonTextDeltas ?? [])
350-
.filter((delta) => ['icon', 'toggle', 'chevron', 'separator'].includes(delta.likelyKind))
340+
.filter((delta) => ['icon', 'toggle', 'chevron'].includes(delta.likelyKind))
351341
.slice(0, 3);
352342
if (controlDeltas.length > 0) {
353-
hints.push(`non-text controls/boundaries: ${controlDeltas.map(formatNonTextHint).join('; ')}`);
343+
hints.push(`non-text controls: ${controlDeltas.map(formatNonTextHint).join('; ')}`);
354344
}
355345

356-
const largestRegion = data.regions?.[0];
357-
if (largestRegion) {
358-
hints.push(
359-
`largest changed region: r${largestRegion.index} ${largestRegion.location} ` +
360-
`${largestRegion.shareOfDiffPercentage}% of diff, ${largestRegion.dominantChange}`,
361-
);
346+
const boundaryDeltas = (data.nonTextDeltas ?? [])
347+
.filter((delta) => delta.likelyKind === 'separator')
348+
.slice(0, 2);
349+
if (boundaryDeltas.length > 0) {
350+
hints.push(`non-text boundaries: ${boundaryDeltas.map(formatNonTextHint).join('; ')}`);
362351
}
363352

364353
return hints.slice(0, 6);
365354
}
366355

367-
function formatTextChanges(
368-
changes: Array<{ text: string; rect: { x: number; y: number; width: number; height: number } }>,
369-
): string {
370-
return changes
371-
.slice(0, 3)
372-
.map((change) => `${JSON.stringify(change.text)} at x=${change.rect.x},y=${change.rect.y}`)
373-
.join(', ');
374-
}
375-
376356
function formatNonTextHint(delta: {
377357
likelyKind: string;
378358
nearestText?: string;

src/utils/screenshot-diff-non-text.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,11 @@ export function summarizeNonTextDiffDeltas(params: {
8484
const rawComponents = findConnectedComponents(maskedDiff, params.width, params.height);
8585
const mergedComponents = mergeNearbyComponents(rawComponents, MERGE_GAP_PX);
8686
const currentRows = groupOcrRows(params.ocr?.currentBlocksRaw ?? []);
87-
const fallbackTextBlocks = getOcrBlocks(params.ocr);
87+
const baselineRows = groupOcrRows(params.ocr?.baselineBlocksRaw ?? []);
8888
return (
8989
mergedComponents
9090
.filter(hasUsefulComponentSize)
91-
.map((component) => toNonTextDelta(component, params, currentRows, fallbackTextBlocks))
91+
.map((component) => toNonTextDelta(component, params, currentRows, baselineRows))
9292
// Status bars and top chrome tend to produce noisy residuals around time,
9393
// signal, and battery text; changed regions still report that area.
9494
.filter((delta) => delta.rect.y >= params.height * MIN_CONTENT_Y_RATIO)
@@ -197,11 +197,11 @@ function toNonTextDelta(
197197
regions: ScreenshotDiffRegion[];
198198
},
199199
currentRows: OcrRow[],
200-
fallbackTextBlocks: ScreenshotOcrBlock[],
200+
baselineRows: OcrRow[],
201201
): ScoredNonTextDelta {
202202
const rect = componentToRect(component);
203203
const regionIndex = findContainingRegionIndex(rect, params.regions);
204-
const textAnchor = findTextAnchor(rect, currentRows, fallbackTextBlocks);
204+
const textAnchor = findTextAnchor(rect, currentRows, baselineRows);
205205
const slot = classifySlot(rect, textAnchor?.block.rect, params.width);
206206
const likelyKind = classifyLikelyKind(rect, slot, component.differentPixels, params);
207207
const scoreParams = {
@@ -341,11 +341,12 @@ function findContainingRegionIndex(
341341
function findTextAnchor(
342342
rect: Rect,
343343
currentRows: OcrRow[],
344-
fallbackTextBlocks: ScreenshotOcrBlock[],
344+
baselineRows: OcrRow[],
345345
): { block: ScreenshotOcrBlock; distance: number } | undefined {
346-
const row = findOverlappingRow(rect, currentRows);
347-
if (row) return findNearestText(rect, row.blocks);
348-
return findNearestText(rect, fallbackTextBlocks);
346+
const currentRow = findOverlappingRow(rect, currentRows);
347+
if (currentRow) return findNearestText(rect, currentRow.blocks);
348+
const baselineRow = findOverlappingRow(rect, baselineRows);
349+
return baselineRow ? findNearestText(rect, baselineRow.blocks) : undefined;
349350
}
350351

351352
function findOverlappingRow(rect: Rect, rows: OcrRow[]): OcrRow | undefined {
@@ -410,10 +411,6 @@ function unionRects(rects: Rect[]): Rect {
410411
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
411412
}
412413

413-
function getOcrBlocks(ocr: ScreenshotOcrAnalysis | undefined): ScreenshotOcrBlock[] {
414-
return ocr ? [...ocr.baselineBlocksRaw, ...ocr.currentBlocksRaw] : [];
415-
}
416-
417414
function cleanOcrAnchorText(text: string): string {
418415
return text
419416
.trim()

0 commit comments

Comments
 (0)