Files
deepseek-harness/packages/attachment/attachment-local/src/encoding.ts
T
creatixchu 30704dc1df fix(attachment): budget master pixels and share the encoding ladder
Master dimensions move from a 2048 long-edge rule to a total-pixel
budget (normalizedImageMaxPixels, default 2048x2048) with an 8192
long-edge cap, so extreme aspect ratios keep short-edge resolution.
The shared quality ladder and lazy execution move to encoding.ts,
review-round doc fixes land across attachment and llm packages, and
the superseded facts in the unified-image-pipeline note now describe
the shipped routing.
2026-08-24 17:10:59 +08:00

84 lines
3.2 KiB
TypeScript

/** Shared quality ladder and lazy candidate execution for normalization and request-image encoders. */
import type { Sharp } from 'sharp'
/** Shared ladder for both encoders: spaced so each step buys a real size reduction. */
export const IMAGE_ENCODING_QUALITIES = [85, 75, 60] as const
/** Fixed lossy-WebP effort; deeper search costs 3-4x encode time for about 5% size. */
export const WEBP_ENCODING_EFFORT = 0
/** One ladder output carrying its complete bytes and exact facts. */
export interface EncodedImage {
data: Uint8Array
mediaType: 'image/jpeg' | 'image/webp'
width: number
height: number
}
async function encode(pipeline: Sharp, mediaType: EncodedImage['mediaType'], quality: number): Promise<EncodedImage> {
const encoded = mediaType === 'image/webp'
? pipeline.webp({ quality, effort: WEBP_ENCODING_EFFORT })
: pipeline.jpeg({ quality })
const { data, info } = await encoded.toBuffer({ resolveWithObject: true })
return { data: new Uint8Array(data), mediaType, width: info.width, height: info.height }
}
/**
* Build the lazy quality ladder for one prepared pipeline: WebP keeps a source
* alpha channel, everything else is JPEG.
* @param prepared - sized sRGB pipeline; cloned per candidate.
* @param hasAlpha - decoded source alpha fact selecting the codec.
* @returns encoders ordered from highest to lowest ladder quality.
*/
export function encodingLadder(prepared: Sharp, hasAlpha: boolean): Array<() => Promise<EncodedImage>> {
const mediaType = hasAlpha ? 'image/webp' : 'image/jpeg'
return IMAGE_ENCODING_QUALITIES.map(quality => (
() => encode(prepared.clone(), mediaType, quality)
))
}
/** One encoded candidate carrying its complete bytes. */
export interface EncodedCandidate {
data: Uint8Array
}
/** Result of exhausting candidates at one raster size without a fitting output. */
export interface ExhaustedEncoding<T extends EncodedCandidate> {
smallest: T
}
/**
* Execute encoding candidates in preference order and stop after the first fitting output.
* @param attempts - lazy encoders ordered from preferred to fallback representation.
* @param maxBytes - positive encoded-byte target.
* @returns the first fitting candidate, otherwise the smallest completed fallback.
*/
export async function encodeFirstWithinLimit<T extends EncodedCandidate>(
attempts: readonly (() => Promise<T>)[],
maxBytes: number,
): Promise<T | ExhaustedEncoding<T>> {
const [first, ...remaining] = attempts
if (first === undefined) throw new Error('image encoding requires at least one candidate')
let smallest = await first()
if (smallest.data.byteLength <= maxBytes) return smallest
for (const attempt of remaining) {
const candidate = await attempt()
if (candidate.data.byteLength <= maxBytes) return candidate
if (candidate.data.byteLength < smallest.data.byteLength) {
smallest = candidate
}
}
return { smallest }
}
/**
* Whether a lazy encoding result exhausted every candidate at one size.
* @param result - first fitting candidate or exhausted result.
* @returns whether every candidate exceeded the byte target.
*/
export function isExhaustedEncoding<T extends EncodedCandidate>(
result: T | ExhaustedEncoding<T>,
): result is ExhaustedEncoding<T> {
return 'smallest' in result
}