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:
+3
-3
@@ -6686,7 +6686,7 @@ async function editImage() {
|
||||
if (inputStillId.value) body.append('stillId', inputStillId.value)
|
||||
if (extraPasses.length) body.append('passes', JSON.stringify(extraPasses))
|
||||
body.append('passMode', imagePassMode.value)
|
||||
if (imageLoraStack.value.length) body.append('loraStack', JSON.stringify(imageLoraStack.value))
|
||||
body.append('loraStack', JSON.stringify(imageLoraStack.value))
|
||||
const downloadName = clipFileName(clipName.value.trim() || autoOutputName.value)
|
||||
const started = await $fetch<{
|
||||
jobId: string
|
||||
@@ -6942,7 +6942,7 @@ async function editImageV2() {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (v2Stack.length) body.append('loraStack', JSON.stringify(v2Stack))
|
||||
body.append('loraStack', JSON.stringify(v2Stack))
|
||||
const downloadName = clipFileName(clipName.value.trim() || autoOutputName.value)
|
||||
const started = await $fetch<{
|
||||
jobId: string
|
||||
@@ -7154,7 +7154,7 @@ async function generate() {
|
||||
const shotLoraStacksList = shots
|
||||
? shots.map(shot => shotLoraStacks.value[shot.n] || [])
|
||||
: [shotLoraStacks.value[1] || [], ...queued.map(item => item.loraStack || [])]
|
||||
if (videoLoraStack.value.length) body.append('loraStack', JSON.stringify(videoLoraStack.value))
|
||||
body.append('loraStack', JSON.stringify(videoLoraStack.value))
|
||||
body.append('shotLoraStacks', JSON.stringify(shotLoraStacksList))
|
||||
if (activeDraftId) body.append('draftId', activeDraftId)
|
||||
if (queued.length) {
|
||||
|
||||
+25
-2
@@ -200,8 +200,23 @@
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<aside class="glass w-full shrink-0 overflow-y-auto rounded-3xl p-4 sm:p-5 lg:h-full lg:w-[22rem]">
|
||||
<aside
|
||||
class="glass overflow-y-auto p-4 sm:p-5"
|
||||
:class="focused
|
||||
? 'fixed inset-0 z-[60] rounded-none lg:static lg:z-auto lg:h-full lg:w-[22rem] lg:shrink-0 lg:rounded-3xl'
|
||||
: 'hidden w-full shrink-0 rounded-3xl lg:block lg:h-full lg:w-[22rem]'"
|
||||
>
|
||||
<template v-if="focused">
|
||||
<div class="mb-3 flex items-center justify-between lg:hidden">
|
||||
<p class="text-sm text-zinc-400">Preview</p>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full border border-white/20 bg-zinc-950 px-3 py-1.5 text-sm text-amber-100"
|
||||
@click="clearFocus"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative overflow-hidden rounded-2xl bg-zinc-900">
|
||||
<span v-if="detailsHidden" class="sensitive-glow block aspect-video w-full" />
|
||||
<video
|
||||
@@ -494,7 +509,7 @@ const previewVideo = computed(() => {
|
||||
const previewStillSrc = computed(() => {
|
||||
if (focused.value?.kind !== 'still' || !focused.value.stillGroup) return ''
|
||||
const id = focusedPartId.value || focused.value.stillGroup.head.id
|
||||
return `/api/library/stills/${id}?thumb=1`
|
||||
return `/api/library/stills/${id}`
|
||||
})
|
||||
const focusedParts = computed(() => {
|
||||
if (focused.value?.kind === 'clip') return focused.value.group?.parts || []
|
||||
@@ -719,6 +734,13 @@ function clearSelection() {
|
||||
lastSelectedKey.value = ''
|
||||
}
|
||||
|
||||
function clearFocus() {
|
||||
focusedKey.value = ''
|
||||
focusedPartId.value = ''
|
||||
detailsHidden.value = false
|
||||
cancelRename()
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
selectedKeys.value = entries.value.map(entry => entry.key)
|
||||
lastSelectedKey.value = selectedKeys.value.at(-1) || ''
|
||||
@@ -978,6 +1000,7 @@ function onKey(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
if (pendingDelete.value) pendingDelete.value = null
|
||||
else if (renaming.value) cancelRename()
|
||||
else if (focusedKey.value) clearFocus()
|
||||
else clearSelection()
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a' && !renaming.value) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 || '')
|
||||
|
||||
@@ -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,
|
||||
|
||||
+1
-1
@@ -126,7 +126,7 @@ export function clampImageV2Denoise(raw: unknown, fallback = IMAGE_V2_DENOISE_DE
|
||||
|
||||
export function snapImageV2Side(raw: unknown, fallback = IMAGE_V2_GENERATE_WIDTH) {
|
||||
const value = Number(raw)
|
||||
if (!Number.isFinite(value)) return fallback
|
||||
if (!Number.isFinite(value) || value <= 0) return fallback
|
||||
return IMAGE_V2_SIZE_SIDES.reduce((best, side) => (
|
||||
Math.abs(side - value) < Math.abs(best - value) ? side : best
|
||||
), IMAGE_V2_SIZE_SIDES[0])
|
||||
|
||||
+19
-3
@@ -15,6 +15,16 @@ export function composePromptParts(pre: string, prompt: string, post: string) {
|
||||
.join('\n\n')
|
||||
}
|
||||
|
||||
/** Combine wrappers for Comfy. If mid already includes pre/post, do not wrap twice. */
|
||||
export function joinPromptParts(pre?: string, mid?: string, post?: string) {
|
||||
const parts = restorePromptParts(mid || '', {
|
||||
pre: pre || '',
|
||||
prompt: mid || '',
|
||||
post: post || ''
|
||||
})
|
||||
return composePromptParts(parts.pre, parts.prompt, parts.post)
|
||||
}
|
||||
|
||||
export function applyPromptParts(parts: PromptParts, middle: string) {
|
||||
return composePromptParts(parts.pre, middle, parts.post)
|
||||
}
|
||||
@@ -45,7 +55,7 @@ function hasWrapperKey(item: object | null | undefined, key: 'promptPre' | 'prom
|
||||
return Boolean(item && Object.prototype.hasOwnProperty.call(item, key))
|
||||
}
|
||||
|
||||
/** First source that actually sent the key wins, including empty string. Omitted keys fall through. */
|
||||
/** First non-empty wrapper wins. Blank strings fall through so a shot can inherit family pre/post. */
|
||||
export function resolvePromptWrappers(
|
||||
...sources: Array<PromptWrapperFields | null | undefined>
|
||||
) {
|
||||
@@ -56,13 +66,19 @@ export function resolvePromptWrappers(
|
||||
for (const source of sources) {
|
||||
if (!source) continue
|
||||
if (!sawPre && hasWrapperKey(source, 'promptPre') && source.promptPre != null) {
|
||||
promptPre = sanitizePromptPart(source.promptPre)
|
||||
const value = sanitizePromptPart(source.promptPre)
|
||||
if (value.trim()) {
|
||||
promptPre = value
|
||||
sawPre = true
|
||||
}
|
||||
}
|
||||
if (!sawPost && hasWrapperKey(source, 'promptPost') && source.promptPost != null) {
|
||||
promptPost = sanitizePromptPart(source.promptPost)
|
||||
const value = sanitizePromptPart(source.promptPost)
|
||||
if (value.trim()) {
|
||||
promptPost = value
|
||||
sawPost = true
|
||||
}
|
||||
}
|
||||
if (sawPre && sawPost) break
|
||||
}
|
||||
return { promptPre, promptPost }
|
||||
|
||||
Reference in New Issue
Block a user