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>
<div <div
v-if="!(studioMode === 'editv2' && v2Mode === 'generate')" class="overflow-hidden rounded-2xl border border-white/15 bg-zinc-950/50"
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="{ 'border-amber-300/70 bg-amber-400/5': dragging }" :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" @dragover.prevent="dragging = true"
@dragleave.prevent="dragging = false" @dragleave.prevent="dragging = false"
@drop.prevent="onDrop" @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> <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> </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"> <div v-if="file || inputStillId" class="mt-2 flex min-w-0 items-center gap-2">
<input <input
@@ -611,29 +662,6 @@
</div> </div>
<div v-if="studioMode === 'video'" class="space-y-3"> <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> <div>
<p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">Start from</p> <p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">Start from</p>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
@@ -757,28 +785,6 @@
</div> </div>
<div v-else-if="studioMode === 'editv2'" class="space-y-3"> <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="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"> <div class="max-w-md">
<label <label
v-if="v2ConceptLoraEnabled && imageV2ConceptFile" v-if="v2ConceptLoraEnabled && imageV2ConceptFile"
@@ -5518,14 +5524,17 @@ async function restoreStudioEditJob(payload: {
imageLoraStack.value = filterLoraStackForStudio(readLoraStack(payload), ltxEnabled.value) imageLoraStack.value = filterLoraStackForStudio(readLoraStack(payload), ltxEnabled.value)
aspect.value = parseImageAspect(payload.aspect) aspect.value = parseImageAspect(payload.aspect)
} }
const generate = v2 && payload.v2Mode === 'generate' // Always restore Still A when the payload has one — including Generate jobs —
if (!generate && payload.stillId) { // 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' }) const blob = await $fetch<Blob>(`/api/library/stills/${payload.stillId}`, { responseType: 'blob' })
if (payload.hideInput && showPrivacyToggles.value) hideInputPreview.value = true 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 }) 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) { if (v2 && payload.v2Mode === 'compose' && payload.referenceStillId) {
await loadStillAsEditRef(payload.referenceStillId).catch(() => null) 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 folderId.value = still.folderId
browseFolderId.value = still.folderId browseFolderId.value = still.folderId
rememberInputSeed(settings?.seed) rememberInputSeed(settings?.seed)
// Stay on the current Video/Image tab. On Video, only restore prompt for applyPromptRestore(still.prompt || '', still)
// video-origin stills; image stills are start-frame pixels only. // Video start frames stay on Video. Image stills open Image with full settings.
if (!imageStudio.value) { if (!dest) {
if (!dest) applyPromptRestore(still.prompt || '', still) studioMode.value = 'video'
outputFocus.value = 'video'
return return
} }
applyPromptRestore(still.prompt || '', still)
if (!dest) return
if (dest === 'imagev2') { if (dest === 'imagev2') {
studioMode.value = 'editv2' studioMode.value = 'editv2'
outputFocus.value = 'edit' outputFocus.value = 'edit'
@@ -6417,39 +6425,37 @@ async function loadStillAsEditRef(stillId: string) {
} }
function stillUseAsInputMessage(still: LibraryStill) { 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 dest = inferStillPipeline(still)
const mode = still.settings?.mode 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') { if (dest === 'imagev2' && mode === 'compose') {
return still.settings?.referenceStillId 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.' : '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' && mode === 'refine') return 'Refine settings and canvas still loaded.'
if (dest === 'imagev2') return 'Image v2 settings loaded. Tweak and generate.' if (dest === 'imagev2') return 'Image settings and still loaded.'
if (dest === 'image' && still.settings) return 'Image edit settings loaded. Tweak and generate.' if (dest === 'image' && still.settings) return 'Image edit settings and still loaded.'
return 'Loaded as input. It stays in Output, not the upload library.' return 'Loaded as input with prompt and still.'
} }
async function useStillAsInput(still: LibraryStill) { async function useStillAsInput(still: LibraryStill) {
try { try {
applyStillSettings(still) applyStillSettings(still)
const dest = inferStillPipeline(still) const dest = inferStillPipeline(still)
const generate = dest === 'imagev2' && still.settings?.mode === 'generate' // Always put the still pixels in the input slot — including Generate outputs —
if (!generate) { // 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' }) const blob = await $fetch<Blob>(`/api/library/stills/${still.id}`, { responseType: 'blob' })
if (still.hideInput && showPrivacyToggles.value) hideInputPreview.value = true if (still.hideInput && showPrivacyToggles.value) hideInputPreview.value = true
readFile(new File([blob], stillFileName(still), { type: blob.type || 'image/png' }), { stillId: still.id }) readFile(new File([blob], stillFileName(still), { type: blob.type || 'image/png' }), { stillId: still.id })
}
if (dest === 'imagev2' && still.settings?.mode === 'compose' && still.settings.referenceStillId) { if (dest === 'imagev2' && still.settings?.mode === 'compose' && still.settings.referenceStillId) {
try { try {
await loadStillAsEditRef(still.settings.referenceStillId) await loadStillAsEditRef(still.settings.referenceStillId)
} catch { } catch {
toast('Compose settings loaded, but still B is missing. Re-add it.') toast('Compose settings loaded, but still B is missing. Re-add it.')
} }
} else if (dest !== 'imagev2' || still.settings?.mode !== 'compose') {
clearEditRef()
} }
scrollToStudioInput() scrollToStudioInput()
toast(stillUseAsInputMessage(still)) toast(stillUseAsInputMessage(still))
@@ -7379,11 +7385,17 @@ async function editImageV2() {
async function useEditAsInput() { async function useEditAsInput() {
if (!editResultUrl.value) return 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 { try {
const blob = await $fetch<Blob>(editResultUrl.value, { responseType: 'blob' }) const blob = await $fetch<Blob>(editResultUrl.value, { responseType: 'blob' })
if (!blob || !blob.size) throw new Error('empty still') if (!blob || !blob.size) throw new Error('empty still')
readFile(new File([blob], `${editDownloadName.value || 'edited'}.png`, { type: blob.type || 'image/png' }), { 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') toast('Edited still is now the input')
} catch { } catch {
+12 -16
View File
@@ -10,32 +10,28 @@
<StudioKindCards current="music" @select="goStudio" /> <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"> <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"> <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> <h2 class="font-display text-2xl font-bold">Music Generator</h2>
<p class="mt-1 text-sm text-zinc-400"> <p class="mt-1 text-sm text-zinc-400">
Tags describe style and voice. Lyrics use structure chips. Instrumental skips the vocal line. Tags describe style and voice. Lyrics use structure chips. Instrumental skips the vocal line.
</p> </p>
</div> </div>
<div class="grid grid-cols-2">
<div>
<p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">Engine</p>
<div class="grid grid-cols-2 gap-2">
<button <button
type="button" type="button"
class="rounded-xl border px-3 py-2 text-left text-sm" class="border-r border-white/10 px-3 py-2.5 text-left text-sm transition"
:class="engineFamily === 'ace' ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 hover:border-white/20'" :class="engineFamily === 'ace' ? 'bg-amber-400/10 text-amber-100' : 'text-zinc-400 hover:bg-white/5 hover:text-zinc-200'"
@click="selectEngineFamily('ace')" @click="selectEngineFamily('ace')"
> >
<span class="block font-semibold">ACE-Step</span> <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> </button>
<div <button
class="cursor-pointer rounded-xl border px-3 py-2 text-left text-sm" type="button"
:class="engineFamily === 'yue' ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 hover:border-white/20'" class="px-3 py-2.5 text-left text-sm transition"
role="button" :class="engineFamily === 'yue' ? 'bg-amber-400/10 text-amber-100' : 'text-zinc-400 hover:bg-white/5 hover:text-zinc-200'"
tabindex="0"
@click="selectEngineFamily('yue')" @click="selectEngineFamily('yue')"
@keydown.enter.prevent="selectEngineFamily('yue')"
> >
<span class="flex items-center gap-2"> <span class="flex items-center gap-2">
<span class="font-semibold">YuE</span> <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." 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>
<span class="text-xs text-zinc-400">Lyrics-to-song. Custom node.</span> <span class="text-xs opacity-80">Lyrics-to-song</span>
</div> </button>
</div> </div>
</div> </div>
+1 -1
View File
@@ -26,7 +26,7 @@ export function loraBasename(name: string) {
} }
export function loraIdentityKey(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) { export function isXaigenOnlyLora(name: string) {