Add same-mode extra passes to Image v2, and hide Shot script from image Recommend.
Library tiles show a check when selected, and xAIGen can hide a preview from the details pane. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+124
-29
@@ -139,32 +139,40 @@
|
|||||||
<div v-if="studioMode === 'editv2'" class="flex flex-wrap gap-2">
|
<div v-if="studioMode === 'editv2'" class="flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-full border px-3 py-1 text-xs font-medium"
|
class="rounded-full border px-3 py-1 text-xs font-medium disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
:class="v2Mode === 'edit' ? 'border-amber-300/70 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400 hover:text-white'"
|
:class="v2Mode === 'edit' ? 'border-amber-300/70 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400 hover:text-white'"
|
||||||
|
:disabled="v2ModeLocked && v2Mode !== 'edit'"
|
||||||
|
:title="v2ModeLocked && v2Mode !== 'edit' ? 'Remove extra passes to change graphs' : ''"
|
||||||
@click="setV2Mode('edit')"
|
@click="setV2Mode('edit')"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-full border px-3 py-1 text-xs font-medium"
|
class="rounded-full border px-3 py-1 text-xs font-medium disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
:class="v2Mode === 'compose' ? 'border-amber-300/70 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400 hover:text-white'"
|
:class="v2Mode === 'compose' ? 'border-amber-300/70 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400 hover:text-white'"
|
||||||
|
:disabled="v2ModeLocked && v2Mode !== 'compose'"
|
||||||
|
:title="v2ModeLocked && v2Mode !== 'compose' ? 'Remove extra passes to change graphs' : ''"
|
||||||
@click="setV2Mode('compose')"
|
@click="setV2Mode('compose')"
|
||||||
>
|
>
|
||||||
Compose
|
Compose
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-full border px-3 py-1 text-xs font-medium"
|
class="rounded-full border px-3 py-1 text-xs font-medium disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
:class="v2Mode === 'refine' ? 'border-amber-300/70 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400 hover:text-white'"
|
:class="v2Mode === 'refine' ? 'border-amber-300/70 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400 hover:text-white'"
|
||||||
|
:disabled="v2ModeLocked && v2Mode !== 'refine'"
|
||||||
|
:title="v2ModeLocked && v2Mode !== 'refine' ? 'Remove extra passes to change graphs' : ''"
|
||||||
@click="setV2Mode('refine')"
|
@click="setV2Mode('refine')"
|
||||||
>
|
>
|
||||||
Refine
|
Refine
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-full border px-3 py-1 text-xs font-medium"
|
class="rounded-full border px-3 py-1 text-xs font-medium disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
:class="v2Mode === 'generate' ? 'border-amber-300/70 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400 hover:text-white'"
|
:class="v2Mode === 'generate' ? 'border-amber-300/70 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400 hover:text-white'"
|
||||||
|
:disabled="v2ModeLocked && v2Mode !== 'generate'"
|
||||||
|
:title="v2ModeLocked && v2Mode !== 'generate' ? 'Remove extra passes to change graphs' : ''"
|
||||||
@click="setV2Mode('generate')"
|
@click="setV2Mode('generate')"
|
||||||
>
|
>
|
||||||
Generate
|
Generate
|
||||||
@@ -244,7 +252,7 @@
|
|||||||
:disabled="recommendBusy"
|
:disabled="recommendBusy"
|
||||||
:title="recommendHint"
|
:title="recommendHint"
|
||||||
>
|
>
|
||||||
<option v-for="item in promptPresets" :key="item.id" :value="item.id">{{ item.name }}</option>
|
<option v-for="item in visiblePromptPresets" :key="item.id" :value="item.id">{{ item.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -376,7 +384,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="studioMode === 'edit'" class="mt-3 space-y-3">
|
<div v-if="studioMode === 'edit' || v2PassChaining" class="mt-3 space-y-3">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in editPassQueue"
|
v-for="(item, index) in editPassQueue"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
@@ -397,7 +405,7 @@
|
|||||||
v-model="item.prompt"
|
v-model="item.prompt"
|
||||||
rows="3"
|
rows="3"
|
||||||
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 placeholder:text-zinc-600 focus:ring-2"
|
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 placeholder:text-zinc-600 focus:ring-2"
|
||||||
placeholder="Next edit prompt, applied to the previous pass output…"
|
:placeholder="extraPassPlaceholder"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -406,8 +414,9 @@
|
|||||||
:disabled="editBusy"
|
:disabled="editBusy"
|
||||||
@click="queueEditPass"
|
@click="queueEditPass"
|
||||||
>
|
>
|
||||||
+ Pass
|
{{ extraPassButtonLabel }}
|
||||||
</button>
|
</button>
|
||||||
|
<p class="text-xs text-zinc-500">{{ extraPassHint }}</p>
|
||||||
</div>
|
</div>
|
||||||
<details v-if="identityPrompting && prompt.trim()" class="mt-2 rounded-xl border border-white/10 bg-zinc-950/40 px-3 py-2">
|
<details v-if="identityPrompting && prompt.trim()" class="mt-2 rounded-xl border border-white/10 bg-zinc-950/40 px-3 py-2">
|
||||||
<summary class="cursor-pointer text-xs text-zinc-400">Prompt sent to Reference to Video</summary>
|
<summary class="cursor-pointer text-xs text-zinc-400">Prompt sent to Reference to Video</summary>
|
||||||
@@ -1561,14 +1570,27 @@
|
|||||||
>
|
>
|
||||||
<template v-if="entry.kind === 'clip'">
|
<template v-if="entry.kind === 'clip'">
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<label class="flex shrink-0 items-start pt-1" @click.stop>
|
<button
|
||||||
<input
|
type="button"
|
||||||
type="checkbox"
|
role="checkbox"
|
||||||
class="mt-0.5 h-4 w-4 rounded border-white/20 bg-zinc-950 accent-amber-400"
|
class="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center self-start rounded-[4px] border"
|
||||||
:checked="selectedKeys.includes(entry.key)"
|
:class="selectedKeys.includes(entry.key)
|
||||||
@click.prevent="onSelectClick(entry.key, $event)"
|
? 'border-white bg-amber-400 text-zinc-950'
|
||||||
|
: 'border-white/30 bg-black/45'"
|
||||||
|
:aria-checked="selectedKeys.includes(entry.key)"
|
||||||
|
:aria-label="selectedKeys.includes(entry.key) ? 'Deselect' : 'Select'"
|
||||||
|
@click.stop="onSelectClick(entry.key, $event)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
v-if="selectedKeys.includes(entry.key)"
|
||||||
|
class="h-3 w-3"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
</label>
|
<path d="M3.2 8.2 6.4 11.3 12.8 4.6" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="relative h-16 w-16 shrink-0 overflow-hidden rounded-xl bg-zinc-900"
|
class="relative h-16 w-16 shrink-0 overflow-hidden rounded-xl bg-zinc-900"
|
||||||
@@ -1742,14 +1764,27 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else-if="entry.kind === 'still' && entry.stillGroup">
|
<template v-else-if="entry.kind === 'still' && entry.stillGroup">
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<label class="flex shrink-0 items-start pt-1" @click.stop>
|
<button
|
||||||
<input
|
type="button"
|
||||||
type="checkbox"
|
role="checkbox"
|
||||||
class="mt-0.5 h-4 w-4 rounded border-white/20 bg-zinc-950 accent-amber-400"
|
class="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center self-start rounded-[4px] border"
|
||||||
:checked="selectedKeys.includes(entry.key)"
|
:class="selectedKeys.includes(entry.key)
|
||||||
@click.prevent="onSelectClick(entry.key, $event)"
|
? 'border-white bg-amber-400 text-zinc-950'
|
||||||
|
: 'border-white/30 bg-black/45'"
|
||||||
|
:aria-checked="selectedKeys.includes(entry.key)"
|
||||||
|
:aria-label="selectedKeys.includes(entry.key) ? 'Deselect' : 'Select'"
|
||||||
|
@click.stop="onSelectClick(entry.key, $event)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
v-if="selectedKeys.includes(entry.key)"
|
||||||
|
class="h-3 w-3"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
</label>
|
<path d="M3.2 8.2 6.4 11.3 12.8 4.6" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button type="button" class="relative h-16 w-16 shrink-0 overflow-hidden rounded-xl bg-zinc-900" @click="openStill(entry.stillGroup.head)">
|
<button type="button" class="relative h-16 w-16 shrink-0 overflow-hidden rounded-xl bg-zinc-900" @click="openStill(entry.stillGroup.head)">
|
||||||
<span v-if="entry.stillGroup.head.hideInput" class="sensitive-glow block h-full w-full" />
|
<span v-if="entry.stillGroup.head.hideInput" class="sensitive-glow block h-full w-full" />
|
||||||
<img
|
<img
|
||||||
@@ -2271,6 +2306,10 @@ const promptPresets = ref<Array<{ id: string; name: string; description?: string
|
|||||||
{ id: 'enhance', name: 'Enhance', builtin: true },
|
{ id: 'enhance', name: 'Enhance', builtin: true },
|
||||||
{ id: 'shot-script', name: 'Shot script', builtin: true }
|
{ id: 'shot-script', name: 'Shot script', builtin: true }
|
||||||
])
|
])
|
||||||
|
const visiblePromptPresets = computed(() => {
|
||||||
|
if (studioMode.value === 'video') return promptPresets.value
|
||||||
|
return promptPresets.value.filter(item => item.id !== 'shot-script')
|
||||||
|
})
|
||||||
const recommendBusy = ref(false)
|
const recommendBusy = ref(false)
|
||||||
const recommendText = ref('')
|
const recommendText = ref('')
|
||||||
const recommendError = ref('')
|
const recommendError = ref('')
|
||||||
@@ -2849,6 +2888,7 @@ const editV2Blocked = computed(() => {
|
|||||||
if (v2Mode.value !== 'generate' && !file.value) return true
|
if (v2Mode.value !== 'generate' && !file.value) return true
|
||||||
if (v2Mode.value === 'compose' && !editRefFile.value) return true
|
if (v2Mode.value === 'compose' && !editRefFile.value) return true
|
||||||
if (v2Mode.value === 'refine' && !refineMaskDirty.value) return true
|
if (v2Mode.value === 'refine' && !refineMaskDirty.value) return true
|
||||||
|
if (v2PassChaining.value && editPassQueue.value.some(item => !item.prompt.trim())) return true
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
const editV2BlockReason = computed(() => {
|
const editV2BlockReason = computed(() => {
|
||||||
@@ -2860,14 +2900,37 @@ const editV2BlockReason = computed(() => {
|
|||||||
? 'Write a prompt first.'
|
? 'Write a prompt first.'
|
||||||
: 'Write a prompt, or pick Landscape / Portrait / Square to fill the frame.'
|
: 'Write a prompt, or pick Landscape / Portrait / Square to fill the frame.'
|
||||||
}
|
}
|
||||||
|
if (v2PassChaining.value && editPassQueue.value.some(item => !item.prompt.trim())) return 'Each extra pass needs a prompt.'
|
||||||
if (!folderId.value) return 'Choose a library folder before generating.'
|
if (!folderId.value) return 'Choose a library folder before generating.'
|
||||||
if (!imageComfyConfigured.value && !comfyOk.value) return 'Image v2 is not configured. Set COMFY_HOST.'
|
if (!imageComfyConfigured.value && !comfyOk.value) return 'Image v2 is not configured. Set COMFY_HOST.'
|
||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
|
const v2PassChaining = computed(() => studioMode.value === 'editv2' && (v2Mode.value === 'edit' || v2Mode.value === 'compose'))
|
||||||
|
const v2ModeLocked = computed(() => v2PassChaining.value && editPassQueue.value.length > 0)
|
||||||
|
const extraPassButtonLabel = computed(() => (
|
||||||
|
studioMode.value === 'editv2' && v2Mode.value === 'compose' ? '+ Compose pass' : '+ Edit pass'
|
||||||
|
))
|
||||||
|
const extraPassHint = computed(() => {
|
||||||
|
if (studioMode.value === 'editv2' && v2Mode.value === 'compose') {
|
||||||
|
return 'Each extra pass is another Compose on the last output, with the same still B. You cannot switch graphs between passes.'
|
||||||
|
}
|
||||||
|
return 'Each extra pass is another Edit on the last output. You cannot switch graphs between passes.'
|
||||||
|
})
|
||||||
|
const extraPassPlaceholder = computed(() => (
|
||||||
|
studioMode.value === 'editv2' && v2Mode.value === 'compose'
|
||||||
|
? 'Next compose prompt, applied to the previous pass output…'
|
||||||
|
: 'Next edit prompt, applied to the previous pass output…'
|
||||||
|
))
|
||||||
const editV2SubmitLabel = computed(() => {
|
const editV2SubmitLabel = computed(() => {
|
||||||
const occupied = videoBusy.value || editBusy.value || studioJobs.value.some(job => job.status === 'running')
|
const occupied = videoBusy.value || editBusy.value || studioJobs.value.some(job => job.status === 'running')
|
||||||
|
const n = v2PassChaining.value ? editPassQueue.value.length : 0
|
||||||
const label = v2Mode.value === 'generate' ? 'Generate image' : v2Mode.value === 'refine' ? 'Refine image' : v2Mode.value === 'compose' ? 'Compose image' : 'Edit image'
|
const label = v2Mode.value === 'generate' ? 'Generate image' : v2Mode.value === 'refine' ? 'Refine image' : v2Mode.value === 'compose' ? 'Compose image' : 'Edit image'
|
||||||
return occupied ? `Queue ${label.toLowerCase()}` : label
|
if (!occupied) {
|
||||||
|
if (!n) return label
|
||||||
|
return n === 1 ? `${label} + 1 pass` : `${label} + ${n} passes`
|
||||||
|
}
|
||||||
|
if (n) return `Queue ${n + 1} passes`
|
||||||
|
return `Queue ${label.toLowerCase()}`
|
||||||
})
|
})
|
||||||
const generateLabel = computed(() => {
|
const generateLabel = computed(() => {
|
||||||
const n = queuedExtensionCount.value
|
const n = queuedExtensionCount.value
|
||||||
@@ -3582,6 +3645,12 @@ watch(recommendPresetId, (id) => {
|
|||||||
try { localStorage.setItem('aigen-prompt-preset', id) } catch { /* ignore */ }
|
try { localStorage.setItem('aigen-prompt-preset', id) } catch { /* ignore */ }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(studioMode, () => {
|
||||||
|
if (studioMode.value !== 'video' && recommendPresetId.value === 'shot-script') {
|
||||||
|
recommendPresetId.value = 'enhance'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
watch([browseFolderId, libraryFilter], () => {
|
watch([browseFolderId, libraryFilter], () => {
|
||||||
selectedKeys.value = []
|
selectedKeys.value = []
|
||||||
lastSelectedKey.value = ''
|
lastSelectedKey.value = ''
|
||||||
@@ -3987,6 +4056,7 @@ function applyGenerationPreset(preset: GenerationPreset) {
|
|||||||
const settings = preset.settings as ImageV2PresetSettings
|
const settings = preset.settings as ImageV2PresetSettings
|
||||||
persistImageV2LoraStack(imageV2LoraStack.value, v2Engine.value)
|
persistImageV2LoraStack(imageV2LoraStack.value, v2Engine.value)
|
||||||
v2Mode.value = settings.mode === 'compose' ? 'compose' : settings.mode === 'refine' ? 'refine' : settings.mode === 'generate' ? 'generate' : 'edit'
|
v2Mode.value = settings.mode === 'compose' ? 'compose' : settings.mode === 'refine' ? 'refine' : settings.mode === 'generate' ? 'generate' : 'edit'
|
||||||
|
if (v2Mode.value !== 'edit' && v2Mode.value !== 'compose') editPassQueue.value = []
|
||||||
v2Engine.value = parseImageV2Engine(settings.engine, 'flux')
|
v2Engine.value = parseImageV2Engine(settings.engine, 'flux')
|
||||||
v2Task.value = v2Mode.value === 'compose' ? (settings.task || 'scene') : v2Mode.value === 'refine' ? 'refine' : v2Mode.value === 'generate' ? 't2i' : 'scene'
|
v2Task.value = v2Mode.value === 'compose' ? (settings.task || 'scene') : v2Mode.value === 'refine' ? 'refine' : v2Mode.value === 'generate' ? 't2i' : 'scene'
|
||||||
if (typeof settings.prompt === 'string') prompt.value = settings.prompt
|
if (typeof settings.prompt === 'string') prompt.value = settings.prompt
|
||||||
@@ -4203,6 +4273,9 @@ async function loadPromptPresets() {
|
|||||||
} else if (!promptPresets.value.some(item => item.id === recommendPresetId.value)) {
|
} else if (!promptPresets.value.some(item => item.id === recommendPresetId.value)) {
|
||||||
recommendPresetId.value = promptPresets.value[0]?.id || 'enhance'
|
recommendPresetId.value = promptPresets.value[0]?.id || 'enhance'
|
||||||
}
|
}
|
||||||
|
if (studioMode.value !== 'video' && recommendPresetId.value === 'shot-script') {
|
||||||
|
recommendPresetId.value = 'enhance'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function recommendPrompt() {
|
async function recommendPrompt() {
|
||||||
@@ -4220,7 +4293,7 @@ async function recommendPrompt() {
|
|||||||
body.append('presetId', recommendPresetId.value)
|
body.append('presetId', recommendPresetId.value)
|
||||||
body.append(
|
body.append(
|
||||||
'mode',
|
'mode',
|
||||||
studioMode.value === 'edit' ? 'edit' : (shotScriptMode.value ? 'shot-script' : 'video')
|
studioMode.value === 'edit' || studioMode.value === 'editv2' ? 'edit' : (shotScriptMode.value ? 'shot-script' : 'video')
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
const started = await $fetch<PromptRecommendSnap>('/api/prompt/recommend', { method: 'POST', body })
|
const started = await $fetch<PromptRecommendSnap>('/api/prompt/recommend', { method: 'POST', body })
|
||||||
@@ -4258,7 +4331,7 @@ function applyRecommend() {
|
|||||||
promptBeforeRecommend.value = prompt.value
|
promptBeforeRecommend.value = prompt.value
|
||||||
editPassesBeforeRecommend.value = editPassQueue.value.map(item => ({ ...item }))
|
editPassesBeforeRecommend.value = editPassQueue.value.map(item => ({ ...item }))
|
||||||
shotScriptBeforeRecommend.value = shotScriptMode.value
|
shotScriptBeforeRecommend.value = shotScriptMode.value
|
||||||
if (studioMode.value === 'edit') {
|
if (studioMode.value === 'edit' || v2PassChaining.value) {
|
||||||
const passes = parseEditPasses(text)
|
const passes = parseEditPasses(text)
|
||||||
if (passes && passes.length > 1) {
|
if (passes && passes.length > 1) {
|
||||||
prompt.value = passes[0]
|
prompt.value = passes[0]
|
||||||
@@ -5909,6 +5982,11 @@ async function editImage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setV2Mode(mode: ImageV2Mode) {
|
function setV2Mode(mode: ImageV2Mode) {
|
||||||
|
if (v2Mode.value === mode) return
|
||||||
|
if (editPassQueue.value.length && v2PassChaining.value) {
|
||||||
|
toast('Remove extra passes to change graphs. Extra passes stay in the current mode.')
|
||||||
|
return
|
||||||
|
}
|
||||||
v2Mode.value = mode
|
v2Mode.value = mode
|
||||||
if (mode === 'edit') v2Task.value = 'scene'
|
if (mode === 'edit') v2Task.value = 'scene'
|
||||||
if (mode === 'refine') {
|
if (mode === 'refine') {
|
||||||
@@ -5994,6 +6072,7 @@ async function ensureRefineCanvas() {
|
|||||||
|
|
||||||
async function sendToEdit() {
|
async function sendToEdit() {
|
||||||
if (!editResultUrl.value) return
|
if (!editResultUrl.value) return
|
||||||
|
editPassQueue.value = []
|
||||||
studioMode.value = 'editv2'
|
studioMode.value = 'editv2'
|
||||||
outputFocus.value = 'edit'
|
outputFocus.value = 'edit'
|
||||||
try {
|
try {
|
||||||
@@ -6007,6 +6086,7 @@ async function sendToEdit() {
|
|||||||
|
|
||||||
async function sendToRefine() {
|
async function sendToRefine() {
|
||||||
if (!editResultUrl.value) return
|
if (!editResultUrl.value) return
|
||||||
|
editPassQueue.value = []
|
||||||
studioMode.value = 'editv2'
|
studioMode.value = 'editv2'
|
||||||
outputFocus.value = 'edit'
|
outputFocus.value = 'edit'
|
||||||
try {
|
try {
|
||||||
@@ -6031,6 +6111,11 @@ async function editImageV2() {
|
|||||||
toast('Paint a mask on Still A first. Refine will not fall back to Edit.')
|
toast('Paint a mask on Still A first. Refine will not fall back to Edit.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (v2PassChaining.value && editPassQueue.value.some(item => !item.prompt.trim())) {
|
||||||
|
toast('Each extra pass needs a prompt.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const extraPasses = v2PassChaining.value ? editPassQueue.value.map(item => ({ prompt: item.prompt.trim() })) : []
|
||||||
const hideOut = hideThumbnail.value
|
const hideOut = hideThumbnail.value
|
||||||
try {
|
try {
|
||||||
const body = new FormData()
|
const body = new FormData()
|
||||||
@@ -6072,6 +6157,7 @@ async function editImageV2() {
|
|||||||
body.append('hideThumbnail', String(showPrivacyToggles.value && hideThumbnail.value))
|
body.append('hideThumbnail', String(showPrivacyToggles.value && hideThumbnail.value))
|
||||||
body.append('name', clipName.value.trim())
|
body.append('name', clipName.value.trim())
|
||||||
if (inputStillId.value) body.append('stillId', inputStillId.value)
|
if (inputStillId.value) body.append('stillId', inputStillId.value)
|
||||||
|
if (extraPasses.length) body.append('passes', JSON.stringify(extraPasses))
|
||||||
const v2Stack = filterLoraStackForImageEngine(
|
const v2Stack = filterLoraStackForImageEngine(
|
||||||
filterLoraStackForStudio(imageV2LoraStack.value, ltxEnabled.value),
|
filterLoraStackForStudio(imageV2LoraStack.value, ltxEnabled.value),
|
||||||
v2Engine.value
|
v2Engine.value
|
||||||
@@ -6097,7 +6183,10 @@ async function editImageV2() {
|
|||||||
body
|
body
|
||||||
})
|
})
|
||||||
if (started.queued) {
|
if (started.queued) {
|
||||||
toast(v2Engine.value === 'krea' ? 'Queued Krea generate behind the current job.' : 'Queued Image v2 behind the current job.')
|
const n = 1 + extraPasses.length
|
||||||
|
toast(n > 1
|
||||||
|
? `Queued ${n} Image v2 passes behind the current job.`
|
||||||
|
: (v2Engine.value === 'krea' ? 'Queued Krea generate behind the current job.' : 'Queued Image v2 behind the current job.'))
|
||||||
await refreshStudioQueue()
|
await refreshStudioQueue()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -6113,11 +6202,17 @@ async function editImageV2() {
|
|||||||
editStatusMessage.value = 'Checking Beast ComfyUI...'
|
editStatusMessage.value = 'Checking Beast ComfyUI...'
|
||||||
editMaxStep.value = started.steps
|
editMaxStep.value = started.steps
|
||||||
editChainStep.value = 1
|
editChainStep.value = 1
|
||||||
editChainTotal.value = 1
|
editChainTotal.value = 1 + extraPasses.length
|
||||||
editChainLabel.value = ''
|
editChainLabel.value = extraPasses.length ? 'Pass 1' : ''
|
||||||
editOverallProgress.value = 0
|
editOverallProgress.value = 0
|
||||||
editCompletedChainStep.value = 0
|
editCompletedChainStep.value = 0
|
||||||
editActiveChainPlan.value = [{ label: v2Mode.value === 'generate' ? 'Generate' : v2Mode.value === 'refine' ? 'Refine' : v2Mode.value === 'compose' ? 'Compose' : 'Edit', prompt: prompt.value.trim() }]
|
const passNoun = v2Mode.value === 'compose' ? 'Compose' : v2Mode.value === 'refine' ? 'Refine' : v2Mode.value === 'generate' ? 'Generate' : 'Edit'
|
||||||
|
editActiveChainPlan.value = extraPasses.length
|
||||||
|
? [
|
||||||
|
{ label: 'Pass 1', prompt: prompt.value.trim() },
|
||||||
|
...extraPasses.map((item, index) => ({ label: `Pass ${index + 2}`, prompt: item.prompt }))
|
||||||
|
]
|
||||||
|
: [{ label: passNoun, prompt: prompt.value.trim() }]
|
||||||
startTimer('edit')
|
startTimer('edit')
|
||||||
editDownloadName.value = downloadName
|
editDownloadName.value = downloadName
|
||||||
editJobId.value = started.jobId
|
editJobId.value = started.jobId
|
||||||
|
|||||||
+85
-11
@@ -162,14 +162,27 @@
|
|||||||
v-if="partCount(entry) > 1"
|
v-if="partCount(entry) > 1"
|
||||||
class="absolute right-2 top-2 rounded-md bg-black/60 px-1.5 py-0.5 text-[10px] text-amber-100"
|
class="absolute right-2 top-2 rounded-md bg-black/60 px-1.5 py-0.5 text-[10px] text-amber-100"
|
||||||
>{{ partCount(entry) }}</span>
|
>{{ partCount(entry) }}</span>
|
||||||
<label class="absolute bottom-2 left-2" @click.stop>
|
<button
|
||||||
<input
|
type="button"
|
||||||
type="checkbox"
|
role="checkbox"
|
||||||
class="h-4 w-4 rounded border-white/20 bg-zinc-950 accent-amber-400"
|
class="absolute bottom-2 left-2 flex h-4 w-4 items-center justify-center rounded-[4px] border shadow-[0_1px_4px_rgba(0,0,0,0.55)]"
|
||||||
:checked="selectedKeys.includes(entry.key)"
|
:class="selectedKeys.includes(entry.key)
|
||||||
@click.prevent="onSelectClick(entry.key, $event)"
|
? 'border-white bg-amber-400 text-zinc-950'
|
||||||
|
: 'border-white/30 bg-black/45'"
|
||||||
|
:aria-checked="selectedKeys.includes(entry.key)"
|
||||||
|
:aria-label="selectedKeys.includes(entry.key) ? 'Deselect' : 'Select'"
|
||||||
|
@click.stop="onSelectClick(entry.key, $event)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
v-if="selectedKeys.includes(entry.key)"
|
||||||
|
class="h-3 w-3"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
</label>
|
<path d="M3.2 8.2 6.4 11.3 12.8 4.6" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-2.5 py-2">
|
<div class="px-2.5 py-2">
|
||||||
<p class="truncate text-xs font-medium text-zinc-100">{{ entry.name }}</p>
|
<p class="truncate text-xs font-medium text-zinc-100">{{ entry.name }}</p>
|
||||||
@@ -184,8 +197,8 @@
|
|||||||
|
|
||||||
<aside class="glass w-full shrink-0 overflow-y-auto rounded-3xl p-4 sm:p-5 lg:h-full lg:w-[22rem]">
|
<aside class="glass w-full shrink-0 overflow-y-auto rounded-3xl p-4 sm:p-5 lg:h-full lg:w-[22rem]">
|
||||||
<template v-if="focused">
|
<template v-if="focused">
|
||||||
<div class="overflow-hidden rounded-2xl bg-zinc-900">
|
<div class="relative overflow-hidden rounded-2xl bg-zinc-900">
|
||||||
<span v-if="thumbHidden(focused)" class="sensitive-glow block aspect-video w-full" />
|
<span v-if="previewHidden" class="sensitive-glow block aspect-video w-full" />
|
||||||
<video
|
<video
|
||||||
v-else-if="focused.kind === 'clip' && previewVideo"
|
v-else-if="focused.kind === 'clip' && previewVideo"
|
||||||
:src="previewVideo"
|
:src="previewVideo"
|
||||||
@@ -194,8 +207,8 @@
|
|||||||
playsinline
|
playsinline
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
v-else-if="focused.kind === 'still' && focused.stillGroup"
|
v-else-if="focused.kind === 'still' && previewStillSrc"
|
||||||
:src="`/api/library/stills/${focused.stillGroup.head.id}`"
|
:src="previewStillSrc"
|
||||||
alt=""
|
alt=""
|
||||||
class="aspect-video w-full object-contain"
|
class="aspect-video w-full object-contain"
|
||||||
>
|
>
|
||||||
@@ -205,6 +218,24 @@
|
|||||||
alt=""
|
alt=""
|
||||||
class="aspect-video w-full object-cover"
|
class="aspect-video w-full object-cover"
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
v-if="showPrivacyToggles"
|
||||||
|
type="button"
|
||||||
|
class="absolute right-2 top-2 z-10 flex h-8 w-8 items-center justify-center rounded-full border border-white/20 bg-black/60 text-zinc-100 hover:border-amber-300/50 hover:text-amber-100 disabled:opacity-50"
|
||||||
|
:title="previewHidden ? 'Show in library' : 'Hide in library'"
|
||||||
|
:aria-label="previewHidden ? 'Show in library' : 'Hide in library'"
|
||||||
|
:disabled="hideBusy"
|
||||||
|
@click="togglePreviewHidden"
|
||||||
|
>
|
||||||
|
<svg v-if="previewHidden" class="h-4 w-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<path d="M3 3l18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||||
|
<path d="M10.6 10.7a2.5 2.5 0 0 0 3.7 3.6M9.9 5.2A11 11 0 0 1 12 5c6.5 0 10 7 10 7a18 18 0 0 1-4.1 4.8M6.7 6.8A18 18 0 0 0 2 12s3.5 7 10 7c1.3 0 2.5-.3 3.6-.7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else class="h-4 w-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
|
||||||
|
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
v-if="renaming"
|
v-if="renaming"
|
||||||
@@ -321,6 +352,7 @@ import {
|
|||||||
type LibrarySort,
|
type LibrarySort,
|
||||||
type LibraryStill
|
type LibraryStill
|
||||||
} from '~/utils/libraryBrowse'
|
} from '~/utils/libraryBrowse'
|
||||||
|
import { isXaigenStudio } from '~/utils/videoModels'
|
||||||
|
|
||||||
const CARD_MIN = 156
|
const CARD_MIN = 156
|
||||||
const GAP = 10
|
const GAP = 10
|
||||||
@@ -335,6 +367,10 @@ const filterOptions = [
|
|||||||
|
|
||||||
const runtimeConfig = useRuntimeConfig()
|
const runtimeConfig = useRuntimeConfig()
|
||||||
const instanceName = ref(String(runtimeConfig.public.instanceName || 'AIGen'))
|
const instanceName = ref(String(runtimeConfig.public.instanceName || 'AIGen'))
|
||||||
|
const showPrivacyToggles = computed(() => isXaigenStudio({
|
||||||
|
studio: runtimeConfig.public.studio,
|
||||||
|
instanceName: runtimeConfig.public.instanceName || instanceName.value
|
||||||
|
}))
|
||||||
const authEnabled = ref(false)
|
const authEnabled = ref(false)
|
||||||
const authMode = ref('')
|
const authMode = ref('')
|
||||||
const userName = ref('')
|
const userName = ref('')
|
||||||
@@ -357,6 +393,7 @@ const renaming = ref(false)
|
|||||||
const renameDraft = ref('')
|
const renameDraft = ref('')
|
||||||
const renameInput = ref<HTMLInputElement | null>(null)
|
const renameInput = ref<HTMLInputElement | null>(null)
|
||||||
const deleteBusy = ref(false)
|
const deleteBusy = ref(false)
|
||||||
|
const hideBusy = ref(false)
|
||||||
const pendingDelete = ref<{ title: string; name: string; detail?: string; consequence: string; keys: string[] } | null>(null)
|
const pendingDelete = ref<{ title: string; name: string; detail?: string; consequence: string; keys: string[] } | null>(null)
|
||||||
const toasts = ref<Array<{ id: number; message: string }>>([])
|
const toasts = ref<Array<{ id: number; message: string }>>([])
|
||||||
let toastId = 0
|
let toastId = 0
|
||||||
@@ -406,6 +443,24 @@ const previewVideo = computed(() => {
|
|||||||
const id = focusedPartId.value || focused.value.group.head.id
|
const id = focusedPartId.value || focused.value.group.head.id
|
||||||
return `/api/library/clips/${id}/video`
|
return `/api/library/clips/${id}/video`
|
||||||
})
|
})
|
||||||
|
const previewStillSrc = computed(() => {
|
||||||
|
if (focused.value?.kind !== 'still' || !focused.value.stillGroup) return ''
|
||||||
|
const id = focusedPartId.value || focused.value.stillGroup.head.id
|
||||||
|
return `/api/library/stills/${id}`
|
||||||
|
})
|
||||||
|
const focusedPreviewItem = computed(() => {
|
||||||
|
const entry = focused.value
|
||||||
|
if (!entry) return null
|
||||||
|
if (entry.kind === 'clip') {
|
||||||
|
return entry.group?.parts.find(part => part.id === focusedPartId.value) || entry.group?.head || null
|
||||||
|
}
|
||||||
|
return entry.stillGroup?.parts.find(part => part.id === focusedPartId.value) || entry.stillGroup?.head || null
|
||||||
|
})
|
||||||
|
const previewHidden = computed(() => {
|
||||||
|
const item = focusedPreviewItem.value
|
||||||
|
if (!item) return false
|
||||||
|
return 'hideThumbnail' in item ? Boolean(item.hideThumbnail) : Boolean(item.hideInput)
|
||||||
|
})
|
||||||
const focusedParts = computed(() => {
|
const focusedParts = computed(() => {
|
||||||
if (focused.value?.kind === 'clip') return focused.value.group?.parts || []
|
if (focused.value?.kind === 'clip') return focused.value.group?.parts || []
|
||||||
return focused.value?.stillGroup?.parts || []
|
return focused.value?.stillGroup?.parts || []
|
||||||
@@ -677,6 +732,25 @@ async function saveRename() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function togglePreviewHidden() {
|
||||||
|
const entry = focused.value
|
||||||
|
const item = focusedPreviewItem.value
|
||||||
|
if (!entry || !item || hideBusy.value) return
|
||||||
|
hideBusy.value = true
|
||||||
|
const next = !previewHidden.value
|
||||||
|
try {
|
||||||
|
if (entry.kind === 'clip') {
|
||||||
|
applyLibrary(await $fetch(`/api/library/clips/${item.id}`, { method: 'PATCH', body: { hideThumbnail: next } }))
|
||||||
|
} else {
|
||||||
|
applyLibrary(await $fetch(`/api/library/stills/${item.id}`, { method: 'PATCH', body: { hideInput: next } }))
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
toast(error?.data?.statusMessage || 'Could not update visibility')
|
||||||
|
} finally {
|
||||||
|
hideBusy.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function askRemoveFocused() {
|
function askRemoveFocused() {
|
||||||
if (!focused.value) return
|
if (!focused.value) return
|
||||||
askRemoveKeys([focused.value.key])
|
askRemoveKeys([focused.value.key])
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const { owner } = assertLibraryOwner(event)
|
const { owner } = assertLibraryOwner(event)
|
||||||
const id = String(getRouterParam(event, 'id') || '')
|
const id = String(getRouterParam(event, 'id') || '')
|
||||||
const body = await readBody<{ folderId?: string; name?: string; family?: boolean; globalLocks?: string; permanenceRefs?: unknown }>(event).catch(() => ({} as { folderId?: string; name?: string; family?: boolean; globalLocks?: string; permanenceRefs?: unknown }))
|
const body = await readBody<{ folderId?: string; name?: string; family?: boolean; globalLocks?: string; permanenceRefs?: unknown; hideThumbnail?: boolean }>(event).catch(() => ({} as { folderId?: string; name?: string; family?: boolean; globalLocks?: string; permanenceRefs?: unknown; hideThumbnail?: boolean }))
|
||||||
const folderId = String(body?.folderId || '')
|
const folderId = String(body?.folderId || '')
|
||||||
const hasName = typeof body?.name === 'string'
|
const hasName = typeof body?.name === 'string'
|
||||||
const family = body?.family === true
|
const family = body?.family === true
|
||||||
const hasLocks = typeof body?.globalLocks === 'string'
|
const hasLocks = typeof body?.globalLocks === 'string'
|
||||||
const hasRefs = Array.isArray(body?.permanenceRefs)
|
const hasRefs = Array.isArray(body?.permanenceRefs)
|
||||||
if (!folderId && !hasName && !hasLocks && !hasRefs) {
|
const hasHide = typeof body?.hideThumbnail === 'boolean'
|
||||||
|
if (!folderId && !hasName && !hasLocks && !hasRefs && !hasHide) {
|
||||||
throw createError({ statusCode: 400, statusMessage: 'Choose a library folder or a new name' })
|
throw createError({ statusCode: 400, statusMessage: 'Choose a library folder or a new name' })
|
||||||
}
|
}
|
||||||
const clip = getClip(owner, id)
|
const clip = getClip(owner, id)
|
||||||
@@ -28,5 +29,6 @@ export default defineEventHandler(async (event) => {
|
|||||||
family: family || hasLocks
|
family: family || hasLocks
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (hasHide) setClipHidden(owner, id, body.hideThumbnail === true)
|
||||||
return publicLibrary(event)
|
return publicLibrary(event)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const { owner } = assertLibraryOwner(event)
|
const { owner } = assertLibraryOwner(event)
|
||||||
const id = String(getRouterParam(event, 'id') || '')
|
const id = String(getRouterParam(event, 'id') || '')
|
||||||
const body = await readBody<{ folderId?: string; name?: string; family?: boolean }>(event).catch(() => ({} as { folderId?: string; name?: string; family?: boolean }))
|
const body = await readBody<{ folderId?: string; name?: string; family?: boolean; hideInput?: boolean }>(event).catch(() => ({} as { folderId?: string; name?: string; family?: boolean; hideInput?: boolean }))
|
||||||
const folderId = String(body?.folderId || '')
|
const folderId = String(body?.folderId || '')
|
||||||
const hasName = typeof body?.name === 'string'
|
const hasName = typeof body?.name === 'string'
|
||||||
const family = body?.family === true
|
const family = body?.family === true
|
||||||
if (!folderId && !hasName) {
|
const hasHide = typeof body?.hideInput === 'boolean'
|
||||||
|
if (!folderId && !hasName && !hasHide) {
|
||||||
throw createError({ statusCode: 400, statusMessage: 'Choose a library folder or a new name' })
|
throw createError({ statusCode: 400, statusMessage: 'Choose a library folder or a new name' })
|
||||||
}
|
}
|
||||||
const still = getStill(owner, id)
|
const still = getStill(owner, id)
|
||||||
@@ -19,5 +20,6 @@ export default defineEventHandler(async (event) => {
|
|||||||
if (family) await renameStillFamily(owner, id, body.name || '')
|
if (family) await renameStillFamily(owner, id, body.name || '')
|
||||||
else await renameStill(owner, id, body.name || '')
|
else await renameStill(owner, id, body.name || '')
|
||||||
}
|
}
|
||||||
|
if (hasHide) setStillHidden(owner, id, body.hideInput === true)
|
||||||
return publicLibrary(event)
|
return publicLibrary(event)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -32,6 +32,18 @@ import { existsSync, readFileSync } from 'node:fs'
|
|||||||
|
|
||||||
type ImageFile = { filename: string; data: Buffer; type?: string }
|
type ImageFile = { filename: string; data: Buffer; type?: string }
|
||||||
|
|
||||||
|
function parsePasses(raw: unknown) {
|
||||||
|
if (!raw) return [] as { prompt: string }[]
|
||||||
|
let parsed = raw
|
||||||
|
if (typeof raw === 'string') {
|
||||||
|
try { parsed = JSON.parse(raw) } catch { return [] }
|
||||||
|
}
|
||||||
|
if (!Array.isArray(parsed)) return []
|
||||||
|
return parsed
|
||||||
|
.map((item: { prompt?: unknown }) => ({ prompt: String(item?.prompt || '').trim() }))
|
||||||
|
.filter(item => item.prompt)
|
||||||
|
}
|
||||||
|
|
||||||
function parseBool(raw: unknown) {
|
function parseBool(raw: unknown) {
|
||||||
return raw === true || raw === 'true' || raw === '1' || raw === 1
|
return raw === true || raw === 'true' || raw === '1' || raw === 1
|
||||||
}
|
}
|
||||||
@@ -124,10 +136,20 @@ export default defineEventHandler(async (event) => {
|
|||||||
mode === 'generate' ? 't2i' : mode === 'refine' ? 'refine' : 'scene'
|
mode === 'generate' ? 't2i' : mode === 'refine' ? 'refine' : 'scene'
|
||||||
)
|
)
|
||||||
const prompt = String(fields.prompt || '').trim()
|
const prompt = String(fields.prompt || '').trim()
|
||||||
|
const extraPasses = parsePasses(fields.passes)
|
||||||
const aspect = parseImageAspect(fields.aspect)
|
const aspect = parseImageAspect(fields.aspect)
|
||||||
if (!prompt && (mode === 'generate' || aspect === 'auto')) {
|
if (!prompt && (mode === 'generate' || aspect === 'auto')) {
|
||||||
throw createError({ statusCode: 400, statusMessage: 'A prompt is required' })
|
throw createError({ statusCode: 400, statusMessage: 'A prompt is required' })
|
||||||
}
|
}
|
||||||
|
if (extraPasses.length && (mode === 'refine' || mode === 'generate')) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Extra passes are only for Edit and Compose. Refine and Generate stay a single run.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (extraPasses.some(item => !item.prompt)) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: 'Each extra pass needs a prompt' })
|
||||||
|
}
|
||||||
if (!comfyConfigured()) {
|
if (!comfyConfigured()) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 503,
|
statusCode: 503,
|
||||||
@@ -301,6 +323,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
extensions: [],
|
extensions: [],
|
||||||
queueAutoRun: false,
|
queueAutoRun: false,
|
||||||
negative: String(fields.negative || '').trim(),
|
negative: String(fields.negative || '').trim(),
|
||||||
|
passes: extraPasses,
|
||||||
referenceStillId: savedRef?.id,
|
referenceStillId: savedRef?.id,
|
||||||
referenceStillFilename: savedRef?.filename,
|
referenceStillFilename: savedRef?.filename,
|
||||||
scaleToTotalPixels: mode !== 'generate',
|
scaleToTotalPixels: mode !== 'generate',
|
||||||
@@ -342,6 +365,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
height: size?.height || 0,
|
height: size?.height || 0,
|
||||||
strength: refineStrength,
|
strength: refineStrength,
|
||||||
hideThumbnail,
|
hideThumbnail,
|
||||||
folderLocked
|
folderLocked,
|
||||||
|
chainTotal: 1 + extraPasses.length
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+236
-160
@@ -6,7 +6,7 @@ import { buildImageV2Workflow, IMAGE_V2_NODE_LABELS, resolveKreaGenerateAssets }
|
|||||||
import { ensureComfyLoraNames, persistLoraFields } from '~/server/utils/loras'
|
import { ensureComfyLoraNames, persistLoraFields } from '~/server/utils/loras'
|
||||||
import { imageDimensions } from '~/server/utils/resolution'
|
import { imageDimensions } from '~/server/utils/resolution'
|
||||||
import { emitChainJob } from '~/server/utils/watch'
|
import { emitChainJob } from '~/server/utils/watch'
|
||||||
import { saveStill } from '~/server/utils/library'
|
import { saveStill, stillChainName } from '~/server/utils/library'
|
||||||
import { comfyJobPrefix } from '~/utils/outputNames'
|
import { comfyJobPrefix } from '~/utils/outputNames'
|
||||||
import { parseImageV2Engine, type ImageV2Engine, type ImageV2Mode, type ImageV2Task } from '~/utils/imageV2'
|
import { parseImageV2Engine, type ImageV2Engine, type ImageV2Mode, type ImageV2Task } from '~/utils/imageV2'
|
||||||
import type { EditImageFile } from '~/server/utils/imageChain'
|
import type { EditImageFile } from '~/server/utils/imageChain'
|
||||||
@@ -36,6 +36,7 @@ export type EditV2RunParams = {
|
|||||||
sourceStillId?: string
|
sourceStillId?: string
|
||||||
referenceStillId?: string
|
referenceStillId?: string
|
||||||
loraStack?: import('~/utils/loras').LoraStackItem[]
|
loraStack?: import('~/utils/loras').LoraStackItem[]
|
||||||
|
passes?: { prompt: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runEditV2(job: Job, params: EditV2RunParams) {
|
export async function runEditV2(job: Job, params: EditV2RunParams) {
|
||||||
@@ -54,6 +55,15 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
|||||||
throw new Error('This v2 mode requires still A.')
|
throw new Error('This v2 mode requires still A.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canChain = params.mode === 'edit' || params.mode === 'compose'
|
||||||
|
const extraPasses = canChain
|
||||||
|
? (params.passes || []).map(item => ({ prompt: String(item.prompt || '').trim() })).filter(item => item.prompt)
|
||||||
|
: []
|
||||||
|
const prompts = [params.prompt, ...extraPasses.map(item => item.prompt)]
|
||||||
|
const chainTotal = prompts.length
|
||||||
|
library.chainTotal = chainTotal
|
||||||
|
library.familyId = chainTotal > 1 ? (library.familyId || crypto.randomUUID()) : library.familyId
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ensureComfyReady((status) => {
|
await ensureComfyReady((status) => {
|
||||||
emitChainJob(job, {
|
emitChainJob(job, {
|
||||||
@@ -70,189 +80,255 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
|||||||
const generate = params.mode === 'generate'
|
const generate = params.mode === 'generate'
|
||||||
const engine = parseImageV2Engine(params.engine, 'flux')
|
const engine = parseImageV2Engine(params.engine, 'flux')
|
||||||
const engineLabel = engine === 'krea' ? 'Krea' : 'Flux'
|
const engineLabel = engine === 'krea' ? 'Krea' : 'Flux'
|
||||||
emitChainJob(job, {
|
|
||||||
type: 'status',
|
|
||||||
message: generate
|
|
||||||
? `Queueing ${engineLabel} generate on Beast...`
|
|
||||||
: params.mode === 'refine'
|
|
||||||
? 'Uploading canvas and mask to Beast...'
|
|
||||||
: params.mode === 'compose' ? 'Uploading stills A and B to Beast...' : 'Uploading still A to Beast...',
|
|
||||||
progress: generate ? 12 : 8
|
|
||||||
})
|
|
||||||
const { applyImageFill } = await import('~/server/utils/imagePad')
|
const { applyImageFill } = await import('~/server/utils/imagePad')
|
||||||
const { composeFillPrompt } = await import('~/utils/imageAspect')
|
const { composeFillPrompt } = await import('~/utils/imageAspect')
|
||||||
let image = params.image
|
let current = params.image || null
|
||||||
let mask = params.mask
|
let mask = params.mask
|
||||||
let prompt = params.prompt
|
let parentStillId: string | undefined
|
||||||
let padded = false
|
let uploadedRef: { name: string } | null = null
|
||||||
if (!generate && image) {
|
let uploadedMask: { name: string } | null = null
|
||||||
const filled = await applyImageFill({
|
|
||||||
image,
|
|
||||||
mask: params.mode === 'refine' ? mask : null,
|
|
||||||
aspect: params.aspect || 'auto'
|
|
||||||
})
|
|
||||||
image = filled.image
|
|
||||||
if (filled.mask) mask = filled.mask
|
|
||||||
padded = filled.padded
|
|
||||||
prompt = composeFillPrompt(prompt, padded)
|
|
||||||
if (padded) {
|
|
||||||
emitChainJob(job, {
|
|
||||||
type: 'status',
|
|
||||||
message: `Filling to ${filled.aspect} without stretching...`,
|
|
||||||
progress: 7
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const uploaded = generate || !image ? null : await uploadImage(image, job.id)
|
|
||||||
const uploadedRef = generate || params.mode === 'refine'
|
|
||||||
? null
|
|
||||||
: params.reference
|
|
||||||
? await uploadImage({
|
|
||||||
...params.reference,
|
|
||||||
filename: `ref_${params.reference.filename || 'image_b.png'}`
|
|
||||||
}, job.id)
|
|
||||||
: null
|
|
||||||
const uploadedMask = !generate && params.mode === 'refine' && mask
|
|
||||||
? await uploadImage({
|
|
||||||
...mask,
|
|
||||||
filename: `mask_${mask.filename || 'refine-mask.png'}`
|
|
||||||
}, job.id)
|
|
||||||
: null
|
|
||||||
if (job.status === 'cancelled') throw new Error('Job interrupted.')
|
|
||||||
|
|
||||||
if (!generate) {
|
|
||||||
emitChainJob(job, {
|
|
||||||
type: 'status',
|
|
||||||
message: params.mode === 'refine'
|
|
||||||
? `Queueing ${engineLabel} v2 refine on Beast...`
|
|
||||||
: params.mode === 'compose'
|
|
||||||
? `Queueing ${engineLabel} v2 compose (${params.task}) on Beast...`
|
|
||||||
: `Queueing ${engineLabel} v2 edit on Beast...`,
|
|
||||||
progress: 12
|
|
||||||
})
|
|
||||||
}
|
|
||||||
await ensureComfyLoraNames('image')
|
await ensureComfyLoraNames('image')
|
||||||
if (!generate) await assertImageScaleToTotalPixelsNode()
|
if (!generate) await assertImageScaleToTotalPixelsNode()
|
||||||
const krea = engine === 'krea' ? await resolveKreaGenerateAssets() : null
|
const krea = engine === 'krea' ? await resolveKreaGenerateAssets() : null
|
||||||
const built = buildImageV2Workflow({
|
|
||||||
mode: params.mode,
|
|
||||||
engine,
|
|
||||||
task: params.task,
|
|
||||||
prompt,
|
|
||||||
negative: params.negative,
|
|
||||||
imageAName: uploaded?.name,
|
|
||||||
imageBName: uploadedRef?.name,
|
|
||||||
maskName: uploadedMask?.name,
|
|
||||||
strength: params.strength,
|
|
||||||
snofsModel: params.snofsModel,
|
|
||||||
snofsClip: params.snofsClip,
|
|
||||||
consistencyModel: params.consistencyModel,
|
|
||||||
consistencyClip: params.consistencyClip,
|
|
||||||
steps: params.steps,
|
|
||||||
cfg: params.cfg,
|
|
||||||
seed: params.seed,
|
|
||||||
megapixels: params.megapixels,
|
|
||||||
width: params.width,
|
|
||||||
height: params.height,
|
|
||||||
turbo: params.turbo === true,
|
|
||||||
filenamePrefix: comfyJobPrefix(job.id),
|
|
||||||
kreaUnetName: krea?.unet,
|
|
||||||
kreaClipName: krea?.clip,
|
|
||||||
kreaVaeName: krea?.vae,
|
|
||||||
kreaConceptLora: krea?.conceptLora,
|
|
||||||
loraStack: params.loraStack
|
|
||||||
})
|
|
||||||
const queued = await queuePrompt(built.graph, job.clientId)
|
|
||||||
job.promptId = queued.prompt_id
|
|
||||||
job.status = 'running'
|
|
||||||
emitChainJob(job, {
|
|
||||||
type: 'status',
|
|
||||||
message: `Running ${built.workflowFile}...`,
|
|
||||||
progress: 18,
|
|
||||||
maxStep: params.steps
|
|
||||||
})
|
|
||||||
|
|
||||||
const output = await waitForImageEdit({
|
if (!generate && params.mode === 'compose' && params.reference) {
|
||||||
promptId: queued.prompt_id,
|
uploadedRef = await uploadImage({
|
||||||
clientId: job.clientId,
|
...params.reference,
|
||||||
timeoutMs: 10 * 60 * 1000,
|
filename: `ref_${params.reference.filename || 'image_b.png'}`
|
||||||
engineLabel,
|
}, job.id)
|
||||||
nodeLabel: (node) => {
|
}
|
||||||
if (node === '15' || node === '19') return `Sampling ${engineLabel}`
|
|
||||||
return IMAGE_V2_NODE_LABELS[node]
|
for (let index = 0; index < prompts.length; index++) {
|
||||||
},
|
if (job.status === 'cancelled') throw new Error('Job interrupted.')
|
||||||
onProgress: (event) => {
|
if (index > 0 && library.stopAfterCurrent === true) break
|
||||||
|
const last = index === prompts.length - 1
|
||||||
|
const seed = index === 0 ? params.seed : Math.floor(Math.random() * 2_147_483_647)
|
||||||
|
library.prompt = prompts[index]
|
||||||
|
library.seed = seed
|
||||||
|
library.chainIndex = index
|
||||||
|
library.chainStep = index + 1
|
||||||
|
library.chainLabel = chainTotal > 1 ? `Pass ${index + 1}` : undefined
|
||||||
|
const passName = stillChainName(library.name || '', index)
|
||||||
|
let prompt = prompts[index]
|
||||||
|
let image = current
|
||||||
|
let padded = false
|
||||||
|
|
||||||
|
if (index > 0) {
|
||||||
|
await ensureComfyReady((status) => {
|
||||||
|
emitChainJob(job, {
|
||||||
|
type: status.state === 'busy' ? 'busy' : 'status',
|
||||||
|
message: status.message,
|
||||||
|
progress: status.state === 'online' ? 4 : 2,
|
||||||
|
busy: status.state === 'busy'
|
||||||
|
})
|
||||||
|
}, { skipBusyWait: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
emitChainJob(job, {
|
||||||
|
type: 'status',
|
||||||
|
message: generate
|
||||||
|
? `Queueing ${engineLabel} generate on Beast...`
|
||||||
|
: params.mode === 'refine'
|
||||||
|
? 'Uploading canvas and mask to Beast...'
|
||||||
|
: params.mode === 'compose' ? 'Uploading stills A and B to Beast...' : 'Uploading still A to Beast...',
|
||||||
|
progress: generate ? 12 : 8
|
||||||
|
})
|
||||||
|
|
||||||
|
if (index === 0 && !generate && image) {
|
||||||
|
const filled = await applyImageFill({
|
||||||
|
image,
|
||||||
|
mask: params.mode === 'refine' ? mask : null,
|
||||||
|
aspect: params.aspect || 'auto'
|
||||||
|
})
|
||||||
|
image = filled.image
|
||||||
|
current = filled.image
|
||||||
|
if (filled.mask) {
|
||||||
|
mask = filled.mask
|
||||||
|
uploadedMask = await uploadImage({
|
||||||
|
...filled.mask,
|
||||||
|
filename: `mask_${filled.mask.filename || 'refine-mask.png'}`
|
||||||
|
}, job.id)
|
||||||
|
}
|
||||||
|
padded = filled.padded
|
||||||
|
prompt = composeFillPrompt(prompt, padded)
|
||||||
|
if (padded) {
|
||||||
|
emitChainJob(job, {
|
||||||
|
type: 'status',
|
||||||
|
message: `Filling to ${filled.aspect} without stretching...`,
|
||||||
|
progress: 7
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === 0 && params.mode === 'refine' && mask && !uploadedMask) {
|
||||||
|
uploadedMask = await uploadImage({
|
||||||
|
...mask,
|
||||||
|
filename: `mask_${mask.filename || 'refine-mask.png'}`
|
||||||
|
}, job.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploaded = generate || !image ? null : await uploadImage(image, job.id)
|
||||||
|
if (job.status === 'cancelled') throw new Error('Job interrupted.')
|
||||||
|
|
||||||
|
if (!generate) {
|
||||||
emitChainJob(job, {
|
emitChainJob(job, {
|
||||||
type: 'status',
|
type: 'status',
|
||||||
message: event.message,
|
message: params.mode === 'refine'
|
||||||
progress: event.progress,
|
? `Queueing ${engineLabel} v2 refine on Beast...`
|
||||||
step: event.step,
|
: params.mode === 'compose'
|
||||||
maxStep: event.maxStep || params.steps,
|
? `Queueing ${engineLabel} v2 compose (${params.task}) on Beast...`
|
||||||
node: event.node
|
: `Queueing ${engineLabel} v2 edit on Beast...`,
|
||||||
|
progress: 12
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
isCancelled: () => job.status === 'cancelled'
|
|
||||||
})
|
|
||||||
|
|
||||||
emitChainJob(job, { type: 'status', message: `Saving ${engineLabel} v2 still...`, progress: 94 })
|
const built = buildImageV2Workflow({
|
||||||
const buffer = await downloadEditedImage(output)
|
|
||||||
const size = imageDimensions(buffer)
|
|
||||||
const still = await saveStill({
|
|
||||||
ownerKey: library.ownerKey,
|
|
||||||
folderId: library.folderId,
|
|
||||||
filename: library.name ? `${library.name}.png` : output.filename,
|
|
||||||
data: buffer,
|
|
||||||
width: size?.width || 0,
|
|
||||||
height: size?.height || 0,
|
|
||||||
hideInput: job.hideThumbnail === true,
|
|
||||||
role: 'output',
|
|
||||||
name: library.name || undefined,
|
|
||||||
prompt: params.prompt,
|
|
||||||
settings: {
|
|
||||||
kind: 'imagev2',
|
|
||||||
mode: params.mode,
|
mode: params.mode,
|
||||||
engine,
|
engine,
|
||||||
task: params.task,
|
task: params.task,
|
||||||
|
prompt,
|
||||||
negative: params.negative,
|
negative: params.negative,
|
||||||
|
imageAName: uploaded?.name,
|
||||||
|
imageBName: uploadedRef?.name,
|
||||||
|
maskName: uploadedMask?.name,
|
||||||
|
strength: params.strength,
|
||||||
snofsModel: params.snofsModel,
|
snofsModel: params.snofsModel,
|
||||||
snofsClip: params.snofsClip,
|
snofsClip: params.snofsClip,
|
||||||
consistencyModel: params.consistencyModel,
|
consistencyModel: params.consistencyModel,
|
||||||
consistencyClip: params.consistencyClip,
|
consistencyClip: params.consistencyClip,
|
||||||
steps: params.steps,
|
steps: params.steps,
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
megapixels: params.mode === 'generate' ? undefined : params.megapixels,
|
seed,
|
||||||
|
megapixels: params.megapixels,
|
||||||
|
width: params.width,
|
||||||
|
height: params.height,
|
||||||
turbo: params.turbo === true,
|
turbo: params.turbo === true,
|
||||||
strength: params.mode === 'generate' ? undefined : params.strength,
|
filenamePrefix: chainTotal > 1 ? `${comfyJobPrefix(job.id)}_p${index + 1}` : comfyJobPrefix(job.id),
|
||||||
width: params.mode === 'generate' ? params.width : undefined,
|
kreaUnetName: krea?.unet,
|
||||||
height: params.mode === 'generate' ? params.height : undefined,
|
kreaClipName: krea?.clip,
|
||||||
aspect: params.aspect || 'auto',
|
kreaVaeName: krea?.vae,
|
||||||
seed: params.seed,
|
kreaConceptLora: krea?.conceptLora,
|
||||||
sourceStillId: params.sourceStillId || library.stillId,
|
loraStack: params.loraStack
|
||||||
referenceStillId: params.referenceStillId,
|
})
|
||||||
...persistLoraFields(params.loraStack)
|
const queued = await queuePrompt(built.graph, job.clientId)
|
||||||
}
|
job.promptId = queued.prompt_id
|
||||||
})
|
job.status = 'running'
|
||||||
job.stillId = still?.id
|
emitChainJob(job, {
|
||||||
await purgeComfyArtifacts({
|
type: 'status',
|
||||||
video: { filename: output.filename, subfolder: output.subfolder, type: output.type },
|
message: `Running ${built.workflowFile}...`,
|
||||||
imageName: uploaded?.name,
|
progress: 18,
|
||||||
imageSubfolder: uploaded?.subfolder,
|
maxStep: params.steps
|
||||||
extraImageNames: [uploadedRef?.name, uploadedMask?.name].filter((name): name is string => Boolean(name)),
|
})
|
||||||
promptId: job.promptId
|
|
||||||
})
|
|
||||||
|
|
||||||
job.status = 'complete'
|
const output = await waitForImageEdit({
|
||||||
emitChainJob(job, {
|
promptId: queued.prompt_id,
|
||||||
type: 'complete',
|
clientId: job.clientId,
|
||||||
message: `${engineLabel} v2 finished on Beast`,
|
timeoutMs: 10 * 60 * 1000,
|
||||||
progress: 100,
|
engineLabel,
|
||||||
stillId: still?.id,
|
nodeLabel: (node) => {
|
||||||
filename: output.filename,
|
if (node === '15' || node === '19') return `Sampling ${engineLabel}`
|
||||||
subfolder: output.subfolder,
|
return IMAGE_V2_NODE_LABELS[node]
|
||||||
mediaType: 'image',
|
},
|
||||||
hideThumbnail: job.hideThumbnail,
|
onProgress: (event) => {
|
||||||
folderLocked: library.folderLocked
|
emitChainJob(job, {
|
||||||
})
|
type: 'status',
|
||||||
|
message: event.message,
|
||||||
|
progress: event.progress,
|
||||||
|
step: event.step,
|
||||||
|
maxStep: event.maxStep || params.steps,
|
||||||
|
node: event.node
|
||||||
|
})
|
||||||
|
},
|
||||||
|
isCancelled: () => job.status === 'cancelled'
|
||||||
|
})
|
||||||
|
|
||||||
|
emitChainJob(job, { type: 'status', message: `Saving ${engineLabel} v2 still...`, progress: 94 })
|
||||||
|
const buffer = await downloadEditedImage(output)
|
||||||
|
const size = imageDimensions(buffer)
|
||||||
|
const still = await saveStill({
|
||||||
|
ownerKey: library.ownerKey,
|
||||||
|
folderId: library.folderId,
|
||||||
|
filename: passName ? `${passName}.png` : output.filename,
|
||||||
|
data: buffer,
|
||||||
|
width: size?.width || 0,
|
||||||
|
height: size?.height || 0,
|
||||||
|
hideInput: job.hideThumbnail === true,
|
||||||
|
role: 'output',
|
||||||
|
name: passName || undefined,
|
||||||
|
prompt,
|
||||||
|
familyId: library.familyId,
|
||||||
|
parentStillId,
|
||||||
|
chainIndex: index,
|
||||||
|
settings: {
|
||||||
|
kind: 'imagev2',
|
||||||
|
mode: params.mode,
|
||||||
|
engine,
|
||||||
|
task: params.task,
|
||||||
|
negative: params.negative,
|
||||||
|
snofsModel: params.snofsModel,
|
||||||
|
snofsClip: params.snofsClip,
|
||||||
|
consistencyModel: params.consistencyModel,
|
||||||
|
consistencyClip: params.consistencyClip,
|
||||||
|
steps: params.steps,
|
||||||
|
cfg: params.cfg,
|
||||||
|
megapixels: params.mode === 'generate' ? undefined : params.megapixels,
|
||||||
|
turbo: params.turbo === true,
|
||||||
|
strength: params.mode === 'generate' ? undefined : params.strength,
|
||||||
|
width: params.mode === 'generate' ? params.width : undefined,
|
||||||
|
height: params.mode === 'generate' ? params.height : undefined,
|
||||||
|
aspect: params.aspect || 'auto',
|
||||||
|
seed,
|
||||||
|
sourceStillId: params.sourceStillId || library.stillId,
|
||||||
|
referenceStillId: params.referenceStillId,
|
||||||
|
...persistLoraFields(params.loraStack)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
job.stillId = still?.id
|
||||||
|
parentStillId = still?.id
|
||||||
|
await purgeComfyArtifacts({
|
||||||
|
video: { filename: output.filename, subfolder: output.subfolder, type: output.type },
|
||||||
|
imageName: uploaded?.name,
|
||||||
|
imageSubfolder: uploaded?.subfolder,
|
||||||
|
extraImageNames: last
|
||||||
|
? [uploadedRef?.name, uploadedMask?.name].filter((name): name is string => Boolean(name))
|
||||||
|
: [],
|
||||||
|
promptId: job.promptId
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!last) {
|
||||||
|
emitChainJob(job, {
|
||||||
|
type: 'checkpoint',
|
||||||
|
message: `Pass ${index + 1} saved`,
|
||||||
|
progress: 100,
|
||||||
|
stillId: still?.id,
|
||||||
|
hideThumbnail: job.hideThumbnail,
|
||||||
|
folderLocked: library.folderLocked
|
||||||
|
})
|
||||||
|
current = {
|
||||||
|
filename: still?.filename || `pass_${index + 1}.png`,
|
||||||
|
data: buffer,
|
||||||
|
type: 'image/png'
|
||||||
|
}
|
||||||
|
if (library.stopAfterCurrent === true) break
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
job.status = 'complete'
|
||||||
|
emitChainJob(job, {
|
||||||
|
type: 'complete',
|
||||||
|
message: chainTotal > 1
|
||||||
|
? `${engineLabel} v2 chain finished on Beast`
|
||||||
|
: `${engineLabel} v2 finished on Beast`,
|
||||||
|
progress: 100,
|
||||||
|
stillId: still?.id,
|
||||||
|
filename: output.filename,
|
||||||
|
subfolder: output.subfolder,
|
||||||
|
mediaType: 'image',
|
||||||
|
hideThumbnail: job.hideThumbnail,
|
||||||
|
folderLocked: library.folderLocked
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (job.status !== 'error' && job.status !== 'cancelled') {
|
if (job.status !== 'error' && job.status !== 'cancelled') {
|
||||||
|
|||||||
@@ -1391,6 +1391,15 @@ export function moveStillFamily(owner: string, id: string, folderId: string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setStillHidden(owner: string, id: string, hide: boolean) {
|
||||||
|
return mutate(owner, (catalog) => {
|
||||||
|
const still = catalog.stills.find(item => item.id === id)
|
||||||
|
if (!still) throw createError({ statusCode: 404, statusMessage: 'Image not found' })
|
||||||
|
still.hideInput = hide === true
|
||||||
|
return still
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function renameStill(owner: string, id: string, name: string) {
|
export function renameStill(owner: string, id: string, name: string) {
|
||||||
const trimmed = name.trim().slice(0, 80)
|
const trimmed = name.trim().slice(0, 80)
|
||||||
if (!trimmed) throw createError({ statusCode: 400, statusMessage: 'A name is required' })
|
if (!trimmed) throw createError({ statusCode: 400, statusMessage: 'A name is required' })
|
||||||
@@ -1594,6 +1603,15 @@ export function moveClipFamily(owner: string, id: string, folderId: string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setClipHidden(owner: string, id: string, hide: boolean) {
|
||||||
|
return mutate(owner, (catalog) => {
|
||||||
|
const clip = catalog.clips.find(item => item.id === id)
|
||||||
|
if (!clip) throw createError({ statusCode: 404, statusMessage: 'Clip not found' })
|
||||||
|
clip.hideThumbnail = hide === true
|
||||||
|
return clip
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function patchClipMeta(owner: string, id: string, patch: {
|
export function patchClipMeta(owner: string, id: string, patch: {
|
||||||
globalLocks?: string
|
globalLocks?: string
|
||||||
permanenceRefs?: PermanenceRef[]
|
permanenceRefs?: PermanenceRef[]
|
||||||
|
|||||||
@@ -780,7 +780,9 @@ async function startStudioEditJob(item: StudioJob) {
|
|||||||
familyId: item.familyId,
|
familyId: item.familyId,
|
||||||
chainIndex: 0,
|
chainIndex: 0,
|
||||||
chainStep: 1,
|
chainStep: 1,
|
||||||
chainTotal: 1,
|
chainTotal: 1 + ((mode === 'edit' || mode === 'compose') ? (payload.passes?.length || 0) : 0),
|
||||||
|
chainLabel: (mode === 'edit' || mode === 'compose') && payload.passes?.length ? 'Pass 1' : undefined,
|
||||||
|
passes: payload.passes,
|
||||||
...persistLoraFields(payload.loraStack || payload.loraName)
|
...persistLoraFields(payload.loraStack || payload.loraName)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -793,6 +795,7 @@ async function startStudioEditJob(item: StudioJob) {
|
|||||||
reference: mode === 'refine' || mode === 'generate' ? null : reference,
|
reference: mode === 'refine' || mode === 'generate' ? null : reference,
|
||||||
mask,
|
mask,
|
||||||
prompt: payload.prompt,
|
prompt: payload.prompt,
|
||||||
|
passes: payload.passes || [],
|
||||||
negative: payload.negative || '',
|
negative: payload.negative || '',
|
||||||
steps: payload.steps,
|
steps: payload.steps,
|
||||||
seed: live.library?.seed || payload.seed,
|
seed: live.library?.seed || payload.seed,
|
||||||
|
|||||||
Reference in New Issue
Block a user