visual-ai-assertions 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/dist/index.cjs +60 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -11
- package/dist/index.d.ts +50 -11
- package/dist/index.js +60 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -346,6 +346,31 @@ How it works: the library samples frames with ffmpeg and sends them to the provi
|
|
|
346
346
|
|
|
347
347
|
**ffmpeg setup.** Video support works out of the box — `fluent-ffmpeg`, `@ffmpeg-installer/ffmpeg`, and `@ffprobe-installer/ffprobe` ship as regular dependencies and bundle platform-specific ffmpeg/ffprobe binaries. If you ran `npm install` you already have everything you need. On platforms where the prebuilt binary is unavailable (or if you've pruned dependencies), `check()` and `ask()` throw `VisualAIVideoError` (import from `visual-ai-assertions` to `instanceof`-narrow it) when called with video input.
|
|
348
348
|
|
|
349
|
+
### Pre-sampled Frames
|
|
350
|
+
|
|
351
|
+
When you already have an array of screenshots — or can't pass a video file — hand `check()` and `ask()` a `FramesInput` (`{ frames, fps? }`) instead of a `MediaInput`. Each frame is a plain `ImageInput` (buffer, data URL, base64, file path, or URL) or a `{ image, timestampSeconds }` pair. Frames are treated as the same chronological timeline a sampled video produces — no ffmpeg is loaded on this path.
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
const frames = [await page.screenshot(), await page.screenshot()];
|
|
355
|
+
|
|
356
|
+
const result = await ai.check({ frames }, ['A success toast with text "Saved" appears']);
|
|
357
|
+
console.log(result.frames);
|
|
358
|
+
// { count: 2, timestampsSeconds: [0, 1], durationSeconds: 1 }
|
|
359
|
+
|
|
360
|
+
// Control timestamps: give an explicit fps, or per-frame timestampSeconds
|
|
361
|
+
await ai.ask(
|
|
362
|
+
{
|
|
363
|
+
frames: [
|
|
364
|
+
{ image: before, timestampSeconds: 0 },
|
|
365
|
+
{ image: after, timestampSeconds: 2.5 },
|
|
366
|
+
],
|
|
367
|
+
},
|
|
368
|
+
"What changed between the frames?",
|
|
369
|
+
);
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Timestamps for bare frames are derived as `index / fps` (default `fps` is `1`); a per-frame `timestampSeconds` overrides that. The frame count is subject to the same 60-frame hard cap as video sampling, and the `video` sampling option is ignored for this path.
|
|
373
|
+
|
|
349
374
|
### Formatting & Assertion Helpers
|
|
350
375
|
|
|
351
376
|
```typescript
|
package/dist/index.cjs
CHANGED
|
@@ -1773,7 +1773,57 @@ function isVideoInput(input) {
|
|
|
1773
1773
|
}
|
|
1774
1774
|
return false;
|
|
1775
1775
|
}
|
|
1776
|
+
function isFramesInput(input) {
|
|
1777
|
+
return typeof input === "object" && input !== null && !Buffer.isBuffer(input) && !(input instanceof Uint8Array) && Array.isArray(input.frames);
|
|
1778
|
+
}
|
|
1779
|
+
function isTimestampedFrameInput(frame) {
|
|
1780
|
+
return typeof frame === "object" && !Buffer.isBuffer(frame) && !(frame instanceof Uint8Array) && "image" in frame;
|
|
1781
|
+
}
|
|
1782
|
+
async function normalizeFrames(input) {
|
|
1783
|
+
const rawFrames = input.frames;
|
|
1784
|
+
const fps = input.fps ?? DEFAULT_FPS;
|
|
1785
|
+
if (rawFrames.length === 0) {
|
|
1786
|
+
throw new VisualAIVideoError("frames must be a non-empty array of image inputs");
|
|
1787
|
+
}
|
|
1788
|
+
if (rawFrames.length > MAX_FRAMES_HARD_CAP) {
|
|
1789
|
+
throw new VisualAIVideoError(
|
|
1790
|
+
`frames length ${rawFrames.length} exceeds the hard cap of ${MAX_FRAMES_HARD_CAP}. Pass fewer frames or open an issue if you need a larger limit.`
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
if (!Number.isFinite(fps) || fps <= 0) {
|
|
1794
|
+
throw new VisualAIVideoError(`Invalid fps: ${fps}. Must be a finite number > 0.`);
|
|
1795
|
+
}
|
|
1796
|
+
const frames = await Promise.all(
|
|
1797
|
+
rawFrames.map(async (raw, index) => {
|
|
1798
|
+
const timestamped = isTimestampedFrameInput(raw);
|
|
1799
|
+
const imageInput = timestamped ? raw.image : raw;
|
|
1800
|
+
const explicit = timestamped ? raw.timestampSeconds : void 0;
|
|
1801
|
+
const timestampSeconds = explicit ?? index / fps;
|
|
1802
|
+
if (!Number.isFinite(timestampSeconds) || timestampSeconds < 0) {
|
|
1803
|
+
throw new VisualAIVideoError(
|
|
1804
|
+
`Invalid timestampSeconds for frame ${index}: ${String(timestampSeconds)}. Must be a finite number >= 0.`
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
const image = await normalizeImage(imageInput);
|
|
1808
|
+
return {
|
|
1809
|
+
data: image.data,
|
|
1810
|
+
mimeType: image.mimeType,
|
|
1811
|
+
get base64() {
|
|
1812
|
+
return image.base64;
|
|
1813
|
+
},
|
|
1814
|
+
timestampSeconds,
|
|
1815
|
+
index
|
|
1816
|
+
};
|
|
1817
|
+
})
|
|
1818
|
+
);
|
|
1819
|
+
const durationSeconds = frames.reduce((max, f) => Math.max(max, f.timestampSeconds), 0);
|
|
1820
|
+
await saveDebugFrames(frames);
|
|
1821
|
+
return { kind: "video", frames, durationSeconds };
|
|
1822
|
+
}
|
|
1776
1823
|
async function normalizeMedia(input, videoOptions) {
|
|
1824
|
+
if (isFramesInput(input)) {
|
|
1825
|
+
return normalizeFrames(input);
|
|
1826
|
+
}
|
|
1777
1827
|
if (isVideoInput(input)) {
|
|
1778
1828
|
const { path, cleanup } = await resolveVideoToPath(input);
|
|
1779
1829
|
try {
|
|
@@ -1853,8 +1903,12 @@ var AskResultSchema = import_zod.z.object({
|
|
|
1853
1903
|
/**
|
|
1854
1904
|
* For video inputs, the indices of frames the model relied on to answer.
|
|
1855
1905
|
* Indices are 0-based and refer to entries in `frames.timestampsSeconds`.
|
|
1906
|
+
*
|
|
1907
|
+
* Nullable because providers with strict structured-output schemas (e.g. OpenAI)
|
|
1908
|
+
* must mark every field required and represent "no value" as `null` rather than
|
|
1909
|
+
* omitting the key, even for image inputs that were never asked to populate it.
|
|
1856
1910
|
*/
|
|
1857
|
-
frameReferences: import_zod.z.array(import_zod.z.number().int().nonnegative()).optional(),
|
|
1911
|
+
frameReferences: import_zod.z.array(import_zod.z.number().int().nonnegative()).nullable().optional(),
|
|
1858
1912
|
usage: UsageInfoSchema.optional()
|
|
1859
1913
|
});
|
|
1860
1914
|
|
|
@@ -1906,7 +1960,11 @@ function parseCheckResponse(raw) {
|
|
|
1906
1960
|
return reconcileCheckResult(result);
|
|
1907
1961
|
}
|
|
1908
1962
|
function parseAskResponse(raw) {
|
|
1909
|
-
|
|
1963
|
+
const result = parseResponse(raw, AskResponseSchema);
|
|
1964
|
+
return {
|
|
1965
|
+
...result,
|
|
1966
|
+
frameReferences: result.frameReferences ?? void 0
|
|
1967
|
+
};
|
|
1910
1968
|
}
|
|
1911
1969
|
function parseCompareResponse(raw) {
|
|
1912
1970
|
return parseResponse(raw, CompareResponseSchema);
|