function frameLength(seconds: number) { const raw = Math.max(5, Math.round(seconds * 24)) return raw + (5 - (raw % 17) + 17) % 17 } export default defineEventHandler(async (event) => { const form = await readMultipartFormData(event) if (!form?.length) { throw createError({ statusCode: 400, statusMessage: 'Expected multipart form data' }) } const fields: Record = {} let image: { filename: string; data: Buffer; type?: string } | null = null for (const part of form) { if (part.name === 'image' && part.filename && part.data?.length) { image = { filename: part.filename, data: part.data, type: part.type } } else if (part.name && part.data) { fields[part.name] = part.data.toString('utf8') } } if (!image) { throw createError({ statusCode: 400, statusMessage: 'An initial image is required' }) } const prompt = (fields.prompt || '').trim() if (!prompt) { throw createError({ statusCode: 400, statusMessage: 'A motion prompt is required' }) } const { width, height } = resolveOutputSize(fields.aspect, image.data, Number(fields.width), Number(fields.height)) const turbo = fields.turbo !== 'false' const steps = Number(fields.steps || (turbo ? 8 : 20)) const seed = fields.seed && fields.seed !== 'random' ? Number(fields.seed) : Math.floor(Math.random() * 2_147_483_647) const length = frameLength(Number(fields.duration || 5)) const hideThumbnail = fields.hideThumbnail === 'true' assertLibraryAccess(event) const library = publicLibrary(event) const folderId = library.folders.some(folder => folder.id === fields.folderId) ? fields.folderId : library.folders[0]?.id if (!folderId) { throw createError({ statusCode: 400, statusMessage: 'Create a library folder before generating' }) } const job = createJob() job.maxStep = steps job.hideThumbnail = hideThumbnail job.library = { folderId, hideThumbnail, prompt, aspect: fields.aspect || 'auto', width, height, steps, turbo, seed, thumb: image.data } emitJob(job, { type: 'status', message: 'Uploading image...', progress: 2 }) void runGeneration(job, { prompt, image, width, height, steps, seed, turbo, length }).catch((error) => { const message = error instanceof Error ? error.message : String(error) job.status = 'error' job.error = message emitJob(job, { type: 'error', error: message, message }) }) return { jobId: job.id, clientId: job.clientId, seed, width, height, aspect: fields.aspect || 'auto', steps, turbo, folderId, hideThumbnail } }) async function runGeneration( job: ReturnType, params: { prompt: string image: { filename: string; data: Buffer; type?: string } width: number height: number steps: number seed: number turbo: boolean length: number } ) { const done = watchComfyJob(job) job.status = 'uploading' const uploaded = await uploadImage(params.image) emitJob(job, { type: 'status', message: 'Queueing MiniMax H3 job...', progress: 6 }) await waitForComfySocket(job, 4000) const graph = buildWorkflow({ prompt: params.prompt, imageName: uploaded.name, width: params.width, height: params.height, steps: params.steps, seed: params.seed, turbo: params.turbo, length: params.length }) const queued = await queuePrompt(graph, job.clientId) job.promptId = queued.prompt_id job.status = 'running' emitJob(job, { type: 'status', message: 'Job queued on ComfyUI', progress: 8 }) await done }