Files
aigen/server/api/generate.post.ts
T
towsty a9bf4f6a95 Add automatic aspect matching with explicit ratio overrides.
Keep video output on the still by default, and only force 16:9, 9:16, or 1:1 when a preset is selected.
2026-08-25 17:55:40 -05:00

95 lines
2.9 KiB
TypeScript

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<string, string> = {}
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 job = createJob()
job.maxStep = steps
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
}
})
async function runGeneration(
job: ReturnType<typeof createJob>,
params: {
prompt: string
image: { filename: string; data: Buffer; type?: string }
width: number
height: number
steps: number
seed: number
turbo: boolean
length: number
}
) {
job.status = 'uploading'
const uploaded = await uploadImage(params.image)
emitJob(job, { type: 'status', message: 'Queueing MiniMax H3 job...', progress: 6 })
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 watchComfyJob(job)
}