Name outputs from the input, engine, and LoRA keys instead of aigen_0001. Image-edit presets now store prompts, and aspect fill pads stills without stretching.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+17
-3
@@ -2,6 +2,7 @@ import { addStudioJob, kickStudioQueue, listStudioJobs, videoJobsBusy } from '~/
|
||||
import { parsePostedLoraStack, listStudioLoras, persistLoraFields } from '~/server/utils/loras'
|
||||
import { assertImageScaleToTotalPixelsNode, comfyConfigured } from '~/server/utils/comfy'
|
||||
import { imageDimensions } from '~/server/utils/resolution'
|
||||
import { parseImageAspect } from '~/utils/imageAspect'
|
||||
import { clampImageCfg, clampImageScaleMegapixels, clampImageSteps, parseImageScaleToTotalPixels } from '~/utils/generationPresets'
|
||||
|
||||
function parsePasses(raw: string | undefined) {
|
||||
@@ -34,10 +35,11 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const prompt = (fields.prompt || '').trim()
|
||||
const extraPasses = parsePasses(fields.passes)
|
||||
const aspect = parseImageAspect(fields.aspect)
|
||||
if (!image) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'An input image is required' })
|
||||
}
|
||||
if (!prompt) {
|
||||
if (!prompt && aspect === 'auto') {
|
||||
throw createError({ statusCode: 400, statusMessage: 'An edit prompt is required' })
|
||||
}
|
||||
if (extraPasses.some(item => !item.prompt)) {
|
||||
@@ -80,9 +82,21 @@ export default defineEventHandler(async (event) => {
|
||||
? Number(fields.seed)
|
||||
: Math.floor(Math.random() * 2_147_483_647)
|
||||
const size = imageDimensions(image.data)
|
||||
const clipName = (fields.name || '').trim().slice(0, 80)
|
||||
await listStudioLoras()
|
||||
const loraStack = parsePostedLoraStack(fields.loraStack || fields.lora, 'image')
|
||||
let inputFilename = image.filename
|
||||
const postedStillId = (fields.stillId || '').trim()
|
||||
if (postedStillId) {
|
||||
try {
|
||||
const existing = getStill(ownerKey, postedStillId)
|
||||
inputFilename = existing.name || existing.filename || inputFilename
|
||||
} catch { /* use the uploaded filename */ }
|
||||
}
|
||||
const clipName = allocateOutputName(ownerKey, folderId, (fields.name || '').trim(), {
|
||||
inputFilename,
|
||||
engine: 'klein',
|
||||
loraStack
|
||||
})
|
||||
|
||||
const still = await rememberInputStill({
|
||||
ownerKey,
|
||||
@@ -114,7 +128,7 @@ export default defineEventHandler(async (event) => {
|
||||
prompt,
|
||||
name: clipName,
|
||||
folderId,
|
||||
aspect: 'auto',
|
||||
aspect,
|
||||
width: size?.width || 0,
|
||||
height: size?.height || 0,
|
||||
steps,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { addStudioJob, kickStudioQueue, listStudioJobs, videoJobsBusy } from '~/
|
||||
import { listStudioLoras, parsePostedLoraStack, parseShotLoraStacks, persistLoraFields } from '~/server/utils/loras'
|
||||
import type { LoraStackItem } from '~/utils/loras'
|
||||
import { clampVideoCfg } from '~/utils/generationPresets'
|
||||
import { defaultVideoSteps, isLtxWorkflow, isTextToVideo, LTX_DISABLED_MESSAGE, ltxWorkflowEnabled, parseVideoWorkflow } from '~/utils/videoModels'
|
||||
import { defaultVideoSteps, isLtxWorkflow, isTextToVideo, LTX_DISABLED_MESSAGE, ltxWorkflowEnabled, parseVideoWorkflow, videoEngineOf } from '~/utils/videoModels'
|
||||
import { allowIdentityRefs, normalizePermanenceRefs, resolveGlobalLocks, type PermanenceRef } from '~/utils/globalLocks'
|
||||
|
||||
function parseDuration(raw: unknown) {
|
||||
@@ -115,16 +115,16 @@ export default defineEventHandler(async (event) => {
|
||||
const textToVideo = isTextToVideo(workflow)
|
||||
const ownerKey = libraryOwnerKey(event)
|
||||
const postedStillId = (fields.stillId || '').trim()
|
||||
let libraryStill: { id: string; filename: string } | null = null
|
||||
let libraryStill: { id: string; filename: string; name?: string } | null = null
|
||||
if (postedStillId) {
|
||||
const path = stillPath(ownerKey, postedStillId)
|
||||
if (existsSync(path)) {
|
||||
try {
|
||||
const existing = getStill(ownerKey, postedStillId)
|
||||
libraryStill = { id: existing.id, filename: existing.filename }
|
||||
libraryStill = { id: existing.id, filename: existing.filename, name: existing.name }
|
||||
if (!image) {
|
||||
image = {
|
||||
filename: existing.filename || 'still.png',
|
||||
filename: existing.name ? `${existing.name}.png` : (existing.filename || 'still.png'),
|
||||
data: readFileSync(path),
|
||||
type: 'image/png'
|
||||
}
|
||||
@@ -179,7 +179,6 @@ export default defineEventHandler(async (event) => {
|
||||
const hideThumbnail = fields.hideThumbnail === 'true'
|
||||
const hideInput = fields.hideInput === 'true'
|
||||
const sound = !isLtxWorkflow(workflow) && fields.sound !== 'false'
|
||||
const clipName = (fields.name || '').trim().slice(0, 80)
|
||||
const library = publicLibrary(event)
|
||||
const folderId = library.folders.some(folder => folder.id === fields.folderId)
|
||||
? fields.folderId
|
||||
@@ -204,6 +203,11 @@ export default defineEventHandler(async (event) => {
|
||||
if (!textToVideo && !still?.id) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'An initial image is required' })
|
||||
}
|
||||
const clipName = allocateOutputName(ownerKey, folderId, (fields.name || '').trim(), {
|
||||
inputFilename: libraryStill?.name || image?.filename || still?.filename || '',
|
||||
engine: videoEngineOf(workflow),
|
||||
loraStack: defaultStack
|
||||
})
|
||||
console.info('[aigen generate]', {
|
||||
hasImage: Boolean(image?.data?.length),
|
||||
imageBytes: image?.data?.length || 0,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { libraryOwnerKey, publicLibrary, setLoraAlias } from '~/server/utils/library'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<{ key?: unknown; alias?: unknown }>(event)
|
||||
const owner = libraryOwnerKey(event)
|
||||
await setLoraAlias(owner, String(body?.key || ''), String(body?.alias || ''))
|
||||
return publicLibrary(event)
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import { addStudioJob, kickStudioQueue, listStudioJobs, videoJobsBusy } from '~/server/utils/studioQueue'
|
||||
import { comfyConfigured } from '~/server/utils/comfy'
|
||||
import { parseImageAspect, generateSizeForAspect } from '~/utils/imageAspect'
|
||||
import { imageDimensions } from '~/server/utils/resolution'
|
||||
import { clampImageCfg, clampImageScaleMegapixels, clampImageSteps } from '~/utils/generationPresets'
|
||||
import {
|
||||
@@ -63,7 +64,7 @@ function fileFromStill(ownerKey: string, id: string): ImageFile {
|
||||
throw createError({ statusCode: 400, statusMessage: 'That still is missing from the library' })
|
||||
}
|
||||
return {
|
||||
filename: still.filename || `${still.id}.png`,
|
||||
filename: still.name ? `${still.name}.png` : (still.filename || `${still.id}.png`),
|
||||
data: readFileSync(path),
|
||||
type: 'image/png'
|
||||
}
|
||||
@@ -123,7 +124,8 @@ export default defineEventHandler(async (event) => {
|
||||
mode === 'generate' ? 't2i' : mode === 'refine' ? 'refine' : 'scene'
|
||||
)
|
||||
const prompt = String(fields.prompt || '').trim()
|
||||
if (!prompt) {
|
||||
const aspect = parseImageAspect(fields.aspect)
|
||||
if (!prompt && (mode === 'generate' || aspect === 'auto')) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'A prompt is required' })
|
||||
}
|
||||
if (!comfyConfigured()) {
|
||||
@@ -193,11 +195,12 @@ export default defineEventHandler(async (event) => {
|
||||
const seed = fields.seed && String(fields.seed) !== 'random'
|
||||
? Number(fields.seed)
|
||||
: Math.floor(Math.random() * 2_147_483_647)
|
||||
const generateSize = clampImageV2Size(fields.width, fields.height, fields.aspect)
|
||||
const generateSize = mode === 'generate'
|
||||
? generateSizeForAspect(aspect)
|
||||
: clampImageV2Size(fields.width, fields.height, fields.aspect)
|
||||
const size = mode === 'generate'
|
||||
? { width: generateSize.width, height: generateSize.height }
|
||||
: imageDimensions(imageA!.data)
|
||||
const clipName = String(fields.name || '').trim().slice(0, 80)
|
||||
const v2Mode = mode as ImageV2Mode
|
||||
const v2Task = (
|
||||
mode === 'generate' ? 't2i' : mode === 'refine' ? 'refine' : mode === 'edit' ? 'scene' : task
|
||||
@@ -223,6 +226,19 @@ export default defineEventHandler(async (event) => {
|
||||
const generateSnofsClipFallback = 0
|
||||
await listStudioLoras()
|
||||
const loraStack = assertImageV2LoraStack(parsePostedLoraStack(fields.loraStack || fields.lora, 'image'), engine)
|
||||
let inputFilename = imageA?.filename || ''
|
||||
const postedStillId = String(fields.stillId || '').trim()
|
||||
if (postedStillId) {
|
||||
try {
|
||||
const existing = getStill(ownerKey, postedStillId)
|
||||
inputFilename = existing.name || existing.filename || inputFilename
|
||||
} catch { /* use the uploaded filename */ }
|
||||
}
|
||||
const clipName = allocateOutputName(ownerKey, folderId, String(fields.name || '').trim(), {
|
||||
inputFilename,
|
||||
engine,
|
||||
loraStack
|
||||
})
|
||||
|
||||
const still = imageA
|
||||
? await rememberInputStill({
|
||||
@@ -262,7 +278,7 @@ export default defineEventHandler(async (event) => {
|
||||
prompt,
|
||||
name: clipName,
|
||||
folderId,
|
||||
aspect: mode === 'generate' ? (generateSize.aspect || '1:1') : 'auto',
|
||||
aspect: mode === 'generate' ? generateSize.aspect : aspect,
|
||||
width: size?.width || 0,
|
||||
height: size?.height || 0,
|
||||
steps,
|
||||
|
||||
Reference in New Issue
Block a user