feat(image): 实现图像生成质量与尺寸自适应功能
- 新增 QUALITY_BASE 配置对象定义低中高质量的基础像素值 - 实现 resolveSize 函数将质量等级与宽高比转换为精确像素尺寸 - 在图像生成请求中集成像素尺寸解析逻辑 - 更新图像编辑功能以支持动态像素尺寸设置 - 优化 API 请求参数传递方式使用展开运算符 - 调整页面组件中的质量选项显示标签增加像素信息
This commit is contained in:
@@ -51,7 +51,12 @@ type GenerationLog = {
|
|||||||
type UpdateAiConfig = <K extends keyof AiConfig>(key: K, value: AiConfig[K]) => void;
|
type UpdateAiConfig = <K extends keyof AiConfig>(key: K, value: AiConfig[K]) => void;
|
||||||
|
|
||||||
const sizeOptions = ["auto", "1:1", "3:2", "2:3", "4:3", "3:4", "16:9", "9:16"].map((value) => ({ label: value, value }));
|
const sizeOptions = ["auto", "1:1", "3:2", "2:3", "4:3", "3:4", "16:9", "9:16"].map((value) => ({ label: value, value }));
|
||||||
const qualityOptions = ["auto", "low", "medium", "high"].map((value) => ({ label: value, value }));
|
const qualityOptions = [
|
||||||
|
{ label: "auto", value: "auto" },
|
||||||
|
{ label: "low / 1K", value: "low" },
|
||||||
|
{ label: "medium / 2K", value: "medium" },
|
||||||
|
{ label: "high / 4K", value: "high" },
|
||||||
|
];
|
||||||
const LOG_STORE_KEY = "infinite-canvas:image_generation_logs";
|
const LOG_STORE_KEY = "infinite-canvas:image_generation_logs";
|
||||||
const LOG_STORE_PREFIX = `${LOG_STORE_KEY}:`;
|
const LOG_STORE_PREFIX = `${LOG_STORE_KEY}:`;
|
||||||
const logStore = localforage.createInstance({ name: "infinite-canvas", storeName: "image_generation_logs" });
|
const logStore = localforage.createInstance({ name: "infinite-canvas", storeName: "image_generation_logs" });
|
||||||
|
|||||||
@@ -18,6 +18,37 @@ type ImageApiResponse = {
|
|||||||
msg?: string;
|
msg?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const QUALITY_BASE: Record<string, number> = {
|
||||||
|
low: 1024,
|
||||||
|
medium: 2048,
|
||||||
|
high: 2880,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Map "quality + ratio" to an explicit pixel dimension like "3840x2160". Returns undefined when quality is auto. */
|
||||||
|
function resolveSize(quality: string, ratio: string): string | undefined {
|
||||||
|
const basePixels = QUALITY_BASE[quality];
|
||||||
|
if (!basePixels || ratio === "auto" || !ratio) return undefined;
|
||||||
|
|
||||||
|
const parts = ratio.split(":");
|
||||||
|
if (parts.length !== 2) return undefined;
|
||||||
|
const w = Number(parts[0]);
|
||||||
|
const h = Number(parts[1]);
|
||||||
|
if (!w || !h) return undefined;
|
||||||
|
|
||||||
|
const targetPixels = basePixels * basePixels;
|
||||||
|
const isLandscape = w >= h;
|
||||||
|
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 height = isLandscape ? shortSide : longSide;
|
||||||
|
|
||||||
|
return `${width}x${height}`;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveImageDataUrl(item: Record<string, unknown>) {
|
function resolveImageDataUrl(item: Record<string, unknown>) {
|
||||||
if (typeof item.b64_json === "string" && item.b64_json) {
|
if (typeof item.b64_json === "string" && item.b64_json) {
|
||||||
return `data:image/png;base64,${item.b64_json}`;
|
return `data:image/png;base64,${item.b64_json}`;
|
||||||
@@ -94,6 +125,7 @@ function withSystemMessage(config: AiConfig, messages: ChatCompletionMessage[])
|
|||||||
|
|
||||||
export async function requestGeneration(config: AiConfig, prompt: string) {
|
export async function requestGeneration(config: AiConfig, prompt: string) {
|
||||||
const n = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(config.count)) || 1)));
|
const n = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(config.count)) || 1)));
|
||||||
|
const pixelSize = resolveSize(config.quality, config.size);
|
||||||
try {
|
try {
|
||||||
const response = await axios.post<ImageApiResponse>(
|
const response = await axios.post<ImageApiResponse>(
|
||||||
aiApiUrl(config, "/images/generations"),
|
aiApiUrl(config, "/images/generations"),
|
||||||
@@ -101,8 +133,7 @@ export async function requestGeneration(config: AiConfig, prompt: string) {
|
|||||||
model: config.model,
|
model: config.model,
|
||||||
prompt: withSystemPrompt(config, prompt),
|
prompt: withSystemPrompt(config, prompt),
|
||||||
n,
|
n,
|
||||||
quality: config.quality || undefined,
|
...(pixelSize ? { quality: config.quality, size: pixelSize } : {}),
|
||||||
size: config.size || undefined,
|
|
||||||
response_format: "b64_json",
|
response_format: "b64_json",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -117,16 +148,15 @@ export async function requestGeneration(config: AiConfig, prompt: string) {
|
|||||||
|
|
||||||
export async function requestEdit(config: AiConfig, prompt: string, references: ReferenceImage[]) {
|
export async function requestEdit(config: AiConfig, prompt: string, references: ReferenceImage[]) {
|
||||||
const n = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(config.count)) || 1)));
|
const n = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(config.count)) || 1)));
|
||||||
|
const pixelSize = resolveSize(config.quality, config.size);
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.set("model", config.model);
|
formData.set("model", config.model);
|
||||||
formData.set("prompt", withSystemPrompt(config, prompt));
|
formData.set("prompt", withSystemPrompt(config, prompt));
|
||||||
formData.set("n", String(n));
|
formData.set("n", String(n));
|
||||||
formData.set("response_format", "b64_json");
|
formData.set("response_format", "b64_json");
|
||||||
if (config.quality) {
|
if (pixelSize) {
|
||||||
formData.set("quality", config.quality);
|
formData.set("quality", config.quality);
|
||||||
}
|
formData.set("size", pixelSize);
|
||||||
if (config.size) {
|
|
||||||
formData.set("size", config.size);
|
|
||||||
}
|
}
|
||||||
const files = await Promise.all(references.map(async (image) => dataUrlToFile({ ...image, dataUrl: await imageToDataUrl(image) })));
|
const files = await Promise.all(references.map(async (image) => dataUrlToFile({ ...image, dataUrl: await imageToDataUrl(image) })));
|
||||||
files.forEach((file) => formData.append("image", file));
|
files.forEach((file) => formData.append("image", file));
|
||||||
|
|||||||
Reference in New Issue
Block a user