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:
2026-09-04 22:14:21 -05:00
parent b481c4e3ad
commit eb0953b0e6
3 changed files with 124 additions and 116 deletions
+80 -68
View File
@@ -27,9 +27,13 @@
</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 }"
>
<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"
@@ -59,6 +63,53 @@
<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="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-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-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>
<div v-if="file || inputStillId" class="mt-2 flex min-w-0 items-center gap-2">
<input
@@ -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) {
// 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 {
+12 -16
View File
@@ -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>
<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>
<p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">Engine</p>
<div class="grid grid-cols-2 gap-2">
<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>
+1 -1
View File
@@ -26,7 +26,7 @@ export function loraBasename(name: string) {
}
export function loraIdentityKey(name: string) {
return loraBasename(name).replace(/^xaigen-?/i, '').toLowerCase()
return loraBasename(name).replace(/^(xai|xia)gen-?/i, '').toLowerCase()
}
export function isXaigenOnlyLora(name: string) {