refactor(core): 任务与模型协议治理 - 拆分服务职责并禁止已发起任务取消
This commit is contained in:
140
web/src/services/api/image-validation.ts
Normal file
140
web/src/services/api/image-validation.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { imageSizeRequest, type ImageCapabilityConfig } from "@/lib/model-capabilities";
|
||||
import type { ReferenceImage } from "@/types/image";
|
||||
|
||||
const QUALITY_BASE: Record<string, number> = {
|
||||
low: 1024,
|
||||
medium: 2048,
|
||||
high: 2880,
|
||||
standard: 1024,
|
||||
hd: 2048,
|
||||
};
|
||||
const QUALITY_ALIASES: Record<string, string> = {
|
||||
"1k": "low",
|
||||
"2k": "medium",
|
||||
"4k": "high",
|
||||
};
|
||||
const DEFAULT_IMAGE_SHORT_SIDE = 1024;
|
||||
const IMAGE_SIZE_STEP = 16;
|
||||
const IMAGE_MIN_PIXELS = 655360;
|
||||
const IMAGE_MAX_PIXELS = 8294400;
|
||||
const IMAGE_MAX_EDGE = 3840;
|
||||
const IMAGE_MAX_RATIO = 3;
|
||||
const VOLCENGINE_ARK_IMAGE_MAX_PIXELS = 4624220;
|
||||
|
||||
export function normalizeQuality(quality: string) {
|
||||
const value = quality.trim().toLowerCase();
|
||||
const normalized = QUALITY_ALIASES[value] || value;
|
||||
return QUALITY_BASE[normalized] ? normalized : undefined;
|
||||
}
|
||||
|
||||
/** grok2api / xAI Imagine:画布 quality 映射为 resolution(1k/2k)。 */
|
||||
export function normalizeGrokImageResolution(quality: string | undefined) {
|
||||
const value = (quality || "").trim().toLowerCase();
|
||||
if (!value || value === "auto") return undefined;
|
||||
if (value === "1k" || value === "low" || value === "standard") return "1k";
|
||||
if (value === "2k" || value === "medium" || value === "hd" || value === "high" || value === "4k") return "2k";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Map "quality + ratio" to an explicit pixel dimension like "3840x2160". */
|
||||
export function resolveSize(quality: string | undefined, ratio: string): string {
|
||||
const parsedRatio = parseImageRatio(ratio);
|
||||
const basePixels = quality ? QUALITY_BASE[quality] : undefined;
|
||||
const isLandscape = parsedRatio.width >= parsedRatio.height;
|
||||
const longRatio = isLandscape ? parsedRatio.width / parsedRatio.height : parsedRatio.height / parsedRatio.width;
|
||||
let longSide: number;
|
||||
let shortSide: number;
|
||||
|
||||
if (basePixels) {
|
||||
const targetPixels = basePixels * basePixels;
|
||||
const longSideRaw = Math.sqrt(targetPixels * longRatio);
|
||||
longSide = Math.floor(longSideRaw / IMAGE_SIZE_STEP) * IMAGE_SIZE_STEP;
|
||||
shortSide = Math.round(longSide / longRatio / IMAGE_SIZE_STEP) * IMAGE_SIZE_STEP;
|
||||
} else {
|
||||
shortSide = DEFAULT_IMAGE_SHORT_SIDE;
|
||||
longSide = Math.round((shortSide * longRatio) / IMAGE_SIZE_STEP) * IMAGE_SIZE_STEP;
|
||||
}
|
||||
|
||||
const width = isLandscape ? longSide : shortSide;
|
||||
const height = isLandscape ? shortSide : longSide;
|
||||
validateImageSize(width, height);
|
||||
return `${width}x${height}`;
|
||||
}
|
||||
|
||||
export function parseImageRatio(value: string) {
|
||||
const parts = value.split(":");
|
||||
if (parts.length !== 2) throw new Error("图像尺寸格式不支持,请使用 auto、9:16 或 1024x1024");
|
||||
const w = Number(parts[0]);
|
||||
const h = Number(parts[1]);
|
||||
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) throw new Error("图像比例必须是正数,例如 9:16");
|
||||
if (Math.max(w, h) / Math.min(w, h) > IMAGE_MAX_RATIO) throw new Error("图像宽高比不能超过 3:1,请调整尺寸");
|
||||
return { width: w, height: h };
|
||||
}
|
||||
|
||||
export function parseImageDimensions(value: string) {
|
||||
const match = value.match(/^(\d+)x(\d+)$/i);
|
||||
if (!match) return null;
|
||||
return { width: Number(match[1]), height: Number(match[2]) };
|
||||
}
|
||||
|
||||
export function validateImageSize(width: number, height: number) {
|
||||
if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) throw new Error("图像尺寸必须是正整数,例如 1024x1024");
|
||||
if (width % IMAGE_SIZE_STEP !== 0 || height % IMAGE_SIZE_STEP !== 0) throw new Error("图像尺寸的宽高必须是 16 的倍数,请调整尺寸");
|
||||
if (Math.max(width, height) > IMAGE_MAX_EDGE) throw new Error("图像尺寸最长边不能超过 3840px,请调整尺寸");
|
||||
if (Math.max(width, height) / Math.min(width, height) > IMAGE_MAX_RATIO) throw new Error("图像宽高比不能超过 3:1,请调整尺寸");
|
||||
const pixels = width * height;
|
||||
if (pixels < IMAGE_MIN_PIXELS || pixels > IMAGE_MAX_PIXELS) throw new Error("图像总像素需在 655360 到 8294400 之间,请调整尺寸");
|
||||
}
|
||||
|
||||
export function resolveRequestSize(quality: string | undefined, size: string) {
|
||||
const value = size.trim();
|
||||
if (!value || value.toLowerCase() === "auto") return undefined;
|
||||
const dimensions = parseImageDimensions(value);
|
||||
if (dimensions) {
|
||||
validateImageSize(dimensions.width, dimensions.height);
|
||||
return `${dimensions.width}x${dimensions.height}`;
|
||||
}
|
||||
if (value.includes(":")) return resolveSize(quality, value);
|
||||
throw new Error("图像尺寸格式不支持,请使用 auto、9:16 或 1024x1024");
|
||||
}
|
||||
|
||||
export function resolveAspectRatio(value: string) {
|
||||
const normalized = value.trim().toLowerCase().replace("×", "x");
|
||||
if (normalized.includes(":")) return normalized;
|
||||
const dimensions = parseImageDimensions(normalized);
|
||||
if (!dimensions) throw new Error("图像比例格式不支持,请使用 3:4 或 1024x1360");
|
||||
const divisor = dimensionGCD(dimensions.width, dimensions.height);
|
||||
return `${dimensions.width / divisor}:${dimensions.height / divisor}`;
|
||||
}
|
||||
|
||||
function dimensionGCD(left: number, right: number) {
|
||||
while (right) [left, right] = [right, left % right];
|
||||
return Math.max(1, left);
|
||||
}
|
||||
|
||||
export function resolveImageRequestSize(profile: ImageCapabilityConfig, quality: string | undefined, size: string) {
|
||||
const request = imageSizeRequest(profile, size);
|
||||
if (!request) return undefined;
|
||||
const value = request.parameter === "size" ? resolveRequestSize(quality, request.value) : resolveAspectRatio(request.value);
|
||||
return value ? { parameter: request.parameter, value } : undefined;
|
||||
}
|
||||
|
||||
export function validateImageCapability(profile: ImageCapabilityConfig, references: ReferenceImage[], mask?: ReferenceImage) {
|
||||
if (references.length > profile.references.maxImages) throw new Error(`当前图片模型最多支持 ${profile.references.maxImages} 张参考图`);
|
||||
if (mask && !profile.references.maskSupported) throw new Error("当前图片模型不支持蒙版编辑");
|
||||
if (profile.references.maxImageBytes > 0 && references.some((image) => (image.bytes || 0) > profile.references.maxImageBytes)) throw new Error("参考图片文件超过当前模型大小限制");
|
||||
}
|
||||
|
||||
export function normalizeVolcengineArkImageSize(size: string | undefined) {
|
||||
if (!size) return undefined;
|
||||
const dimensions = parseImageDimensions(size);
|
||||
if (!dimensions || dimensions.width * dimensions.height <= VOLCENGINE_ARK_IMAGE_MAX_PIXELS) return size;
|
||||
const scale = Math.sqrt(VOLCENGINE_ARK_IMAGE_MAX_PIXELS / (dimensions.width * dimensions.height));
|
||||
let width = Math.floor((dimensions.width * scale) / 2) * 2;
|
||||
let height = Math.floor((dimensions.height * scale) / 2) * 2;
|
||||
while (width > 2 && height > 2 && width * height > VOLCENGINE_ARK_IMAGE_MAX_PIXELS) {
|
||||
if (width >= height) width -= 2;
|
||||
else height -= 2;
|
||||
}
|
||||
return `${width}x${height}`;
|
||||
}
|
||||
Reference in New Issue
Block a user