Join pre/prompt/post for Comfy, stop zero-size generates snapping to a small square, and give the phone library a real closeable preview.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-09-02 20:29:44 -05:00
co-authored by Cursor
parent d471d1bdd6
commit 369eb595d7
11 changed files with 76 additions and 28 deletions
+4 -2
View File
@@ -4,6 +4,7 @@ import { assertImageScaleToTotalPixelsNode, comfyConfigured } from '~/server/uti
import { imageDimensions } from '~/server/utils/resolution'
import { parseImageAspect } from '~/utils/imageAspect'
import { clampImageCfg, clampImageScaleMegapixels, clampImageSteps, parseImagePassMode, parseImageScaleToTotalPixels } from '~/utils/generationPresets'
import { joinPromptParts } from '~/utils/promptParts'
function parsePasses(raw: string | undefined) {
if (!raw) return [] as { prompt: string }[]
@@ -33,16 +34,17 @@ export default defineEventHandler(async (event) => {
}
}
const prompt = (fields.prompt || '').trim()
const promptMid = (fields.prompt || '').trim()
const promptPre = (fields.promptPre || '').trim()
const promptPost = (fields.promptPost || '').trim()
const prompt = joinPromptParts(promptPre, promptMid, promptPost)
const extraPasses = parsePasses(fields.passes)
const passMode = parseImagePassMode(fields.passMode)
const aspect = parseImageAspect(fields.aspect)
if (!image) {
throw createError({ statusCode: 400, statusMessage: 'An input image is required' })
}
if (!prompt && aspect === 'auto') {
if (!promptMid && aspect === 'auto') {
throw createError({ statusCode: 400, statusMessage: 'An edit prompt is required' })
}
if (extraPasses.some(item => !item.prompt)) {
+2 -2
View File
@@ -4,7 +4,7 @@ import { listStudioLoras, parsePostedLoraStack, parseShotLoraStacks, persistLora
import { clampVideoCfg } from '~/utils/generationPresets'
import { defaultVideoSteps, isLtxWorkflow, isTextToVideo, LTX_DISABLED_MESSAGE, ltxWorkflowEnabled, parseVideoWorkflow, videoEngineOf } from '~/utils/videoModels'
import { allowIdentityRefs, normalizePermanenceRefs, resolveGlobalLocks, type PermanenceRef } from '~/utils/globalLocks'
import { composePromptParts, persistPromptWrappers } from '~/utils/promptParts'
import { composePromptParts, joinPromptParts, persistPromptWrappers } from '~/utils/promptParts'
import type { QueuedExtension } from '~/server/utils/library'
function parseDuration(raw: unknown) {
@@ -150,7 +150,7 @@ export default defineEventHandler(async (event) => {
throw createError({ statusCode: 400, statusMessage: 'A motion prompt is required' })
}
const resolvedLocks = resolveGlobalLocks(fields.globalLocks || '', promptMid)
const prompt = resolvedLocks.prompt
const prompt = joinPromptParts(promptPre, resolvedLocks.prompt, promptPost)
const globalLocks = resolvedLocks.locks
const extensions = parseExtensions(fields.extensions)
const permanenceRefs = parseFamilyPermanenceRefs(fields.permanenceRefs)
+4 -2
View File
@@ -26,6 +26,7 @@ import {
type ImageV2Task
} from '~/utils/imageV2'
import { isXaigenStudio } from '~/utils/videoModels'
import { joinPromptParts } from '~/utils/promptParts'
import { listStudioLoras, parsePostedLoraStack, persistLoraFields, assertImageV2LoraStack } from '~/server/utils/loras'
import { getStill, rememberInputStill, stillPath } from '~/server/utils/library'
import { existsSync, readFileSync } from 'node:fs'
@@ -135,13 +136,14 @@ export default defineEventHandler(async (event) => {
fields.task,
mode === 'generate' ? 't2i' : mode === 'refine' ? 'refine' : 'scene'
)
const prompt = String(fields.prompt || '').trim()
const promptMid = String(fields.prompt || '').trim()
const promptPre = String(fields.promptPre || '').trim()
const promptPost = String(fields.promptPost || '').trim()
const prompt = joinPromptParts(promptPre, promptMid, promptPost)
const extraPasses = parsePasses(fields.passes)
const passMode = parseImagePassMode(fields.passMode)
const aspect = parseImageAspect(fields.aspect)
if (!prompt && (mode === 'generate' || aspect === 'auto')) {
if (!promptMid && (mode === 'generate' || aspect === 'auto')) {
throw createError({ statusCode: 400, statusMessage: 'A prompt is required' })
}
if (extraPasses.length && (mode === 'refine' || mode === 'generate')) {
+2 -2
View File
@@ -8,7 +8,7 @@ import { imageDimensions } from '~/server/utils/resolution'
import { emitChainJob } from '~/server/utils/watch'
import { saveStill, stillChainName } from '~/server/utils/library'
import { comfyJobPrefix } from '~/utils/outputNames'
import { composePromptParts } from '~/utils/promptParts'
import { joinPromptParts } from '~/utils/promptParts'
export type EditImageFile = { filename: string; data: Buffer; type?: string }
@@ -68,7 +68,7 @@ export async function runEdit(job: Job, params: EditRunParams) {
if (job.status === 'cancelled') throw new Error('Job interrupted.')
if (index > 0 && library.stopAfterCurrent === true) break
const mid = (batch || index === 0) ? composeFillPrompt(prompts[index], filled.padded) : prompts[index]
const prompt = composePromptParts(library.promptPre || '', mid, library.promptPost || '')
const prompt = joinPromptParts(library.promptPre, mid, library.promptPost)
const last = index === prompts.length - 1
if (index > 0) {
await ensureComfyReady((status) => {
+4 -3
View File
@@ -8,7 +8,7 @@ import { imageDimensions } from '~/server/utils/resolution'
import { emitChainJob } from '~/server/utils/watch'
import { saveStill, stillChainName } from '~/server/utils/library'
import { comfyJobPrefix } from '~/utils/outputNames'
import { composePromptParts } from '~/utils/promptParts'
import { joinPromptParts } from '~/utils/promptParts'
import { parseImageV2Engine, type ImageV2Engine, type ImageV2Mode, type ImageV2Task } from '~/utils/imageV2'
import type { EditImageFile } from '~/server/utils/imageChain'
@@ -195,7 +195,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
mode: params.mode,
engine,
task: params.task,
prompt: composePromptParts(library.promptPre || '', prompt, library.promptPost || ''),
prompt: joinPromptParts(library.promptPre, prompt, library.promptPost),
negative: params.negative,
imageAName: uploaded?.name,
imageBName: uploadedRef?.name,
@@ -211,13 +211,14 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
megapixels: params.megapixels,
width: params.width,
height: params.height,
aspect: params.aspect,
turbo: params.turbo === true,
filenamePrefix: chainTotal > 1 ? `${comfyJobPrefix(job.id)}_p${index + 1}` : comfyJobPrefix(job.id),
kreaUnetName: krea?.unet,
kreaClipName: krea?.clip,
kreaVaeName: krea?.vae,
kreaConceptLora: krea?.conceptLora,
loraStack: params.loraStack
loraStack: params.loraStack?.length ? params.loraStack : (library.loraStack || undefined)
})
const queued = await queuePrompt(built.graph, job.clientId)
job.promptId = queued.prompt_id
+3 -2
View File
@@ -70,6 +70,7 @@ export interface ImageV2BuildParams {
strength?: number
width?: number
height?: number
aspect?: string
turbo?: boolean
snofsModel?: number
snofsClip?: number
@@ -458,7 +459,7 @@ function buildKreaGenerateWorkflow(params: ImageV2BuildParams) {
const graph = structuredClone(kreaGenerateTemplate) as WorkflowGraph
const prompt = composeImageV2Prompt('generate', 't2i', params.prompt)
const negative = String(params.negative || '')
const size = clampImageV2Size(params.width, params.height)
const size = clampImageV2Size(params.width, params.height, params.aspect)
const steps = clampImageSteps(params.steps, IMAGE_V2_KREA_STEPS)
const cfg = clampImageCfg(params.cfg, IMAGE_V2_KREA_CFG)
const workflowFile = IMAGE_V2_KREA_GENERATE_WORKFLOW
@@ -520,7 +521,7 @@ export function buildImageV2Workflow(params: ImageV2BuildParams) {
const steps = clampImageSteps(params.steps, 24)
const cfg = clampImageCfg(params.cfg, 4)
const strength = refine ? clampImageV2Denoise(params.strength, IMAGE_V2_DENOISE_DEFAULT) : undefined
const size = generate ? clampImageV2Size(params.width, params.height) : null
const size = generate ? clampImageV2Size(params.width, params.height, params.aspect) : null
const workflowFile = imageV2WorkflowFile(params.mode, engine)
if (!generate) setInput(graph, LOAD_A, 'image', params.imageAName || '')
+7 -4
View File
@@ -22,7 +22,7 @@ import {
updateShotQueue
} from '~/server/utils/shotQueue'
import { composeShotPrompt, allowIdentityRefs, type PermanenceRef } from '~/utils/globalLocks'
import { composePromptParts, resolvePromptWrappers } from '~/utils/promptParts'
import { composePromptParts, joinPromptParts, resolvePromptWrappers } from '~/utils/promptParts'
import type { QueuedExtension } from '~/server/utils/library'
import { persistLoraFields, ensureComfyLoraNames } from '~/server/utils/loras'
import { readLoraStack, resolveLoraStack } from '~/utils/loras'
@@ -150,7 +150,10 @@ export async function queueMiniMax(
job.status = 'uploading'
const chainIndex = job.library?.chainIndex || 0
const graphId = chainIndex > 0 ? workflowForExtension(params.workflow) : params.workflow
if (job.library) Object.assign(job.library, persistLoraFields(params.loraStack || params.loraName || job.library.loraStack || job.library.loraName))
if (job.library) {
const stack = resolveLoraStack(job.library.loraStack || job.library.loraName, params.loraStack || params.loraName)
if (stack.length) Object.assign(job.library, persistLoraFields(stack))
}
const engineName = isLtxWorkflow(graphId) ? 'LTX-2.3' : 'MiniMax H3'
const hasImage = Boolean(params.image?.data?.length)
const uploading = !hasImage
@@ -188,7 +191,7 @@ export async function queueMiniMax(
const shotIndex = job.library?.chainIndex || 0
const composedPrompt = composeShotPrompt({
globalLocks: job.library?.globalLocks || params.globalLocks,
prompt: composePromptParts(job.library?.promptPre || '', params.prompt, job.library?.promptPost || ''),
prompt: joinPromptParts(job.library?.promptPre, params.prompt, job.library?.promptPost),
shotIndex,
familyRefs: job.library?.permanenceRefs || params.permanenceRefs,
shotRefs: job.library?.shotPermanenceRefs?.[shotIndex] || params.shotPermanenceRefs?.[shotIndex]
@@ -417,7 +420,7 @@ export async function continueQueuedExtensions(
break
}
await queueMiniMax(job, {
prompt: ext.prompt,
prompt: joinPromptParts(wrappers.promptPre, ext.prompt, wrappers.promptPost),
image: { filename: 'last_frame.png', data: frame, type: 'image/png' },
width: params.width,
height: params.height,