32fb2cdeaf
Add chat file/image upload UX, attachment proxying, vision thumbnails, and per-turn image scoping so agents only use the current upload. Extend MindSpace asset context, billing token state, OA/scenario verify scripts, and related runtime config. Co-authored-by: Cursor <cursoragent@cursor.com>
74 lines
2.1 KiB
JavaScript
74 lines
2.1 KiB
JavaScript
import sharp from 'sharp';
|
|
|
|
export const VISION_THUMB_MAX_SIDE = 1280;
|
|
export const VISION_THUMB_MAX_BYTES = 600 * 1024;
|
|
export const VISION_THUMB_JPEG_QUALITY = 78;
|
|
|
|
/**
|
|
* Build a smaller JPEG buffer for vision-model analysis only.
|
|
* Page generation still uses the full public standard image elsewhere.
|
|
*/
|
|
export async function buildVisionThumbnailBuffer(
|
|
buffer,
|
|
mimeType = 'image/jpeg',
|
|
{
|
|
maxSide = VISION_THUMB_MAX_SIDE,
|
|
maxBytes = VISION_THUMB_MAX_BYTES,
|
|
quality = VISION_THUMB_JPEG_QUALITY,
|
|
} = {},
|
|
) {
|
|
if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
|
|
throw new Error('vision thumb: empty buffer');
|
|
}
|
|
|
|
const metadata = await sharp(buffer, { sequentialRead: true }).metadata();
|
|
const width = Number(metadata.width ?? 0);
|
|
const height = Number(metadata.height ?? 0);
|
|
if (!width || !height) {
|
|
throw new Error('vision thumb: invalid dimensions');
|
|
}
|
|
|
|
const longest = Math.max(width, height);
|
|
const resizeWidth = longest > maxSide ? Math.max(1, Math.round((width * maxSide) / longest)) : null;
|
|
const resizeHeight = longest > maxSide ? Math.max(1, Math.round((height * maxSide) / longest)) : null;
|
|
|
|
let pipeline = sharp(buffer, { sequentialRead: true }).rotate();
|
|
if (resizeWidth && resizeHeight) {
|
|
pipeline = pipeline.resize({
|
|
width: resizeWidth,
|
|
height: resizeHeight,
|
|
fit: 'inside',
|
|
withoutEnlargement: true,
|
|
});
|
|
}
|
|
|
|
let nextQuality = quality;
|
|
let candidate = await pipeline
|
|
.flatten({ background: '#ffffff' })
|
|
.jpeg({ quality: nextQuality, mozjpeg: true })
|
|
.toBuffer();
|
|
|
|
while (candidate.length > maxBytes && nextQuality > 52) {
|
|
nextQuality = Math.max(52, nextQuality - 8);
|
|
candidate = await sharp(buffer, { sequentialRead: true })
|
|
.rotate()
|
|
.resize({
|
|
width: resizeWidth ?? width,
|
|
height: resizeHeight ?? height,
|
|
fit: 'inside',
|
|
withoutEnlargement: true,
|
|
})
|
|
.flatten({ background: '#ffffff' })
|
|
.jpeg({ quality: nextQuality, mozjpeg: true })
|
|
.toBuffer();
|
|
}
|
|
|
|
return candidate;
|
|
}
|
|
|
|
export const visionThumbInternals = {
|
|
VISION_THUMB_MAX_SIDE,
|
|
VISION_THUMB_MAX_BYTES,
|
|
VISION_THUMB_JPEG_QUALITY,
|
|
};
|