diff --git a/.env.example b/.env.example index 7b8b79f..08a794b 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,12 @@ PORT=3000 HOST=0.0.0.0 COMFY_HOST=http://192.168.77.7:8188 +COMFY_PORT=8188 +# Optional: command used when this server runs on the Comfy host +COMFY_LAUNCH_CMD= +# Windows host agent (scripts/comfy-host-agent.mjs) so Coolify can start Comfy remotely +COMFY_CONTROL_URL=http://192.168.77.7:8199 +COMFY_CONTROL_TOKEN= NUXT_SESSION_SECRET=change-me NUXT_PUBLIC_AUTH_ENABLED=true diff --git a/nuxt.config.ts b/nuxt.config.ts index 6a764ed..c8b455b 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -25,6 +25,10 @@ export default defineNuxtConfig({ }, runtimeConfig: { comfyHost: process.env.COMFY_HOST || '', + comfyPort: process.env.COMFY_PORT || '', + comfyLaunchCmd: process.env.COMFY_LAUNCH_CMD || '', + comfyControlUrl: process.env.COMFY_CONTROL_URL || '', + comfyControlToken: process.env.COMFY_CONTROL_TOKEN || '', sessionSecret: process.env.NUXT_SESSION_SECRET || '', oidcClientId: process.env.NUXT_OIDC_CLIENT_ID || '', oidcClientSecret: process.env.NUXT_OIDC_CLIENT_SECRET || '', diff --git a/pages/index.vue b/pages/index.vue index f1c1f3f..13a9c7c 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -169,7 +169,7 @@
- {{ statusMessage }} + {{ statusMessage }} {{ progress }}%
@@ -404,6 +404,7 @@ const showThumbnail = computed({ const busy = ref(false) const progress = ref(0) const statusMessage = ref('Waiting for a still.') +const statusBusy = ref(false) const step = ref(0) const maxStep = ref(8) const elapsedMs = ref(0) @@ -661,13 +662,14 @@ async function generate() { toast('Unlock the selected folder before generating.') return } + statusBusy.value = false busy.value = true settledUi = false progress.value = 1 videoUrl.value = '' awaitingReveal.value = false playerSize.value = outputSize.value - statusMessage.value = 'Uploading image...' + statusMessage.value = 'Checking ComfyUI...' startTimer() try { const body = new FormData() @@ -694,6 +696,7 @@ async function generate() { listen(started.jobId, started.hideThumbnail === true || hideThumbnail.value) } catch (error: any) { busy.value = false + statusBusy.value = false stopTimer() const message = error?.data?.statusMessage || error?.statusMessage || error?.message || 'Failed to start generation' statusMessage.value = message @@ -712,6 +715,7 @@ function stopListen() { function applyJobEvent(payload: Record, hidden: boolean) { if (payload.message) statusMessage.value = payload.message + statusBusy.value = payload.type === 'busy' || payload.busy === true if (typeof payload.progress === 'number') progress.value = payload.progress if (typeof payload.step === 'number') step.value = payload.step if (typeof payload.maxStep === 'number') maxStep.value = payload.maxStep @@ -728,6 +732,7 @@ function applyJobEvent(payload: Record, hidden: boolean) { videoUrl.value = clipUrl awaitingReveal.value = Boolean(payload.hideThumbnail || hidden) busy.value = false + statusBusy.value = false stopTimer() stopListen() loadLibrary().catch(() => null) @@ -737,6 +742,7 @@ function applyJobEvent(payload: Record, hidden: boolean) { if (failed) { settledUi = true busy.value = false + statusBusy.value = false stopTimer() stopListen() toast(payload.error || payload.message || 'Generation failed') diff --git a/scripts/comfy-host-agent.mjs b/scripts/comfy-host-agent.mjs new file mode 100644 index 0000000..7507d5a --- /dev/null +++ b/scripts/comfy-host-agent.mjs @@ -0,0 +1,79 @@ +import http from 'node:http' +import { execFile, spawn } from 'node:child_process' +import { promisify } from 'node:util' + +const execFileAsync = promisify(execFile) +const port = Number(process.env.COMFY_CONTROL_PORT || 8199) +const token = process.env.COMFY_CONTROL_TOKEN || '' +const comfyHttp = (process.env.COMFY_HOST || 'http://127.0.0.1:8188').replace(/\/$/, '') + +function json(res, status, body) { + const payload = JSON.stringify(body) + res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }) + res.end(payload) +} + +function authorized(req) { + if (!token) return true + const header = String(req.headers.authorization || '') + return header === `Bearer ${token}` +} + +async function httpUp() { + try { + const res = await fetch(`${comfyHttp}/system_stats`, { signal: AbortSignal.timeout(2500) }) + return res.ok + } catch { + return false + } +} + +async function processUp() { + try { + const { stdout } = await execFileAsync('tasklist', ['/FO', 'CSV', '/NH'], { windowsHide: true, timeout: 4000 }) + if (/Comfy Desktop|ComfyUI/i.test(stdout)) return true + } catch { /* ignore */ } + try { + const { stdout } = await execFileAsync('powershell.exe', [ + '-NoProfile', '-Command', + "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'ComfyUI\\\\main.py|ComfyUI/main.py' } | Select-Object -First 1 -ExpandProperty ProcessId" + ], { windowsHide: true, timeout: 6000 }) + return Boolean(String(stdout).trim()) + } catch { + return false + } +} + +function launchCommand() { + if (process.env.COMFY_LAUNCH_CMD) return process.env.COMFY_LAUNCH_CMD + const local = process.env.LOCALAPPDATA || '' + const desktop = `${local}\\Programs\\Comfy Desktop\\Comfy Desktop.exe` + return `start "" "${desktop}"` +} + +function startComfy() { + const command = launchCommand() + const child = spawn(command, { shell: true, detached: true, stdio: 'ignore', windowsHide: true }) + child.unref() + return { started: true, command } +} + +const server = http.createServer(async (req, res) => { + if (!authorized(req)) return json(res, 401, { ok: false, error: 'unauthorized' }) + const url = new URL(req.url || '/', 'http://localhost') + if (req.method === 'GET' && url.pathname === '/status') { + const [httpReady, process] = await Promise.all([httpUp(), processUp()]) + return json(res, 200, { ok: true, http: httpReady, process }) + } + if (req.method === 'POST' && url.pathname === '/start') { + if (await httpUp()) return json(res, 200, { ok: true, started: false, already: true }) + if (await processUp()) return json(res, 200, { ok: true, started: false, booting: true }) + const launched = startComfy() + return json(res, 200, { ok: true, ...launched }) + } + json(res, 404, { ok: false, error: 'not found' }) +}) + +server.listen(port, '0.0.0.0', () => { + console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'listen', port, comfyHttp })) +}) diff --git a/server/api/generate.post.ts b/server/api/generate.post.ts index aa0b01b..8e2fc31 100644 --- a/server/api/generate.post.ts +++ b/server/api/generate.post.ts @@ -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 }) diff --git a/server/api/health.get.ts b/server/api/health.get.ts index 995aebf..ba52cd6 100644 --- a/server/api/health.get.ts +++ b/server/api/health.get.ts @@ -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 + } } }) diff --git a/server/assets/workflow_minimax_video.json b/server/assets/workflow_minimax_video.json new file mode 100644 index 0000000..684d444 --- /dev/null +++ b/server/assets/workflow_minimax_video.json @@ -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)" + } + } +} \ No newline at end of file diff --git a/server/utils/comfy.ts b/server/utils/comfy.ts index 3387160..bab2e9e 100644 --- a/server/utils/comfy.ts +++ b/server/utils/comfy.ts @@ -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() { diff --git a/server/utils/comfyLifecycle.ts b/server/utils/comfyLifecycle.ts new file mode 100644 index 0000000..048c856 --- /dev/null +++ b/server/utils/comfyLifecycle.ts @@ -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 = Promise.resolve() +let lastStartAt = 0 + +function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +function log(event: string, detail: Record = {}) { + 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 +} diff --git a/server/utils/jobs.ts b/server/utils/jobs.ts index 1ed016b..c3b764f 100644 --- a/server/utils/jobs.ts +++ b/server/utils/jobs.ts @@ -14,6 +14,9 @@ export interface JobEvent { hideThumbnail?: boolean error?: string elapsedMs?: number + busy?: boolean + queueRunning?: number + queuePending?: number } export interface Job { diff --git a/server/utils/workflow.ts b/server/utils/workflow.ts index b0994f9..0d8f29e 100644 --- a/server/utils/workflow.ts +++ b/server/utils/workflow.ts @@ -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