Add ACE-Step 1.5 and YuE on Music, and put Video/Image/Music cards back.
Recover finished Comfy audio when queueing hangs so a completed track is not stuck on Queueing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+152
-8
@@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<div class="min-h-dvh min-w-0">
|
||||
<AppHeader
|
||||
subtitle="ACE-Step · vocals"
|
||||
:subtitle="headerSubtitle"
|
||||
current="music"
|
||||
:queue-count="queueCount"
|
||||
/>
|
||||
|
||||
<main class="mx-auto grid min-w-0 max-w-7xl gap-6 px-4 py-6 sm:px-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] lg:items-stretch">
|
||||
<main class="mx-auto min-w-0 max-w-7xl space-y-6 px-4 py-6 sm:px-6">
|
||||
<StudioKindCards current="music" />
|
||||
<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>
|
||||
@@ -15,6 +17,56 @@
|
||||
</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">
|
||||
<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'"
|
||||
@click="selectEngineFamily('ace')"
|
||||
>
|
||||
<span class="block font-semibold">ACE-Step</span>
|
||||
<span class="text-xs text-zinc-400">Native Comfy checkpoint. 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"
|
||||
@click="selectEngineFamily('yue')"
|
||||
@keydown.enter.prevent="selectEngineFamily('yue')"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="font-semibold">YuE</span>
|
||||
<InfoTip
|
||||
aria-label="What YuE does"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="engineFamily === 'ace'" class="flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3 text-sm">
|
||||
<span class="min-w-0">
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="block font-medium text-zinc-200">ACE-Step 1.5</span>
|
||||
<InfoTip
|
||||
aria-label="What ACE-Step 1.5 does"
|
||||
:text="ace15Info"
|
||||
/>
|
||||
</span>
|
||||
<span class="mt-0.5 block text-xs text-zinc-500">Turbo AIO checkpoint. Different graph than v1, usually 8 steps.</span>
|
||||
</span>
|
||||
<label class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer items-center" title="Toggle ACE-Step 1.5">
|
||||
<input v-model="ace15" type="checkbox" class="peer sr-only" role="switch">
|
||||
<span class="absolute inset-0 rounded-full bg-zinc-700 transition peer-checked:bg-amber-400" />
|
||||
<span class="absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-white transition peer-checked:translate-x-5" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="rounded-2xl border border-red-400/30 bg-red-400/10 px-4 py-3 text-sm text-red-200">{{ error }}</p>
|
||||
|
||||
<label class="block text-sm">
|
||||
@@ -66,9 +118,9 @@
|
||||
:max="durationMax"
|
||||
step="5"
|
||||
>
|
||||
<span class="mt-1 block text-[11px] text-zinc-500">{{ durationMin }}–{{ durationMax }} seconds</span>
|
||||
<span class="mt-1 block text-[11px] text-zinc-500">{{ engineFamily === 'yue' ? 'YuE uses this as stage-A length' : `${durationMin}–${durationMax} seconds` }}</span>
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
<label v-if="engineFamily !== 'yue'" class="block text-sm">
|
||||
<span class="mb-1 block font-medium text-zinc-300">Steps · {{ steps }}</span>
|
||||
<input
|
||||
v-model.number="steps"
|
||||
@@ -78,7 +130,7 @@
|
||||
:max="stepsMax"
|
||||
step="1"
|
||||
>
|
||||
<span class="mt-1 block text-[11px] text-zinc-500">30–50 is the usual range</span>
|
||||
<span class="mt-1 block text-[11px] text-zinc-500">{{ ace15 ? '8 is the usual 1.5 turbo range' : '30–50 is the usual range' }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -167,6 +219,7 @@
|
||||
<NuxtLink to="/library" class="text-amber-200 hover:text-amber-100">Open Library</NuxtLink>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<QueuedJobModal :job-id="inspectJobId" @close="inspectJobId = ''" />
|
||||
</div>
|
||||
@@ -176,13 +229,18 @@
|
||||
import {
|
||||
DEFAULT_MUSIC_LYRICS,
|
||||
DEFAULT_MUSIC_TAGS,
|
||||
MUSIC_CFG_DEFAULT,
|
||||
MUSIC_CFG_DEFAULT_15,
|
||||
MUSIC_DURATION_DEFAULT,
|
||||
MUSIC_DURATION_MAX,
|
||||
MUSIC_DURATION_MIN,
|
||||
MUSIC_STEPS_DEFAULT,
|
||||
MUSIC_STEPS_DEFAULT_15,
|
||||
MUSIC_STEPS_MAX,
|
||||
MUSIC_STEPS_MIN,
|
||||
MUSIC_STRUCTURE_CHIPS
|
||||
MUSIC_STRUCTURE_CHIPS,
|
||||
parseMusicEngine,
|
||||
type MusicEngine
|
||||
} from '~/utils/music'
|
||||
import { ensureGenerationNotifyPermission, notifyGenerationComplete } from '~/utils/generationNotify'
|
||||
import type { LibraryFolder } from '~/utils/libraryBrowse'
|
||||
@@ -197,6 +255,8 @@ const durationMax = MUSIC_DURATION_MAX
|
||||
const stepsMin = MUSIC_STEPS_MIN
|
||||
const stepsMax = MUSIC_STEPS_MAX
|
||||
|
||||
const ace15Info = 'Turns on ACE-Step 1.5 Turbo, the all-in-one checkpoint already on Beast. 1.5 uses a different Comfy graph than v1: a 1.5 text encoder (tags, BPM, key, duration) and EmptyAceStep1.5LatentAudio, usually with 8 sampler steps and CFG 1. Songs often come back faster and cleaner. Lyrics-strength from v1 does not apply. Leave this off to stay on ACE-Step v1 3.5B, which is the graph that already generated on this GPU.'
|
||||
|
||||
const tags = ref(DEFAULT_MUSIC_TAGS)
|
||||
const lyrics = ref(DEFAULT_MUSIC_LYRICS)
|
||||
const instrumental = ref(false)
|
||||
@@ -207,6 +267,8 @@ const name = ref('')
|
||||
const folderId = ref('')
|
||||
const folders = ref<LibraryFolder[]>([])
|
||||
const lyricsEl = ref<HTMLTextAreaElement | null>(null)
|
||||
const ace15 = ref(false)
|
||||
const engineFamily = ref<'ace' | 'yue'>('ace')
|
||||
const busy = ref(false)
|
||||
const error = ref('')
|
||||
const status = ref('')
|
||||
@@ -226,8 +288,17 @@ let source: EventSource | null = null
|
||||
let listenGen = 0
|
||||
let settled = false
|
||||
let queuePoll: ReturnType<typeof setInterval> | null = null
|
||||
let recoverPoll: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const blocked = computed(() => !tags.value.trim() || (!instrumental.value && !lyrics.value.trim()) || !folderId.value)
|
||||
const headerSubtitle = computed(() => {
|
||||
if (engineFamily.value === 'yue') return 'YuE · lyrics-to-song'
|
||||
return ace15.value ? 'ACE-Step 1.5 · vocals' : 'ACE-Step · vocals'
|
||||
})
|
||||
const selectedEngine = computed<MusicEngine>(() => {
|
||||
if (engineFamily.value === 'yue') return 'yue'
|
||||
return ace15.value ? 'ace-step-1.5' : 'ace-step'
|
||||
})
|
||||
const blockReason = computed(() => {
|
||||
if (!folderId.value) return 'Choose a library folder before generating.'
|
||||
if (!tags.value.trim()) return 'Add genre and style tags.'
|
||||
@@ -235,6 +306,32 @@ const blockReason = computed(() => {
|
||||
return ''
|
||||
})
|
||||
|
||||
function selectEngineFamily(family: 'ace' | 'yue') {
|
||||
engineFamily.value = family
|
||||
if (family === 'ace' && ace15.value && steps.value === MUSIC_STEPS_DEFAULT) {
|
||||
steps.value = MUSIC_STEPS_DEFAULT_15
|
||||
}
|
||||
if (family === 'ace' && !ace15.value && steps.value === MUSIC_STEPS_DEFAULT_15) {
|
||||
steps.value = MUSIC_STEPS_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
function applyEngine(engine: MusicEngine) {
|
||||
if (engine === 'yue') {
|
||||
engineFamily.value = 'yue'
|
||||
ace15.value = false
|
||||
return
|
||||
}
|
||||
engineFamily.value = 'ace'
|
||||
ace15.value = engine === 'ace-step-1.5'
|
||||
}
|
||||
|
||||
watch(ace15, (on) => {
|
||||
if (engineFamily.value !== 'ace') return
|
||||
if (on && (steps.value === MUSIC_STEPS_DEFAULT || steps.value >= 30)) steps.value = MUSIC_STEPS_DEFAULT_15
|
||||
if (!on && steps.value === MUSIC_STEPS_DEFAULT_15) steps.value = MUSIC_STEPS_DEFAULT
|
||||
})
|
||||
|
||||
function insertChip(chip: string) {
|
||||
const el = lyricsEl.value
|
||||
const token = `${chip}\n`
|
||||
@@ -273,6 +370,26 @@ function stopQueuePoll() {
|
||||
}
|
||||
}
|
||||
|
||||
function stopRecoverPoll() {
|
||||
if (recoverPoll) {
|
||||
clearInterval(recoverPoll)
|
||||
recoverPoll = null
|
||||
}
|
||||
}
|
||||
|
||||
async function tryRecover() {
|
||||
if (settled || !busy.value || queued.value) return
|
||||
try {
|
||||
const recovered = await $fetch<Record<string, any>>('/api/generate/recover', {
|
||||
method: 'POST',
|
||||
body: { jobId: jobId.value || undefined }
|
||||
})
|
||||
if (recovered?.trackId) applyEvent(recovered)
|
||||
} catch {
|
||||
/* still running */
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshStudioQueue() {
|
||||
const data = await $fetch<{
|
||||
jobs?: Array<{
|
||||
@@ -341,6 +458,7 @@ function applyEvent(payload: Record<string, any>) {
|
||||
stopListen()
|
||||
queued.value = false
|
||||
studioJobId.value = ''
|
||||
stopRecoverPoll()
|
||||
stopQueuePoll()
|
||||
queuePoll = setInterval(() => { void refreshStudioQueue() }, 8000)
|
||||
void notifyGenerationComplete('music')
|
||||
@@ -352,6 +470,7 @@ function applyEvent(payload: Record<string, any>) {
|
||||
status.value = error.value
|
||||
queued.value = false
|
||||
stopListen()
|
||||
stopRecoverPoll()
|
||||
stopQueuePoll()
|
||||
queuePoll = setInterval(() => { void refreshStudioQueue() }, 8000)
|
||||
}
|
||||
@@ -375,6 +494,9 @@ function listen(id: string) {
|
||||
listen(id)
|
||||
}, 2000)
|
||||
}
|
||||
if (!recoverPoll) {
|
||||
recoverPoll = setInterval(() => { void tryRecover() }, 8000)
|
||||
}
|
||||
}
|
||||
|
||||
async function generate() {
|
||||
@@ -392,7 +514,11 @@ async function generate() {
|
||||
trackId.value = ''
|
||||
jobId.value = ''
|
||||
studioJobId.value = ''
|
||||
status.value = 'Queueing ACE-Step…'
|
||||
status.value = engineFamily.value === 'yue'
|
||||
? 'Queueing YuE…'
|
||||
: ace15.value
|
||||
? 'Queueing ACE-Step 1.5…'
|
||||
: 'Queueing ACE-Step…'
|
||||
try {
|
||||
const started = await $fetch<{ jobId: string; studioJobId?: string; queued?: boolean; name?: string }>(
|
||||
'/api/generate/music',
|
||||
@@ -404,9 +530,11 @@ async function generate() {
|
||||
duration: duration.value,
|
||||
steps: steps.value,
|
||||
seed: seed.value || 'random',
|
||||
cfg: selectedEngine.value === 'ace-step-1.5' ? MUSIC_CFG_DEFAULT_15 : MUSIC_CFG_DEFAULT,
|
||||
instrumental: instrumental.value,
|
||||
folderId: folderId.value,
|
||||
name: name.value.trim()
|
||||
name: name.value.trim(),
|
||||
engine: selectedEngine.value
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -422,6 +550,9 @@ async function generate() {
|
||||
}
|
||||
jobId.value = started.jobId
|
||||
listen(started.jobId)
|
||||
stopRecoverPoll()
|
||||
recoverPoll = setInterval(() => { void tryRecover() }, 8000)
|
||||
window.setTimeout(() => { void tryRecover() }, 12000)
|
||||
} catch (err: any) {
|
||||
busy.value = false
|
||||
queued.value = false
|
||||
@@ -448,6 +579,7 @@ function applyIncomingMusic() {
|
||||
steps?: number
|
||||
seed?: string
|
||||
folderId?: string
|
||||
engine?: string
|
||||
}
|
||||
if (typeof loaded.tags === 'string') tags.value = loaded.tags
|
||||
if (typeof loaded.lyrics === 'string') lyrics.value = loaded.lyrics
|
||||
@@ -457,6 +589,7 @@ function applyIncomingMusic() {
|
||||
if (typeof loaded.seed === 'string') seed.value = loaded.seed
|
||||
if (typeof loaded.name === 'string') name.value = loaded.name
|
||||
if (loaded.folderId && folders.value.some(folder => folder.id === loaded.folderId)) folderId.value = loaded.folderId
|
||||
if (loaded.engine) applyEngine(parseMusicEngine(loaded.engine))
|
||||
status.value = 'Loaded from the queue'
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
@@ -476,6 +609,7 @@ function applyPreset() {
|
||||
if (typeof settings.duration === 'number') duration.value = settings.duration
|
||||
if (typeof settings.steps === 'number') steps.value = settings.steps
|
||||
if (typeof settings.seed === 'number') seed.value = String(settings.seed)
|
||||
if (settings.engine) applyEngine(settings.engine)
|
||||
name.value = preset.name
|
||||
}
|
||||
|
||||
@@ -491,11 +625,21 @@ onMounted(async () => {
|
||||
audioUrl.value = `/api/library/tracks/${query.track}/audio`
|
||||
trackId.value = query.track
|
||||
status.value = 'Loaded from library'
|
||||
return
|
||||
}
|
||||
if (!audioUrl.value) {
|
||||
try {
|
||||
const recovered = await $fetch<Record<string, any>>('/api/generate/recover', { method: 'POST', body: {} })
|
||||
if (recovered?.trackId && /recovered/i.test(String(recovered.message || ''))) {
|
||||
applyEvent({ ...recovered, type: 'complete' })
|
||||
}
|
||||
} catch { /* nothing waiting in ComfyUI */ }
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopListen()
|
||||
stopRecoverPoll()
|
||||
stopQueuePoll()
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user