Overhaul Studio controls and add image batch from one still.

Video and Image pickers, thumbwheels, aspect icons, and a Scale to MP paddle; extra prompts default to batch so each run starts from the original image.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-30 23:16:19 -05:00
co-authored by Cursor
parent 8df265cbf3
commit 755aaeede8
17 changed files with 791 additions and 233 deletions
+17 -10
View File
@@ -25,6 +25,7 @@ export type EditRunParams = {
loraStack?: import('~/utils/loras').LoraStackItem[]
scaleToTotalPixels?: boolean
scaleMegapixels?: number
passMode?: 'batch' | 'chain'
}
export async function runEdit(job: Job, params: EditRunParams) {
@@ -32,6 +33,8 @@ export async function runEdit(job: Job, params: EditRunParams) {
if (!library) throw new Error('Edit job is missing library metadata')
const prompts = [params.prompt, ...params.passes.map(item => item.prompt)]
const chainTotal = prompts.length
const batch = params.passMode !== 'chain'
const unit = batch ? 'Batch' : 'Pass'
library.chainTotal = chainTotal
library.familyId = chainTotal > 1 ? (library.familyId || crypto.randomUUID()) : library.familyId
@@ -63,7 +66,7 @@ export async function runEdit(job: Job, params: EditRunParams) {
for (let index = 0; index < prompts.length; index++) {
if (job.status === 'cancelled') throw new Error('Job interrupted.')
if (index > 0 && library.stopAfterCurrent === true) break
const prompt = index === 0 ? composeFillPrompt(prompts[index], filled.padded) : prompts[index]
const prompt = (batch || index === 0) ? composeFillPrompt(prompts[index], filled.padded) : prompts[index]
const last = index === prompts.length - 1
if (index > 0) {
await ensureComfyReady((status) => {
@@ -80,7 +83,7 @@ export async function runEdit(job: Job, params: EditRunParams) {
library.seed = seed
library.chainIndex = index
library.chainStep = index + 1
library.chainLabel = chainTotal > 1 ? `Pass ${index + 1}` : undefined
library.chainLabel = chainTotal > 1 ? `${unit} ${index + 1}` : undefined
const reference = index === 0 ? params.reference : null
const dual = Boolean(reference)
const passName = stillChainName(library.name || '', index)
@@ -163,7 +166,7 @@ export async function runEdit(job: Job, params: EditRunParams) {
name: passName || undefined,
prompt,
familyId: library.familyId,
parentStillId,
parentStillId: batch ? library.stillId : parentStillId,
chainIndex: index,
settings: {
kind: 'image',
@@ -180,7 +183,7 @@ export async function runEdit(job: Job, params: EditRunParams) {
}
})
job.stillId = still?.id
parentStillId = still?.id
if (!batch) parentStillId = still?.id
await purgeComfyArtifacts({
video: { filename: output.filename, subfolder: output.subfolder, type: output.type },
imageName: uploaded.name,
@@ -192,16 +195,18 @@ export async function runEdit(job: Job, params: EditRunParams) {
if (!last) {
emitChainJob(job, {
type: 'checkpoint',
message: `Pass ${index + 1} saved`,
message: `${unit} ${index + 1} of ${chainTotal} saved`,
progress: 100,
stillId: still?.id,
hideThumbnail: job.hideThumbnail,
folderLocked: library.folderLocked
})
current = {
filename: still?.filename || `pass_${index + 1}.png`,
data: buffer,
type: 'image/png'
if (!batch) {
current = {
filename: still?.filename || `pass_${index + 1}.png`,
data: buffer,
type: 'image/png'
}
}
if (library.stopAfterCurrent === true) break
continue
@@ -210,7 +215,9 @@ export async function runEdit(job: Job, params: EditRunParams) {
job.status = 'complete'
emitChainJob(job, {
type: 'complete',
message: chainTotal > 1 ? 'Edit chain finished on Beast' : 'Edit finished on Beast',
message: chainTotal > 1
? (batch ? 'Edit batch finished on Beast' : 'Edit chain finished on Beast')
: 'Edit finished on Beast',
progress: 100,
stillId: still?.id,
filename: output.filename,