Restore Use as input images and move engine pickers up.
Generated stills always fill Still A again (including Generate and queue restore), and Flux/Krea, MiniMax/LTX, and ACE/YuE sit on the input holder so you pick the engine before the prompt.
This commit is contained in:
+106
-94
@@ -27,36 +27,87 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!(studioMode === 'editv2' && v2Mode === 'generate')"
|
||||
class="relative cursor-pointer rounded-2xl border border-dashed border-white/15 bg-zinc-950/50 p-4 transition hover:border-amber-300/50"
|
||||
class="overflow-hidden rounded-2xl border border-white/15 bg-zinc-950/50"
|
||||
:class="{ 'border-amber-300/70 bg-amber-400/5': dragging }"
|
||||
@dragover.prevent="dragging = true"
|
||||
@dragleave.prevent="dragging = false"
|
||||
@drop.prevent="onDrop"
|
||||
@click="openPicker('main')"
|
||||
>
|
||||
<div v-if="studioMode === 'editv2' && v2Mode === 'refine' && preview" class="relative" @click.stop>
|
||||
<ImageV2MaskPainter
|
||||
ref="refinePainter"
|
||||
:src="preview"
|
||||
:hide-preview="hideInputPreview"
|
||||
@painted="refineMaskDirty = $event"
|
||||
/>
|
||||
<button type="button" class="absolute right-3 top-3 z-10 rounded-full bg-zinc-950/80 px-3 py-1 text-xs" @click.stop="resetImage">Reset</button>
|
||||
<div
|
||||
v-if="!(studioMode === 'editv2' && v2Mode === 'generate')"
|
||||
class="relative cursor-pointer border-dashed p-4 transition hover:border-amber-300/50"
|
||||
:class="studioMode === 'editv2' || (studioMode === 'video' && ltxEnabled) ? 'border-b border-white/10' : ''"
|
||||
@dragover.prevent="dragging = true"
|
||||
@dragleave.prevent="dragging = false"
|
||||
@drop.prevent="onDrop"
|
||||
@click="openPicker('main')"
|
||||
>
|
||||
<div v-if="studioMode === 'editv2' && v2Mode === 'refine' && preview" class="relative" @click.stop>
|
||||
<ImageV2MaskPainter
|
||||
ref="refinePainter"
|
||||
:src="preview"
|
||||
:hide-preview="hideInputPreview"
|
||||
@painted="refineMaskDirty = $event"
|
||||
/>
|
||||
<button type="button" class="absolute right-3 top-3 z-10 rounded-full bg-zinc-950/80 px-3 py-1 text-xs" @click.stop="resetImage">Reset</button>
|
||||
</div>
|
||||
<div v-else-if="preview && !hideInputPreview" class="relative">
|
||||
<img :src="preview" alt="Source still" class="max-h-72 w-full rounded-xl object-contain bg-black/40">
|
||||
<button type="button" class="absolute right-3 top-3 rounded-full bg-zinc-950/80 px-3 py-1 text-xs" @click.stop="resetImage">Reset</button>
|
||||
</div>
|
||||
<div v-else-if="preview && hideInputPreview" class="relative flex min-h-44 flex-col items-center justify-center text-center">
|
||||
<span class="sensitive-glow absolute inset-3 rounded-xl" />
|
||||
<p class="relative font-medium">Still loaded · preview hidden</p>
|
||||
<p class="relative mt-1 text-sm text-zinc-500">Toggle Hide input still off to show it</p>
|
||||
<button type="button" class="relative mt-3 rounded-full bg-zinc-950/80 px-3 py-1 text-xs" @click.stop="resetImage">Reset</button>
|
||||
</div>
|
||||
<div v-else class="flex min-h-44 flex-col items-center justify-center text-center">
|
||||
<p class="font-medium">{{ studioMode === 'edit' || studioMode === 'editv2' ? (studioMode === 'editv2' ? (v2Mode === 'refine' ? 'Still A · canvas' : 'Still A') : 'Image 1') : (textToVideo ? 'Start still · optional' : 'Choose an image') }}</p>
|
||||
<p class="mt-1 text-sm text-zinc-500">{{ textToVideo ? 'Text-to-video does not need a still. Shot 2+ will still extend from the last frame.' : 'Browse the library or upload a new PNG, JPG, or WEBP' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="preview && !hideInputPreview" class="relative">
|
||||
<img :src="preview" alt="Source still" class="max-h-72 w-full rounded-xl object-contain bg-black/40">
|
||||
<button type="button" class="absolute right-3 top-3 rounded-full bg-zinc-950/80 px-3 py-1 text-xs" @click.stop="resetImage">Reset</button>
|
||||
<div
|
||||
v-else-if="studioMode === 'editv2' && v2Mode === 'generate' && (file || inputStillId)"
|
||||
class="border-b border-white/10 px-4 py-3 text-sm text-zinc-400"
|
||||
>
|
||||
Still A is loaded for Edit / Compose / Refine. Generate ignores it and uses the prompt only.
|
||||
</div>
|
||||
<div v-else-if="preview && hideInputPreview" class="relative flex min-h-44 flex-col items-center justify-center text-center">
|
||||
<span class="sensitive-glow absolute inset-3 rounded-xl" />
|
||||
<p class="relative font-medium">Still loaded · preview hidden</p>
|
||||
<p class="relative mt-1 text-sm text-zinc-500">Toggle Hide input still off to show it</p>
|
||||
<button type="button" class="relative mt-3 rounded-full bg-zinc-950/80 px-3 py-1 text-xs" @click.stop="resetImage">Reset</button>
|
||||
<div v-if="studioMode === 'editv2'" class="grid grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
class="border-r border-white/10 px-3 py-2.5 text-left text-sm transition"
|
||||
:class="v2Engine === 'flux' ? 'bg-amber-400/10 text-amber-100' : 'text-zinc-400 hover:bg-white/5 hover:text-zinc-200'"
|
||||
@click="setV2Engine('flux')"
|
||||
>
|
||||
<span class="block font-semibold">Flux</span>
|
||||
<span class="text-xs opacity-80">Klein 9B</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2.5 text-left text-sm transition"
|
||||
:class="v2Engine === 'krea' ? 'bg-amber-400/10 text-amber-100' : 'text-zinc-400 hover:bg-white/5 hover:text-zinc-200'"
|
||||
@click="setV2Engine('krea')"
|
||||
>
|
||||
<span class="block font-semibold">Krea</span>
|
||||
<span class="text-xs opacity-80">Krea 2 Turbo</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="flex min-h-44 flex-col items-center justify-center text-center">
|
||||
<p class="font-medium">{{ studioMode === 'edit' || studioMode === 'editv2' ? (studioMode === 'editv2' ? (v2Mode === 'refine' ? 'Still A · canvas' : 'Still A') : 'Image 1') : (textToVideo ? 'Start still · optional' : 'Choose an image') }}</p>
|
||||
<p class="mt-1 text-sm text-zinc-500">{{ textToVideo ? 'Text-to-video does not need a still. Shot 2+ will still extend from the last frame.' : 'Browse the library or upload a new PNG, JPG, or WEBP' }}</p>
|
||||
<div v-else-if="studioMode === 'video' && ltxEnabled" class="grid grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
class="border-r border-white/10 px-3 py-2.5 text-left text-sm transition"
|
||||
:class="videoEngine === 'minimax' ? 'bg-amber-400/10 text-amber-100' : 'text-zinc-400 hover:bg-white/5 hover:text-zinc-200'"
|
||||
@click="selectVideoEngine('minimax')"
|
||||
>
|
||||
<span class="block font-semibold">MiniMax H3</span>
|
||||
<span class="text-xs opacity-80">Stereo · identity refs</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2.5 text-left text-sm transition"
|
||||
:class="videoEngine === 'ltx' ? 'bg-amber-400/10 text-amber-100' : 'text-zinc-400 hover:bg-white/5 hover:text-zinc-200'"
|
||||
@click="selectVideoEngine('ltx')"
|
||||
>
|
||||
<span class="block font-semibold">LTX-2.3</span>
|
||||
<span class="text-xs opacity-80">PinkCherry</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -611,29 +662,6 @@
|
||||
</div>
|
||||
|
||||
<div v-if="studioMode === 'video'" class="space-y-3">
|
||||
<div v-if="ltxEnabled">
|
||||
<p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">Video model</p>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-xl border px-3 py-2 text-left text-sm"
|
||||
:class="videoEngine === 'minimax' ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 hover:border-white/20'"
|
||||
@click="selectVideoEngine('minimax')"
|
||||
>
|
||||
<span class="block font-semibold">MiniMax H3</span>
|
||||
<span class="text-xs text-zinc-400">Native stereo audio. Identity refs on v2.</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-xl border px-3 py-2 text-left text-sm"
|
||||
:class="videoEngine === 'ltx' ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 hover:border-white/20'"
|
||||
@click="selectVideoEngine('ltx')"
|
||||
>
|
||||
<span class="block font-semibold">LTX-2.3 PinkCherry</span>
|
||||
<span class="text-xs text-zinc-400">PinkCherry + distilled LoRA. I2V and T2V.</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">Start from</p>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
@@ -757,28 +785,6 @@
|
||||
</div>
|
||||
<div v-else-if="studioMode === 'editv2'" class="space-y-3">
|
||||
<p class="text-xs text-zinc-500">Edit, Compose, Refine, and Generate are separate graphs. Shares the desktop GPU with video. Landscape / Portrait / Square fill new edges without stretching the still.</p>
|
||||
<p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">Image engine</p>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-xl border px-3 py-2 text-left text-sm"
|
||||
:class="v2Engine === 'flux' ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 hover:border-white/20'"
|
||||
@click="setV2Engine('flux')"
|
||||
>
|
||||
<span class="block font-semibold">Flux</span>
|
||||
<span class="text-xs text-zinc-400">Klein 9B. Edit, compose, refine, and generate.</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-xl border px-3 py-2 text-left text-sm"
|
||||
:class="v2Engine === 'krea' ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 hover:border-white/20'"
|
||||
@click="setV2Engine('krea')"
|
||||
>
|
||||
<span class="block font-semibold">Krea</span>
|
||||
<span class="text-xs text-zinc-400">Krea 2 Turbo. Photograph engine for stills.</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-zinc-500">Flux is Klein 9B. Krea is Krea 2 Turbo. Video stays MiniMax or LTX.</p>
|
||||
<div class="max-w-md">
|
||||
<label
|
||||
v-if="v2ConceptLoraEnabled && imageV2ConceptFile"
|
||||
@@ -5518,14 +5524,17 @@ async function restoreStudioEditJob(payload: {
|
||||
imageLoraStack.value = filterLoraStackForStudio(readLoraStack(payload), ltxEnabled.value)
|
||||
aspect.value = parseImageAspect(payload.aspect)
|
||||
}
|
||||
const generate = v2 && payload.v2Mode === 'generate'
|
||||
if (!generate && payload.stillId) {
|
||||
// Always restore Still A when the payload has one — including Generate jobs —
|
||||
// so switching to Edit / Compose / Refine keeps the image in the input slot.
|
||||
if (payload.stillId) {
|
||||
const blob = await $fetch<Blob>(`/api/library/stills/${payload.stillId}`, { responseType: 'blob' })
|
||||
if (payload.hideInput && showPrivacyToggles.value) hideInputPreview.value = true
|
||||
readFile(new File([blob], stillFileName(stills.value.find(item => item.id === payload.stillId)) || payload.stillFilename || 'still.png', { type: blob.type || 'image/png' }), { stillId: payload.stillId })
|
||||
}
|
||||
if (v2 && payload.v2Mode === 'compose' && payload.referenceStillId) {
|
||||
await loadStillAsEditRef(payload.referenceStillId).catch(() => null)
|
||||
} else if (!(v2 && payload.v2Mode === 'compose')) {
|
||||
clearEditRef()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6349,14 +6358,13 @@ function applyStillSettings(still: LibraryStill) {
|
||||
folderId.value = still.folderId
|
||||
browseFolderId.value = still.folderId
|
||||
rememberInputSeed(settings?.seed)
|
||||
// Stay on the current Video/Image tab. On Video, only restore prompt for
|
||||
// video-origin stills; image stills are start-frame pixels only.
|
||||
if (!imageStudio.value) {
|
||||
if (!dest) applyPromptRestore(still.prompt || '', still)
|
||||
applyPromptRestore(still.prompt || '', still)
|
||||
// Video start frames stay on Video. Image stills open Image with full settings.
|
||||
if (!dest) {
|
||||
studioMode.value = 'video'
|
||||
outputFocus.value = 'video'
|
||||
return
|
||||
}
|
||||
applyPromptRestore(still.prompt || '', still)
|
||||
if (!dest) return
|
||||
if (dest === 'imagev2') {
|
||||
studioMode.value = 'editv2'
|
||||
outputFocus.value = 'edit'
|
||||
@@ -6417,39 +6425,37 @@ async function loadStillAsEditRef(stillId: string) {
|
||||
}
|
||||
|
||||
function stillUseAsInputMessage(still: LibraryStill) {
|
||||
if (!imageStudio.value) {
|
||||
return 'Loaded as start still. Stays on Video — switch to Image only if you want to edit it there.'
|
||||
}
|
||||
const dest = inferStillPipeline(still)
|
||||
const mode = still.settings?.mode
|
||||
if (dest === 'imagev2' && mode === 'generate') return 'Generate settings loaded. Tweak and generate.'
|
||||
if (dest === 'imagev2' && mode === 'generate') return 'Generate settings and still loaded. Switch to Edit anytime — the image is already in Still A.'
|
||||
if (dest === 'imagev2' && mode === 'compose') {
|
||||
return still.settings?.referenceStillId
|
||||
? 'Compose settings loaded. Tweak and generate.'
|
||||
? 'Compose settings loaded with stills A and B.'
|
||||
: 'Compose settings loaded. Re-add still B, then generate.'
|
||||
}
|
||||
if (dest === 'imagev2' && mode === 'refine') return 'Refine settings loaded. Paint a mask, then generate.'
|
||||
if (dest === 'imagev2') return 'Image v2 settings loaded. Tweak and generate.'
|
||||
if (dest === 'image' && still.settings) return 'Image edit settings loaded. Tweak and generate.'
|
||||
return 'Loaded as input. It stays in Output, not the upload library.'
|
||||
if (dest === 'imagev2' && mode === 'refine') return 'Refine settings and canvas still loaded.'
|
||||
if (dest === 'imagev2') return 'Image settings and still loaded.'
|
||||
if (dest === 'image' && still.settings) return 'Image edit settings and still loaded.'
|
||||
return 'Loaded as input with prompt and still.'
|
||||
}
|
||||
|
||||
async function useStillAsInput(still: LibraryStill) {
|
||||
try {
|
||||
applyStillSettings(still)
|
||||
const dest = inferStillPipeline(still)
|
||||
const generate = dest === 'imagev2' && still.settings?.mode === 'generate'
|
||||
if (!generate) {
|
||||
const blob = await $fetch<Blob>(`/api/library/stills/${still.id}`, { responseType: 'blob' })
|
||||
if (still.hideInput && showPrivacyToggles.value) hideInputPreview.value = true
|
||||
readFile(new File([blob], stillFileName(still), { type: blob.type || 'image/png' }), { stillId: still.id })
|
||||
}
|
||||
// Always put the still pixels in the input slot — including Generate outputs —
|
||||
// so Edit / Compose / Refine / Video all have an image after Use as input.
|
||||
const blob = await $fetch<Blob>(`/api/library/stills/${still.id}`, { responseType: 'blob' })
|
||||
if (still.hideInput && showPrivacyToggles.value) hideInputPreview.value = true
|
||||
readFile(new File([blob], stillFileName(still), { type: blob.type || 'image/png' }), { stillId: still.id })
|
||||
if (dest === 'imagev2' && still.settings?.mode === 'compose' && still.settings.referenceStillId) {
|
||||
try {
|
||||
await loadStillAsEditRef(still.settings.referenceStillId)
|
||||
} catch {
|
||||
toast('Compose settings loaded, but still B is missing. Re-add it.')
|
||||
}
|
||||
} else if (dest !== 'imagev2' || still.settings?.mode !== 'compose') {
|
||||
clearEditRef()
|
||||
}
|
||||
scrollToStudioInput()
|
||||
toast(stillUseAsInputMessage(still))
|
||||
@@ -7379,11 +7385,17 @@ async function editImageV2() {
|
||||
|
||||
async function useEditAsInput() {
|
||||
if (!editResultUrl.value) return
|
||||
const stillId = lastV2StillId.value || stillIdFromLibraryUrl(editResultUrl.value)
|
||||
const libraryStill = stillId ? stills.value.find(item => item.id === stillId) : undefined
|
||||
if (libraryStill) {
|
||||
await useStillAsInput(libraryStill)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const blob = await $fetch<Blob>(editResultUrl.value, { responseType: 'blob' })
|
||||
if (!blob || !blob.size) throw new Error('empty still')
|
||||
readFile(new File([blob], `${editDownloadName.value || 'edited'}.png`, { type: blob.type || 'image/png' }), {
|
||||
stillId: stillIdFromLibraryUrl(editResultUrl.value)
|
||||
stillId: stillId || undefined
|
||||
})
|
||||
toast('Edited still is now the input')
|
||||
} catch {
|
||||
|
||||
+17
-21
@@ -10,32 +10,28 @@
|
||||
<StudioKindCards current="music" @select="goStudio" />
|
||||
<div class="grid min-w-0 gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] lg:items-stretch">
|
||||
<section class="glass min-w-0 max-w-full space-y-5 rounded-3xl p-5 sm:p-6">
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold">Music Generator</h2>
|
||||
<p class="mt-1 text-sm text-zinc-400">
|
||||
Tags describe style and voice. Lyrics use structure chips. Instrumental skips the vocal line.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">Engine</p>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="overflow-hidden rounded-2xl border border-white/15 bg-zinc-950/50">
|
||||
<div class="border-b border-white/10 px-4 py-3">
|
||||
<h2 class="font-display text-2xl font-bold">Music Generator</h2>
|
||||
<p class="mt-1 text-sm text-zinc-400">
|
||||
Tags describe style and voice. Lyrics use structure chips. Instrumental skips the vocal line.
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-xl border px-3 py-2 text-left text-sm"
|
||||
:class="engineFamily === 'ace' ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 hover:border-white/20'"
|
||||
class="border-r border-white/10 px-3 py-2.5 text-left text-sm transition"
|
||||
:class="engineFamily === 'ace' ? 'bg-amber-400/10 text-amber-100' : 'text-zinc-400 hover:bg-white/5 hover:text-zinc-200'"
|
||||
@click="selectEngineFamily('ace')"
|
||||
>
|
||||
<span class="block font-semibold">ACE-Step</span>
|
||||
<span class="text-xs text-zinc-400">Native Comfy checkpoint. Fast vocals.</span>
|
||||
<span class="text-xs opacity-80">Fast vocals</span>
|
||||
</button>
|
||||
<div
|
||||
class="cursor-pointer rounded-xl border px-3 py-2 text-left text-sm"
|
||||
:class="engineFamily === 'yue' ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 hover:border-white/20'"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2.5 text-left text-sm transition"
|
||||
:class="engineFamily === 'yue' ? 'bg-amber-400/10 text-amber-100' : 'text-zinc-400 hover:bg-white/5 hover:text-zinc-200'"
|
||||
@click="selectEngineFamily('yue')"
|
||||
@keydown.enter.prevent="selectEngineFamily('yue')"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="font-semibold">YuE</span>
|
||||
@@ -44,8 +40,8 @@
|
||||
text="YuE is a separate lyrics-to-song engine (stage-1 7B + stage-2 1B + xcodec). It needs the ComfyUI_YuE custom node on Beast and is slower and heavier than ACE-Step. Use it when you want a full sung track from lyrics rather than ACE-Step’s tag-conditioned sampler."
|
||||
/>
|
||||
</span>
|
||||
<span class="text-xs text-zinc-400">Lyrics-to-song. Custom node.</span>
|
||||
</div>
|
||||
<span class="text-xs opacity-80">Lyrics-to-song</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user