Add Image v2 Refine for on-device regional edits.

Mask painter and denoise strength sit beside Edit and Compose. The canned hand/chest prompt helper is gone so that text stays yours.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-28 22:03:20 -05:00
co-authored by Cursor
parent 21fe2412f9
commit a2895b8054
10 changed files with 825 additions and 56 deletions
+27 -8
View File
@@ -15,6 +15,7 @@ export type EditV2RunParams = {
task: ImageV2Task
image: EditImageFile
reference: EditImageFile | null
mask?: EditImageFile | null
prompt: string
negative: string
steps: number
@@ -25,11 +26,15 @@ export type EditV2RunParams = {
consistencyModel: number
consistencyClip: number
megapixels: number
strength?: number
}
export async function runEditV2(job: Job, params: EditV2RunParams) {
const library = job.library
if (!library) throw new Error('Edit job is missing library metadata')
if (params.mode === 'refine' && !params.mask) {
throw new Error('Refine requires a mask. Refusing to fall back to Edit.')
}
if (params.mode === 'compose' && !params.reference) {
throw new Error('Compose requires image B. Refusing to fall back to one-image generation.')
}
@@ -52,23 +57,35 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
await withImageComfyHost(job.imageComfyHost, async () => {
emitChainJob(job, {
type: 'status',
message: params.mode === 'compose' ? 'Uploading stills A and B to Beast...' : 'Uploading still A to Beast...',
message: params.mode === 'refine'
? 'Uploading canvas and mask to Beast...'
: params.mode === 'compose' ? 'Uploading stills A and B to Beast...' : 'Uploading still A to Beast...',
progress: 8
})
const uploaded = await uploadImage(params.image, job.id)
const uploadedRef = params.reference
const uploadedRef = params.mode === 'refine'
? null
: params.reference
? await uploadImage({
...params.reference,
filename: `ref_${params.reference.filename || 'image_b.png'}`
}, job.id)
: null
const uploadedMask = params.mode === 'refine' && params.mask
? await uploadImage({
...params.reference,
filename: `ref_${params.reference.filename || 'image_b.png'}`
...params.mask,
filename: `mask_${params.mask.filename || 'refine-mask.png'}`
}, job.id)
: null
if (job.status === 'cancelled') throw new Error('Job interrupted.')
emitChainJob(job, {
type: 'status',
message: params.mode === 'compose'
? `Queueing Klein v2 compose (${params.task}) on Beast...`
: 'Queueing Klein v2 edit on Beast...',
message: params.mode === 'refine'
? 'Queueing Klein v2 refine on Beast...'
: params.mode === 'compose'
? `Queueing Klein v2 compose (${params.task}) on Beast...`
: 'Queueing Klein v2 edit on Beast...',
progress: 12
})
await ensureComfyLoraNames('image')
@@ -80,6 +97,8 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
negative: params.negative,
imageAName: uploaded.name,
imageBName: uploadedRef?.name,
maskName: uploadedMask?.name,
strength: params.strength,
snofsModel: params.snofsModel,
snofsClip: params.snofsClip,
consistencyModel: params.consistencyModel,
@@ -137,7 +156,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
video: { filename: output.filename, subfolder: output.subfolder, type: output.type },
imageName: uploaded.name,
imageSubfolder: uploaded.subfolder,
extraImageNames: uploadedRef?.name ? [uploadedRef.name] : [],
extraImageNames: [uploadedRef?.name, uploadedMask?.name].filter((name): name is string => Boolean(name)),
promptId: job.promptId
})