Add image-to-text Describe on the bench via exclusive llama.cpp caption jobs.
This commit is contained in:
@@ -234,6 +234,18 @@
|
||||
>
|
||||
<button type="button" @click="applyUseImage()">Use image</button>
|
||||
<button type="button" @click="applyUsePrompt()">Use prompt</button>
|
||||
<button
|
||||
v-if="selected.kind==='still'"
|
||||
type="button"
|
||||
:disabled="submitting"
|
||||
@click="describeSelected"
|
||||
>Describe</button>
|
||||
<label v-if="selected.kind==='still'" class="inline-flex items-center gap-1 text-[var(--type-sm)] text-[var(--muted)]">
|
||||
<span class="sr-only">Caption style</span>
|
||||
<select v-model="captionStyle" class="field-control !w-auto !py-1 !text-[var(--type-sm)]" aria-label="Caption style">
|
||||
<option v-for="style in captionStyles" :key="style" :value="style">{{ style }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="stage-split">
|
||||
<button
|
||||
type="button"
|
||||
@@ -382,6 +394,11 @@
|
||||
/>
|
||||
<span v-if="card.elapsed != null" class="job-elapsed">{{ card.elapsed }}</span>
|
||||
</div>
|
||||
<p v-if="card.resultText" class="mt-2 whitespace-pre-wrap text-[var(--type-sm)]">{{ card.resultText }}</p>
|
||||
<div v-if="card.resultText" class="mt-2 flex flex-wrap gap-2">
|
||||
<button type="button" class="text-[var(--type-sm)] text-[var(--amber)]" @click.stop="copyCaption(card.resultText)">Copy</button>
|
||||
<button type="button" class="text-[var(--type-sm)] text-[var(--amber)]" @click.stop="useCaptionAsPrompt(card.resultText)">Use as prompt</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!queueCards.length" class="px-1 text-[var(--type-sm)] text-[var(--muted)]">No active jobs</p>
|
||||
</div>
|
||||
@@ -460,6 +477,7 @@ import {aspectOptions,resolveSize,bindClip,restoreAll,useAsSource,hydrateStudio2
|
||||
import {folderAssets,newestOutput,assetLabel,reconcileSelection,previewHidden} from '~/shared/studio2/viewer.mjs'
|
||||
import {queueJobs,jobStageLabel,jobProgressView,elapsedSeconds} from '~/shared/studio2/queue-view.mjs'
|
||||
import {gpuStatus} from '~/shared/studio2/status.mjs'
|
||||
import {CAPTION_STYLES} from '~/shared/caption.mjs'
|
||||
import {applyUploadedStill} from '~/shared/studio2/upload.mjs'
|
||||
import {defaultNegatives,parseBatch} from '~/shared/studio2/styles.mjs'
|
||||
import {randomSeed} from '~/shared/studio2/seed.mjs'
|
||||
@@ -614,6 +632,9 @@ const promptArea=ref<HTMLTextAreaElement|null>(null)
|
||||
const promptCaret=ref({start:0,end:0})
|
||||
const music=reactive({tags:DEFAULT_MUSIC_TAGS,lyrics:DEFAULT_MUSIC_LYRICS,duration:60,seed:'',name:''})
|
||||
const tagsPlaceholder=DEFAULT_MUSIC_TAGS,lyricsPlaceholder=DEFAULT_MUSIC_LYRICS
|
||||
const captionStyles=CAPTION_STYLES
|
||||
const captionStyle=ref('descriptive')
|
||||
const liveCaption=ref<Record<string,any>>({})
|
||||
let loraMemory=createLoraMemory(xaigen)
|
||||
const currentLoraUse=computed(()=>loraUse(form))
|
||||
const pickerLoras=computed(()=>!xaigen && currentLoraUse.value==='ltx'?[]:filterLorasForUse(filterLorasForStudio([...loraNames.value.image,...loraNames.value.video],xaigen),currentLoraUse.value))
|
||||
@@ -725,7 +746,10 @@ const benchJobs=computed(()=>queueJobs(folderJobs.value))
|
||||
const musicJobs=computed(()=>studioQueue.value
|
||||
.filter((j:any)=>j.kind==='music' && j.folderId===form.folderId && !['complete','done'].includes(j.status))
|
||||
.map((j:any)=>({...j,live:j.liveJobId?liveMusic.value[j.liveJobId]:null})))
|
||||
const gpu=computed(()=>gpuStatus(jobs.value,healthState.value,upscales.value,musicJobs.value))
|
||||
const captionJobs=computed(()=>studioQueue.value
|
||||
.filter((j:any)=>j.kind==='caption' && j.folderId===form.folderId)
|
||||
.map((j:any)=>({...j,live:j.liveJobId?liveCaption.value[j.liveJobId]:null})))
|
||||
const gpu=computed(()=>gpuStatus(jobs.value,healthState.value,upscales.value,musicJobs.value,captionJobs.value))
|
||||
const gpuChipLabel=computed(()=>{
|
||||
if(!healthState.value) return 'GPU ?'
|
||||
const raw=String(healthState.value?.comfy?.gpu || healthState.value?.gpuName || healthState.value?.comfy?.device || '').trim()
|
||||
@@ -792,6 +816,22 @@ const queueCards=computed(()=>{
|
||||
cancelable:!['complete','done','error','cancelled'].includes(job.status)
|
||||
})
|
||||
}
|
||||
for(const job of captionJobs.value){
|
||||
const live=job.live
|
||||
const text=live?.resultText || job.resultText || ''
|
||||
const state=live?.status || job.status
|
||||
cards.push({
|
||||
id:job.id,
|
||||
job,
|
||||
code:'#'+String(job.id).replace(/\D/g,'').slice(-3).padStart(3,'0'),
|
||||
title:job.name || `Describe · ${job.captionStyle || job.payload?.captionStyle || 'caption'}`,
|
||||
meta:`Caption · ${job.captionStyle || job.payload?.captionStyle || 'descriptive'}`,
|
||||
state,
|
||||
statusLabel:live?.message || state,
|
||||
resultText:text,
|
||||
cancelable:!['complete','done','error','cancelled'].includes(job.status) && !['complete','error','cancelled'].includes(String(state))
|
||||
})
|
||||
}
|
||||
for(const job of upscales.value.filter((j:any)=>j.folderId===form.folderId && ['queued','running'].includes(j.status))){
|
||||
cards.push({
|
||||
id:job.id,
|
||||
@@ -807,7 +847,7 @@ const queueCards=computed(()=>{
|
||||
return cards
|
||||
})
|
||||
const queueCount=computed(()=>queueCards.value.length)
|
||||
const canClearFinished=computed(()=>folderJobs.value.some(j=>['complete','failed','cancelled'].includes(j.state)) || musicJobs.value.some(j=>['complete','done','error','cancelled'].includes(j.status)))
|
||||
const canClearFinished=computed(()=>folderJobs.value.some(j=>['complete','failed','cancelled'].includes(j.state)) || musicJobs.value.some(j=>['complete','done','error','cancelled'].includes(j.status)) || captionJobs.value.some(j=>['complete','done','error','cancelled'].includes(j.status) || ['complete','error','cancelled'].includes(j.live?.status)))
|
||||
const transportLabel=computed(()=>{
|
||||
if(selected.value?.kind==='clip') return `${formatTime(videoTime.value)} / ${formatTime(videoDuration.value)}`
|
||||
if(selected.value?.kind==='track') return 'Audio'
|
||||
@@ -1000,6 +1040,23 @@ function applyUseImage(item?:any){
|
||||
uploadMessage.value=`Added ${assetLabel(asset)} to Photos.`
|
||||
saveBenchDraft(true)
|
||||
}
|
||||
async function copyCaption(text:string){
|
||||
try{await navigator.clipboard.writeText(text)}catch{error.value='Could not copy caption.'}
|
||||
}
|
||||
function useCaptionAsPrompt(text:string){
|
||||
const current=String(form.promptSections.action||'').trim()
|
||||
form.promptSections.action=current?`${current}\n\n${text}`:text
|
||||
}
|
||||
async function describeSelected(){
|
||||
const id=selected.value?.kind==='still'?selected.value.id:''
|
||||
if(!id){error.value='Select a still to describe.';return}
|
||||
submitting.value=true;error.value=''
|
||||
try{
|
||||
await $fetch('/api/generate/caption',{method:'POST',body:{folderId:form.folderId,stillId:id,sourcePath:id,captionStyle:captionStyle.value}})
|
||||
await refreshJobs()
|
||||
}catch(e:any){error.value=e.data?.statusMessage||e.message}
|
||||
finally{submitting.value=false}
|
||||
}
|
||||
function applyUsePrompt(item?:any){
|
||||
const asset=resolveActionAsset(item)
|
||||
if(!asset){
|
||||
@@ -1369,6 +1426,14 @@ async function refreshJobs(){
|
||||
const nextLive:Record<string,any>={}
|
||||
for(const [id,snap] of liveEntries){if(snap)nextLive[id]=snap}
|
||||
liveMusic.value=nextLive
|
||||
const captionRows=studioQueue.value.filter((j:any)=>j.kind==='caption' && j.liveJobId)
|
||||
const captionEntries=await Promise.all(captionRows.map(async(j:any)=>{
|
||||
try{return [j.liveJobId,await $fetch<any>(`/api/generate/${j.liveJobId}`)] as const}
|
||||
catch{return [j.liveJobId,null] as const}
|
||||
}))
|
||||
const nextCaption:Record<string,any>={}
|
||||
for(const [id,snap] of captionEntries){if(snap)nextCaption[id]=snap}
|
||||
liveCaption.value=nextCaption
|
||||
ingestNewOutputs(nextJobs)
|
||||
}
|
||||
/** Merge freshly completed outputs into the local catalog — never refetch the whole library. */
|
||||
@@ -1559,7 +1624,14 @@ async function queue(){
|
||||
logTiming('done')
|
||||
}
|
||||
}
|
||||
async function cancel(job:any){await $fetch('/api/studio-2/cancel',{method:'POST',body:{id:job.id}});await refreshJobs()}
|
||||
async function cancel(job:any){
|
||||
if(job?.kind==='music' || job?.kind==='caption'){
|
||||
await $fetch(`/api/studio-queue/${job.id}`,{method:'DELETE'})
|
||||
}else{
|
||||
await $fetch('/api/studio-2/cancel',{method:'POST',body:{id:job.id}})
|
||||
}
|
||||
await refreshJobs()
|
||||
}
|
||||
|
||||
async function loadLibraryOnce(){
|
||||
// Current folder page only — never the yearly Inbox JSON on /
|
||||
|
||||
Reference in New Issue
Block a user