feat(video): add conservative frame deduplication

This commit is contained in:
Xiangzhe
2026-08-18 01:09:05 -03:00
parent 2c33638643
commit 596a1035c3
5 changed files with 142 additions and 2 deletions

View File

@@ -317,7 +317,11 @@ applied after selection in every policy.
Each frame is limited to 4 MiB, all raw frames together to 23 MiB, and the
serialized broker response to 32 MiB. A private temporary directory is removed
in `finally`. OmniRoute does not bundle FFmpeg and does not accept a custom
executable path.
executable path. Before captioning, the bridge applies a conservative visual
deduplication pass: each JPEG is reduced to a 16×16 grayscale buffer and is
compared only with the last frame retained. The first and final timeline frames
are always retained; comparator or decoder errors fail open and keep coverage.
The output metadata reports how many frames were dropped.
Frames are captioned sequentially with the configured Video model. An empty
Video override inherits the Vision setting; if both are empty, the Vision

View File

@@ -55,6 +55,7 @@ interface VideoResultCacheMetadata {
framesRequested: number;
framesExtracted: number;
framesUsed: number;
dedupDropped?: number;
samplingCandidateCount?: number;
samplingPolicyEffective?: "uniform" | "scene_aware";
samplingPolicyRequested?: "uniform" | "scene_aware";
@@ -91,6 +92,8 @@ function isVideoResultCacheMetadata(value: unknown): value is VideoResultCacheMe
typeof record.framesRequested === "number" &&
typeof record.framesExtracted === "number" &&
typeof record.framesUsed === "number" &&
(record.dedupDropped === undefined ||
(typeof record.dedupDropped === "number" && record.dedupDropped >= 0)) &&
typeof record.cacheBytes === "number" &&
typeof record.modelUsed === "string" &&
(record.samplingCandidateCount === undefined ||
@@ -165,6 +168,7 @@ export class VideoBridgeGuardrail extends BaseGuardrail {
let totalDurationSeconds = 0;
let totalCacheHits = 0;
let totalSamplingCandidateCount = 0;
let totalDedupDropped = 0;
let samplingPolicyEffective: "uniform" | "scene_aware" = "uniform";
let failures = 0;
@@ -205,6 +209,7 @@ export class VideoBridgeGuardrail extends BaseGuardrail {
totalFramesRequested += meta.framesRequested;
totalFramesExtracted += meta.framesExtracted;
totalFramesUsed += meta.framesUsed;
totalDedupDropped += meta.dedupDropped ?? 0;
totalDurationSeconds += meta.durationSeconds;
totalSamplingCandidateCount += meta.samplingCandidateCount ?? 0;
if (meta.samplingPolicyEffective === "scene_aware") {
@@ -246,6 +251,7 @@ export class VideoBridgeGuardrail extends BaseGuardrail {
totalFramesRequested += described.framesRequested;
totalFramesExtracted += described.framesExtracted ?? described.framesUsed;
totalFramesUsed += described.framesUsed;
totalDedupDropped += described.dedupDropped ?? 0;
totalDurationSeconds += described.durationSeconds;
totalSamplingCandidateCount += described.sampling?.candidateCount ?? 0;
if (described.sampling?.policyEffective === "scene_aware") {
@@ -271,6 +277,7 @@ export class VideoBridgeGuardrail extends BaseGuardrail {
framesRequested: described.framesRequested,
framesExtracted: described.framesExtracted ?? described.framesUsed,
framesUsed: described.framesUsed,
dedupDropped: described.dedupDropped ?? 0,
cacheBytes: resultCacheBytes,
modelUsed: described.modelUsed ?? selectedModel,
samplingCandidateCount: described.sampling?.candidateCount ?? 0,
@@ -340,6 +347,7 @@ export class VideoBridgeGuardrail extends BaseGuardrail {
framesExtracted: totalFramesExtracted,
framesRequested: totalFramesRequested,
framesUsed: totalFramesUsed,
dedupDropped: totalDedupDropped,
samplingCandidateCount: totalSamplingCandidateCount,
samplingPolicyEffective,
samplingPolicyRequested: runtime.samplingPolicy,

View File

@@ -111,6 +111,81 @@ export interface DescribedVideo {
framesUsed: number;
modelUsed?: string;
sampling?: VideoSamplingMetadata;
dedupDropped?: number;
}
export interface VideoCaptionFrame {
dataUri: string;
timestampSeconds: number;
}
export interface VideoFrameDeduplicationResult {
dropped: number;
frames: VideoCaptionFrame[];
}
type VideoFrameComparator = (
previous: VideoCaptionFrame,
current: VideoCaptionFrame
) => Promise<number>;
const VIDEO_DEDUP_THRESHOLD = 0.04;
async function compareVideoFramesByGrayscale(
previous: VideoCaptionFrame,
current: VideoCaptionFrame
): Promise<number> {
const decode = (dataUri: string): Buffer => {
const match = /^data:image\/jpeg;base64,([A-Za-z0-9+/=]+)$/i.exec(dataUri);
if (!match) throw new Error("Video frame is not a JPEG data URI");
return Buffer.from(match[1], "base64");
};
const { default: sharp } = await import("sharp");
const [left, right] = await Promise.all(
[previous, current].map((frame) =>
sharp(decode(frame.dataUri)).resize(16, 16, { fit: "fill" }).greyscale().raw().toBuffer()
)
);
if (left.length !== right.length || left.length === 0) {
throw new Error("Video frame comparison returned invalid dimensions");
}
let difference = 0;
for (let index = 0; index < left.length; index++) {
difference += Math.abs(left[index] - right[index]) / 255;
}
return difference / left.length;
}
export async function deduplicateVideoFrames(
frames: readonly VideoCaptionFrame[],
options: { compare?: VideoFrameComparator; threshold?: number } = {}
): Promise<VideoFrameDeduplicationResult> {
if (frames.length < 2) return { dropped: 0, frames: [...frames] };
const compare = options.compare ?? compareVideoFramesByGrayscale;
const threshold =
typeof options.threshold === "number" && Number.isFinite(options.threshold)
? Math.max(0, Math.min(1, options.threshold))
: VIDEO_DEDUP_THRESHOLD;
const kept: VideoCaptionFrame[] = [frames[0]];
let dropped = 0;
for (let index = 1; index < frames.length; index++) {
const current = frames[index];
if (index === frames.length - 1) {
kept.push(current);
continue;
}
try {
const distance = await compare(kept[kept.length - 1], current);
if (Number.isFinite(distance) && distance <= threshold) {
dropped += 1;
continue;
}
} catch {
// A malformed or unsupported frame must never reduce visual coverage.
}
kept.push(current);
}
return { dropped, frames: kept };
}
function normalizeBase64(base64: string): string {
@@ -223,8 +298,9 @@ export async function describeVideoPart(
timeoutMs: options.timeoutMs,
});
const deduplicated = await deduplicateVideoFrames(extracted.frames);
const descriptions: string[] = [];
for (const frame of extracted.frames) {
for (const frame of deduplicated.frames) {
if (signal.aborted) throw new Error("Video Bridge processing timed out or was aborted");
try {
const caption = (await captionFrame(frame.dataUri, frame.timestampSeconds, signal)).trim();
@@ -247,6 +323,7 @@ export async function describeVideoPart(
framesExtracted: extracted.frames.length,
framesRequested: options.frameCount,
framesUsed: descriptions.length,
dedupDropped: deduplicated.dropped,
sampling: extracted.sampling,
};
} catch (error) {

View File

@@ -100,6 +100,7 @@ test("preserves scene-aware sampler metadata in guardrail meta and the transpare
framesRequested: 4,
framesExtracted: 4,
framesUsed: 4,
dedupDropped: 1,
sampling: {
candidateCount: 3,
policyEffective: "scene_aware",
@@ -113,6 +114,7 @@ test("preserves scene-aware sampler metadata in guardrail meta and the transpare
assert.equal(result.meta?.samplingPolicyRequested, "scene_aware");
assert.equal(result.meta?.samplingPolicyEffective, "scene_aware");
assert.equal(result.meta?.samplingCandidateCount, 3);
assert.equal(result.meta?.dedupDropped, 1);
assert.equal(
buildModalityBridgeHeader([{ guardrail: "video-bridge", meta: result.meta }]),
"video->text;model=openai/gpt-4o-mini;parts=1;sampling=scene_aware;candidates=3"

View File

@@ -0,0 +1,49 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
deduplicateVideoFrames,
type VideoCaptionFrame,
} from "../../../src/lib/guardrails/videoBridgeHelpers.ts";
const frame = (
timestampSeconds: number,
dataUri = "data:image/jpeg;base64,QQ=="
): VideoCaptionFrame => ({
dataUri,
timestampSeconds,
});
test("deduplication keeps the first frame and the final frame while dropping redundant middle frames", async () => {
const result = await deduplicateVideoFrames([frame(1), frame(2), frame(3), frame(4)], {
compare: async () => 0.01,
threshold: 0.05,
});
assert.deepEqual(
result.frames.map((item) => item.timestampSeconds),
[1, 4]
);
assert.equal(result.dropped, 2);
});
test("deduplication keeps visually distinct frames", async () => {
const result = await deduplicateVideoFrames([frame(1), frame(2), frame(3)], {
compare: async () => 0.2,
threshold: 0.05,
});
assert.equal(result.frames.length, 3);
assert.equal(result.dropped, 0);
});
test("deduplication fails open when the visual comparator errors", async () => {
const result = await deduplicateVideoFrames([frame(1), frame(2)], {
compare: async () => {
throw new Error("invalid JPEG");
},
});
assert.equal(result.frames.length, 2);
assert.equal(result.dropped, 0);
});