fix(gateway): image vision analysis + media cache lock failures
Two root causes behind 'all image analysis failing':
1. imageResizer still emitted lossless PNG for vision input. A 1024px
Facebook photo balloons to multi-MB PNG base64 that the vision model
silently rejects ('Vision API null response'). Switch to JPEG q85
(no upscaling) — same photo drops to ~100-400KB, model processes fine.
Re-encodes even already-small images so raw originals never bloat the
data URL. Added tests/imageResizer.test.ts covering both cases.
2. acquireMediaAnalysisLock INSERT aborted with 'index row requires N
bytes, maximum size is 8191'. text_analysis_cache.text is the PK in a
B-tree index (8191-byte/row cap); callers pass the raw image URL as the
key, and base64 data URLs / very long URLs blow past the limit, so the
lock INSERT fails and every media analysis is skipped. Hash the URL in
makeImageCacheKey (image:<sha256[:32]>) — fixed-length, deterministic,
well under the limit. All store/get/lock/delete callers already route
through this function so lookup stays consistent.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import sharp from "sharp";
|
||||
import { resizeImageForVision } from "../src/modules/attachment-upload/imageResizer.js";
|
||||
|
||||
// Build a worst-case (poorly-compressing) 1024x1024 image, like a real photo.
|
||||
async function makeNoisyBuffer(): Promise<Buffer> {
|
||||
const w = 1024,
|
||||
h = 1024;
|
||||
const buf = Buffer.alloc(w * h * 3);
|
||||
let seed = 99;
|
||||
const rnd = () => {
|
||||
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
|
||||
return seed / 0x7fffffff;
|
||||
};
|
||||
for (let i = 0; i < buf.length; i++) buf[i] = Math.floor(rnd() * 256);
|
||||
return sharp(buf, { raw: { width: w, height: h, channels: 3 } })
|
||||
.png({ compressionLevel: 1 })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
describe("resizeImageForVision — vision input encoding", () => {
|
||||
it("always encodes to JPEG (not lossless PNG)", async () => {
|
||||
const src = await makeNoisyBuffer();
|
||||
// Source PNG is large & lossless (the old failure mode)
|
||||
expect(src.length).toBeGreaterThan(300_000);
|
||||
|
||||
const { data, mimeType } = await resizeImageForVision(src, 1024);
|
||||
expect(mimeType).toBe("image/jpeg");
|
||||
// JPEG encoding must shrink the data URL payload well below MB range.
|
||||
expect(data.length).toBeLessThan(800_000);
|
||||
});
|
||||
|
||||
it("re-encodes already-small images (no passthrough of raw originals)", async () => {
|
||||
const small = await sharp({
|
||||
create: {
|
||||
width: 200,
|
||||
height: 200,
|
||||
channels: 3,
|
||||
background: { r: 200, g: 100, b: 50 },
|
||||
},
|
||||
})
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
const { data, mimeType } = await resizeImageForVision(small, 1024);
|
||||
expect(mimeType).toBe("image/jpeg");
|
||||
// Even a tiny PNG must come out re-encoded (bounded), not the raw PNG bytes.
|
||||
expect(data.length).toBeLessThan(small.length);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user