jev-compare 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,470 @@
1
+ import { TypeSafeClient, TypeSafeClientConfig } from "@typesafe-ai/sdk";
2
+ //#region src/image.d.ts
3
+ /**
4
+ * An image to compare: a file path, a `data:` URL, `{ base64 }`, raw bytes, or a `Blob`.
5
+ * Only PNG and JPEG are supported.
6
+ */
7
+ type ImageInput = string | Uint8Array | ArrayBuffer | Blob | {
8
+ base64: string;
9
+ };
10
+ type ImageFormat = "png" | "jpeg";
11
+ /** Image bytes with their detected format and pixel size. */
12
+ interface LoadedImage {
13
+ readonly bytes: Uint8Array;
14
+ readonly format: ImageFormat;
15
+ readonly width: number;
16
+ readonly height: number;
17
+ }
18
+ export declare class ImageFormatError extends Error {
19
+ constructor(message: string);
20
+ }
21
+ /** Read an `ImageInput` into bytes and detect its format and dimensions. */
22
+ export declare function loadImage(input: ImageInput): Promise<LoadedImage>;
23
+ /** Detect PNG/JPEG and read the pixel size from the file header. */
24
+ export declare function readImageInfo(bytes: Uint8Array): {
25
+ format: ImageFormat;
26
+ width: number;
27
+ height: number;
28
+ };
29
+ //#endregion
30
+ //#region src/cache.d.ts
31
+ /**
32
+ * Where API answers are cached. Implement it to use your own storage, such as Redis.
33
+ * Values are plain JSON; `get` returns `undefined` on a miss.
34
+ */
35
+ interface CacheStore {
36
+ get(key: string): Promise<unknown> | unknown;
37
+ set(key: string, value: unknown): Promise<void> | void;
38
+ }
39
+ interface MemoryCacheOptions {
40
+ /** Most entries kept; the least recently used are evicted first. Default: 500. */
41
+ maxEntries?: number;
42
+ /** Entry lifetime in milliseconds. Default: no expiry. */
43
+ ttl?: number;
44
+ }
45
+ interface MemoryCache extends CacheStore {
46
+ readonly size: number;
47
+ clear(): void;
48
+ }
49
+ /** An in-process LRU cache, shared by every comparison that is given it. */
50
+ export declare function memoryCache(options?: MemoryCacheOptions): MemoryCache;
51
+ interface FileCacheOptions {
52
+ /** Directory that holds the cache files. */
53
+ dir: string;
54
+ /** Entry lifetime in milliseconds, counted from when it was written. Default: no expiry. */
55
+ ttl?: number;
56
+ /** Most entries kept; the least recently used are evicted first. Default: no limit. */
57
+ maxEntries?: number;
58
+ /** Most bytes kept; the least recently used entries are evicted first. Default: no limit. */
59
+ maxBytes?: number;
60
+ /**
61
+ * Prune automatically after writes, at most once per `pruneInterval` across every process
62
+ * that shares the directory. Default: true when `ttl`, `maxEntries` or `maxBytes` is set.
63
+ */
64
+ autoPrune?: boolean;
65
+ /** Minimum time between automatic prunes, in milliseconds. Default: 1 hour. */
66
+ pruneInterval?: number;
67
+ }
68
+ /** What `prune` deleted and what is left. */
69
+ interface CachePruneResult {
70
+ /** Entries past their `ttl`. */
71
+ expired: number;
72
+ /** Least recently used entries over `maxEntries` or `maxBytes`. */
73
+ evicted: number;
74
+ /** Unreadable entries. */
75
+ invalid: number;
76
+ /** Temp files left by interrupted writes. */
77
+ temp: number;
78
+ /** Bytes freed. */
79
+ freedBytes: number;
80
+ keptEntries: number;
81
+ keptBytes: number;
82
+ }
83
+ interface FileCache extends CacheStore {
84
+ readonly dir: string;
85
+ /** Delete every entry. */
86
+ clear(): Promise<void>;
87
+ /** Delete expired, unreadable and over-limit entries now. */
88
+ prune(): Promise<CachePruneResult>;
89
+ }
90
+ /**
91
+ * A cache on disk, one JSON file per entry, that survives restarts and can be shared by
92
+ * processes. Writes are atomic; unreadable or expired entries count as misses.
93
+ *
94
+ * With `maxEntries` or `maxBytes`, reads mark an entry as recently used (by its file time),
95
+ * and pruning evicts the least recently used entries first. Limits are enforced when pruning,
96
+ * so between prunes the cache can briefly grow past them.
97
+ */
98
+ export declare function fileCache(options: FileCacheOptions | string): FileCache;
99
+ //#endregion
100
+ //#region src/report.d.ts
101
+ /** The `report.json` format version; bumped on incompatible changes. */
102
+ export declare const REPORT_SCHEMA_VERSION = 1;
103
+ /** A JSON-compatible value, for report metadata. */
104
+ type ReportMetaValue = string | number | boolean | null | ReportMetaValue[] | {
105
+ [key: string]: ReportMetaValue;
106
+ };
107
+ type ReportFormat = "full" | "json";
108
+ interface SaveReportOptions {
109
+ /** Directory that holds reports; each report gets its own subdirectory. */
110
+ dir: string;
111
+ /** Report ID and subdirectory name. Default: a UTC timestamp plus a random suffix, so IDs sort by time. */
112
+ id?: string;
113
+ /**
114
+ * What to write: `full` writes `report.json`, `report.html` and copies of both images;
115
+ * `json` writes only `report.json`. `images` and `html` override it. Default: `full`.
116
+ */
117
+ format?: ReportFormat;
118
+ /** Copy both images into the report. Default: true for `full`, false for `json`. */
119
+ images?: boolean;
120
+ /** Write a self-contained `report.html`. Default: true for `full`, false for `json`. */
121
+ html?: boolean;
122
+ /** Your own data to keep with the report, such as a page name, build number or commit. */
123
+ meta?: Record<string, ReportMetaValue>;
124
+ /**
125
+ * Delete old reports in `dir` after this one is saved. The new report is never deleted.
126
+ * Default: keep everything.
127
+ */
128
+ retention?: RetentionPolicy;
129
+ }
130
+ /**
131
+ * Which reports to keep. A report is deleted when it breaks any limit, oldest first.
132
+ * Reports that `keep` protects are never deleted and don't count toward the limits.
133
+ */
134
+ interface RetentionPolicy {
135
+ /** Delete reports older than this many milliseconds. */
136
+ maxAge?: number;
137
+ /** Keep at most this many reports. */
138
+ maxCount?: number;
139
+ /** Keep the newest reports whose total size fits in this many bytes. */
140
+ maxBytes?: number;
141
+ /** Return true to protect a report, such as a baseline, from deletion. */
142
+ keep?: (report: ReportSummary) => boolean;
143
+ }
144
+ interface PruneOptions extends RetentionPolicy {
145
+ /** Work out what would be deleted without deleting anything. Default: false. */
146
+ dryRun?: boolean;
147
+ /**
148
+ * IDs that are never deleted but still count toward the limits, such as the report that
149
+ * was just saved. (Reports that `keep` protects don't count.)
150
+ */
151
+ protect?: string[];
152
+ }
153
+ interface PruneResult {
154
+ /** Reports deleted (or, with `dryRun`, that would be deleted), oldest first. */
155
+ removed: Array<ReportSummary & {
156
+ bytes: number;
157
+ reason: "maxAge" | "maxCount" | "maxBytes";
158
+ }>;
159
+ /** Reports left in place. */
160
+ kept: number;
161
+ /** Bytes freed by the deleted reports. */
162
+ freedBytes: number;
163
+ /** Leftover temporary directories from interrupted saves that were deleted. */
164
+ removedTemp: number;
165
+ /** Reports that could not be deleted. */
166
+ errors: Array<{
167
+ id: string;
168
+ message: string;
169
+ }>;
170
+ }
171
+ interface ReportInput {
172
+ /** The file path the image was read from, or `null` when it was given as bytes. */
173
+ path: string | null;
174
+ sha256: string;
175
+ width: number;
176
+ height: number;
177
+ format: string;
178
+ /** The image's file name inside the report directory, or `null` when images aren't copied. */
179
+ file: string | null;
180
+ }
181
+ /** The contents of `report.json`. */
182
+ interface StoredReport {
183
+ schemaVersion: number;
184
+ id: string;
185
+ /** ISO 8601 time the report was saved. */
186
+ createdAt: string;
187
+ tool: {
188
+ name: string;
189
+ version: string;
190
+ };
191
+ inputs: {
192
+ reference: ReportInput;
193
+ candidate: ReportInput;
194
+ };
195
+ meta: Record<string, ReportMetaValue>;
196
+ result: CompareResult;
197
+ }
198
+ /** Where a report was written. */
199
+ interface SavedReport {
200
+ id: string;
201
+ /** Absolute path of the report directory. */
202
+ dir: string;
203
+ files: {
204
+ json: string;
205
+ html: string | null;
206
+ reference: string | null;
207
+ candidate: string | null;
208
+ };
209
+ /** What `retention` deleted, when it was given. */
210
+ pruned?: PruneResult;
211
+ }
212
+ /** One line of `listReports`. */
213
+ interface ReportSummary {
214
+ id: string;
215
+ dir: string;
216
+ createdAt: string;
217
+ score: number;
218
+ differenceCount: number;
219
+ summary: string;
220
+ reference: {
221
+ path: string | null;
222
+ sha256: string;
223
+ };
224
+ candidate: {
225
+ path: string | null;
226
+ sha256: string;
227
+ };
228
+ meta: Record<string, ReportMetaValue>;
229
+ }
230
+ /**
231
+ * Save a comparison as a report directory with `report.json`, an offline `report.html`, and
232
+ * copies of both images. The directory appears complete or not at all.
233
+ */
234
+ export declare function saveReport(result: CompareResult, reference: ImageInput, candidate: ImageInput, options: SaveReportOptions): Promise<SavedReport>;
235
+ /** Read a saved report from its directory or its `report.json` path. */
236
+ export declare function loadReport(path: string): Promise<StoredReport>;
237
+ /** List the reports in `dir`, newest first. Directories without a readable report are skipped. */
238
+ export declare function listReports(dir: string, options?: {
239
+ limit?: number;
240
+ }): Promise<ReportSummary[]>;
241
+ /**
242
+ * Delete old reports from `dir` by age, count and total size, oldest first. Only directories
243
+ * holding a readable `report.json` are touched; anything else in `dir` is left alone.
244
+ */
245
+ export declare function pruneReports(dir: string, options: PruneOptions): Promise<PruneResult>;
246
+ //#endregion
247
+ //#region src/resize.d.ts
248
+ /** Longest side, in pixels, the TypeSafe API accepts; larger images are rejected. */
249
+ export declare const API_MAX_DIMENSION = 1568;
250
+ /** How images are downscaled before upload. */
251
+ interface ResizeOptions {
252
+ /** Longest side in pixels, at most 1568 (the API's limit). Default: 1568. */
253
+ maxDimension?: number;
254
+ /** Most pixels (width × height) to send; fewer pixels use fewer tokens. Default: 1,150,000. */
255
+ maxPixels?: number;
256
+ /** JPEG quality, 1–100, when a JPEG is re-encoded. Default: 90. */
257
+ quality?: number;
258
+ }
259
+ /** Image bytes ready to upload, with the size actually sent. */
260
+ interface PreparedImage {
261
+ readonly bytes: Uint8Array;
262
+ readonly width: number;
263
+ readonly height: number;
264
+ readonly resized: boolean;
265
+ }
266
+ /** The size to send an image at, keeping its aspect ratio, or `null` when it already fits. */
267
+ export declare function targetSize(width: number, height: number, options?: ResizeOptions): {
268
+ width: number;
269
+ height: number;
270
+ } | null;
271
+ /** Downscale `img` to fit `options`, re-encoding it in its own format; small images pass through. */
272
+ export declare function prepareImage(img: LoadedImage, options?: ResizeOptions): PreparedImage;
273
+ //#endregion
274
+ //#region src/prompts.d.ts
275
+ type Locale = "zh" | "en";
276
+ /** What kind of difference was found. */
277
+ type DifferenceType = "element_missing" | "content_missing" | "background_missing" | "element_extra" | "text_changed" | "color_changed" | "moved" | "resized" | "font_changed" | "icon_changed" | "style_changed";
278
+ /** What kind of element the difference is on. */
279
+ type ElementType = "text" | "button" | "container" | "icon" | "image" | "input" | "navigation" | "other";
280
+ //#endregion
281
+ //#region src/compare.d.ts
282
+ /** A box as fractions (0–1) of the image's width and height. */
283
+ interface NormalizedBox {
284
+ top: number;
285
+ bottom: number;
286
+ left: number;
287
+ right: number;
288
+ }
289
+ /** A box in pixels of the image it refers to. */
290
+ interface PixelBox {
291
+ x: number;
292
+ y: number;
293
+ width: number;
294
+ height: number;
295
+ }
296
+ /** One located difference between the reference and the candidate. */
297
+ interface Difference {
298
+ /** Rank, starting at 1 for the most significant difference. */
299
+ rank: number;
300
+ type: DifferenceType;
301
+ /** Localized label for `type`. */
302
+ typeLabel: string;
303
+ element: ElementType;
304
+ /** Localized label for `element`. */
305
+ elementLabel: string;
306
+ /** Localized fix suggestion. */
307
+ suggestion: string;
308
+ /** Probability, 0–1, that this difference exists. */
309
+ probability: number;
310
+ /** Confidence, 0–1, in the classified `type`. */
311
+ typeConfidence: number;
312
+ /** Where the difference is in the candidate image. */
313
+ box: NormalizedBox;
314
+ boxPx: PixelBox;
315
+ /** Confidence, 0–1, in `box`, or `null` when not reported. */
316
+ boxConfidence: number | null;
317
+ /** Where the difference is in the reference image. */
318
+ referenceBox: NormalizedBox;
319
+ referenceBoxPx: PixelBox;
320
+ }
321
+ /** A kind of difference detected somewhere in the image, with no location. */
322
+ interface Issue {
323
+ type: DifferenceType;
324
+ label: string;
325
+ /** Probability, 0–1, that this kind of difference is present. */
326
+ probability: number;
327
+ suggestion: string;
328
+ }
329
+ interface CompareResult {
330
+ /** Similarity score from 1 (completely different) to 10 (identical), one decimal place. */
331
+ score: number;
332
+ /** The model's confidence, 0–1, in the holistic score. */
333
+ scoreConfidence: number;
334
+ /** The judgments `score` is combined from; see `combineScore`. */
335
+ scoreBreakdown: ScoreBreakdown;
336
+ /** Probability, 0–1, that the images are visually identical. */
337
+ identicalProbability: number;
338
+ /** `true` when `identicalProbability` reaches the threshold and no differences were located. */
339
+ identical: boolean;
340
+ /** One-line localized summary. */
341
+ summary: string;
342
+ /** Located differences, most significant first. */
343
+ differences: Difference[];
344
+ /** Kinds of difference detected over the whole image, most likely first. */
345
+ issues: Issue[];
346
+ /** Original sizes, which pixel boxes refer to, and the sizes sent after downscaling. */
347
+ images: {
348
+ reference: ImageSummary;
349
+ candidate: ImageSummary;
350
+ };
351
+ model: string;
352
+ usage: {
353
+ inputTokens: number;
354
+ outputTokens: number;
355
+ };
356
+ /** Language of the labels, suggestions and summary. */
357
+ locale: Locale;
358
+ /** API requests this comparison needed, and how many were answered from the cache. */
359
+ requests: {
360
+ total: number;
361
+ cached: number;
362
+ };
363
+ /** Where the report was saved, when the `report` option was given. */
364
+ report?: SavedReport;
365
+ }
366
+ interface ScoreBreakdown {
367
+ /** How closely the images match overall, 1–10, on a rubric anchored with examples. */
368
+ holistic: number;
369
+ /** How little of the image differs, 1–10, mapped from the estimated fraction that differs. */
370
+ extent: number;
371
+ /** Probability, 0–1, that the images are visually identical. */
372
+ identicalProbability: number;
373
+ }
374
+ /**
375
+ * Combine the two judgments into the 1–10 score. Each covers the other's blind spot: the
376
+ * holistic rubric tells "same layout, different colors" apart from "unrelated", while extent
377
+ * keeps a small local difference from dragging the score down as far as a large one. The
378
+ * mean is then pulled toward 10 by how likely the images are identical, since neither scale
379
+ * fully commits to its top step.
380
+ */
381
+ export declare function combineScore(b: ScoreBreakdown): number;
382
+ /** Map the expected extent bucket (0 = none … 5 = most of the image) to 1–10, interpolating. */
383
+ export declare function extentScore(bucket: number): number;
384
+ interface ImageSummary {
385
+ width: number;
386
+ height: number;
387
+ format: string;
388
+ /** Size sent to the API; smaller than `width` × `height` when the image was downscaled. */
389
+ sentWidth: number;
390
+ sentHeight: number;
391
+ resized: boolean;
392
+ }
393
+ interface CompareOptions {
394
+ /** TypeSafe API key. Default: `JEV_KEY`, then `TYPESAFE_API_KEY`. */
395
+ apiKey?: string;
396
+ /** Model name. `box` questions need `jev-preview`. Default: `jev-preview`. */
397
+ model?: string;
398
+ /** Language of labels, suggestions and summary. Default: `zh`. */
399
+ locale?: Locale;
400
+ /** How many ranked differences to look for, 0–8. Default: 4. */
401
+ maxDifferences?: number;
402
+ /** Minimum probability, 0–1, for a difference or issue to be reported. Default: 0.5. */
403
+ threshold?: number;
404
+ /**
405
+ * Look again at each difference found: classify it more precisely and widen its boxes to the
406
+ * whole affected element. Costs one extra request per difference, sent in parallel. Default: true.
407
+ */
408
+ refine?: boolean;
409
+ /** Extra context for the model, such as "a mobile app screen; ignore the status bar". */
410
+ context?: string;
411
+ /**
412
+ * How images are downscaled before upload. The API rejects images whose longest side is over
413
+ * 1568 px, and tokens grow with pixel count. Boxes are returned in the original images' pixels
414
+ * either way. `false` sends the original bytes. Default: longest side 1568, at most 1.15 MP.
415
+ */
416
+ resize?: ResizeOptions | false;
417
+ /** An existing client to use instead of creating one. It must send the image opt-in header. */
418
+ client?: TypeSafeClient;
419
+ /** Options for the created client, such as `baseURL`, `timeout` or a proxy-aware `fetch`. */
420
+ clientOptions?: TypeSafeClientConfig;
421
+ /**
422
+ * Cache API answers: a directory for an on-disk cache, or any `CacheStore`, such as
423
+ * `memoryCache()` or `fileCache({ dir, ttl })`. Requests are keyed by everything they send
424
+ * (images, model, prompt and questions), so changing `locale` reuses every answer, and
425
+ * changing other options reuses the ones that still apply. Default: no cache.
426
+ */
427
+ cache?: CacheStore | string | false;
428
+ /**
429
+ * Save a report after comparing: a directory to save into, or report options.
430
+ * The result's `report` says where it was written.
431
+ */
432
+ report?: string | SaveReportOptions;
433
+ /** Cancels the comparison. */
434
+ signal?: AbortSignal;
435
+ }
436
+ /** Create a client configured for image comparison. */
437
+ export declare function createClient(options?: Pick<CompareOptions, "apiKey" | "clientOptions">): TypeSafeClient;
438
+ /**
439
+ * Compare a candidate image against a reference image.
440
+ *
441
+ * Returns a 1–10 similarity score, the located differences with boxes in both images,
442
+ * and a fix suggestion for each.
443
+ */
444
+ export declare function compareImages(reference: ImageInput, candidate: ImageInput, options?: CompareOptions): Promise<CompareResult>;
445
+ /** Convert a fractional box to pixels of `img`. */
446
+ export declare function toPixels(box: NormalizedBox, img: {
447
+ width: number;
448
+ height: number;
449
+ }): PixelBox;
450
+ /** Intersection over union of two fractional boxes. */
451
+ export declare function iou(a: NormalizedBox, b: NormalizedBox): number;
452
+ //#endregion
453
+ //#region src/annotate.d.ts
454
+ interface AnnotateOptions {
455
+ /** Which image the boxes are drawn on. Default: `candidate`. */
456
+ target?: "candidate" | "reference";
457
+ /** Box stroke color. Default: `#ff3b30`. */
458
+ color?: string;
459
+ }
460
+ /**
461
+ * Render an SVG of the image with each difference outlined and numbered by rank.
462
+ * The image is embedded, so the SVG can be saved and opened on its own.
463
+ */
464
+ export declare function annotateDifferences(img: ImageInput, result: CompareResult, options?: AnnotateOptions): Promise<string>;
465
+ //#endregion
466
+ //#region src/report-html.d.ts
467
+ /** Render a report as one self-contained HTML page, with both images embedded. */
468
+ export declare function renderReportHtml(report: StoredReport, reference: LoadedImage, candidate: LoadedImage): string;
469
+ //#endregion
470
+ export type { AnnotateOptions, CachePruneResult, CacheStore, CompareOptions, CompareResult, Difference, DifferenceType, ElementType, FileCache, FileCacheOptions, ImageFormat, ImageInput, ImageSummary, Issue, LoadedImage, Locale, MemoryCache, MemoryCacheOptions, NormalizedBox, PixelBox, PreparedImage, PruneOptions, PruneResult, ReportFormat, ReportInput, ReportMetaValue, ReportSummary, ResizeOptions, RetentionPolicy, SaveReportOptions, SavedReport, ScoreBreakdown, StoredReport };