feat: add ComfyUI integration for image generation and management
- Implemented ComfyGenerateRequest type and associated functions for generating images using various architectures and modes. - Added functions for listing generation options and handling image uploads. - Created workflows for different generation modes including SDXL, Z-Image, Z-Image Turbo, and Anima. - Introduced GenerationJobStatus component to display the status of ongoing generation jobs. - Developed MaskControls for managing mask operations and displaying mask analysis. - Created palette items for tool selection, layer management, and generation settings.
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import type { Asset } from "@core/asset";
|
||||
import type { AssetGenerationProvenance, GeneratedAssetAcceptance } from "@core/asset-provenance";
|
||||
import type { ImageDocument } from "@core/document";
|
||||
import type { ArtboardId, AssetId, LayerId } from "@core/id";
|
||||
import type { ArtboardId, AssetId, GenerationCandidateId, GenerationJobId, LayerId } from "@core/id";
|
||||
import type { ImageLayer } from "@core/image-layer";
|
||||
import type { Layer } from "@core/layer";
|
||||
import type { GenerationCandidate, GenerationCompareMode, GenerationState } from "@editor/state";
|
||||
import type { AppState, GenerationCandidate, GenerationCompareMode, GenerationJobKind, GenerationOptions, GenerationState } from "@editor/state";
|
||||
import type { Command } from "./command";
|
||||
import { commandIds } from "./ids";
|
||||
|
||||
@@ -13,7 +13,7 @@ export type GenerationAddCandidatePayload = {
|
||||
};
|
||||
|
||||
export type GenerationSelectCandidatePayload = {
|
||||
candidateId?: string;
|
||||
candidateId?: GenerationCandidateId;
|
||||
};
|
||||
|
||||
export type GenerationSetCompareModePayload = {
|
||||
@@ -21,22 +21,29 @@ export type GenerationSetCompareModePayload = {
|
||||
};
|
||||
|
||||
export type GenerationRemoveCandidatePayload = {
|
||||
candidateId: string;
|
||||
candidateId: GenerationCandidateId;
|
||||
};
|
||||
|
||||
export type GenerationApplyCandidateAsLayerPayload = {
|
||||
candidateId: string;
|
||||
candidateId: GenerationCandidateId;
|
||||
assetId: AssetId;
|
||||
layerId: LayerId;
|
||||
};
|
||||
|
||||
export type GenerationReplaceCandidatePixelsPayload = {
|
||||
candidateId: string;
|
||||
candidateId: GenerationCandidateId;
|
||||
source: string;
|
||||
mimeType?: string;
|
||||
};
|
||||
|
||||
export type GenerationStartJobPayload = { jobId: GenerationJobId; kind: GenerationJobKind; label: string; startedAt: number };
|
||||
export type GenerationSucceedJobPayload = { jobId: GenerationJobId; finishedAt: number };
|
||||
export type GenerationFailJobPayload = { jobId: GenerationJobId; finishedAt: number; error: string };
|
||||
export type GenerationSetResourcesPayload = { options: GenerationOptions };
|
||||
export type GenerationFailResourcesPayload = { error: string };
|
||||
|
||||
const maxCandidates = 12;
|
||||
const maxJobs = 20;
|
||||
const generationCompareModes = new Set<GenerationCompareMode>(["result", "before", "split"]);
|
||||
|
||||
export const generationAddCandidateCommand: Command<GenerationAddCandidatePayload> = {
|
||||
@@ -50,6 +57,7 @@ export const generationAddCandidateCommand: Command<GenerationAddCandidatePayloa
|
||||
editor: {
|
||||
...state.editor,
|
||||
generation: {
|
||||
...state.editor.generation,
|
||||
candidates,
|
||||
selectedCandidateId: payload.candidate.id,
|
||||
compareMode: "result",
|
||||
@@ -126,7 +134,7 @@ export const generationClearCandidatesCommand: Command = {
|
||||
...state,
|
||||
editor: {
|
||||
...state.editor,
|
||||
generation: { candidates: [], selectedCandidateId: undefined, compareMode: "result" },
|
||||
generation: { ...state.editor.generation, candidates: [], selectedCandidateId: undefined, compareMode: "result" },
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -210,6 +218,66 @@ export const generationReplaceCandidatePixelsCommand: Command<GenerationReplaceC
|
||||
},
|
||||
};
|
||||
|
||||
export const generationStartJobCommand: Command<GenerationStartJobPayload> = {
|
||||
id: commandIds.generationStartJob,
|
||||
name: "Start generation job",
|
||||
history: { mode: "ignore" },
|
||||
execute({ state }, payload) {
|
||||
if (!payload.jobId || !payload.label.trim() || !Number.isFinite(payload.startedAt)) return state;
|
||||
if (state.editor.generation.jobs.some((job) => job.status === "running" || job.id === payload.jobId)) return state;
|
||||
const job: GenerationState["jobs"][number] = { id: payload.jobId, kind: payload.kind, label: payload.label.trim(), status: "running", startedAt: payload.startedAt };
|
||||
return updateJobs(state, [job, ...state.editor.generation.jobs].slice(0, maxJobs));
|
||||
},
|
||||
};
|
||||
|
||||
export const generationSucceedJobCommand: Command<GenerationSucceedJobPayload> = {
|
||||
id: commandIds.generationSucceedJob,
|
||||
name: "Complete generation job",
|
||||
history: { mode: "ignore" },
|
||||
execute({ state }, payload) {
|
||||
return settleJob(state, payload.jobId, payload.finishedAt, "succeeded");
|
||||
},
|
||||
};
|
||||
|
||||
export const generationFailJobCommand: Command<GenerationFailJobPayload> = {
|
||||
id: commandIds.generationFailJob,
|
||||
name: "Fail generation job",
|
||||
history: { mode: "ignore" },
|
||||
execute({ state }, payload) {
|
||||
if (!payload.error.trim()) return state;
|
||||
return settleJob(state, payload.jobId, payload.finishedAt, "failed", payload.error.trim());
|
||||
},
|
||||
};
|
||||
|
||||
export const generationLoadResourcesCommand: Command = {
|
||||
id: commandIds.generationLoadResources,
|
||||
name: "Load generation resources",
|
||||
history: { mode: "ignore" },
|
||||
execute({ state }) {
|
||||
if (state.editor.generation.resources.status === "loading") return state;
|
||||
return updateResources(state, { status: "loading" });
|
||||
},
|
||||
};
|
||||
|
||||
export const generationSetResourcesCommand: Command<GenerationSetResourcesPayload> = {
|
||||
id: commandIds.generationSetResources,
|
||||
name: "Set generation resources",
|
||||
history: { mode: "ignore" },
|
||||
execute({ state }, payload) {
|
||||
return updateResources(state, { status: "ready", options: payload.options });
|
||||
},
|
||||
};
|
||||
|
||||
export const generationFailResourcesCommand: Command<GenerationFailResourcesPayload> = {
|
||||
id: commandIds.generationFailResources,
|
||||
name: "Fail generation resources",
|
||||
history: { mode: "ignore" },
|
||||
execute({ state }, payload) {
|
||||
if (!payload.error.trim()) return state;
|
||||
return updateResources(state, { status: "failed", error: payload.error.trim() });
|
||||
},
|
||||
};
|
||||
|
||||
export const generationCommands = [
|
||||
generationAddCandidateCommand,
|
||||
generationSelectCandidateCommand,
|
||||
@@ -218,8 +286,29 @@ export const generationCommands = [
|
||||
generationClearCandidatesCommand,
|
||||
generationApplyCandidateAsLayerCommand,
|
||||
generationReplaceCandidatePixelsCommand,
|
||||
generationStartJobCommand,
|
||||
generationSucceedJobCommand,
|
||||
generationFailJobCommand,
|
||||
generationLoadResourcesCommand,
|
||||
generationSetResourcesCommand,
|
||||
generationFailResourcesCommand,
|
||||
] satisfies Command<unknown>[];
|
||||
|
||||
function updateJobs(state: AppState, jobs: GenerationState["jobs"]): AppState {
|
||||
return { ...state, editor: { ...state.editor, generation: { ...state.editor.generation, jobs } } };
|
||||
}
|
||||
|
||||
function updateResources(state: AppState, resources: GenerationState["resources"]): AppState {
|
||||
return { ...state, editor: { ...state.editor, generation: { ...state.editor.generation, resources } } };
|
||||
}
|
||||
|
||||
function settleJob(state: AppState, jobId: GenerationJobId, finishedAt: number, status: "succeeded" | "failed", error?: string): AppState {
|
||||
if (!Number.isFinite(finishedAt)) return state;
|
||||
const job = state.editor.generation.jobs.find((candidate) => candidate.id === jobId);
|
||||
if (!job || job.status !== "running" || finishedAt < job.startedAt) return state;
|
||||
return updateJobs(state, state.editor.generation.jobs.map((candidate) => candidate.id === jobId ? { ...candidate, status, finishedAt, error } : candidate));
|
||||
}
|
||||
|
||||
type LayerLocation = {
|
||||
artboardId: ArtboardId;
|
||||
layer: Layer;
|
||||
@@ -251,7 +340,7 @@ function findLayerInTree(layers: readonly Layer[], layerId: LayerId): Layer | un
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function removeGenerationCandidate(generation: GenerationState, candidateId: string): GenerationState {
|
||||
function removeGenerationCandidate(generation: GenerationState, candidateId: GenerationCandidateId): GenerationState {
|
||||
const removedIndex = generation.candidates.findIndex((candidate) => candidate.id === candidateId);
|
||||
if (removedIndex < 0) return generation;
|
||||
|
||||
@@ -264,6 +353,7 @@ function removeGenerationCandidate(generation: GenerationState, candidateId: str
|
||||
: candidates[Math.min(removedIndex, candidates.length - 1)]?.id;
|
||||
|
||||
return {
|
||||
...generation,
|
||||
candidates,
|
||||
selectedCandidateId,
|
||||
compareMode: candidates.length > 0 ? generation.compareMode : "result",
|
||||
|
||||
Reference in New Issue
Block a user