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:
Towsty
2026-08-30 16:24:49 -05:00
co-authored by Cursor
parent 4d411f83d4
commit 0e1222d035
23 changed files with 668 additions and 93 deletions
+17 -3
View File
@@ -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,
+9 -5
View File
@@ -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,
+8
View File
@@ -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)
})
+21 -5
View File
@@ -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,