Add sidecar Qwen VL prompt recommend, fix queued shot bursts, and leave unused MiniMax identity slots empty.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-27 17:04:43 -05:00
co-authored by Cursor
parent 2bb30395d8
commit df5ac24622
23 changed files with 1068 additions and 25 deletions
+15 -7
View File
@@ -1,13 +1,14 @@
import { checkComfyHttp, fetchQueue } from '~/server/utils/comfyLifecycle'
import { comfyConfigured } from '~/server/utils/comfy'
import { imageComfyConfigured, getBeastImageHost } from '~/server/utils/imageComfy'
import { imageComfyConfigured, getBeastImageHost, getSidecarImageHost } from '~/server/utils/imageComfy'
import { checkImageComfyHttp, fetchQueueAt } from '~/server/utils/imageComfyLifecycle'
export default defineEventHandler(async () => {
const videoConfigured = comfyConfigured()
const imageConfigured = imageComfyConfigured()
const beastHost = getBeastImageHost()
const [comfy, queue, beast, beastQueue] = await Promise.all([
const sidecarHost = getSidecarImageHost()
const [comfy, queue, beast, beastQueue, sidecar, sidecarQueue] = await Promise.all([
videoConfigured
? checkComfyHttp().catch(() => ({ ok: false, host: '', stats: null }))
: Promise.resolve({ ok: false, host: '', stats: null }),
@@ -15,10 +16,15 @@ export default defineEventHandler(async () => {
beastHost
? checkImageComfyHttp(2500, beastHost).catch(() => ({ ok: false, host: beastHost, stats: null }))
: Promise.resolve({ ok: false, host: beastHost, stats: null }),
beastHost ? fetchQueueAt(beastHost) : Promise.resolve({ running: 0, pending: 0 })
beastHost ? fetchQueueAt(beastHost) : Promise.resolve({ running: 0, pending: 0 }),
sidecarHost
? checkImageComfyHttp(2500, sidecarHost).catch(() => ({ ok: false, host: sidecarHost, stats: null }))
: Promise.resolve({ ok: false, host: sidecarHost, stats: null }),
sidecarHost ? fetchQueueAt(sidecarHost) : Promise.resolve({ running: 0, pending: 0 })
])
const busy = Boolean(comfy.ok && queue.running > 0)
const beastBusy = Boolean(beast.ok && beastQueue.running > 0)
const sidecarBusy = Boolean(sidecar.ok && sidecarQueue.running > 0)
const imageOk = Boolean(beast.ok || comfy.ok)
const imageBusy = Boolean(imageConfigured && (beastBusy || busy))
const preferredHost = beastHost || comfy.host || ''
@@ -50,10 +56,12 @@ export default defineEventHandler(async () => {
},
sidecar: {
label: 'Sidecar',
configured: false,
ok: false,
busy: false,
host: ''
configured: Boolean(sidecarHost),
ok: Boolean(sidecar.ok),
busy: sidecarBusy,
host: sidecarHost,
queueRunning: sidecarQueue.running || 0,
queuePending: sidecarQueue.pending || 0
}
}
}
+14
View File
@@ -0,0 +1,14 @@
import { listPromptPresets } from '~/server/utils/promptPresets'
export default defineEventHandler(async (event) => {
const owner = libraryOwnerKey(event)
return {
presets: listPromptPresets(owner).map(item => ({
id: item.id,
name: item.name,
description: item.description || '',
builtin: Boolean(item.builtin),
systemPrompt: item.systemPrompt
}))
}
})
+15
View File
@@ -0,0 +1,15 @@
import { createPromptPreset, listPromptPresets } from '~/server/utils/promptPresets'
export default defineEventHandler(async (event) => {
const body = await readBody<{ name?: string; systemPrompt?: string; description?: string }>(event).catch(() => ({}))
const owner = libraryOwnerKey(event)
const created = await createPromptPreset(owner, {
name: body.name || '',
systemPrompt: body.systemPrompt || '',
description: body.description
})
return {
preset: created,
presets: listPromptPresets(owner)
}
})
+12
View File
@@ -0,0 +1,12 @@
import { deletePromptPreset, listPromptPresets } from '~/server/utils/promptPresets'
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
if (!id) throw createError({ statusCode: 400, statusMessage: 'Missing instruction set id' })
const owner = libraryOwnerKey(event)
await deletePromptPreset(owner, id)
return {
ok: true,
presets: listPromptPresets(owner)
}
})
+13
View File
@@ -0,0 +1,13 @@
import { updatePromptPreset, listPromptPresets } from '~/server/utils/promptPresets'
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
if (!id) throw createError({ statusCode: 400, statusMessage: 'Missing instruction set id' })
const body = await readBody<{ name?: string; systemPrompt?: string; description?: string }>(event).catch(() => ({}))
const owner = libraryOwnerKey(event)
const preset = await updatePromptPreset(owner, id, body)
return {
preset,
presets: listPromptPresets(owner)
}
})
+48
View File
@@ -0,0 +1,48 @@
import { promptComfyConfigured, createPromptJob, runPromptRecommend, failPromptJob, promptJobSnapshot } from '~/server/utils/promptComfy'
import { getPromptPreset } from '~/server/utils/promptPresets'
export default defineEventHandler(async (event) => {
if (!promptComfyConfigured()) {
throw createError({
statusCode: 503,
statusMessage: 'Sidecar Qwen VL is not configured. Set IMAGE_COMFY_HOST.'
})
}
const form = await readMultipartFormData(event).catch(() => null)
if (!form?.length) {
throw createError({ statusCode: 400, statusMessage: 'Expected an image and a draft prompt' })
}
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: 'Load a still before recommending a prompt' })
}
const draftPrompt = (fields.prompt || '').trim()
if (!draftPrompt) {
throw createError({ statusCode: 400, statusMessage: 'Type a draft prompt to enhance' })
}
const owner = libraryOwnerKey(event)
const preset = getPromptPreset(owner, fields.presetId || 'enhance')
const mode = ['edit', 'shot-script', 'video'].includes(fields.mode) ? fields.mode : 'video'
const job = createPromptJob()
void runPromptRecommend(job, {
image,
draftPrompt,
systemPrompt: preset.systemPrompt,
mode
}).catch(error => failPromptJob(job, error))
return promptJobSnapshot(job)
})
+10
View File
@@ -0,0 +1,10 @@
import { getPromptJob, promptJobSnapshot } from '~/server/utils/promptComfy'
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
const job = id ? getPromptJob(id) : undefined
if (!job) {
throw createError({ statusCode: 404, statusMessage: 'Recommend job not found' })
}
return promptJobSnapshot(job)
})
+10 -3
View File
@@ -1,10 +1,17 @@
import { startQueueBurst } from '~/server/utils/videoChain'
function parseCount(raw: unknown): number | 'all' {
if (raw === 'all' || raw === 'remaining') return 'all'
const n = typeof raw === 'number' ? raw : Number(raw)
if (!Number.isFinite(n)) return 1
return Math.max(1, Math.min(50, Math.floor(n)))
}
export default defineEventHandler(async (event) => {
const { owner } = assertLibraryOwner(event)
const id = String(getRouterParam(event, 'id') || '')
const body = await readBody<{ count?: number | 'all' }>().catch(() => ({}))
const raw = body?.count
const count = raw === 'all' || raw === 'remaining' ? 'all' as const : Math.max(1, Math.min(50, Number(raw) || 1))
const body = await readBody<{ count?: number | 'all' | 'remaining' }>(event).catch(() => ({} as { count?: number | 'all' }))
const query = getQuery(event)
const count = parseCount(body?.count ?? query.count)
return startQueueBurst(owner, id, count)
})
+6
View File
@@ -0,0 +1,6 @@
import { clearShotQueues } from '~/server/utils/shotQueue'
export default defineEventHandler(async (event) => {
const { owner } = assertLibraryOwner(event)
return clearShotQueues(owner)
})