Run image edit on Beast with Flux.2 Klein and stop routing jobs to Sidecar.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+69
-33
@@ -1,12 +1,13 @@
|
||||
import { imageComfyConfigured, uploadImageEdit, queueImagePrompt, waitForImageEdit, downloadEditedImage, purgeImageComfyArtifacts, withImageComfyHost } from '~/server/utils/imageComfy'
|
||||
import { pickImageEditHost } from '~/server/utils/imageComfyLifecycle'
|
||||
import { ensureComfyReady } from '~/server/utils/comfyLifecycle'
|
||||
import { getComfyHost, comfyConfigured, uploadImage, queuePrompt, purgeComfyArtifacts } from '~/server/utils/comfy'
|
||||
import { withImageComfyHost, waitForImageEdit, downloadEditedImage } from '~/server/utils/imageComfy'
|
||||
import { buildEditWorkflow } from '~/server/utils/imageWorkflow'
|
||||
import { imageDimensions } from '~/server/utils/resolution'
|
||||
|
||||
function parseSteps(raw: unknown) {
|
||||
const value = Number(raw)
|
||||
if (!Number.isFinite(value)) return 4
|
||||
return Math.min(20, Math.max(1, Math.round(value)))
|
||||
if (!Number.isFinite(value)) return 20
|
||||
return Math.min(40, Math.max(1, Math.round(value)))
|
||||
}
|
||||
|
||||
function parseCfg(raw: unknown) {
|
||||
@@ -15,19 +16,16 @@ function parseCfg(raw: unknown) {
|
||||
return Math.min(8, Math.max(0.5, Math.round(value * 10) / 10))
|
||||
}
|
||||
|
||||
function parseDenoise(raw: unknown) {
|
||||
const value = Number(raw)
|
||||
if (!Number.isFinite(value)) return 0.75
|
||||
return Math.min(1, Math.max(0.05, Math.round(value * 100) / 100))
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const form = await readMultipartFormData(event).catch(() => null)
|
||||
const fields: Record<string, string> = {}
|
||||
let image: { filename: string; data: Buffer; type?: string } | null = null
|
||||
let reference: { filename: string; data: Buffer; type?: string } | null = null
|
||||
for (const part of form || []) {
|
||||
if (part.name === 'image' && part.filename && part.data?.length) {
|
||||
image = { filename: part.filename, data: part.data, type: part.type }
|
||||
} else if ((part.name === 'image2' || part.name === 'refImage') && part.filename && part.data?.length) {
|
||||
reference = { filename: part.filename, data: part.data, type: part.type }
|
||||
} else if (part.name && part.data) {
|
||||
fields[part.name] = part.data.toString('utf8')
|
||||
}
|
||||
@@ -40,10 +38,10 @@ export default defineEventHandler(async (event) => {
|
||||
if (!prompt) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'An edit prompt is required' })
|
||||
}
|
||||
if (!imageComfyConfigured()) {
|
||||
if (!comfyConfigured()) {
|
||||
throw createError({
|
||||
statusCode: 503,
|
||||
statusMessage: 'Image edit ComfyUI is not configured yet. Set COMFY_HOST for Beast and IMAGE_COMFY_HOST for Sidecar overflow.'
|
||||
statusMessage: 'Beast ComfyUI is not configured. Set COMFY_HOST.'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -62,12 +60,32 @@ export default defineEventHandler(async (event) => {
|
||||
const hideInput = fields.hideInput === 'true'
|
||||
const steps = parseSteps(fields.steps)
|
||||
const cfg = parseCfg(fields.cfg)
|
||||
const denoise = parseDenoise(fields.denoise)
|
||||
const seed = fields.seed && fields.seed !== 'random'
|
||||
? Number(fields.seed)
|
||||
: Math.floor(Math.random() * 2_147_483_647)
|
||||
const size = imageDimensions(image.data)
|
||||
|
||||
const still = isPipelineFrameFilename(image.filename)
|
||||
? null
|
||||
: await saveStill({
|
||||
ownerKey,
|
||||
folderId,
|
||||
filename: image.filename,
|
||||
data: image.data,
|
||||
width: size?.width || 0,
|
||||
height: size?.height || 0,
|
||||
hideInput
|
||||
})
|
||||
if (reference && !isPipelineFrameFilename(reference.filename)) {
|
||||
await saveStill({
|
||||
ownerKey,
|
||||
folderId,
|
||||
filename: reference.filename,
|
||||
data: reference.data,
|
||||
hideInput
|
||||
})
|
||||
}
|
||||
|
||||
const job = createJob()
|
||||
job.kind = 'edit'
|
||||
job.maxStep = steps
|
||||
@@ -86,18 +104,19 @@ export default defineEventHandler(async (event) => {
|
||||
steps,
|
||||
turbo: true,
|
||||
seed,
|
||||
cfg
|
||||
cfg,
|
||||
stillId: still?.id
|
||||
}
|
||||
emitJob(job, { type: 'status', message: 'Choosing Beast or Sidecar...', progress: 2 })
|
||||
emitJob(job, { type: 'status', message: 'Checking Beast ComfyUI...', progress: 2 })
|
||||
|
||||
void runEdit(job, {
|
||||
image,
|
||||
reference,
|
||||
prompt,
|
||||
negative: (fields.negative || '').trim(),
|
||||
steps,
|
||||
seed,
|
||||
cfg,
|
||||
denoise
|
||||
cfg
|
||||
}).catch(async (error) => {
|
||||
if (job.status === 'error' || job.status === 'cancelled') return
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
@@ -111,7 +130,8 @@ export default defineEventHandler(async (event) => {
|
||||
seed,
|
||||
steps,
|
||||
hideThumbnail: hideInput,
|
||||
folderLocked
|
||||
folderLocked,
|
||||
dual: Boolean(reference)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -119,15 +139,15 @@ async function runEdit(
|
||||
job: ReturnType<typeof createJob>,
|
||||
params: {
|
||||
image: { filename: string; data: Buffer; type?: string }
|
||||
reference: { filename: string; data: Buffer; type?: string } | null
|
||||
prompt: string
|
||||
negative: string
|
||||
steps: number
|
||||
seed: number
|
||||
cfg: number
|
||||
denoise: number
|
||||
}
|
||||
) {
|
||||
const target = await pickImageEditHost((status) => {
|
||||
await ensureComfyReady((status) => {
|
||||
emitJob(job, {
|
||||
type: status.state === 'busy' ? 'busy' : 'status',
|
||||
message: status.message,
|
||||
@@ -135,41 +155,56 @@ async function runEdit(
|
||||
busy: status.state === 'busy'
|
||||
})
|
||||
})
|
||||
job.imageComfyHost = target.host
|
||||
if (job.status === 'cancelled') throw new Error('Job interrupted.')
|
||||
job.imageComfyHost = getComfyHost()
|
||||
|
||||
await withImageComfyHost(target.host, async () => {
|
||||
await withImageComfyHost(job.imageComfyHost, async () => {
|
||||
const dual = Boolean(params.reference)
|
||||
emitJob(job, {
|
||||
type: 'status',
|
||||
message: `Uploading still to ${target.label}...`,
|
||||
message: dual ? 'Uploading both stills to Beast...' : 'Uploading still to Beast...',
|
||||
progress: 8
|
||||
})
|
||||
const uploaded = await uploadImageEdit(params.image, job.id)
|
||||
const uploaded = await uploadImage(params.image, job.id)
|
||||
const uploadedRef = params.reference
|
||||
? await uploadImage({
|
||||
...params.reference,
|
||||
filename: `ref_${params.reference.filename || 'image2.png'}`
|
||||
}, job.id)
|
||||
: null
|
||||
if (job.status === 'cancelled') throw new Error('Job interrupted.')
|
||||
|
||||
emitJob(job, {
|
||||
type: 'status',
|
||||
message: `Queueing Qwen Image Edit on ${target.label}...`,
|
||||
message: dual
|
||||
? 'Queueing Flux.2 Klein two-image edit on Beast...'
|
||||
: 'Queueing Flux.2 Klein edit on Beast...',
|
||||
progress: 12
|
||||
})
|
||||
const graph = buildEditWorkflow({
|
||||
prompt: params.prompt,
|
||||
negative: params.negative,
|
||||
imageName: uploaded.name,
|
||||
referenceImageName: uploadedRef?.name,
|
||||
steps: params.steps,
|
||||
seed: params.seed,
|
||||
cfg: params.cfg,
|
||||
denoise: params.denoise,
|
||||
filenamePrefix: `aigen_edit_${job.id.slice(0, 8)}`
|
||||
})
|
||||
const queued = await queueImagePrompt(graph, job.clientId)
|
||||
const queued = await queuePrompt(graph, job.clientId)
|
||||
job.promptId = queued.prompt_id
|
||||
job.status = 'running'
|
||||
emitJob(job, { type: 'status', message: `Editing still on ${target.label}...`, progress: 18, maxStep: params.steps })
|
||||
emitJob(job, {
|
||||
type: 'status',
|
||||
message: dual ? 'Applying image 2 onto image 1...' : 'Editing still on Beast...',
|
||||
progress: 18,
|
||||
maxStep: params.steps
|
||||
})
|
||||
|
||||
const output = await waitForImageEdit({
|
||||
promptId: queued.prompt_id,
|
||||
clientId: job.clientId,
|
||||
timeoutMs: 10 * 60 * 1000,
|
||||
onProgress: (event) => {
|
||||
emitJob(job, {
|
||||
type: 'status',
|
||||
@@ -196,16 +231,17 @@ async function runEdit(
|
||||
hideInput: job.library?.hideInput
|
||||
})
|
||||
job.stillId = still?.id
|
||||
await purgeImageComfyArtifacts({
|
||||
output,
|
||||
inputName: uploaded.name,
|
||||
inputSubfolder: uploaded.subfolder,
|
||||
await purgeComfyArtifacts({
|
||||
video: { filename: output.filename, subfolder: output.subfolder, type: output.type },
|
||||
imageName: uploaded.name,
|
||||
imageSubfolder: uploaded.subfolder,
|
||||
extraImageNames: uploadedRef?.name ? [uploadedRef.name] : [],
|
||||
promptId: job.promptId
|
||||
})
|
||||
job.status = 'complete'
|
||||
emitJob(job, {
|
||||
type: 'complete',
|
||||
message: `Edit finished on ${target.label}`,
|
||||
message: 'Edit finished on Beast',
|
||||
progress: 100,
|
||||
stillId: still?.id,
|
||||
filename: output.filename,
|
||||
|
||||
Reference in New Issue
Block a user