Add Studio 2 caption jobs via Qwen2.5-VL GGUF on the host agent.

Exclusive llama-server load/unload on the 5080, queued Describe UI with resultText, Copy, and Use as prompt.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-09-30 22:06:10 -05:00
co-authored by Cursor
parent 1f10082087
commit 1b311d6529
14 changed files with 1285 additions and 41 deletions
+66
View File
@@ -0,0 +1,66 @@
export const CAPTION_STYLES = ['descriptive', 'klein_prompt', 'delta', 'tags']
export const DELTA_LOCKED_LOOK =
'short light-brown bob, pink tank, light blue shorts, open green Slytherin robe with red-gold trim, barefoot'
export function captionStylePrompt(style) {
switch (style) {
case 'klein_prompt':
return 'Write one self-contained image generation prompt for Flux Klein. Include subject, pose, clothing, setting, lighting, and camera. No preamble or quotes.'
case 'delta':
return `The character's locked baseline look is: ${DELTA_LOCKED_LOOK}. Describe ONLY what changed from that baseline in this image. Do not restate the locked look.`
case 'tags':
return 'List comma-separated descriptive tags for this image. Tags only, no sentences.'
case 'descriptive':
default:
return 'Describe this image in clear, detailed prose. Cover subject, appearance, clothing, pose, setting, and lighting.'
}
}
export function validateCaptionRequest(raw) {
const fail = message => { throw Object.assign(new Error(message), { statusCode: 400 }) }
const body = raw || {}
const style = String(body.captionStyle || body.style || 'descriptive')
if (!CAPTION_STYLES.includes(style)) fail('Unknown caption style.')
const folderId = String(body.folderId || '')
if (!folderId) fail('Choose a folder.')
const stillId = String(body.stillId || body.imageAId || '')
const sourcePath = String(body.sourcePath || '')
if (!stillId && !sourcePath) fail('Choose a still to describe.')
return {
mode: 'caption',
folderId,
stillId,
sourcePath,
captionStyle: style,
promptSections: { action: `Describe · ${style}` },
compiledPrompt: captionStylePrompt(style),
engine: 'caption',
imageStyles: { positive: [], negative: [] },
hideOutput: false,
lockFace: false,
lockOutfit: false,
hideInput: false,
identityStillId: '',
imageAId: stillId,
imageBId: '',
maskId: '',
startClipId: '',
endStillId: '',
guides: [],
batch: [],
settings: {
aspect: 'auto',
fps: 24,
width: 0,
height: 0,
steps: 1,
cfg: 0,
seedMode: 'random',
seed: 0,
duration: 0,
turbo: true,
loraStack: []
}
}
}
+4 -1
View File
@@ -1,4 +1,4 @@
export function gpuStatus(jobs, health, upscales=[], musicJobs=[]) {
export function gpuStatus(jobs, health, upscales=[], musicJobs=[], captionJobs=[]) {
if (!health) return {label:'Status unavailable',tone:'unknown',detail:'Checking GPU connection…'}
const upscale=upscales.find(j=>j.status==='running')
if(upscale)return {label:'Upscaling',tone:'busy',detail:upscale.name || 'Video upscale'}
@@ -7,6 +7,9 @@ export function gpuStatus(jobs, health, upscales=[], musicJobs=[]) {
const stage=music.live?.message || music.live?.stage || music.waitReason || 'YuE2'
return {label:music.status==='waiting'?'Queued':'Generating',tone:'busy',detail:String(stage)}
}
const caption=captionJobs.find(j=>['waiting','waking','submitting','rendering','saving'].includes(j.state) || j.state==='queued' || j.state==='held')
if(caption)return {label:caption.state==='waiting'?'Queued':'Describing',tone:'busy',detail:caption.request?.captionStyle || 'Caption'}
if(health.caption?.busy)return {label:'Describing',tone:'busy',detail:'Caption model is using the GPU'}
if(health.yue2?.busy)return {label:'Generating',tone:'busy',detail:'YuE2 is using the GPU'}
const job=jobs.find(j=>['waking','submitting','rendering','saving'].includes(j.state))
if(job)return {label:{waking:'Waking up',submitting:'Submitting',rendering:'Generating',saving:'Saving'}[job.state],tone:'busy',detail:job.request?.promptSections?.action || 'Generation in progress'}