Compare commits
3
Commits
c4e74628ee
...
322e5a276e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
322e5a276e | ||
|
|
5f6a7aa9bc | ||
|
|
930bbf7c7b |
@@ -7,7 +7,7 @@
|
|||||||
:aria-label="asset ? 'Replace photo' : 'Choose a photo'"
|
:aria-label="asset ? 'Replace photo' : 'Choose a photo'"
|
||||||
@click="$emit('pick')"
|
@click="$emit('pick')"
|
||||||
>
|
>
|
||||||
<span v-if="asset && hidden" class="p-2 text-center text-[var(--type-sm)] text-[var(--muted)]">Still loaded · preview hidden</span>
|
<span v-if="asset && hidden" class="p-2 text-center text-[var(--type-sm)] text-[var(--muted)]">Photo loaded</span>
|
||||||
<img v-else-if="asset" :src="thumbSrc" alt="">
|
<img v-else-if="asset" :src="thumbSrc" alt="">
|
||||||
<span v-else class="inline-flex flex-col items-center gap-1 px-2 text-center">
|
<span v-else class="inline-flex flex-col items-center gap-1 px-2 text-center">
|
||||||
<span>Choose a photo</span>
|
<span>Choose a photo</span>
|
||||||
|
|||||||
+253
-45
@@ -89,24 +89,13 @@
|
|||||||
>{{ hideInputs ? 'Show inputs' : 'Hide inputs' }}</button>
|
>{{ hideInputs ? 'Show inputs' : 'Hide inputs' }}</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="photosPlanState.handoffReadonly" class="text-[var(--type-sm)] text-[var(--muted)]">Continues from last frame</p>
|
<p v-if="photosPlanState.handoffReadonly" class="text-[var(--type-sm)] text-[var(--muted)]">Continues from last frame</p>
|
||||||
<template v-else-if="hideInputs">
|
|
||||||
<p class="photos-collapsed">
|
|
||||||
<span>{{ photoFilledCount }} photo{{ photoFilledCount===1 ? '' : 's' }}</span>
|
|
||||||
<button
|
|
||||||
v-if="form.photos.length < photosPlanState.max"
|
|
||||||
type="button"
|
|
||||||
class="photos-add-link"
|
|
||||||
@click="hideInputs=false; addPhoto()"
|
|
||||||
>+ Add photo</button>
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<p v-if="!form.photos.length && photosPlanState.emptyHint" class="text-[var(--type-sm)] text-[var(--muted)]">{{ photosPlanState.emptyHint }}</p>
|
<p v-if="!form.photos.length && photosPlanState.emptyHint" class="text-[var(--type-sm)] text-[var(--muted)]">{{ photosPlanState.emptyHint }}</p>
|
||||||
<Studio2AssetSlot
|
<Studio2AssetSlot
|
||||||
v-for="(card, index) in form.photos"
|
v-for="(card, index) in form.photos"
|
||||||
:key="'photo-'+index+'-'+card.stillId"
|
:key="'photo-'+index+'-'+card.stillId"
|
||||||
:asset="stillById(card.stillId)"
|
:asset="stillById(card.stillId)"
|
||||||
:hidden="false"
|
:hidden="hideInputs"
|
||||||
:role="card.role"
|
:role="card.role"
|
||||||
:role-options="photoRoleOptions"
|
:role-options="photoRoleOptions"
|
||||||
@pick="picker='photo:'+index"
|
@pick="picker='photo:'+index"
|
||||||
@@ -171,6 +160,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Studio2StyleChips v-if="!video && form.mode!=='music'" v-model="form.imageStyles" class="mt-1" />
|
||||||
|
<Studio2PromptControls v-if="video" v-model="form.promptSections" class="mt-2" />
|
||||||
|
|
||||||
<div v-if="['flux','krea','qwen21'].includes(form.engine)">
|
<div v-if="['flux','krea','qwen21'].includes(form.engine)">
|
||||||
<span class="field-label">LoRA</span>
|
<span class="field-label">LoRA</span>
|
||||||
<LoraSelect
|
<LoraSelect
|
||||||
@@ -253,9 +245,12 @@
|
|||||||
class="stage-actions"
|
class="stage-actions"
|
||||||
aria-label="Selected still actions"
|
aria-label="Selected still actions"
|
||||||
>
|
>
|
||||||
<button type="button" @click.stop="applyUseAsSource">Use as photo</button>
|
<button type="button" @click.stop="applyUseImage">Use image</button>
|
||||||
|
<button type="button" @click.stop="applyUsePrompt">Use prompt</button>
|
||||||
<button type="button" @click.stop="copySelectedPrompt">Copy prompt</button>
|
<button type="button" @click.stop="copySelectedPrompt">Copy prompt</button>
|
||||||
<button type="button" @click.stop="loadSelectedJob">Load</button>
|
<button type="button" @click.stop="copySelectedRawPrompt">Copy raw prompt</button>
|
||||||
|
<button type="button" @click.stop="loadInputUsed">Load input</button>
|
||||||
|
<button type="button" @click.stop="loadInputRaw">Load input raw</button>
|
||||||
<button type="button" class="stage-action-danger" @click.stop="deleteOutput">Delete</button>
|
<button type="button" class="stage-action-danger" @click.stop="deleteOutput">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -318,9 +313,12 @@
|
|||||||
:preview-hidden="!eyeOpen"
|
:preview-hidden="!eyeOpen"
|
||||||
@close="libraryOpen=false"
|
@close="libraryOpen=false"
|
||||||
@select="selectOutput"
|
@select="selectOutput"
|
||||||
@use-as-photo="onLibraryUseAsPhoto"
|
@use-image="onLibraryUseImage"
|
||||||
|
@use-prompt="onLibraryUsePrompt"
|
||||||
@copy-prompt="onLibraryCopyPrompt"
|
@copy-prompt="onLibraryCopyPrompt"
|
||||||
@load="onLibraryLoad"
|
@copy-raw-prompt="onLibraryCopyRawPrompt"
|
||||||
|
@load-input="onLibraryLoadInput"
|
||||||
|
@load-input-raw="onLibraryLoadInputRaw"
|
||||||
@delete="onLibraryDelete"
|
@delete="onLibraryDelete"
|
||||||
@toggle-preview="toggleReveal()"
|
@toggle-preview="toggleReveal()"
|
||||||
/>
|
/>
|
||||||
@@ -451,6 +449,8 @@ import {LOAD_GENERATION_PAYLOAD_KEY} from '~/utils/queuedJob'
|
|||||||
const NEG_STORAGE_KEY='studio2-negative-chips'
|
const NEG_STORAGE_KEY='studio2-negative-chips'
|
||||||
const ENHANCE_STORAGE_KEY='studio2-qwen21-enhance-prompt'
|
const ENHANCE_STORAGE_KEY='studio2-qwen21-enhance-prompt'
|
||||||
const TURBO_STORAGE_KEY='studio2-qwen21-turbo'
|
const TURBO_STORAGE_KEY='studio2-qwen21-turbo'
|
||||||
|
const BENCH_DRAFT_KEY='aigen:bench-draft'
|
||||||
|
let draftTimer:ReturnType<typeof setTimeout>|null=null
|
||||||
function readStoredNegatives(){
|
function readStoredNegatives(){
|
||||||
if(!import.meta.client)return [...defaultNegatives]
|
if(!import.meta.client)return [...defaultNegatives]
|
||||||
try{
|
try{
|
||||||
@@ -480,6 +480,84 @@ function writeStoredTurbo(on:boolean){
|
|||||||
if(!import.meta.client)return
|
if(!import.meta.client)return
|
||||||
try{localStorage.setItem(TURBO_STORAGE_KEY,on?'1':'0')}catch{/* ignore */}
|
try{localStorage.setItem(TURBO_STORAGE_KEY,on?'1':'0')}catch{/* ignore */}
|
||||||
}
|
}
|
||||||
|
function benchDraftPayload(){
|
||||||
|
return {
|
||||||
|
v:1,
|
||||||
|
mode:form.mode,
|
||||||
|
engine:form.engine,
|
||||||
|
folderId:form.folderId,
|
||||||
|
enhancePrompt:!!form.enhancePrompt,
|
||||||
|
turbo:!!form.turbo,
|
||||||
|
lockFace:!!form.lockFace,
|
||||||
|
lockOutfit:!!form.lockOutfit,
|
||||||
|
promptSections:structuredClone(form.promptSections||{}),
|
||||||
|
photos:(form.photos||[]).filter((p:any)=>p?.stillId).map((p:any)=>({stillId:p.stillId,role:p.role})),
|
||||||
|
imageStyles:structuredClone(form.imageStyles||{positive:[],negative:[]}),
|
||||||
|
settings:{
|
||||||
|
aspect:form.settings?.aspect||'auto',
|
||||||
|
seed:form.settings?.seed ?? null,
|
||||||
|
seedMode:form.settings?.seedMode||'random',
|
||||||
|
steps:form.settings?.steps,
|
||||||
|
cfg:form.settings?.cfg,
|
||||||
|
duration:form.settings?.duration,
|
||||||
|
fps:form.settings?.fps,
|
||||||
|
loraStack:structuredClone(form.settings?.loraStack||[])
|
||||||
|
},
|
||||||
|
eyeOpen:!!eyeOpen.value,
|
||||||
|
hideInputs:!!hideInputs.value,
|
||||||
|
music:structuredClone({tags:music.tags,lyrics:music.lyrics,duration:music.duration,seed:music.seed,name:music.name})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function saveBenchDraft(immediate=false){
|
||||||
|
if(!import.meta.client)return
|
||||||
|
const write=()=>{
|
||||||
|
try{localStorage.setItem(BENCH_DRAFT_KEY,JSON.stringify(benchDraftPayload()))}catch{/* ignore */}
|
||||||
|
}
|
||||||
|
if(draftTimer)clearTimeout(draftTimer)
|
||||||
|
if(immediate){write();return}
|
||||||
|
draftTimer=setTimeout(write,400)
|
||||||
|
}
|
||||||
|
function restoreBenchDraft(){
|
||||||
|
if(!import.meta.client)return
|
||||||
|
let raw=''
|
||||||
|
try{raw=localStorage.getItem(BENCH_DRAFT_KEY)||''}catch{return}
|
||||||
|
if(!raw)return
|
||||||
|
let draft:any
|
||||||
|
try{draft=JSON.parse(raw)}catch{return}
|
||||||
|
if(!draft||draft.v!==1)return
|
||||||
|
if(draft.mode)form.mode=draft.mode
|
||||||
|
if(draft.engine)form.engine=draft.engine
|
||||||
|
if(draft.folderId)form.folderId=draft.folderId
|
||||||
|
if(draft.enhancePrompt!=null)form.enhancePrompt=!!draft.enhancePrompt
|
||||||
|
if(draft.turbo!=null)form.turbo=!!draft.turbo
|
||||||
|
if(draft.lockFace!=null)form.lockFace=!!draft.lockFace
|
||||||
|
if(draft.lockOutfit!=null)form.lockOutfit=!!draft.lockOutfit
|
||||||
|
if(draft.promptSections)form.promptSections=sectionsFrom(draft.promptSections)
|
||||||
|
if(Array.isArray(draft.photos))form.photos=draft.photos.filter((p:any)=>p?.stillId).map((p:any)=>({stillId:String(p.stillId),role:String(p.role||'change')}))
|
||||||
|
if(draft.imageStyles)form.imageStyles={positive:draft.imageStyles.positive||[],negative:draft.imageStyles.negative||[]}
|
||||||
|
if(draft.settings){
|
||||||
|
Object.assign(form.settings,{
|
||||||
|
aspect:draft.settings.aspect||form.settings.aspect,
|
||||||
|
seed:draft.settings.seed ?? form.settings.seed,
|
||||||
|
seedMode:draft.settings.seedMode||form.settings.seedMode,
|
||||||
|
steps:draft.settings.steps ?? form.settings.steps,
|
||||||
|
cfg:draft.settings.cfg ?? form.settings.cfg,
|
||||||
|
duration:draft.settings.duration ?? form.settings.duration,
|
||||||
|
fps:draft.settings.fps ?? form.settings.fps,
|
||||||
|
loraStack:Array.isArray(draft.settings.loraStack)?draft.settings.loraStack:form.settings.loraStack
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if(draft.eyeOpen!=null)eyeOpen.value=!!draft.eyeOpen
|
||||||
|
if(draft.hideInputs!=null)hideInputs.value=!!draft.hideInputs
|
||||||
|
if(draft.music){
|
||||||
|
music.tags=String(draft.music.tags||music.tags)
|
||||||
|
music.lyrics=String(draft.music.lyrics||music.lyrics)
|
||||||
|
if(draft.music.duration!=null)music.duration=Number(draft.music.duration)||music.duration
|
||||||
|
if(draft.music.seed!=null)music.seed=String(draft.music.seed)
|
||||||
|
if(draft.music.name!=null)music.name=String(draft.music.name)
|
||||||
|
}
|
||||||
|
syncPhotosBindings()
|
||||||
|
}
|
||||||
|
|
||||||
const xaigen=isXaigenStudio(useRuntimeConfig().public)
|
const xaigen=isXaigenStudio(useRuntimeConfig().public)
|
||||||
const instanceName=String(useRuntimeConfig().public.instanceName || 'AIGen')
|
const instanceName=String(useRuntimeConfig().public.instanceName || 'AIGen')
|
||||||
@@ -560,13 +638,19 @@ watch(()=>[form.mode,form.engine],([mode,engine])=>{
|
|||||||
form.lockOutfit=false
|
form.lockOutfit=false
|
||||||
}
|
}
|
||||||
const plan=photosPlan({mode,engine})
|
const plan=photosPlan({mode,engine})
|
||||||
if(!plan.show){form.photos=[];form.imageAId='';form.imageBId='';if(engine==='qwen21')form.identityStillId='';return}
|
if(!plan.show){
|
||||||
|
if(engine==='qwen21')form.identityStillId=''
|
||||||
|
// Keep photos + prompt; section may hide for this mode.
|
||||||
|
saveBenchDraft(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
const allowed=new Set(plan.roles)
|
const allowed=new Set(plan.roles)
|
||||||
form.photos=form.photos
|
form.photos=(form.photos||[])
|
||||||
.filter((p:any)=>p?.stillId)
|
.filter((p:any)=>p?.stillId)
|
||||||
.slice(0,plan.max)
|
.slice(0,plan.max)
|
||||||
.map((p:any)=>({stillId:p.stillId,role:allowed.has(p.role)?p.role:plan.defaultRole}))
|
.map((p:any)=>({stillId:p.stillId,role:allowed.has(p.role)?p.role:plan.defaultRole}))
|
||||||
syncPhotosBindings()
|
syncPhotosBindings()
|
||||||
|
saveBenchDraft(true)
|
||||||
})
|
})
|
||||||
watch(()=>form.folderId,id=>{selected.value=null;form.imageAId='';form.imageBId='';form.photos=[];form.startClipId='';loadRevealed(id);if(import.meta.client && form.engine!=='qwen21')form.identityStillId=localStorage.getItem(`studio2-hero:${id}`)||''},{flush:'sync'})
|
watch(()=>form.folderId,id=>{selected.value=null;form.imageAId='';form.imageBId='';form.photos=[];form.startClipId='';loadRevealed(id);if(import.meta.client && form.engine!=='qwen21')form.identityStillId=localStorage.getItem(`studio2-hero:${id}`)||''},{flush:'sync'})
|
||||||
watch(()=>form.identityStillId,id=>{if(import.meta.client&&form.folderId&&form.engine!=='qwen21')localStorage.setItem(`studio2-hero:${form.folderId}`,id)})
|
watch(()=>form.identityStillId,id=>{if(import.meta.client&&form.folderId&&form.engine!=='qwen21')localStorage.setItem(`studio2-hero:${form.folderId}`,id)})
|
||||||
@@ -718,17 +802,20 @@ function addPhoto(){
|
|||||||
if(form.photos.length>=plan.max)return
|
if(form.photos.length>=plan.max)return
|
||||||
form.photos.push({stillId:'',role:plan.defaultRole||'change'})
|
form.photos.push({stillId:'',role:plan.defaultRole||'change'})
|
||||||
picker.value='photo:'+(form.photos.length-1)
|
picker.value='photo:'+(form.photos.length-1)
|
||||||
|
saveBenchDraft(true)
|
||||||
}
|
}
|
||||||
function removePhoto(index:number){
|
function removePhoto(index:number){
|
||||||
if(!Number.isInteger(index)||index<0||index>=form.photos.length)return
|
if(!Number.isInteger(index)||index<0||index>=form.photos.length)return
|
||||||
form.photos.splice(index,1)
|
form.photos.splice(index,1)
|
||||||
// Explicit empty photos array clears imageAId / imageBId / identityStillId.
|
// Explicit empty photos array clears imageAId / imageBId / identityStillId.
|
||||||
syncPhotosBindings()
|
syncPhotosBindings()
|
||||||
|
saveBenchDraft(true)
|
||||||
}
|
}
|
||||||
function setPhotoRole(index:number,role:string){
|
function setPhotoRole(index:number,role:string){
|
||||||
if(!form.photos[index])return
|
if(!form.photos[index])return
|
||||||
form.photos[index].role=role
|
form.photos[index].role=role
|
||||||
syncPhotosBindings()
|
syncPhotosBindings()
|
||||||
|
saveBenchDraft(true)
|
||||||
}
|
}
|
||||||
function setPhotoStill(index:number,stillId:string){
|
function setPhotoStill(index:number,stillId:string){
|
||||||
if(!Number.isInteger(index)||!form.photos[index])return
|
if(!Number.isInteger(index)||!form.photos[index])return
|
||||||
@@ -738,6 +825,7 @@ function setPhotoStill(index:number,stillId:string){
|
|||||||
form.photos[index].stillId=stillId
|
form.photos[index].stillId=stillId
|
||||||
}
|
}
|
||||||
syncPhotosBindings()
|
syncPhotosBindings()
|
||||||
|
saveBenchDraft(true)
|
||||||
}
|
}
|
||||||
function discardEmptyPhotoDraft(index:number){
|
function discardEmptyPhotoDraft(index:number){
|
||||||
if(!Number.isInteger(index)||!form.photos[index])return
|
if(!Number.isInteger(index)||!form.photos[index])return
|
||||||
@@ -765,33 +853,118 @@ function mentionPhoto(index:number){
|
|||||||
promptCaret.value={start:next.selectionStart,end:next.selectionEnd}
|
promptCaret.value={start:next.selectionStart,end:next.selectionEnd}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
function applyUseAsSource(){
|
function usedPromptOf(item:any){
|
||||||
if(!selected.value||selected.value.kind==='track')return
|
return String(
|
||||||
|
item?.studio2?.prompt
|
||||||
|
|| item?.prompt
|
||||||
|
|| item?.studio2?.compiledPrompt
|
||||||
|
|| item?.studio2?.promptSections?.action
|
||||||
|
|| ''
|
||||||
|
).trim()
|
||||||
|
}
|
||||||
|
function rawPromptOf(item:any){
|
||||||
|
return String(item?.promptRaw || item?.studio2?.promptRaw || '').trim()
|
||||||
|
}
|
||||||
|
function sourceStillIdsOf(item:any){
|
||||||
|
const meta=item?.studio2 || {}
|
||||||
|
const ids:string[]=[]
|
||||||
|
const push=(id:any)=>{const v=String(id||'').trim();if(v && !ids.includes(v))ids.push(v)}
|
||||||
|
push(meta.sourceStillId)
|
||||||
|
push(meta.imageAId)
|
||||||
|
for(const card of meta.photos||[]) push(card?.stillId)
|
||||||
|
if(meta.imageBId) push(meta.imageBId)
|
||||||
|
return ids.filter(id=>stillById(id))
|
||||||
|
}
|
||||||
|
function useImageRole(){
|
||||||
const plan=photosPlanState.value
|
const plan=photosPlanState.value
|
||||||
if(!plan.show||plan.handoffReadonly){
|
const filled=(form.photos||[]).filter((p:any)=>p?.stillId)
|
||||||
Object.assign(form,useAsSource(form,selected.value))
|
if((form.mode==='edit' || video.value) && !filled.length && plan.roles.includes('change')) return 'change'
|
||||||
uploadMessage.value=`Using ${assetLabel(selected.value)}.`
|
if(plan.roles.includes('extra')) return 'extra'
|
||||||
|
if(plan.roles.includes('outfit')) return 'outfit'
|
||||||
|
return plan.defaultRole || 'change'
|
||||||
|
}
|
||||||
|
function applyUseImage(item?:any){
|
||||||
|
const asset=item || selected.value
|
||||||
|
if(!asset||asset.kind==='track')return
|
||||||
|
const plan=photosPlanState.value
|
||||||
|
const role=useImageRole()
|
||||||
|
if(!plan.show){
|
||||||
|
form.photos=[{stillId:asset.id,role}]
|
||||||
|
syncPhotosBindings()
|
||||||
|
uploadMessage.value=`Added ${assetLabel(asset)} to Photos.`
|
||||||
|
saveBenchDraft(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if(form.photos.length>=plan.max){
|
if(form.photos.length>=plan.max){
|
||||||
form.photos[form.photos.length-1]={stillId:selected.value.id,role:plan.defaultRole||form.photos.at(-1)?.role||'change'}
|
form.photos[form.photos.length-1]={stillId:asset.id,role}
|
||||||
}else{
|
}else{
|
||||||
form.photos.push({stillId:selected.value.id,role:plan.defaultRole||'change'})
|
form.photos.push({stillId:asset.id,role})
|
||||||
}
|
}
|
||||||
syncPhotosBindings()
|
syncPhotosBindings()
|
||||||
uploadMessage.value=`Added ${assetLabel(selected.value)} to Photos.`
|
uploadMessage.value=`Added ${assetLabel(asset)} to Photos.`
|
||||||
|
saveBenchDraft(true)
|
||||||
}
|
}
|
||||||
async function copySelectedPrompt(){
|
function applyUsePrompt(item?:any){
|
||||||
const text=selected.value?.studio2?.prompt
|
const asset=item || selected.value
|
||||||
|| selected.value?.studio2?.compiledPrompt
|
if(!asset)return
|
||||||
|| selected.value?.prompt
|
const text=usedPromptOf(asset)
|
||||||
|| selected.value?.studio2?.promptSections?.action
|
form.promptSections.action=text
|
||||||
|| ''
|
uploadMessage.value=text?'Prompt set from this item.':'No Used prompt on this item.'
|
||||||
|
saveBenchDraft(true)
|
||||||
|
}
|
||||||
|
async function copySelectedPrompt(item?:any){
|
||||||
|
const asset=item || selected.value
|
||||||
|
const text=usedPromptOf(asset)
|
||||||
try{
|
try{
|
||||||
if(import.meta.client && navigator.clipboard)await navigator.clipboard.writeText(String(text||''))
|
if(import.meta.client && navigator.clipboard)await navigator.clipboard.writeText(String(text||''))
|
||||||
uploadMessage.value=text?'Prompt copied.':'No prompt on this item.'
|
uploadMessage.value=text?'Prompt copied.':'No prompt on this item.'
|
||||||
}catch{error.value='Could not copy prompt.'}
|
}catch{error.value='Could not copy prompt.'}
|
||||||
}
|
}
|
||||||
|
async function copySelectedRawPrompt(item?:any){
|
||||||
|
const asset=item || selected.value
|
||||||
|
const raw=rawPromptOf(asset)
|
||||||
|
const used=usedPromptOf(asset)
|
||||||
|
const text=raw || used
|
||||||
|
try{
|
||||||
|
if(import.meta.client && navigator.clipboard)await navigator.clipboard.writeText(String(text||''))
|
||||||
|
uploadMessage.value=raw?'Raw prompt copied.':(used?'Used prompt copied (no Typed on this item).':'No prompt on this item.')
|
||||||
|
}catch{error.value='Could not copy prompt.'}
|
||||||
|
}
|
||||||
|
function loadInputPhotos(ids:string[]){
|
||||||
|
if(!ids.length)return
|
||||||
|
const plan=photosPlanState.value
|
||||||
|
const roles=plan.roles?.length?plan.roles:['change']
|
||||||
|
form.photos=ids.slice(0,plan.max || ids.length).map((id,i)=>({
|
||||||
|
stillId:id,
|
||||||
|
role:i===0 && roles.includes('change')?'change':(roles.includes('extra')?'extra':roles[Math.min(i,roles.length-1)]||'change')
|
||||||
|
}))
|
||||||
|
syncPhotosBindings()
|
||||||
|
}
|
||||||
|
function loadInputUsed(item?:any){
|
||||||
|
const asset=item || selected.value
|
||||||
|
if(!asset)return
|
||||||
|
const ids=sourceStillIdsOf(asset)
|
||||||
|
const text=usedPromptOf(asset)
|
||||||
|
if(ids.length) loadInputPhotos(ids)
|
||||||
|
if(text) form.promptSections.action=text
|
||||||
|
uploadMessage.value=ids.length
|
||||||
|
?(text?'Input photos and Used prompt loaded.':'Input photos loaded.')
|
||||||
|
:(text?'Used prompt loaded (no source still on this item).':'Nothing to load from this item.')
|
||||||
|
saveBenchDraft(true)
|
||||||
|
}
|
||||||
|
function loadInputRaw(item?:any){
|
||||||
|
const asset=item || selected.value
|
||||||
|
if(!asset)return
|
||||||
|
const ids=sourceStillIdsOf(asset)
|
||||||
|
const raw=rawPromptOf(asset)
|
||||||
|
const used=usedPromptOf(asset)
|
||||||
|
if(ids.length) loadInputPhotos(ids)
|
||||||
|
form.promptSections.action=raw || used
|
||||||
|
uploadMessage.value=ids.length
|
||||||
|
?(raw?'Input photos and Typed prompt loaded.':(used?'Input photos loaded; Typed missing so Used was applied.':'Input photos loaded.'))
|
||||||
|
:(raw?'Typed prompt loaded (no source still).':(used?'Used prompt loaded (no Typed / source still).':'Nothing to load from this item.'))
|
||||||
|
saveBenchDraft(true)
|
||||||
|
}
|
||||||
function stillIdSet(){return new Set(stills.value.map((s:any)=>s.id))}
|
function stillIdSet(){return new Set(stills.value.map((s:any)=>s.id))}
|
||||||
function clipIdSet(){return new Set(clips.value.map((c:any)=>c.id))}
|
function clipIdSet(){return new Set(clips.value.map((c:any)=>c.id))}
|
||||||
function applyHydratedJob(saved:any,output?:any){
|
function applyHydratedJob(saved:any,output?:any){
|
||||||
@@ -806,6 +979,7 @@ function applyHydratedJob(saved:any,output?:any){
|
|||||||
error.value=missing||''
|
error.value=missing||''
|
||||||
if(missing)uploadMessage.value=missing
|
if(missing)uploadMessage.value=missing
|
||||||
else uploadMessage.value='Job loaded into the bench.'
|
else uploadMessage.value='Job loaded into the bench.'
|
||||||
|
saveBenchDraft(true)
|
||||||
}
|
}
|
||||||
function loadSelectedJob(){
|
function loadSelectedJob(){
|
||||||
if(!selected.value)return
|
if(!selected.value)return
|
||||||
@@ -892,17 +1066,29 @@ function stepFamily(delta:number){
|
|||||||
const next=list[(from + delta + list.length) % list.length]
|
const next=list[(from + delta + list.length) % list.length]
|
||||||
if(next) selectOutput(next)
|
if(next) selectOutput(next)
|
||||||
}
|
}
|
||||||
function onLibraryUseAsPhoto(item:any){
|
function onLibraryUseImage(item:any){
|
||||||
if(item)selectOutput(item)
|
if(item)selectOutput(item)
|
||||||
applyUseAsSource()
|
applyUseImage(item)
|
||||||
|
}
|
||||||
|
function onLibraryUsePrompt(item:any){
|
||||||
|
if(item)selectOutput(item)
|
||||||
|
applyUsePrompt(item)
|
||||||
}
|
}
|
||||||
function onLibraryCopyPrompt(item:any){
|
function onLibraryCopyPrompt(item:any){
|
||||||
if(item)selectOutput(item)
|
if(item)selectOutput(item)
|
||||||
void copySelectedPrompt()
|
void copySelectedPrompt(item)
|
||||||
}
|
}
|
||||||
function onLibraryLoad(item:any){
|
function onLibraryCopyRawPrompt(item:any){
|
||||||
if(item)selectOutput(item)
|
if(item)selectOutput(item)
|
||||||
loadSelectedJob()
|
void copySelectedRawPrompt(item)
|
||||||
|
}
|
||||||
|
function onLibraryLoadInput(item:any){
|
||||||
|
if(item)selectOutput(item)
|
||||||
|
loadInputUsed(item)
|
||||||
|
}
|
||||||
|
function onLibraryLoadInputRaw(item:any){
|
||||||
|
if(item)selectOutput(item)
|
||||||
|
loadInputRaw(item)
|
||||||
}
|
}
|
||||||
function onLibraryDelete(item:any){
|
function onLibraryDelete(item:any){
|
||||||
if(item)selectOutput(item)
|
if(item)selectOutput(item)
|
||||||
@@ -1080,14 +1266,23 @@ async function refresh(){
|
|||||||
selected.value=next
|
selected.value=next
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(()=>form.folderId,id=>{selected.value=null;form.imageAId='';form.imageBId='';form.photos=[];form.startClipId='';loadRevealed(id);if(import.meta.client && form.engine!=='qwen21')form.identityStillId=localStorage.getItem(`studio2-hero:${id}`)||''},{flush:'sync'})
|
watch(()=>[
|
||||||
watch(()=>form.identityStillId,id=>{if(import.meta.client&&form.folderId&&form.engine!=='qwen21')localStorage.setItem(`studio2-hero:${form.folderId}`,id)})
|
form.mode,form.engine,form.folderId,form.enhancePrompt,form.turbo,form.lockFace,form.lockOutfit,
|
||||||
|
form.promptSections,form.photos,form.imageStyles,form.settings,
|
||||||
|
eyeOpen.value,hideInputs.value,
|
||||||
|
music.tags,music.lyrics,music.duration,music.seed,music.name
|
||||||
|
],()=>saveBenchDraft(),{deep:true})
|
||||||
let timer:ReturnType<typeof setTimeout>,clock:ReturnType<typeof setInterval>,disposed=false
|
let timer:ReturnType<typeof setTimeout>,clock:ReturnType<typeof setInterval>,disposed=false
|
||||||
onMounted(async()=>{
|
onMounted(async()=>{
|
||||||
let storage;try{storage=localStorage}catch{}
|
let storage;try{storage=localStorage}catch{}
|
||||||
loraMemory=createLoraMemory(xaigen,storage);loraMemory.sync(form);form.settings.seed=randomSeed()
|
loraMemory=createLoraMemory(xaigen,storage);loraMemory.sync(form);form.settings.seed=randomSeed()
|
||||||
clock=setInterval(()=>{now.value=Date.now()},1000);void loadLoras(true)
|
clock=setInterval(()=>{now.value=Date.now()},1000);void loadLoras(true)
|
||||||
library.value=await $fetch('/api/studio-2/library');form.folderId=library.value.folders[0]?.id||'';loadRevealed(form.folderId)
|
library.value=await $fetch('/api/studio-2/library')
|
||||||
|
restoreBenchDraft()
|
||||||
|
if(!form.folderId)form.folderId=library.value.folders[0]?.id||''
|
||||||
|
// Folder watch may have cleared photos when folderId was assigned — re-apply draft photos/prompt.
|
||||||
|
restoreBenchDraft()
|
||||||
|
loadRevealed(form.folderId)
|
||||||
await applyIncomingGenerationLoad()
|
await applyIncomingGenerationLoad()
|
||||||
const id=String(route.query.still||route.query.clip||route.query.track||'')
|
const id=String(route.query.still||route.query.clip||route.query.track||'')
|
||||||
const asset=[...library.value.stills,...library.value.clips,...(library.value.tracks||[])].find((a:any)=>a.id===id)
|
const asset=[...library.value.stills,...library.value.clips,...(library.value.tracks||[])].find((a:any)=>a.id===id)
|
||||||
@@ -1098,7 +1293,11 @@ onMounted(async()=>{
|
|||||||
})
|
})
|
||||||
onBeforeUnmount(()=>{
|
onBeforeUnmount(()=>{
|
||||||
disposed=true;clearTimeout(timer);clearInterval(clock)
|
disposed=true;clearTimeout(timer);clearInterval(clock)
|
||||||
if(import.meta.client)window.removeEventListener('keydown',onStageHotkey)
|
if(draftTimer)clearTimeout(draftTimer)
|
||||||
|
if(import.meta.client){
|
||||||
|
saveBenchDraft(true)
|
||||||
|
window.removeEventListener('keydown',onStageHotkey)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function randomizeSeed(){form.settings.seedMode='random';form.settings.seed=randomSeed()}
|
function randomizeSeed(){form.settings.seedMode='random';form.settings.seed=randomSeed()}
|
||||||
@@ -1128,7 +1327,7 @@ const mobileCtx=computed(()=>({
|
|||||||
photoFilledCount:photoFilledCount.value,
|
photoFilledCount:photoFilledCount.value,
|
||||||
openLibrary:()=>{libraryOpen.value=true},
|
openLibrary:()=>{libraryOpen.value=true},
|
||||||
closeLibrary:()=>{libraryOpen.value=false},
|
closeLibrary:()=>{libraryOpen.value=false},
|
||||||
toggleHideInputs:()=>{hideInputs.value=!hideInputs.value},
|
toggleHideInputs:()=>{hideInputs.value=!hideInputs.value;saveBenchDraft(true)},
|
||||||
showInputsAndAddPhoto:()=>{hideInputs.value=false;addPhoto()},
|
showInputsAndAddPhoto:()=>{hideInputs.value=false;addPhoto()},
|
||||||
sourceSlot:sourceSlot.value,
|
sourceSlot:sourceSlot.value,
|
||||||
video:video.value,
|
video:video.value,
|
||||||
@@ -1168,12 +1367,21 @@ const mobileCtx=computed(()=>({
|
|||||||
removePhoto,
|
removePhoto,
|
||||||
setPhotoRole,
|
setPhotoRole,
|
||||||
mentionPhoto,
|
mentionPhoto,
|
||||||
applyUseAsSource,
|
applyUseAsSource:applyUseImage,
|
||||||
|
applyUseImage,
|
||||||
|
applyUsePrompt,
|
||||||
copySelectedPrompt,
|
copySelectedPrompt,
|
||||||
|
copySelectedRawPrompt,
|
||||||
|
loadInputUsed,
|
||||||
|
loadInputRaw,
|
||||||
loadSelectedJob,
|
loadSelectedJob,
|
||||||
onLibraryUseAsPhoto,
|
onLibraryUseAsPhoto:onLibraryUseImage,
|
||||||
|
onLibraryUseImage,
|
||||||
|
onLibraryUsePrompt,
|
||||||
onLibraryCopyPrompt,
|
onLibraryCopyPrompt,
|
||||||
onLibraryLoad,
|
onLibraryCopyRawPrompt,
|
||||||
|
onLibraryLoadInput,
|
||||||
|
onLibraryLoadInputRaw,
|
||||||
onLibraryDelete,
|
onLibraryDelete,
|
||||||
randomizeSeed,
|
randomizeSeed,
|
||||||
setPicker,
|
setPicker,
|
||||||
|
|||||||
@@ -97,9 +97,12 @@
|
|||||||
<aside v-if="detailAsset" class="lib-modal-detail">
|
<aside v-if="detailAsset" class="lib-modal-detail">
|
||||||
<p class="lib-modal-detail-name">{{ detailLabel }}</p>
|
<p class="lib-modal-detail-name">{{ detailLabel }}</p>
|
||||||
<div v-if="detailAsset.kind !== 'track'" class="lib-modal-actions">
|
<div v-if="detailAsset.kind !== 'track'" class="lib-modal-actions">
|
||||||
<button type="button" @click="$emit('use-as-photo', detailAsset)">Use as photo</button>
|
<button type="button" @click="$emit('use-image', detailAsset)">Use image</button>
|
||||||
|
<button type="button" @click="$emit('use-prompt', detailAsset)">Use prompt</button>
|
||||||
<button type="button" @click="$emit('copy-prompt', detailAsset)">Copy prompt</button>
|
<button type="button" @click="$emit('copy-prompt', detailAsset)">Copy prompt</button>
|
||||||
<button type="button" @click="$emit('load', detailAsset)">Load</button>
|
<button type="button" @click="$emit('copy-raw-prompt', detailAsset)">Copy raw prompt</button>
|
||||||
|
<button type="button" @click="$emit('load-input', detailAsset)">Load input</button>
|
||||||
|
<button type="button" @click="$emit('load-input-raw', detailAsset)">Load input raw</button>
|
||||||
<button type="button" @click="$emit('toggle-preview')">{{ previewHidden ? 'Show output' : 'Hide output' }}</button>
|
<button type="button" @click="$emit('toggle-preview')">{{ previewHidden ? 'Show output' : 'Hide output' }}</button>
|
||||||
<button type="button" class="danger" @click="onDeleteClick">Delete</button>
|
<button type="button" class="danger" @click="onDeleteClick">Delete</button>
|
||||||
<button type="button" class="primary" @click="useAndClose">Use and close</button>
|
<button type="button" class="primary" @click="useAndClose">Use and close</button>
|
||||||
@@ -139,9 +142,12 @@ const props = defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
close: []
|
close: []
|
||||||
select: [asset: any]
|
select: [asset: any]
|
||||||
'use-as-photo': [asset: any]
|
'use-image': [asset: any]
|
||||||
|
'use-prompt': [asset: any]
|
||||||
'copy-prompt': [asset: any]
|
'copy-prompt': [asset: any]
|
||||||
load: [asset: any]
|
'copy-raw-prompt': [asset: any]
|
||||||
|
'load-input': [asset: any]
|
||||||
|
'load-input-raw': [asset: any]
|
||||||
delete: [asset: any]
|
delete: [asset: any]
|
||||||
'toggle-preview': []
|
'toggle-preview': []
|
||||||
}>()
|
}>()
|
||||||
@@ -234,7 +240,7 @@ function pickEntry(entry: LibraryEntry) {
|
|||||||
function close() { emit('close') }
|
function close() { emit('close') }
|
||||||
|
|
||||||
function useAndClose() {
|
function useAndClose() {
|
||||||
if (detailAsset.value) emit('select', detailAsset.value)
|
if (detailAsset.value) emit('use-image', detailAsset.value)
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,9 +129,12 @@
|
|||||||
:preview-hidden="!ctx.eyeOpen"
|
:preview-hidden="!ctx.eyeOpen"
|
||||||
@close="ctx.closeLibrary()"
|
@close="ctx.closeLibrary()"
|
||||||
@select="ctx.selectOutput"
|
@select="ctx.selectOutput"
|
||||||
@use-as-photo="ctx.onLibraryUseAsPhoto"
|
@use-image="ctx.onLibraryUseImage"
|
||||||
|
@use-prompt="ctx.onLibraryUsePrompt"
|
||||||
@copy-prompt="ctx.onLibraryCopyPrompt"
|
@copy-prompt="ctx.onLibraryCopyPrompt"
|
||||||
@load="ctx.onLibraryLoad"
|
@copy-raw-prompt="ctx.onLibraryCopyRawPrompt"
|
||||||
|
@load-input="ctx.onLibraryLoadInput"
|
||||||
|
@load-input-raw="ctx.onLibraryLoadInputRaw"
|
||||||
@delete="ctx.onLibraryDelete"
|
@delete="ctx.onLibraryDelete"
|
||||||
@toggle-preview="ctx.toggleReveal()"
|
@toggle-preview="ctx.toggleReveal()"
|
||||||
/>
|
/>
|
||||||
@@ -150,9 +153,12 @@
|
|||||||
<button type="button" class="m-handle" aria-label="Close" @click="sheet=''" />
|
<button type="button" class="m-handle" aria-label="Close" @click="sheet=''" />
|
||||||
<div class="m-sheet-head"><strong>Actions</strong><button type="button" @click="sheet=''">Done</button></div>
|
<div class="m-sheet-head"><strong>Actions</strong><button type="button" @click="sheet=''">Done</button></div>
|
||||||
<div class="m-sheet-body m-actions-list">
|
<div class="m-sheet-body m-actions-list">
|
||||||
<button type="button" @click="runAction('use')">Use as photo</button>
|
<button type="button" @click="runAction('use-image')">Use image</button>
|
||||||
|
<button type="button" @click="runAction('use-prompt')">Use prompt</button>
|
||||||
<button type="button" @click="runAction('copy')">Copy prompt</button>
|
<button type="button" @click="runAction('copy')">Copy prompt</button>
|
||||||
<button type="button" @click="runAction('load')">Load</button>
|
<button type="button" @click="runAction('copy-raw')">Copy raw prompt</button>
|
||||||
|
<button type="button" @click="runAction('load-input')">Load input</button>
|
||||||
|
<button type="button" @click="runAction('load-input-raw')">Load input raw</button>
|
||||||
<button type="button" class="m-danger" @click="runAction('delete')">Delete</button>
|
<button type="button" class="m-danger" @click="runAction('delete')">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -172,24 +178,12 @@
|
|||||||
{{ ctx.hideInputs ? 'Show inputs' : 'Hide inputs' }}
|
{{ ctx.hideInputs ? 'Show inputs' : 'Hide inputs' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="ctx.hideInputs">
|
|
||||||
<p class="m-muted">
|
|
||||||
{{ ctx.photoFilledCount || 0 }} photo{{ (ctx.photoFilledCount||0)===1 ? '' : 's' }}
|
|
||||||
<button
|
|
||||||
v-if="(ctx.form.photos?.length||0) < ctx.photosPlanState.max"
|
|
||||||
type="button"
|
|
||||||
class="m-amber"
|
|
||||||
@click="ctx.showInputsAndAddPhoto()"
|
|
||||||
>+ Add photo</button>
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<p v-if="!ctx.form.photos?.length && ctx.photosPlanState.emptyHint" class="m-muted">{{ ctx.photosPlanState.emptyHint }}</p>
|
<p v-if="!ctx.form.photos?.length && ctx.photosPlanState.emptyHint" class="m-muted">{{ ctx.photosPlanState.emptyHint }}</p>
|
||||||
<Studio2AssetSlot
|
<Studio2AssetSlot
|
||||||
v-for="(card, index) in ctx.form.photos"
|
v-for="(card, index) in ctx.form.photos"
|
||||||
:key="'m-photo-'+index+'-'+card.stillId"
|
:key="'m-photo-'+index+'-'+card.stillId"
|
||||||
:asset="ctx.stillById(card.stillId)"
|
:asset="ctx.stillById(card.stillId)"
|
||||||
:hidden="false"
|
:hidden="!!ctx.hideInputs"
|
||||||
:role="card.role"
|
:role="card.role"
|
||||||
:role-options="ctx.photoRoleOptions"
|
:role-options="ctx.photoRoleOptions"
|
||||||
@pick="ctx.setPicker('photo:'+index)"
|
@pick="ctx.setPicker('photo:'+index)"
|
||||||
@@ -199,7 +193,6 @@
|
|||||||
/>
|
/>
|
||||||
<button v-if="(ctx.form.photos?.length||0) < ctx.photosPlanState.max" type="button" class="m-amber" @click="ctx.addPhoto">+ Add photo</button>
|
<button v-if="(ctx.form.photos?.length||0) < ctx.photosPlanState.max" type="button" class="m-amber" @click="ctx.addPhoto">+ Add photo</button>
|
||||||
<p v-if="ctx.photoRoleWarning" class="m-hint">{{ ctx.photoRoleWarning }}</p>
|
<p v-if="ctx.photoRoleWarning" class="m-hint">{{ ctx.photoRoleWarning }}</p>
|
||||||
</template>
|
|
||||||
<template v-if="ctx.showIdentityLocks">
|
<template v-if="ctx.showIdentityLocks">
|
||||||
<label class="m-check"><input v-model="ctx.form.lockFace" type="checkbox"> Lock face</label>
|
<label class="m-check"><input v-model="ctx.form.lockFace" type="checkbox"> Lock face</label>
|
||||||
<label class="m-check"><input v-model="ctx.form.lockOutfit" type="checkbox"> Lock outfit</label>
|
<label class="m-check"><input v-model="ctx.form.lockOutfit" type="checkbox"> Lock outfit</label>
|
||||||
@@ -360,11 +353,14 @@ function toggleSheet(id: string) {
|
|||||||
function closeMenus() {
|
function closeMenus() {
|
||||||
sheet.value = ''
|
sheet.value = ''
|
||||||
}
|
}
|
||||||
function runAction(kind: 'use' | 'copy' | 'load' | 'delete') {
|
function runAction(kind: 'use-image' | 'use-prompt' | 'copy' | 'copy-raw' | 'load-input' | 'load-input-raw' | 'delete') {
|
||||||
sheet.value = ''
|
sheet.value = ''
|
||||||
if (kind === 'use') props.ctx.applyUseAsSource?.()
|
if (kind === 'use-image') props.ctx.applyUseImage?.()
|
||||||
|
else if (kind === 'use-prompt') props.ctx.applyUsePrompt?.()
|
||||||
else if (kind === 'copy') props.ctx.copySelectedPrompt?.()
|
else if (kind === 'copy') props.ctx.copySelectedPrompt?.()
|
||||||
else if (kind === 'load') props.ctx.loadSelectedJob?.()
|
else if (kind === 'copy-raw') props.ctx.copySelectedRawPrompt?.()
|
||||||
|
else if (kind === 'load-input') props.ctx.loadInputUsed?.()
|
||||||
|
else if (kind === 'load-input-raw') props.ctx.loadInputRaw?.()
|
||||||
else if (kind === 'delete') props.ctx.deleteOutput?.()
|
else if (kind === 'delete') props.ctx.deleteOutput?.()
|
||||||
}
|
}
|
||||||
function onStageTouchStart(e: TouchEvent) {
|
function onStageTouchStart(e: TouchEvent) {
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
<div class="flex flex-wrap gap-2"><button v-for="(beat,i) in model.cameraBeats" :key="i" type="button" class="rounded-lg border px-3 py-2" :class="active===i?'border-amber-300 text-amber-200':'border-white/20'" @click="active=i">{{ i+1 }} · {{ beat.moves.join(' + ') }} · {{ beat.durationSec == null ? 'to end' : `${beat.durationSec}s` }}</button></div>
|
<div class="flex flex-wrap gap-2"><button v-for="(beat,i) in model.cameraBeats" :key="i" type="button" class="rounded-lg border px-3 py-2" :class="active===i?'border-amber-300 text-amber-200':'border-white/20'" @click="active=i">{{ i+1 }} · {{ beat.moves.join(' + ') }} · {{ beat.durationSec == null ? 'to end' : `${beat.durationSec}s` }}</button></div>
|
||||||
<div v-if="current" class="space-y-3 rounded-xl border border-white/15 p-3">
|
<div v-if="current" class="space-y-3 rounded-xl border border-white/15 p-3">
|
||||||
<div class="flex flex-wrap gap-2"><button v-for="move in cameraMoves" :key="move" type="button" :aria-pressed="current.moves.includes(move)" class="rounded-full border px-3 py-1 text-sm" :class="current.moves.includes(move)?'border-amber-300 bg-amber-300/10 text-amber-200':'border-white/15'" @click="model.cameraBeats[active]=toggleMove(current,move)">{{ move.replaceAll('-',' ') }}</button></div>
|
<div class="flex flex-wrap gap-2"><button v-for="move in cameraMoves" :key="move" type="button" :aria-pressed="current.moves.includes(move)" class="rounded-full border px-3 py-1 text-sm" :class="current.moves.includes(move)?'border-amber-300 bg-amber-300/10 text-amber-200':'border-white/15'" @click="model.cameraBeats[active]=toggleMove(current,move)">{{ move.replaceAll('-',' ') }}</button></div>
|
||||||
<div class="flex flex-wrap gap-3"><label>Amplitude<select v-model="current.amplitude" class="block rounded bg-zinc-800 p-2"><option v-for="v in ['subtle','moderate','large']" :key="v">{{ v }}</option></select></label><label>Speed<select v-model="current.speed" class="block rounded bg-zinc-800 p-2"><option v-for="v in ['slow','medium','fast']" :key="v">{{ v }}</option></select></label><label>Duration<select v-model="current.durationSec" class="block rounded bg-zinc-800 p-2"><option :value="null">To end</option><option v-for="n in 30" :key="n" :value="n/2">{{ n/2 }}s</option></select></label><button @click="model.cameraBeats.splice(active,1);active=Math.max(0,active-1)">Remove beat</button></div>
|
<div v-if="!isHold" class="flex flex-wrap gap-3"><label>Amplitude<select v-model="current.amplitude" class="block rounded bg-zinc-800 p-2"><option v-for="v in ['subtle','moderate','large']" :key="v">{{ v }}</option></select></label><label>Speed<select v-model="current.speed" class="block rounded bg-zinc-800 p-2"><option v-for="v in ['slow','medium','fast']" :key="v">{{ v }}</option></select></label><label>Duration<select v-model="current.durationSec" class="block rounded bg-zinc-800 p-2"><option :value="null">To end</option><option v-for="n in 30" :key="n" :value="n/2">{{ n/2 }}s</option></select></label><button @click="model.cameraBeats.splice(active,1);active=Math.max(0,active-1)">Remove beat</button></div>
|
||||||
|
<div v-else class="flex flex-wrap gap-3"><button @click="model.cameraBeats.splice(active,1);active=Math.max(0,active-1)">Remove beat</button></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-4 text-sm text-amber-200"><button @click="addBeat">+ Camera beat</button><button @click="model.cameraBeats=[];model.camera='';active=0">Clear camera</button></div>
|
<div class="flex gap-4 text-sm text-amber-200"><button @click="addBeat">+ Camera beat</button><button @click="model.cameraBeats=[];model.camera='';active=0">Clear camera</button></div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
import {cameraMoves,soundscapeOptions,normalizeBeat,toggleMove} from '~/shared/studio2/prompt.mjs'
|
import {cameraMoves,soundscapeOptions,normalizeBeat,toggleMove} from '~/shared/studio2/prompt.mjs'
|
||||||
const model=defineModel<any>({required:true}),active=ref(0)
|
const model=defineModel<any>({required:true}),active=ref(0)
|
||||||
const current=computed(()=>model.value.cameraBeats[active.value])
|
const current=computed(()=>model.value.cameraBeats[active.value])
|
||||||
|
const isHold=computed(()=>!!current.value?.moves?.includes?.('hold'))
|
||||||
function addBeat(){const last=model.value.cameraBeats.at(-1);if(last?.durationSec==null&&last)last.durationSec=2;model.value.cameraBeats.push(normalizeBeat());active.value=model.value.cameraBeats.length-1}
|
function addBeat(){const last=model.value.cameraBeats.at(-1);if(last?.durationSec==null&&last)last.durationSec=2;model.value.cameraBeats.push(normalizeBeat());active.value=model.value.cameraBeats.length-1}
|
||||||
function toggleSound(sound:string){const chips=model.value.soundscapeChips;model.value.soundscapeChips=chips.includes(sound)?chips.filter((s:string)=>s!==sound):[...chips,sound]}
|
function toggleSound(sound:string){const chips=model.value.soundscapeChips;model.value.soundscapeChips=chips.includes(sound)?chips.filter((s:string)=>s!==sound):[...chips,sound]}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -452,6 +452,8 @@ async function run(r: any, job: Job) {
|
|||||||
r.progress = null;
|
r.progress = null;
|
||||||
r.request.promptSections = prompts[r.index];
|
r.request.promptSections = prompts[r.index];
|
||||||
r.request.compiledPrompt = compilePrompt(prompts[r.index], ['video', 'extend'].includes(r.request.mode), r.request);
|
r.request.compiledPrompt = compilePrompt(prompts[r.index], ['video', 'extend'].includes(r.request.mode), r.request);
|
||||||
|
// Typed brief before PE / TextEncode — Copy raw / Load input raw need this even when Enhance is off.
|
||||||
|
r.request.promptRaw = String(r.request.compiledPrompt || '');
|
||||||
update(r, 'waking');
|
update(r, 'waking');
|
||||||
await ensureComfyReady(message => emitJob(job, { type: 'status', message }));
|
await ensureComfyReady(message => emitJob(job, { type: 'status', message }));
|
||||||
if (job.status === 'cancelled')
|
if (job.status === 'cancelled')
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ export function sectionsFrom(value = {}) {
|
|||||||
export function compileSections(value) {
|
export function compileSections(value) {
|
||||||
const s = sectionsFrom(value)
|
const s = sectionsFrom(value)
|
||||||
const camera = s.cameraBeats.length ? ['One continuous take.', ...s.cameraBeats.map((b,i) => {
|
const camera = s.cameraBeats.length ? ['One continuous take.', ...s.cameraBeats.map((b,i) => {
|
||||||
const move = b.moves[0] === 'hold' ? 'hold the camera locked on a tripod' : b.moves.map(m=>m.replaceAll('-',' ')).join(' and ') + (b.moves.length > 1 ? ' simultaneously' : '')
|
const hold = b.moves[0] === 'hold' || b.moves.includes('hold')
|
||||||
|
if (hold) {
|
||||||
|
return `${i ? 'Then' : 'First'}, hold the camera locked on a tripod.`
|
||||||
|
}
|
||||||
|
const move = b.moves.map(m=>m.replaceAll('-',' ')).join(' and ') + (b.moves.length > 1 ? ' simultaneously' : '')
|
||||||
const timing = b.durationSec == null ? 'through the end of the remaining clip' : i === 0 ? `for the first ${b.durationSec} seconds` : `for ${b.durationSec} seconds`
|
const timing = b.durationSec == null ? 'through the end of the remaining clip' : i === 0 ? `for the first ${b.durationSec} seconds` : `for ${b.durationSec} seconds`
|
||||||
return `${i ? 'Then' : 'First'}, ${move}, ${{subtle:'small',moderate:'medium',large:'large'}[b.amplitude]} amplitude, ${b.speed} speed, ${timing}.`
|
return `${i ? 'Then' : 'First'}, ${move}, ${{subtle:'small',moderate:'medium',large:'large'}[b.amplitude]} amplitude, ${b.speed} speed, ${timing}.`
|
||||||
}), 'No cuts. No new setup.'].join(' ') : s.camera
|
}), 'No cuts. No new setup.'].join(' ') : s.camera
|
||||||
|
|||||||
Reference in New Issue
Block a user