diff --git a/pages/index.vue b/pages/index.vue index 0fef34b..b224e62 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -6869,9 +6869,10 @@ async function editImage() { const body = new FormData() body.append('image', file.value) if (editRefFile.value) body.append('image2', editRefFile.value) - body.append('prompt', prompt.value.trim()) - if (promptPre.value.trim()) body.append('promptPre', promptPre.value.trim()) - if (promptPost.value.trim()) body.append('promptPost', promptPost.value.trim()) + body.append('prompt', composePromptParts(promptPre.value, prompt.value.trim(), promptPost.value) || prompt.value.trim()) + body.append('promptMid', prompt.value.trim()) + body.append('promptPre', promptPre.value.trim()) + body.append('promptPost', promptPost.value.trim()) body.append('aspect', aspect.value) body.append('negative', editNegative.value) body.append('steps', String(clampImageSteps(editSteps.value))) @@ -7108,9 +7109,10 @@ async function editImageV2() { } body.append('aspect', aspect.value) body.append('engine', v2Engine.value) - body.append('prompt', prompt.value.trim()) - if (promptPre.value.trim()) body.append('promptPre', promptPre.value.trim()) - if (promptPost.value.trim()) body.append('promptPost', promptPost.value.trim()) + body.append('prompt', composePromptParts(promptPre.value, prompt.value.trim(), promptPost.value) || prompt.value.trim()) + body.append('promptMid', prompt.value.trim()) + body.append('promptPre', promptPre.value.trim()) + body.append('promptPost', promptPost.value.trim()) body.append('negative', v2Negative.value) body.append('snofs_model', String(imageV2EngineLoras().find(item => isImageV2ConceptLora(item.name))?.strengthModel ?? 0)) body.append('snofs_clip', String(imageV2EngineLoras().find(item => isImageV2ConceptLora(item.name))?.strengthClip ?? 0)) @@ -7320,9 +7322,10 @@ async function generate() { const body = new FormData() if (file.value) body.append('image', file.value) if (inputStillId.value) body.append('stillId', inputStillId.value) - body.append('prompt', initialPrompt) - if (promptPre.value.trim()) body.append('promptPre', promptPre.value.trim()) - if (promptPost.value.trim()) body.append('promptPost', promptPost.value.trim()) + body.append('prompt', composePromptParts(promptPre.value, initialPrompt, promptPost.value) || initialPrompt) + body.append('promptMid', initialPrompt) + body.append('promptPre', promptPre.value.trim()) + body.append('promptPost', promptPost.value.trim()) body.append('aspect', aspect.value) body.append('width', String(outputSize.value.width)) body.append('height', String(outputSize.value.height)) diff --git a/server/api/edit.post.ts b/server/api/edit.post.ts index f0cd40a..a652a75 100644 --- a/server/api/edit.post.ts +++ b/server/api/edit.post.ts @@ -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 { ingestPromptParts } from '~/utils/promptParts' function parsePasses(raw: string | undefined) { if (!raw) return [] as { prompt: string }[] @@ -33,10 +34,11 @@ export default defineEventHandler(async (event) => { } } - const promptMid = (fields.prompt || '').trim() - const promptPre = (fields.promptPre || '').trim() - const promptPost = (fields.promptPost || '').trim() - const prompt = promptMid + const posted = ingestPromptParts(fields) + const promptMid = posted.promptMid + const promptPre = posted.promptPre + const promptPost = posted.promptPost + const prompt = posted.prompt const extraPasses = parsePasses(fields.passes) const passMode = parseImagePassMode(fields.passMode) const aspect = parseImageAspect(fields.aspect) diff --git a/server/api/generate.post.ts b/server/api/generate.post.ts index 534375a..e598cad 100644 --- a/server/api/generate.post.ts +++ b/server/api/generate.post.ts @@ -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, ingestPromptParts, persistPromptWrappers } from '~/utils/promptParts' import type { QueuedExtension } from '~/server/utils/library' function parseDuration(raw: unknown) { @@ -143,14 +143,18 @@ export default defineEventHandler(async (event) => { if (!image && !textToVideo) { throw createError({ statusCode: 400, statusMessage: 'An initial image is required' }) } - const promptMid = (fields.prompt || '').trim() - const promptPre = (fields.promptPre || '').trim() - const promptPost = (fields.promptPost || '').trim() + const posted = ingestPromptParts(fields) + const promptPre = posted.promptPre + const promptPost = posted.promptPost + let promptMid = posted.promptMid if (!composePromptParts(promptPre, promptMid, promptPost)) { throw createError({ statusCode: 400, statusMessage: 'A motion prompt is required' }) } const resolvedLocks = resolveGlobalLocks(fields.globalLocks || '', promptMid) - const prompt = resolvedLocks.prompt + promptMid = resolvedLocks.prompt + const prompt = (promptPre || promptPost) + ? composePromptParts(promptPre, promptMid, promptPost) + : (posted.prompt || promptMid) const globalLocks = resolvedLocks.locks const extensions = parseExtensions(fields.extensions) const permanenceRefs = parseFamilyPermanenceRefs(fields.permanenceRefs) @@ -251,7 +255,7 @@ export default defineEventHandler(async (event) => { familyId, payload: { prompt, - promptMid: prompt || undefined, + promptMid: promptMid || undefined, promptPre: promptPre || undefined, promptPost: promptPost || undefined, name: clipName, diff --git a/server/api/v2/generate.post.ts b/server/api/v2/generate.post.ts index eaeb7d7..ed81bab 100644 --- a/server/api/v2/generate.post.ts +++ b/server/api/v2/generate.post.ts @@ -27,6 +27,7 @@ import { type ImageV2Task } from '~/utils/imageV2' import { isXaigenStudio } from '~/utils/videoModels' +import { ingestPromptParts } 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' @@ -136,10 +137,11 @@ export default defineEventHandler(async (event) => { fields.task, mode === 'generate' ? 't2i' : mode === 'refine' ? 'refine' : 'scene' ) - const promptMid = String(fields.prompt || '').trim() - const promptPre = String(fields.promptPre || '').trim() - const promptPost = String(fields.promptPost || '').trim() - const prompt = promptMid + const posted = ingestPromptParts(fields) + const promptMid = posted.promptMid + const promptPre = posted.promptPre + const promptPost = posted.promptPost + const prompt = posted.prompt const extraPasses = parsePasses(fields.passes) const passMode = parseImagePassMode(fields.passMode) const aspect = parseImageAspect(fields.aspect) diff --git a/server/utils/imageChain.ts b/server/utils/imageChain.ts index 7ebc89f..3153e59 100644 --- a/server/utils/imageChain.ts +++ b/server/utils/imageChain.ts @@ -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 { joinPromptParts } from '~/utils/promptParts' +import { restorePromptParts, wrappedPromptForComfy } from '~/utils/promptParts' export type EditImageFile = { filename: string; data: Buffer; type?: string } @@ -32,7 +32,14 @@ export type EditRunParams = { export async function runEdit(job: Job, params: EditRunParams) { const library = job.library if (!library) throw new Error('Edit job is missing library metadata') - const prompts = [params.prompt, ...params.passes.map(item => item.prompt)] + const prompts = [ + library.promptMid || restorePromptParts(params.prompt, { + pre: library.promptPre, + prompt: params.prompt, + post: library.promptPost + }).prompt, + ...params.passes.map(item => item.prompt) + ] const chainTotal = prompts.length const batch = params.passMode !== 'chain' const unit = batch ? 'Batch' : 'Pass' @@ -68,7 +75,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 = joinPromptParts(library.promptPre, mid, library.promptPost) + const prompt = wrappedPromptForComfy(library, mid, index === 0 ? params.prompt : mid) const last = index === prompts.length - 1 if (index > 0) { await ensureComfyReady((status) => { diff --git a/server/utils/imageChainV2.ts b/server/utils/imageChainV2.ts index ecb2d83..c88b447 100644 --- a/server/utils/imageChainV2.ts +++ b/server/utils/imageChainV2.ts @@ -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 { joinPromptParts } from '~/utils/promptParts' +import { restorePromptParts, wrappedPromptForComfy } from '~/utils/promptParts' import { isXaigenStudio } from '~/utils/videoModels' import { imageV2StackSpecials, @@ -69,7 +69,14 @@ export async function runEditV2(job: Job, params: EditV2RunParams) { const extraPasses = canChain ? (params.passes || []).map(item => ({ prompt: String(item.prompt || '').trim() })).filter(item => item.prompt) : [] - const prompts = [params.prompt, ...extraPasses.map(item => item.prompt)] + const prompts = [ + library.promptMid || restorePromptParts(params.prompt, { + pre: library.promptPre, + prompt: params.prompt, + post: library.promptPost + }).prompt, + ...extraPasses.map(item => item.prompt) + ] const chainTotal = prompts.length const batch = params.passMode !== 'chain' const unit = batch ? 'Batch' : 'Pass' @@ -218,7 +225,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) { mode: params.mode, engine, task: params.task, - prompt: joinPromptParts(library.promptPre, prompt, library.promptPost), + prompt: wrappedPromptForComfy(library, prompt, index === 0 ? params.prompt : prompt), negative: params.negative, imageAName: uploaded?.name, imageBName: uploadedRef?.name, diff --git a/server/utils/studioQueue.ts b/server/utils/studioQueue.ts index 64dc86b..bac0935 100644 --- a/server/utils/studioQueue.ts +++ b/server/utils/studioQueue.ts @@ -1179,7 +1179,7 @@ export async function startStudioJob(item: StudioJob) { } void runGeneration(job, { - prompt: payload.promptMid || payload.prompt, + prompt: payload.prompt, image: stillFile, width: payload.width, height: payload.height, diff --git a/utils/promptParts.ts b/utils/promptParts.ts index 3815c33..cbd58b2 100644 --- a/utils/promptParts.ts +++ b/utils/promptParts.ts @@ -30,14 +30,44 @@ export function joinPromptParts(pre?: string, mid?: string, post?: string) { return composePromptParts(parts.pre, parts.prompt, parts.post) } -/** Prefer stored mid so an already-joined prompt is not the source of truth. */ +/** Prefer stored mid when wrappers exist. If wrappers are missing, use the already-joined fallback. */ export function wrappedPromptForComfy( wrappers?: PromptWrapperFields | null, mid?: string, fallback?: string ) { - const body = sanitizePromptPart(mid || '').trim() || sanitizePromptPart(fallback || '').trim() - return joinPromptParts(wrappers?.promptPre, body, wrappers?.promptPost) + const pre = sanitizePromptPart(wrappers?.promptPre || '').trim() + const post = sanitizePromptPart(wrappers?.promptPost || '').trim() + const fallbackText = sanitizePromptPart(fallback || '').trim() + const midText = sanitizePromptPart(mid || '').trim() + if (pre || post) { + return composePromptParts(pre, peelPromptWrappers(midText || fallbackText, pre, post), post) + } + return fallbackText || midText +} + +export function ingestPromptParts(fields: { + prompt?: unknown + promptMid?: unknown + promptPre?: unknown + promptPost?: unknown +}) { + const promptPre = sanitizePromptPart(fields.promptPre).trim() + const promptPost = sanitizePromptPart(fields.promptPost).trim() + const postedPrompt = sanitizePromptPart(fields.prompt).trim() + const postedMid = sanitizePromptPart(fields.promptMid).trim() + const promptMid = postedMid || restorePromptParts(postedPrompt, { + pre: promptPre, + prompt: postedPrompt, + post: promptPost + }).prompt + const fromParts = composePromptParts(promptPre, promptMid, promptPost) + return { + promptPre, + promptPost, + promptMid, + prompt: (promptPre || promptPost) ? fromParts : (postedPrompt || fromParts) + } } export function applyPromptParts(parts: PromptParts, middle: string) {