Start Sidecar Qwen VL from Recommend when the prompt enhancer is offline.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+5
-3
@@ -1848,14 +1848,13 @@ const recommendDisabled = computed(() => {
|
|||||||
if (recommendBusy.value) return true
|
if (recommendBusy.value) return true
|
||||||
if (!file.value || !prompt.value.trim()) return true
|
if (!file.value || !prompt.value.trim()) return true
|
||||||
if (!imageSidecarConfigured.value) return true
|
if (!imageSidecarConfigured.value) return true
|
||||||
if (!imageSidecarOk.value) return true
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
const recommendHint = computed(() => {
|
const recommendHint = computed(() => {
|
||||||
if (!imageSidecarConfigured.value) return 'Sidecar Qwen VL is not configured. Set IMAGE_COMFY_HOST.'
|
if (!imageSidecarConfigured.value) return 'Sidecar Qwen VL is not configured. Set IMAGE_COMFY_HOST.'
|
||||||
if (!imageSidecarOk.value) return 'Sidecar Qwen VL is offline.'
|
|
||||||
if (!file.value) return 'Load a still first.'
|
if (!file.value) return 'Load a still first.'
|
||||||
if (!prompt.value.trim()) return 'Type a draft prompt to enhance.'
|
if (!prompt.value.trim()) return 'Type a draft prompt to enhance.'
|
||||||
|
if (!imageSidecarOk.value) return 'Sidecar is offline. Recommend will start Qwen VL on 192.168.77.101.'
|
||||||
if (imageSidecarBusy.value) return 'Sidecar is busy — this may wait in its queue.'
|
if (imageSidecarBusy.value) return 'Sidecar is busy — this may wait in its queue.'
|
||||||
return 'Send this still and draft to Sidecar Qwen VL.'
|
return 'Send this still and draft to Sidecar Qwen VL.'
|
||||||
})
|
})
|
||||||
@@ -2507,7 +2506,9 @@ async function recommendPrompt() {
|
|||||||
recommendError.value = ''
|
recommendError.value = ''
|
||||||
recommendText.value = ''
|
recommendText.value = ''
|
||||||
recommendApplied.value = false
|
recommendApplied.value = false
|
||||||
recommendMessage.value = 'Uploading still to Sidecar Qwen VL…'
|
recommendMessage.value = imageSidecarOk.value
|
||||||
|
? 'Uploading still to Sidecar Qwen VL…'
|
||||||
|
: 'Sidecar is offline. Starting Qwen VL…'
|
||||||
const body = new FormData()
|
const body = new FormData()
|
||||||
body.append('image', file.value)
|
body.append('image', file.value)
|
||||||
body.append('prompt', prompt.value)
|
body.append('prompt', prompt.value)
|
||||||
@@ -2542,6 +2543,7 @@ async function recommendPrompt() {
|
|||||||
} finally {
|
} finally {
|
||||||
recommendBusy.value = false
|
recommendBusy.value = false
|
||||||
if (recommendJobId.value) recommendJobId.value = ''
|
if (recommendJobId.value) recommendJobId.value = ''
|
||||||
|
void pollComfyHealth()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getImageComfyHost, setImageComfyHostOverride, imageComfyConfigured, getBeastImageHost, withImageComfyHost, type ImageEditBox } from '~/server/utils/imageComfy'
|
import { getImageComfyHost, setImageComfyHostOverride, imageComfyConfigured, getBeastImageHost, getSidecarImageHost, withImageComfyHost, type ImageEditBox } from '~/server/utils/imageComfy'
|
||||||
|
|
||||||
export type ImageComfyState = 'online' | 'booting' | 'busy' | 'starting' | 'offline'
|
export type ImageComfyState = 'online' | 'booting' | 'busy' | 'starting' | 'offline'
|
||||||
|
|
||||||
@@ -229,6 +229,121 @@ export function ensureImageComfyReady(onStatus: StatusFn = () => undefined) {
|
|||||||
return run
|
return run
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sidecarGate: Promise<unknown> = Promise.resolve()
|
||||||
|
let sidecarLastStartAt = 0
|
||||||
|
|
||||||
|
function sidecarControlSettings() {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
return {
|
||||||
|
host: getSidecarImageHost(),
|
||||||
|
controlUrl: String(config.imageComfyControlUrl || process.env.IMAGE_COMFY_CONTROL_URL || '').replace(/\/$/, ''),
|
||||||
|
controlToken: String(config.imageComfyControlToken || process.env.IMAGE_COMFY_CONTROL_TOKEN || ''),
|
||||||
|
healthTimeoutMs: 2500,
|
||||||
|
startTimeoutMs: Number(process.env.IMAGE_COMFY_START_TIMEOUT_MS || 180_000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sidecarControlRequest(path: string, method = 'GET') {
|
||||||
|
const { controlUrl, controlToken } = sidecarControlSettings()
|
||||||
|
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
|
||||||
|
started?: boolean
|
||||||
|
already?: boolean
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startSidecarComfy() {
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - sidecarLastStartAt < 15_000) {
|
||||||
|
log('sidecar-start-skipped', { reason: 'recent-start' })
|
||||||
|
return { started: false, reason: 'recent-start' as const }
|
||||||
|
}
|
||||||
|
sidecarLastStartAt = now
|
||||||
|
const remote = await sidecarControlRequest('/start', 'POST')
|
||||||
|
if (remote?.ok || remote?.started) {
|
||||||
|
log('sidecar-start-remote', {})
|
||||||
|
return { started: true, reason: 'remote' as const }
|
||||||
|
}
|
||||||
|
return { started: false, reason: 'no-launcher' as const }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureSidecarUnlocked(onStatus: StatusFn) {
|
||||||
|
const cfg = sidecarControlSettings()
|
||||||
|
const host = cfg.host
|
||||||
|
if (!host) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 503,
|
||||||
|
statusMessage: 'Sidecar Qwen VL is not configured. Set IMAGE_COMFY_HOST or IMAGE_COMFY_FALLBACK_HOST.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onStatus({ state: 'offline', message: 'Checking Sidecar Qwen VL…', host, box: 'sidecar' })
|
||||||
|
const health = await checkImageComfyHttp(cfg.healthTimeoutMs, host)
|
||||||
|
if (health.ok) {
|
||||||
|
onStatus({ state: 'online', message: 'Sidecar Qwen VL online', host, box: 'sidecar' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onStatus({
|
||||||
|
state: 'starting',
|
||||||
|
message: 'Sidecar is offline. Starting Qwen VL on 192.168.77.101…',
|
||||||
|
host,
|
||||||
|
box: 'sidecar'
|
||||||
|
})
|
||||||
|
const started = await startSidecarComfy()
|
||||||
|
if (!started.started && started.reason === 'no-launcher') {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 503,
|
||||||
|
statusMessage: 'Sidecar is offline and the host agent did not start it. Confirm the agent on 192.168.77.101:8199 is running.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const deadline = Date.now() + cfg.startTimeoutMs
|
||||||
|
let attempt = 0
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
attempt += 1
|
||||||
|
onStatus({
|
||||||
|
state: 'starting',
|
||||||
|
message: `Starting Sidecar Qwen VL… waiting for Comfy (${attempt})`,
|
||||||
|
host,
|
||||||
|
box: 'sidecar'
|
||||||
|
})
|
||||||
|
const next = await checkImageComfyHttp(cfg.healthTimeoutMs, host)
|
||||||
|
if (next.ok) {
|
||||||
|
onStatus({ state: 'online', message: 'Sidecar Qwen VL online', host, box: 'sidecar', processRunning: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await sleep(2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw createError({
|
||||||
|
statusCode: 503,
|
||||||
|
statusMessage: 'Started Sidecar Qwen VL but it never answered. The 101 box can take a couple of minutes to load after a cold boot.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureSidecarReady(onStatus: StatusFn = () => undefined) {
|
||||||
|
const run = sidecarGate.then(() => ensureSidecarUnlocked(onStatus))
|
||||||
|
sidecarGate = run.then(() => undefined, () => undefined)
|
||||||
|
return run
|
||||||
|
}
|
||||||
|
|
||||||
export async function pickImageEditHost(onStatus: StatusFn = () => undefined): Promise<ImageEditTarget> {
|
export async function pickImageEditHost(onStatus: StatusFn = () => undefined): Promise<ImageEditTarget> {
|
||||||
const beast = getBeastImageHost()
|
const beast = getBeastImageHost()
|
||||||
if (!beast) {
|
if (!beast) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getSidecarImageHost } from '~/server/utils/imageComfy'
|
import { getSidecarImageHost } from '~/server/utils/imageComfy'
|
||||||
|
import { ensureSidecarReady } from '~/server/utils/imageComfyLifecycle'
|
||||||
import { buildVisionPromptWorkflow } from '~/server/utils/promptWorkflow'
|
import { buildVisionPromptWorkflow } from '~/server/utils/promptWorkflow'
|
||||||
import { normalizeShotScript } from '~/utils/parseRecommend'
|
import { normalizeShotScript } from '~/utils/parseRecommend'
|
||||||
|
|
||||||
@@ -220,6 +221,10 @@ export async function runPromptRecommend(job: PromptJob, params: {
|
|||||||
mode: string
|
mode: string
|
||||||
}) {
|
}) {
|
||||||
job.status = 'running'
|
job.status = 'running'
|
||||||
|
job.message = 'Checking Sidecar Qwen VL…'
|
||||||
|
await ensureSidecarReady((status) => {
|
||||||
|
job.message = status.message
|
||||||
|
})
|
||||||
job.message = 'Uploading still to Sidecar Qwen VL…'
|
job.message = 'Uploading still to Sidecar Qwen VL…'
|
||||||
const imageName = await uploadPromptImage(params.image, job.id)
|
const imageName = await uploadPromptImage(params.image, job.id)
|
||||||
job.message = 'Qwen VL is reading the still…'
|
job.message = 'Qwen VL is reading the still…'
|
||||||
|
|||||||
Reference in New Issue
Block a user