mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-05 17:04:27 +08:00
feat(image-settings): add transparent background toggle for image generation
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
+ [新增] 图像设置新增「透明背景」开关,开启后生成无背景的透明图像。
|
||||||
|
|
||||||
## v0.8.1 - 2026-07-16
|
## v0.8.1 - 2026-07-16
|
||||||
|
|
||||||
+ [新增] 插件 SDK 扩展:AI 生成能力、面板控制能力。
|
+ [新增] 插件 SDK 扩展:AI 生成能力、面板控制能力。
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode: Can
|
|||||||
model,
|
model,
|
||||||
quality: node.metadata?.quality || globalConfig.quality || defaultConfig.quality,
|
quality: node.metadata?.quality || globalConfig.quality || defaultConfig.quality,
|
||||||
size: node.metadata?.size || globalConfig.size || defaultConfig.size,
|
size: node.metadata?.size || globalConfig.size || defaultConfig.size,
|
||||||
|
background: node.metadata?.background ?? globalConfig.background ?? defaultConfig.background,
|
||||||
videoSeconds: node.metadata?.seconds || globalConfig.videoSeconds || defaultConfig.videoSeconds,
|
videoSeconds: node.metadata?.seconds || globalConfig.videoSeconds || defaultConfig.videoSeconds,
|
||||||
vquality: node.metadata?.vquality || globalConfig.vquality || defaultConfig.vquality,
|
vquality: node.metadata?.vquality || globalConfig.vquality || defaultConfig.vquality,
|
||||||
videoGenerateAudio: node.metadata?.generateAudio || globalConfig.videoGenerateAudio || defaultConfig.videoGenerateAudio,
|
videoGenerateAudio: node.metadata?.generateAudio || globalConfig.videoGenerateAudio || defaultConfig.videoGenerateAudio,
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode: Can
|
|||||||
model,
|
model,
|
||||||
quality: node.metadata?.quality || globalConfig.quality || defaultConfig.quality,
|
quality: node.metadata?.quality || globalConfig.quality || defaultConfig.quality,
|
||||||
size: node.metadata?.size || globalConfig.size || defaultConfig.size,
|
size: node.metadata?.size || globalConfig.size || defaultConfig.size,
|
||||||
|
background: node.metadata?.background ?? globalConfig.background ?? defaultConfig.background,
|
||||||
videoSeconds: node.metadata?.seconds || globalConfig.videoSeconds || defaultConfig.videoSeconds,
|
videoSeconds: node.metadata?.seconds || globalConfig.videoSeconds || defaultConfig.videoSeconds,
|
||||||
vquality: node.metadata?.vquality || globalConfig.vquality || defaultConfig.vquality,
|
vquality: node.metadata?.vquality || globalConfig.vquality || defaultConfig.vquality,
|
||||||
videoGenerateAudio: node.metadata?.generateAudio || globalConfig.videoGenerateAudio || defaultConfig.videoGenerateAudio,
|
videoGenerateAudio: node.metadata?.generateAudio || globalConfig.videoGenerateAudio || defaultConfig.videoGenerateAudio,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const imageAspectOptions = aspectOptions.map((item) => ({ value: item.siz
|
|||||||
|
|
||||||
type ImageSettingsPanelProps = {
|
type ImageSettingsPanelProps = {
|
||||||
config: AiConfig;
|
config: AiConfig;
|
||||||
onConfigChange: (key: "quality" | "size" | "count", value: string) => void;
|
onConfigChange: (key: "quality" | "size" | "count" | "background", value: string) => void;
|
||||||
theme: CanvasTheme;
|
theme: CanvasTheme;
|
||||||
showTitle?: boolean;
|
showTitle?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -46,6 +46,7 @@ export function ImageSettingsPanel({ config, onConfigChange, theme, showTitle =
|
|||||||
const quality = config.quality || "auto";
|
const quality = config.quality || "auto";
|
||||||
const count = Math.max(1, Math.min(maxCount, Math.floor(Math.abs(Number(config.count)) || 1)));
|
const count = Math.max(1, Math.min(maxCount, Math.floor(Math.abs(Number(config.count)) || 1)));
|
||||||
const activeSize = config.size || "auto";
|
const activeSize = config.size || "auto";
|
||||||
|
const transparentBackground = config.background === "transparent";
|
||||||
const selectedAspect = aspectOptions.find((item) => (item.size || item.value) === activeSize || item.value === activeSize);
|
const selectedAspect = aspectOptions.find((item) => (item.size || item.value) === activeSize || item.value === activeSize);
|
||||||
const dimensions = readSizeDimensions(activeSize, selectedAspect || aspectOptions[0]);
|
const dimensions = readSizeDimensions(activeSize, selectedAspect || aspectOptions[0]);
|
||||||
const selectAspect = (value: string) => {
|
const selectAspect = (value: string) => {
|
||||||
@@ -117,6 +118,17 @@ export function ImageSettingsPanel({ config, onConfigChange, theme, showTitle =
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<SettingTitle color={theme.node.muted}>透明背景</SettingTitle>
|
||||||
|
<div className="text-xs" style={{ color: theme.node.muted, opacity: 0.75 }}>
|
||||||
|
开启后生成无背景的透明图像
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span onMouseDown={(event) => event.stopPropagation()}>
|
||||||
|
<Switch size="small" checked={transparentBackground} onChange={(checked) => onConfigChange("background", checked ? "transparent" : "")} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div className="space-y-2.5">
|
<div className="space-y-2.5">
|
||||||
<SettingTitle color={theme.node.muted}>生成张数</SettingTitle>
|
<SettingTitle color={theme.node.muted}>生成张数</SettingTitle>
|
||||||
<div className="grid grid-cols-4 gap-2.5">
|
<div className="grid grid-cols-4 gap-2.5">
|
||||||
|
|||||||
@@ -2522,6 +2522,7 @@ function InfiniteCanvasPage() {
|
|||||||
model: savedImageMetadata.model || effectiveConfig.imageModel || effectiveConfig.model,
|
model: savedImageMetadata.model || effectiveConfig.imageModel || effectiveConfig.model,
|
||||||
quality: savedImageMetadata.quality || effectiveConfig.quality,
|
quality: savedImageMetadata.quality || effectiveConfig.quality,
|
||||||
size: savedImageMetadata.size || effectiveConfig.size,
|
size: savedImageMetadata.size || effectiveConfig.size,
|
||||||
|
background: savedImageMetadata.background ?? effectiveConfig.background,
|
||||||
count: "1",
|
count: "1",
|
||||||
}
|
}
|
||||||
: { ...buildGenerationConfig(effectiveConfig, sourceNode, node.type === CanvasNodeType.Text ? "text" : node.type === CanvasNodeType.Video ? "video" : node.type === CanvasNodeType.Audio ? "audio" : "image"), count: "1" };
|
: { ...buildGenerationConfig(effectiveConfig, sourceNode, node.type === CanvasNodeType.Text ? "text" : node.type === CanvasNodeType.Video ? "video" : node.type === CanvasNodeType.Audio ? "audio" : "image"), count: "1" };
|
||||||
@@ -2579,7 +2580,7 @@ function InfiniteCanvasPage() {
|
|||||||
const imageConfig = NODE_DEFAULT_SIZE[CanvasNodeType.Image];
|
const imageConfig = NODE_DEFAULT_SIZE[CanvasNodeType.Image];
|
||||||
const imageSize = fitNodeSize(uploadedImage.width, uploadedImage.height, imageConfig.width, imageConfig.height);
|
const imageSize = fitNodeSize(uploadedImage.width, uploadedImage.height, imageConfig.width, imageConfig.height);
|
||||||
const generationMetadata = savedImageMetadata?.generationType
|
const generationMetadata = savedImageMetadata?.generationType
|
||||||
? { generationType: savedImageMetadata.generationType, model: generationConfig.model, size: generationConfig.size, quality: generationConfig.quality, count: savedImageMetadata.count || 1, references: savedImageMetadata.references }
|
? { generationType: savedImageMetadata.generationType, model: generationConfig.model, size: generationConfig.size, quality: generationConfig.quality, ...(generationConfig.background ? { background: generationConfig.background } : {}), count: savedImageMetadata.count || 1, references: savedImageMetadata.references }
|
||||||
: buildImageGenerationMetadata(useReferenceImages ? "edit" : "generation", generationConfig, 1, retryImages);
|
: buildImageGenerationMetadata(useReferenceImages ? "edit" : "generation", generationConfig, 1, retryImages);
|
||||||
setNodes((prev) =>
|
setNodes((prev) =>
|
||||||
prev.map((item) =>
|
prev.map((item) =>
|
||||||
@@ -3282,6 +3283,7 @@ function buildImageGenerationMetadata(type: CanvasImageGenerationType, config: A
|
|||||||
model: config.model,
|
model: config.model,
|
||||||
size: config.size,
|
size: config.size,
|
||||||
quality: config.quality,
|
quality: config.quality,
|
||||||
|
...(config.background ? { background: config.background } : {}),
|
||||||
count,
|
count,
|
||||||
references: references.map(referenceUrl).filter((url): url is string => Boolean(url)),
|
references: references.map(referenceUrl).filter((url): url is string => Boolean(url)),
|
||||||
};
|
};
|
||||||
@@ -3460,6 +3462,7 @@ function buildGenerationConfig(config: AiConfig, node: CanvasNodeData | undefine
|
|||||||
model: node?.metadata?.model || defaultModel || (mode === "audio" ? defaultConfig.audioModel : config.model || defaultConfig.model),
|
model: node?.metadata?.model || defaultModel || (mode === "audio" ? defaultConfig.audioModel : config.model || defaultConfig.model),
|
||||||
quality: node?.metadata?.quality || config.quality || defaultConfig.quality,
|
quality: node?.metadata?.quality || config.quality || defaultConfig.quality,
|
||||||
size: node?.metadata?.size || config.size || defaultConfig.size,
|
size: node?.metadata?.size || config.size || defaultConfig.size,
|
||||||
|
background: node?.metadata?.background ?? config.background ?? defaultConfig.background,
|
||||||
videoSeconds: node?.metadata?.seconds || config.videoSeconds || defaultConfig.videoSeconds,
|
videoSeconds: node?.metadata?.seconds || config.videoSeconds || defaultConfig.videoSeconds,
|
||||||
vquality: node?.metadata?.vquality || config.vquality || defaultConfig.vquality,
|
vquality: node?.metadata?.vquality || config.vquality || defaultConfig.vquality,
|
||||||
videoGenerateAudio: node?.metadata?.generateAudio || config.videoGenerateAudio || defaultConfig.videoGenerateAudio,
|
videoGenerateAudio: node?.metadata?.generateAudio || config.videoGenerateAudio || defaultConfig.videoGenerateAudio,
|
||||||
|
|||||||
@@ -122,6 +122,11 @@ function normalizeQuality(quality: string) {
|
|||||||
return QUALITY_BASE[normalized] ? normalized : undefined;
|
return QUALITY_BASE[normalized] ? normalized : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Only "transparent" is forwarded; any other value (incl. empty) means keep the default opaque background. */
|
||||||
|
function normalizeBackground(background: string | undefined) {
|
||||||
|
return background?.trim().toLowerCase() === "transparent" ? "transparent" : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/** Map "quality + ratio" to an explicit pixel dimension like "3840x2160". */
|
/** Map "quality + ratio" to an explicit pixel dimension like "3840x2160". */
|
||||||
function resolveSize(quality: string | undefined, ratio: string): string {
|
function resolveSize(quality: string | undefined, ratio: string): string {
|
||||||
const parsedRatio = parseImageRatio(ratio);
|
const parsedRatio = parseImageRatio(ratio);
|
||||||
@@ -661,6 +666,7 @@ export async function requestGeneration(config: AiConfig, prompt: string, option
|
|||||||
if (script) {
|
if (script) {
|
||||||
const quality = normalizeQuality(config.quality);
|
const quality = normalizeQuality(config.quality);
|
||||||
const requestSize = resolveRequestSize(quality, config.size);
|
const requestSize = resolveRequestSize(quality, config.size);
|
||||||
|
const background = normalizeBackground(config.background);
|
||||||
try {
|
try {
|
||||||
const result = await runModelPlugin({
|
const result = await runModelPlugin({
|
||||||
capability: "image",
|
capability: "image",
|
||||||
@@ -668,7 +674,7 @@ export async function requestGeneration(config: AiConfig, prompt: string, option
|
|||||||
config: requestConfig,
|
config: requestConfig,
|
||||||
prompt: withSystemPrompt(requestConfig, prompt),
|
prompt: withSystemPrompt(requestConfig, prompt),
|
||||||
images: [],
|
images: [],
|
||||||
params: { size: requestSize, quality, count: n },
|
params: { size: requestSize, quality, count: n, ...(background ? { background } : {}) },
|
||||||
signal: options?.signal,
|
signal: options?.signal,
|
||||||
});
|
});
|
||||||
return normalizePluginImages(result).map((dataUrl) => ({ id: nanoid(), dataUrl }));
|
return normalizePluginImages(result).map((dataUrl) => ({ id: nanoid(), dataUrl }));
|
||||||
@@ -685,6 +691,7 @@ export async function requestGeneration(config: AiConfig, prompt: string, option
|
|||||||
}
|
}
|
||||||
const quality = normalizeQuality(config.quality);
|
const quality = normalizeQuality(config.quality);
|
||||||
const requestSize = resolveRequestSize(quality, config.size);
|
const requestSize = resolveRequestSize(quality, config.size);
|
||||||
|
const background = normalizeBackground(config.background);
|
||||||
try {
|
try {
|
||||||
const response = await axios.post<ImageApiResponse>(
|
const response = await axios.post<ImageApiResponse>(
|
||||||
aiApiUrl(requestConfig, "/images/generations"),
|
aiApiUrl(requestConfig, "/images/generations"),
|
||||||
@@ -694,6 +701,7 @@ export async function requestGeneration(config: AiConfig, prompt: string, option
|
|||||||
n,
|
n,
|
||||||
...(quality ? { quality } : {}),
|
...(quality ? { quality } : {}),
|
||||||
...(requestSize ? { size: requestSize } : {}),
|
...(requestSize ? { size: requestSize } : {}),
|
||||||
|
...(background ? { background } : {}),
|
||||||
response_format: "b64_json",
|
response_format: "b64_json",
|
||||||
output_format: IMAGE_OUTPUT_FORMAT,
|
output_format: IMAGE_OUTPUT_FORMAT,
|
||||||
},
|
},
|
||||||
@@ -717,6 +725,7 @@ export async function requestEdit(config: AiConfig, prompt: string, references:
|
|||||||
if (script) {
|
if (script) {
|
||||||
const quality = normalizeQuality(config.quality);
|
const quality = normalizeQuality(config.quality);
|
||||||
const requestSize = resolveRequestSize(quality, config.size);
|
const requestSize = resolveRequestSize(quality, config.size);
|
||||||
|
const background = normalizeBackground(config.background);
|
||||||
const refs = await Promise.all(references.map((image) => imageToDataUrl(image)));
|
const refs = await Promise.all(references.map((image) => imageToDataUrl(image)));
|
||||||
try {
|
try {
|
||||||
const result = await runModelPlugin({
|
const result = await runModelPlugin({
|
||||||
@@ -725,7 +734,7 @@ export async function requestEdit(config: AiConfig, prompt: string, references:
|
|||||||
config: requestConfig,
|
config: requestConfig,
|
||||||
prompt: withSystemPrompt(requestConfig, requestPrompt),
|
prompt: withSystemPrompt(requestConfig, requestPrompt),
|
||||||
images: refs,
|
images: refs,
|
||||||
params: { size: requestSize, quality, count: n },
|
params: { size: requestSize, quality, count: n, ...(background ? { background } : {}) },
|
||||||
signal: options?.signal,
|
signal: options?.signal,
|
||||||
});
|
});
|
||||||
return normalizePluginImages(result).map((dataUrl) => ({ id: nanoid(), dataUrl }));
|
return normalizePluginImages(result).map((dataUrl) => ({ id: nanoid(), dataUrl }));
|
||||||
@@ -743,6 +752,7 @@ export async function requestEdit(config: AiConfig, prompt: string, references:
|
|||||||
}
|
}
|
||||||
const quality = normalizeQuality(config.quality);
|
const quality = normalizeQuality(config.quality);
|
||||||
const requestSize = resolveRequestSize(quality, config.size);
|
const requestSize = resolveRequestSize(quality, config.size);
|
||||||
|
const background = normalizeBackground(config.background);
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.set("model", requestConfig.model);
|
formData.set("model", requestConfig.model);
|
||||||
formData.set("prompt", withSystemPrompt(requestConfig, requestPrompt));
|
formData.set("prompt", withSystemPrompt(requestConfig, requestPrompt));
|
||||||
@@ -755,6 +765,9 @@ export async function requestEdit(config: AiConfig, prompt: string, references:
|
|||||||
if (requestSize) {
|
if (requestSize) {
|
||||||
formData.set("size", requestSize);
|
formData.set("size", requestSize);
|
||||||
}
|
}
|
||||||
|
if (background) {
|
||||||
|
formData.set("background", background);
|
||||||
|
}
|
||||||
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));
|
||||||
if (mask) formData.set("mask", dataUrlToFile(mask));
|
if (mask) formData.set("mask", dataUrlToFile(mask));
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export type AiConfig = {
|
|||||||
models: string[];
|
models: string[];
|
||||||
quality: string;
|
quality: string;
|
||||||
size: string;
|
size: string;
|
||||||
|
background: string;
|
||||||
count: string;
|
count: string;
|
||||||
canvasImageCount: string;
|
canvasImageCount: string;
|
||||||
};
|
};
|
||||||
@@ -99,6 +100,7 @@ export const defaultConfig: AiConfig = {
|
|||||||
models: ["default::gpt-image-2", "default::grok-imagine-video", "default::gpt-5.5", "default::gpt-4o-mini-tts"],
|
models: ["default::gpt-image-2", "default::grok-imagine-video", "default::gpt-5.5", "default::gpt-4o-mini-tts"],
|
||||||
quality: "auto",
|
quality: "auto",
|
||||||
size: "1:1",
|
size: "1:1",
|
||||||
|
background: "",
|
||||||
count: "1",
|
count: "1",
|
||||||
canvasImageCount: "3",
|
canvasImageCount: "3",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export type CanvasNodeMetadata = {
|
|||||||
model?: string;
|
model?: string;
|
||||||
size?: string;
|
size?: string;
|
||||||
quality?: string;
|
quality?: string;
|
||||||
|
background?: string;
|
||||||
count?: number;
|
count?: number;
|
||||||
seconds?: string;
|
seconds?: string;
|
||||||
vquality?: string;
|
vquality?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user