Start ComfyUI automatically before queueing MiniMax jobs.

Health-check /system_stats, wait if the desktop is booting or busy, and start the host instance when it is fully offline.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-25 19:39:28 -05:00
co-authored by Cursor
parent f8a3be0fe8
commit f5829995c8
11 changed files with 791 additions and 8 deletions
+13 -1
View File
@@ -71,7 +71,7 @@ export default defineEventHandler(async (event) => {
seed,
thumb: image.data
}
emitJob(job, { type: 'status', message: 'Uploading image...', progress: 2 })
emitJob(job, { type: 'status', message: 'Checking ComfyUI...', progress: 1 })
void runGeneration(job, { prompt, image, width, height, steps, seed, turbo, length }).catch((error) => {
const message = error instanceof Error ? error.message : String(error)
@@ -107,8 +107,20 @@ async function runGeneration(
length: number
}
) {
await ensureComfyReady((status) => {
emitJob(job, {
type: status.state === 'busy' ? 'busy' : 'status',
message: status.message,
progress: status.state === 'online' ? Math.max(job.progress, 3) : Math.max(job.progress, 1),
busy: status.state === 'busy',
queueRunning: status.queueRunning,
queuePending: status.queuePending
})
})
const done = watchComfyJob(job)
job.status = 'uploading'
emitJob(job, { type: 'status', message: 'Uploading image to ComfyUI...', progress: 4 })
const uploaded = await uploadImage(params.image)
if (job.library) job.library.imageName = uploaded.name
emitJob(job, { type: 'status', message: 'Queueing MiniMax H3 job...', progress: 6 })
+6 -2
View File
@@ -1,8 +1,12 @@
export default defineEventHandler(async () => {
const comfy = await probeComfy().catch(() => ({ ok: false, host: '' }))
const comfy = await checkComfyHttp().catch(() => ({ ok: false, host: '', stats: null }))
return {
ok: true,
service: useRuntimeConfig().public.instanceName || 'aigen',
comfy
comfy: {
ok: comfy.ok,
host: comfy.host || '',
stats: comfy.ok ? comfy.stats : undefined
}
}
})
+381
View File
@@ -0,0 +1,381 @@
{
"92": {
"inputs": {
"filename_prefix": "video/MiniMax_H3",
"format": "auto",
"codec": "auto",
"video": [
"105:91",
0
]
},
"class_type": "SaveVideo",
"_meta": {
"title": "Save Video"
}
},
"114": {
"inputs": {
"image": "Firefly.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"119": {
"inputs": {
"upscale_method": "nearest-exact",
"megapixels": 0.9,
"resolution_steps": 32
},
"class_type": "ImageScaleToTotalPixels",
"_meta": {
"title": "Scale Image to Total Pixels"
}
},
"120": {
"inputs": {
"image": [
"119",
0
]
},
"class_type": "GetImageSize",
"_meta": {
"title": "Get Image Size"
}
},
"127": {
"inputs": {
"image": [
"128",
0
]
},
"class_type": "GetImageSize",
"_meta": {
"title": "Get Image Size"
}
},
"128": {
"inputs": {
"upscale_method": "nearest-exact",
"megapixels": 0.98,
"resolution_steps": 32,
"image": [
"114",
0
]
},
"class_type": "ImageScaleToTotalPixels",
"_meta": {
"title": "Scale Image to Total Pixels"
}
},
"105:11": {
"inputs": {
"vae_name": "minimax_h3_video_vae_fp16.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"105:24": {
"inputs": {
"vae_name": "minimax_h3_audio_vae_fp32.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"105:23": {
"inputs": {
"samples": [
"105:14",
0
],
"vae": [
"105:24",
0
]
},
"class_type": "VAEDecodeAudio",
"_meta": {
"title": "VAE Decode Audio"
}
},
"105:10": {
"inputs": {
"samples": [
"105:14",
0
],
"vae": [
"105:11",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"105:17": {
"inputs": {
"sampler_name": "res_multistep"
},
"class_type": "KSamplerSelect",
"_meta": {
"title": "KSamplerSelect"
}
},
"105:9": {
"inputs": {
"scheduler": "simple",
"steps": [
"105:123",
0
],
"denoise": 1,
"model": [
"105:122",
0
]
},
"class_type": "BasicScheduler",
"_meta": {
"title": "BasicScheduler"
}
},
"105:14": {
"inputs": {
"noise": [
"105:15",
0
],
"guider": [
"105:16",
0
],
"sampler": [
"105:17",
0
],
"sigmas": [
"105:9",
0
],
"latent_image": [
"105:104",
1
]
},
"class_type": "SamplerCustomAdvanced",
"_meta": {
"title": "SamplerCustomAdvanced"
}
},
"105:16": {
"inputs": {
"model": [
"105:122",
0
],
"conditioning": [
"105:104",
0
]
},
"class_type": "BasicGuider",
"_meta": {
"title": "Basic Guider"
}
},
"105:6": {
"inputs": {
"unet_name": "minimax_h3_fl2va_pruned_int8_convrot.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "Load Diffusion Model"
}
},
"105:13": {
"inputs": {
"clip_name": "qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors",
"type": "minimax",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "Load CLIP"
}
},
"105:15": {
"inputs": {
"noise_seed": 757358688076805
},
"class_type": "RandomNoise",
"_meta": {
"title": "RandomNoise"
}
},
"105:91": {
"inputs": {
"fps": 24,
"bit_depth": 8,
"images": [
"105:10",
0
],
"audio": [
"105:23",
0
]
},
"class_type": "CreateVideo",
"_meta": {
"title": "Create Video"
}
},
"105:104": {
"inputs": {
"prompt": "",
"width": [
"127",
0
],
"height": [
"127",
1
],
"length": [
"105:107",
1
],
"clip": [
"105:13",
0
],
"vae": [
"105:11",
0
],
"first_frame": [
"128",
0
]
},
"class_type": "MiniMaxH3ImageToVideo",
"_meta": {
"title": "MiniMax H3 Image to Video"
}
},
"105:107": {
"inputs": {
"expression": "max(5, round(a * 24)) + (5 - (max(5, round(a * 24)) % 17)) % 17",
"values.a": [
"105:111",
0
]
},
"class_type": "ComfyMathExpression",
"_meta": {
"title": "Math Expression"
}
},
"105:111": {
"inputs": {
"value": 10
},
"class_type": "PrimitiveFloat",
"_meta": {
"title": "Float (duration)"
}
},
"105:121": {
"inputs": {
"lora_name": "minimax_h3_fl2v_turbo_8step_v1.0_comfyui_bf16.safetensors",
"strength_model": 1,
"model": [
"105:6",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "Load LoRA"
}
},
"105:122": {
"inputs": {
"switch": [
"105:126",
0
],
"on_false": [
"105:6",
0
],
"on_true": [
"105:121",
0
]
},
"class_type": "ComfySwitchNode",
"_meta": {
"title": "If/Else Switch (Model)"
}
},
"105:123": {
"inputs": {
"switch": [
"105:126",
0
],
"on_false": [
"105:124",
0
],
"on_true": [
"105:125",
0
]
},
"class_type": "ComfySwitchNode",
"_meta": {
"title": "If/Else Switch (Steps)"
}
},
"105:124": {
"inputs": {
"value": 20
},
"class_type": "PrimitiveInt",
"_meta": {
"title": "Int"
}
},
"105:125": {
"inputs": {
"value": 8
},
"class_type": "PrimitiveInt",
"_meta": {
"title": "Int"
}
},
"105:126": {
"inputs": {
"value": false
},
"class_type": "PrimitiveBoolean",
"_meta": {
"title": "Boolean (Enable Lightning LoRA)"
}
}
}
+10 -2
View File
@@ -1,10 +1,18 @@
function comfyBase() {
const config = useRuntimeConfig()
const host = (config.comfyHost || process.env.COMFY_HOST || '').replace(/\/$/, '')
let host = String(config.comfyHost || process.env.COMFY_HOST || '').trim().replace(/\/$/, '')
const port = String(config.comfyPort || process.env.COMFY_PORT || '').trim()
if (!host) {
throw createError({ statusCode: 500, statusMessage: 'COMFY_HOST is not configured' })
}
return host
if (!/^https?:\/\//i.test(host)) host = `http://${host}`
try {
const url = new URL(host)
if (port && !url.port) url.port = port
return url.origin
} catch {
return port ? `${host}:${port}` : host
}
}
export function getComfyHost() {
+280
View File
@@ -0,0 +1,280 @@
import { execFile, spawn } from 'node:child_process'
import { promisify } from 'node:util'
const execFileAsync = promisify(execFile)
export type ComfyLifecycleState = 'online' | 'booting' | 'busy' | 'starting' | 'offline'
export interface ComfyLifecycleStatus {
state: ComfyLifecycleState
message: string
host: string
processRunning?: boolean
queueRunning?: number
queuePending?: number
}
type StatusFn = (status: ComfyLifecycleStatus) => void
let gate: Promise<unknown> = Promise.resolve()
let lastStartAt = 0
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
}
function log(event: string, detail: Record<string, unknown> = {}) {
console.log(JSON.stringify({
src: 'comfy-lifecycle',
event,
at: new Date().toISOString(),
...detail
}))
}
function settings() {
const config = useRuntimeConfig()
return {
host: getComfyHost(),
launchCmd: String(config.comfyLaunchCmd || process.env.COMFY_LAUNCH_CMD || '').trim(),
controlUrl: String(config.comfyControlUrl || process.env.COMFY_CONTROL_URL || '').replace(/\/$/, ''),
controlToken: String(config.comfyControlToken || process.env.COMFY_CONTROL_TOKEN || ''),
healthTimeoutMs: 2500,
bootPollAttempts: 5,
bootPollDelayMs: 2000,
startTimeoutMs: 30_000,
busyWaitMs: Number(process.env.COMFY_BUSY_WAIT_MS || 180_000)
}
}
export async function checkComfyHttp(timeoutMs = 2500) {
const host = getComfyHost()
try {
const res = await fetch(`${host}/system_stats`, { signal: AbortSignal.timeout(timeoutMs) })
if (!res.ok) return { ok: false as const, host, status: res.status, stats: null }
const stats = await res.json().catch(() => null)
return { ok: true as const, host, status: res.status, stats }
} catch {
return { ok: false as const, host, status: 0, stats: null }
}
}
async function fetchQueue() {
const host = getComfyHost()
try {
const res = await fetch(`${host}/queue`, { signal: AbortSignal.timeout(2500) })
if (!res.ok) return { running: 0, pending: 0 }
const payload = await res.json() as { queue_running?: unknown[]; queue_pending?: unknown[] }
return {
running: Array.isArray(payload.queue_running) ? payload.queue_running.length : 0,
pending: Array.isArray(payload.queue_pending) ? payload.queue_pending.length : 0
}
} catch {
return { running: 0, pending: 0 }
}
}
async function controlRequest(path: string, method = 'GET') {
const { controlUrl, controlToken } = settings()
if (!controlUrl) return null
try {
const res = await fetch(`${controlUrl}${path}`, {
method,
headers: {
Accept: 'application/json',
...(controlToken ? { Authorization: `Bearer ${controlToken}` } : {})
},
signal: AbortSignal.timeout(5000)
})
if (!res.ok) return { ok: false, status: res.status }
return await res.json() as {
ok?: boolean
http?: boolean
process?: boolean
pid?: number
started?: boolean
}
} catch {
return null
}
}
async function findLocalComfyProcess() {
const platform = process.platform
try {
if (platform === 'win32') {
const { stdout } = await execFileAsync('tasklist', ['/FO', 'CSV', '/NH'], { timeout: 4000, windowsHide: true })
const hit = stdout.split(/\r?\n/).some(line => /comfy|python/i.test(line) && /comfy/i.test(line))
if (hit) return true
const { stdout: verbose } = await execFileAsync('wmic', ['process', 'where', "name='python.exe'", 'get', 'commandline'], { timeout: 4000, windowsHide: true }).catch(() => ({ stdout: '' }))
return /comfyui|main\.py/i.test(verbose)
}
const { stdout } = await execFileAsync('ps', ['-ax', '-o', 'args='], { timeout: 4000 })
return /comfyui|ComfyUI|main\.py.*8188/i.test(stdout)
} catch {
return false
}
}
async function processRunning() {
const remote = await controlRequest('/status')
if (remote?.process) return true
return findLocalComfyProcess()
}
function spawnLaunchCommand(command: string) {
log('spawn', { command: command.slice(0, 180) })
const child = spawn(command, {
shell: true,
detached: true,
stdio: 'ignore',
windowsHide: true
})
child.unref()
}
async function startComfy() {
const now = Date.now()
if (now - lastStartAt < 15_000) {
log('start-skipped', { reason: 'recent-start' })
return { started: false, reason: 'recent-start' as const }
}
lastStartAt = now
const remote = await controlRequest('/start', 'POST')
if (remote?.ok || remote?.started) {
log('start-remote', { pid: remote.pid || null })
return { started: true, reason: 'remote' as const }
}
const { launchCmd } = settings()
if (launchCmd) {
spawnLaunchCommand(launchCmd)
return { started: true, reason: 'launch-cmd' as const }
}
if (process.platform === 'win32') {
const fallback = process.env.LOCALAPPDATA
? `"${process.env.LOCALAPPDATA}\\Programs\\Comfy Desktop\\Comfy Desktop.exe"`
: ''
if (fallback) {
spawnLaunchCommand(`start "" ${fallback}`)
return { started: true, reason: 'comfy-desktop' as const }
}
}
return { started: false, reason: 'no-launcher' as const }
}
async function pollUntilHealthy(attempts: number, delayMs: number, onStatus: StatusFn, state: ComfyLifecycleState, message: string) {
const host = getComfyHost()
for (let i = 1; i <= attempts; i++) {
onStatus({ state, message: `${message} (${i}/${attempts})`, host, processRunning: true })
const health = await checkComfyHttp()
if (health.ok) return true
if (i < attempts) await sleep(delayMs)
}
return false
}
async function waitWhileBusy(onStatus: StatusFn) {
const { busyWaitMs } = settings()
const host = getComfyHost()
const started = Date.now()
while (Date.now() - started < busyWaitMs) {
const queue = await fetchQueue()
if (queue.running === 0) return
onStatus({
state: 'busy',
message: `ComfyUI is busy with another prompt (${queue.running} running, ${queue.pending} queued). Waiting...`,
host,
queueRunning: queue.running,
queuePending: queue.pending
})
log('busy', queue)
await sleep(2000)
}
throw createError({
statusCode: 409,
statusMessage: 'ComfyUI is busy executing another prompt. Try again in a moment.'
})
}
async function ensureUnlocked(onStatus: StatusFn) {
const cfg = settings()
const host = cfg.host
onStatus({ state: 'offline', message: 'Checking ComfyUI...', host })
const health = await checkComfyHttp(cfg.healthTimeoutMs)
if (health.ok) {
log('http-ok', { host })
await waitWhileBusy(onStatus)
onStatus({ state: 'online', message: 'ComfyUI online', host })
return
}
log('http-fail', { host })
const running = await processRunning()
if (running) {
onStatus({
state: 'booting',
message: 'ComfyUI process is up but the API is not ready yet. Waiting...',
host,
processRunning: true
})
const ready = await pollUntilHealthy(cfg.bootPollAttempts, cfg.bootPollDelayMs, onStatus, 'booting', 'Waiting for ComfyUI to finish booting')
if (!ready) {
throw createError({
statusCode: 503,
statusMessage: 'ComfyUI is running but port 8188 never became ready. Check the desktop instance.'
})
}
await waitWhileBusy(onStatus)
onStatus({ state: 'online', message: 'ComfyUI online', host, processRunning: true })
return
}
onStatus({
state: 'starting',
message: 'ComfyUI is offline. Starting the desktop instance...',
host,
processRunning: false
})
const started = await startComfy()
if (!started.started) {
throw createError({
statusCode: 503,
statusMessage: 'ComfyUI is offline and no launch command is configured. Start Comfy Desktop, or set COMFY_LAUNCH_CMD / COMFY_CONTROL_URL.'
})
}
const deadline = Date.now() + cfg.startTimeoutMs
let attempt = 0
while (Date.now() < deadline) {
attempt += 1
onStatus({
state: 'starting',
message: `Starting ComfyUI... waiting for /system_stats (${attempt})`,
host
})
const next = await checkComfyHttp()
if (next.ok) {
log('started', { attempt, reason: started.reason })
await waitWhileBusy(onStatus)
onStatus({ state: 'online', message: 'ComfyUI online', host, processRunning: true })
return
}
await sleep(2000)
}
throw createError({
statusCode: 503,
statusMessage: 'Started ComfyUI but it did not become healthy within 30 seconds.'
})
}
export function ensureComfyReady(onStatus: StatusFn = () => undefined) {
const run = gate.then(() => ensureUnlocked(onStatus))
gate = run.then(() => undefined, () => undefined)
return run
}
+3
View File
@@ -14,6 +14,9 @@ export interface JobEvent {
hideThumbnail?: boolean
error?: string
elapsedMs?: number
busy?: boolean
queueRunning?: number
queuePending?: number
}
export interface Job {
+1 -1
View File
@@ -1,5 +1,5 @@
// Nitro bundles this JSON into the production server output.
import workflowTemplate from '../assets/workflow_api.json'
import workflowTemplate from '../assets/workflow_minimax_video.json'
export interface GenerateParams {
prompt: string