|
|
|
@@ -28,7 +28,7 @@
|
|
|
|
|
<label v-if="form.mode==='refine'">Mask<select v-model="form.maskId" class="mt-1 w-full rounded-xl bg-zinc-800 p-2"><option value="">Required: white area to refine</option><option v-for="s in stills" :key="s.id" :value="s.id">{{ assetLabel(s) }}</option></select></label>
|
|
|
|
|
<label v-if="form.mode==='extend'">Previous clip<select :value="form.startClipId" @change="chooseClip(($event.target as HTMLSelectElement).value)" class="mt-1 w-full rounded-xl bg-zinc-800 p-2"><option value="">Choose a clip</option><option v-for="c in clips" :key="c.id" :value="c.id">{{ c.name || c.prompt?.slice(0,50) }}</option></select></label>
|
|
|
|
|
<label v-if="video">Optional end still<select v-model="form.endStillId" class="mt-1 w-full rounded-xl bg-zinc-800 p-2"><option value="">None</option><option v-for="s in stills" :key="s.id" :value="s.id">{{ assetLabel(s) }}</option></select></label>
|
|
|
|
|
<label class="text-sm">Add still to folder<input type="file" accept="image/*" class="mt-2 block text-xs" @change="upload"></label>
|
|
|
|
|
<label class="text-sm">Upload start still<input type="file" accept="image/*" :disabled="uploading || !form.folderId" class="mt-2 block text-xs" @change="upload($event,'source')"></label><label class="text-sm">Upload hero<input type="file" accept="image/*" :disabled="uploading || !form.folderId" class="mt-2 block text-xs" @change="upload($event,'hero')"></label><p v-if="uploading || uploadMessage" role="status" class="text-sm text-amber-200">{{ uploading ? 'Uploading image…' : uploadMessage }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<details v-if="video" class="rounded-xl border border-white/10 p-3"><summary>Add Guide stills</summary><div v-for="(g,i) in form.guides" :key="i" class="mt-2 flex gap-2"><select v-model="g.stillId" class="min-w-0 flex-1 bg-zinc-800 p-2"><option value="">Choose guide</option><option v-for="s in stills" :key="s.id" :value="s.id">{{ assetLabel(s) }}</option></select><input v-model.number="g.frame" type="number" min="1" aria-label="Guide frame" class="w-20 bg-zinc-800 p-2"><button @click="form.guides.splice(i,1)">Remove</button></div><button class="mt-3 text-amber-200" @click="form.guides.push({stillId:'',frame:24})">Add guide</button></details>
|
|
|
|
|
</section>
|
|
|
|
@@ -57,6 +57,8 @@
|
|
|
|
|
import {compilePrompt,sectionsFrom} from '~/shared/studio2/contracts.mjs'
|
|
|
|
|
import {aspectOptions,resolveSize,bindClip,restoreAsset,extendSelected} from '~/shared/studio2/input.mjs'
|
|
|
|
|
import {folderAssets,newestOutput,assetLabel} from '~/shared/studio2/viewer.mjs'
|
|
|
|
|
import {applyUploadedStill} from '~/shared/studio2/upload.mjs'
|
|
|
|
|
const uploading=ref(false),uploadMessage=ref('')
|
|
|
|
|
import {randomSeed} from '~/shared/studio2/seed.mjs'
|
|
|
|
|
const measured=reactive<Record<string,{width:number;height:number}>>({}),seenOutputs=new Set<string>()
|
|
|
|
|
const loraNames=ref<any>({image:[],video:[]}),now=ref(Date.now()),gpuLabel=ref('Checking…')
|
|
|
|
@@ -83,7 +85,15 @@ function focusJob(job:any){selectOutput(job.outputs?.at(-1)||null)}
|
|
|
|
|
async function loadLoras(){loraNames.value=await $fetch('/api/loras').catch(()=>({image:[],video:[]}))}
|
|
|
|
|
async function queue(){submitting.value=true;error.value='';try{if(form.settings.seedMode!=='locked')form.settings.seed=randomSeed();const queued=await $fetch<any>('/api/studio-2/jobs',{method:'POST',body:{...form,settings:{...form.settings,...resolvedSize.value},batch:batchText.value.split('\n').map(s=>s.trim()).filter(Boolean)}});form.settings.seed=queued.seed;form.settings.width=queued.width;form.settings.height=queued.height;await refresh()}catch(e:any){error.value=e.data?.statusMessage||e.message}finally{submitting.value=false}}
|
|
|
|
|
async function cancel(job:any){await $fetch('/api/studio-2/cancel',{method:'POST',body:{id:job.id}});await refresh()}
|
|
|
|
|
async function upload(event:Event){const file=(event.target as HTMLInputElement).files?.[0];if(!file)return;const body=new FormData();body.append('image',file);body.append('folderId',form.folderId);try{library.value=await $fetch('/api/library/stills',{method:'POST',body})}catch(e:any){error.value=e.data?.statusMessage||e.message}}
|
|
|
|
|
async function upload(event:Event,target:'source'|'hero'){
|
|
|
|
|
const input=event.target as HTMLInputElement,file=input.files?.[0];if(!file)return
|
|
|
|
|
uploading.value=true;uploadMessage.value='';error.value=''
|
|
|
|
|
const body=new FormData();body.append('image',file);body.append('folderId',form.folderId);body.append('hideInput',String(form.hideInput))
|
|
|
|
|
try{const response=await $fetch<any>('/api/studio-2/stills',{method:'POST',body});library.value=response;Object.assign(form,applyUploadedStill(form,response,target));measured[response.still.id]={width:response.still.width,height:response.still.height};uploadMessage.value=`Loaded ${response.still.name || file.name} as ${target==='hero'?'hero identity':'start still'}.`}
|
|
|
|
|
catch(e:any){error.value=e.data?.statusMessage||e.message}
|
|
|
|
|
finally{uploading.value=false;input.value=''}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function refresh(){
|
|
|
|
|
const [nextJobs,nextLibrary]=await Promise.all([$fetch<any[]>('/api/studio-2/jobs'),$fetch<any>('/api/library?light=1')])
|
|
|
|
|
jobs.value=nextJobs;library.value=nextLibrary
|
|
|
|
|