fix: normalize gpt image request size
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
# 待测试
|
# 待测试
|
||||||
|
|
||||||
|
- GPT Image 生图请求会在前端把 `9:16`、`16:9` 等比例转换成合法 `WIDTHxHEIGHT` 尺寸,并在非法尺寸时直接显示中文错误,避免上游返回 `invalid_value Invalid size`。
|
||||||
- Docker 部署时,`DATABASE_DSN=data/infinite-canvas.db` 会在存在 `/app/data` 挂载目录时自动归一到 `/app/data/infinite-canvas.db`,需要验证后台模型配置不会再因为工作目录变为 `/app/web` 而读到空库。
|
- Docker 部署时,`DATABASE_DSN=data/infinite-canvas.db` 会在存在 `/app/data` 挂载目录时自动归一到 `/app/data/infinite-canvas.db`,需要验证后台模型配置不会再因为工作目录变为 `/app/web` 而读到空库。
|
||||||
- Seedance 参考视频被火山判定包含真人或隐私信息时,前端错误摘要会提示改用不含真人的视频、官方允许的模型产物或已授权的 `asset://` 素材;参考素材上传目录改为跟随 SQLite 数据目录,并补充公开素材的 HEAD 访问。
|
- Seedance 参考视频被火山判定包含真人或隐私信息时,前端错误摘要会提示改用不含真人的视频、官方允许的模型产物或已授权的 `asset://` 素材;参考素材上传目录改为跟随 SQLite 数据目录,并补充公开素材的 HEAD 访问。
|
||||||
- Seedance 参考素材失败原因排查:后端会把火山上游错误摘要返回给前端;`/video` 和画布视频生成会按 `图片1/视频1/音频1` 自动编号参考素材,并在实际请求提示词中注入编号说明;参考视频会在请求前校验大小、时长、宽高、宽高比和像素总量。
|
- Seedance 参考素材失败原因排查:后端会把火山上游错误摘要返回给前端;`/video` 和画布视频生成会按 `图片1/视频1/音频1` 自动编号参考素材,并在实际请求提示词中注入编号说明;参考视频会在请求前校验大小、时长、宽高、宽高比和像素总量。
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ const aspectOptions = [
|
|||||||
{ value: "1:1", label: "1:1", width: 1024, height: 1024, icon: "square" },
|
{ value: "1:1", label: "1:1", width: 1024, height: 1024, icon: "square" },
|
||||||
{ value: "3:2", label: "3:2", width: 1536, height: 1024, icon: "landscape" },
|
{ value: "3:2", label: "3:2", width: 1536, height: 1024, icon: "landscape" },
|
||||||
{ value: "2:3", label: "2:3", width: 1024, height: 1536, icon: "portrait" },
|
{ value: "2:3", label: "2:3", width: 1024, height: 1536, icon: "portrait" },
|
||||||
{ value: "4:3", label: "4:3", width: 1344, height: 1024, icon: "landscape" },
|
{ value: "4:3", label: "4:3", width: 1360, height: 1024, icon: "landscape" },
|
||||||
{ value: "3:4", label: "3:4", width: 1024, height: 1344, icon: "portrait" },
|
{ value: "3:4", label: "3:4", width: 1024, height: 1360, icon: "portrait" },
|
||||||
{ value: "9:16", label: "9:16", width: 1024, height: 1792, icon: "portrait" },
|
{ value: "16:9", label: "16:9", width: 1824, height: 1024, icon: "landscape" },
|
||||||
|
{ value: "9:16", label: "9:16", width: 1024, height: 1824, icon: "portrait" },
|
||||||
{ value: "1:1-2k", label: "1:1(2k)", size: "2048x2048", width: 2048, height: 2048, icon: "square" },
|
{ value: "1:1-2k", label: "1:1(2k)", size: "2048x2048", width: 2048, height: 2048, icon: "square" },
|
||||||
{ value: "16:9-2k", label: "16:9(2k)", size: "2048x1152", width: 2048, height: 1152, icon: "landscape" },
|
{ value: "16:9-2k", label: "16:9(2k)", size: "2048x1152", width: 2048, height: 1152, icon: "landscape" },
|
||||||
{ value: "9:16-2k", label: "9:16(2k)", size: "1152x2048", width: 1152, height: 2048, icon: "portrait" },
|
{ value: "9:16-2k", label: "9:16(2k)", size: "1152x2048", width: 1152, height: 2048, icon: "portrait" },
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ const QUALITY_ALIASES: Record<string, string> = {
|
|||||||
"2k": "medium",
|
"2k": "medium",
|
||||||
"4k": "high",
|
"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;
|
||||||
|
|
||||||
function normalizeQuality(quality: string) {
|
function normalizeQuality(quality: string) {
|
||||||
const value = quality.trim().toLowerCase();
|
const value = quality.trim().toLowerCase();
|
||||||
@@ -38,36 +44,66 @@ function normalizeQuality(quality: string) {
|
|||||||
return QUALITY_BASE[normalized] ? normalized : undefined;
|
return QUALITY_BASE[normalized] ? normalized : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Map "quality + ratio" to an explicit pixel dimension like "3840x2160". Returns undefined when quality is auto. */
|
/** Map "quality + ratio" to an explicit pixel dimension like "3840x2160". */
|
||||||
function resolveSize(quality: string, ratio: string): string | undefined {
|
function resolveSize(quality: string | undefined, ratio: string): string {
|
||||||
const basePixels = QUALITY_BASE[quality];
|
const parsedRatio = parseImageRatio(ratio);
|
||||||
if (!basePixels || ratio === "auto" || !ratio) return undefined;
|
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;
|
||||||
|
|
||||||
const parts = ratio.split(":");
|
if (basePixels) {
|
||||||
if (parts.length !== 2) return undefined;
|
const targetPixels = basePixels * basePixels;
|
||||||
const w = Number(parts[0]);
|
const longSideRaw = Math.sqrt(targetPixels * longRatio);
|
||||||
const h = Number(parts[1]);
|
longSide = Math.floor(longSideRaw / IMAGE_SIZE_STEP) * IMAGE_SIZE_STEP;
|
||||||
if (!w || !h) return undefined;
|
shortSide = Math.round(longSide / longRatio / IMAGE_SIZE_STEP) * IMAGE_SIZE_STEP;
|
||||||
|
} else {
|
||||||
const targetPixels = basePixels * basePixels;
|
shortSide = DEFAULT_IMAGE_SHORT_SIDE;
|
||||||
const isLandscape = w >= h;
|
longSide = Math.round((shortSide * longRatio) / IMAGE_SIZE_STEP) * IMAGE_SIZE_STEP;
|
||||||
const longRatio = isLandscape ? w / h : h / w;
|
}
|
||||||
|
|
||||||
const longSideRaw = Math.sqrt(targetPixels * longRatio);
|
|
||||||
const longSide = Math.floor(longSideRaw / 16) * 16;
|
|
||||||
const shortSide = Math.round((longSide / longRatio) / 16) * 16;
|
|
||||||
|
|
||||||
const width = isLandscape ? longSide : shortSide;
|
const width = isLandscape ? longSide : shortSide;
|
||||||
const height = isLandscape ? shortSide : longSide;
|
const height = isLandscape ? shortSide : longSide;
|
||||||
|
validateImageSize(width, height);
|
||||||
return `${width}x${height}`;
|
return `${width}x${height}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
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]) };
|
||||||
|
}
|
||||||
|
|
||||||
|
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 之间,请调整尺寸");
|
||||||
|
}
|
||||||
|
|
||||||
function resolveRequestSize(quality: string | undefined, size: string) {
|
function resolveRequestSize(quality: string | undefined, size: string) {
|
||||||
const value = size.trim();
|
const value = size.trim();
|
||||||
if (!value || value === "auto") return undefined;
|
if (!value || value.toLowerCase() === "auto") return undefined;
|
||||||
if (/^\d+x\d+$/.test(value)) return value;
|
const dimensions = parseImageDimensions(value);
|
||||||
return (quality && resolveSize(quality, value)) || 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveImageDataUrl(item: Record<string, unknown>) {
|
function resolveImageDataUrl(item: Record<string, unknown>) {
|
||||||
|
|||||||
Reference in New Issue
Block a user