Headless image-to-video UI queues jobs on the desktop ComfyUI instance with live SSE progress and Authentik SSO.
103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
function comfyBase() {
|
|
const config = useRuntimeConfig()
|
|
const host = (config.comfyHost || process.env.COMFY_HOST || '').replace(/\/$/, '')
|
|
if (!host) {
|
|
throw createError({ statusCode: 500, statusMessage: 'COMFY_HOST is not configured' })
|
|
}
|
|
return host
|
|
}
|
|
|
|
export function getComfyHost() {
|
|
return comfyBase()
|
|
}
|
|
|
|
export function comfyWsUrl(clientId: string) {
|
|
return `${comfyBase().replace(/^http/, 'ws')}/ws?clientId=${encodeURIComponent(clientId)}`
|
|
}
|
|
|
|
export async function comfyFetch(path: string, init?: RequestInit) {
|
|
const url = `${comfyBase()}${path}`
|
|
try {
|
|
return await fetch(url, init)
|
|
} catch (error) {
|
|
throw createError({
|
|
statusCode: 502,
|
|
statusMessage: `ComfyUI host unreachable (${comfyBase()})`,
|
|
data: { cause: error instanceof Error ? error.message : String(error) }
|
|
})
|
|
}
|
|
}
|
|
|
|
export async function uploadImage(file: { filename: string; data: Buffer; type?: string }) {
|
|
const body = new FormData()
|
|
const blob = new Blob([new Uint8Array(file.data)], { type: file.type || 'application/octet-stream' })
|
|
body.append('image', blob, file.filename)
|
|
body.append('overwrite', 'true')
|
|
body.append('type', 'input')
|
|
const res = await comfyFetch('/upload/image', { method: 'POST', body })
|
|
if (!res.ok) {
|
|
throw createError({ statusCode: 502, statusMessage: `Image upload failed (${res.status})` })
|
|
}
|
|
return (await res.json()) as { name: string; subfolder?: string; type?: string }
|
|
}
|
|
|
|
export async function queuePrompt(graph: unknown, clientId: string) {
|
|
const res = await comfyFetch('/prompt', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ prompt: graph, client_id: clientId })
|
|
})
|
|
const payload = await res.json().catch(() => ({}))
|
|
if (!res.ok) {
|
|
const message = (payload as { error?: { message?: string } }).error?.message
|
|
|| (payload as { node_errors?: unknown }).node_errors
|
|
|| `Queue failed (${res.status})`
|
|
throw createError({ statusCode: 502, statusMessage: String(message), data: payload })
|
|
}
|
|
return payload as { prompt_id: string; number?: number }
|
|
}
|
|
|
|
export async function interruptComfy() {
|
|
const res = await comfyFetch('/interrupt', { method: 'POST' })
|
|
return res.ok
|
|
}
|
|
|
|
export async function fetchHistory(promptId: string) {
|
|
const res = await comfyFetch(`/history/${encodeURIComponent(promptId)}`)
|
|
if (!res.ok) return null
|
|
return (await res.json()) as Record<string, unknown>
|
|
}
|
|
|
|
export function extractVideo(history: Record<string, unknown> | null, promptId: string) {
|
|
if (!history) return null
|
|
const entry = (history[promptId] || Object.values(history)[0]) as { outputs?: Record<string, Record<string, unknown>> } | undefined
|
|
const outputs = entry?.outputs || {}
|
|
for (const node of Object.values(outputs)) {
|
|
for (const key of ['videos', 'gifs', 'images', 'files']) {
|
|
const items = node[key]
|
|
if (!Array.isArray(items)) continue
|
|
const match = items.find((item: { filename?: string }) => String(item.filename || '').toLowerCase().endsWith('.mp4'))
|
|
|| items[0]
|
|
if (match?.filename) {
|
|
return {
|
|
filename: String(match.filename),
|
|
subfolder: String(match.subfolder || 'video'),
|
|
type: String(match.type || 'output')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
export async function probeComfy() {
|
|
try {
|
|
const res = await fetch(`${comfyBase()}/system_stats`, { signal: AbortSignal.timeout(2500) })
|
|
if (!res.ok) return { ok: false, host: comfyBase() }
|
|
const stats = await res.json()
|
|
return { ok: true, host: comfyBase(), stats }
|
|
} catch {
|
|
return { ok: false, host: comfyBase() }
|
|
}
|
|
}
|