Put ACE-Step music jobs on the same studio queue as video and image.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+5
-1
@@ -1195,7 +1195,8 @@
|
|||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<p class="truncate text-sm font-medium text-zinc-100">{{ job.name }}</p>
|
<p class="truncate text-sm font-medium text-zinc-100">{{ job.name }}</p>
|
||||||
<p class="mt-0.5 text-xs text-zinc-500">
|
<p class="mt-0.5 text-xs text-zinc-500">
|
||||||
<span v-if="job.kind === 'edit'" class="mr-1 text-amber-200/80">{{ job.imagePipeline === 'v2' ? 'IMAGE V2' : ((job.shotCount || 1) > 1 ? 'EDIT' : 'IMAGE') }} ·</span>
|
<span v-if="job.kind === 'music'" class="mr-1 text-amber-200/80">MUSIC ·</span>
|
||||||
|
<span v-else-if="job.kind === 'edit'" class="mr-1 text-amber-200/80">{{ job.imagePipeline === 'v2' ? 'IMAGE V2' : ((job.shotCount || 1) > 1 ? 'EDIT' : 'IMAGE') }} ·</span>
|
||||||
{{ studioJobStatusLabel(job) }}
|
{{ studioJobStatusLabel(job) }}
|
||||||
· {{ studioJobShotLine(job) }}
|
· {{ studioJobShotLine(job) }}
|
||||||
</p>
|
</p>
|
||||||
@@ -5170,6 +5171,7 @@ function studioJobStatusLabel(job: { status: string; cutIn?: boolean; holdForCut
|
|||||||
}
|
}
|
||||||
|
|
||||||
function studioJobShotLine(job: { kind?: string; shotCount: number; completedCount?: number; status: string }) {
|
function studioJobShotLine(job: { kind?: string; shotCount: number; completedCount?: number; status: string }) {
|
||||||
|
if (job.kind === 'music') return 'ACE-Step'
|
||||||
if (job.kind === 'edit') {
|
if (job.kind === 'edit') {
|
||||||
const total = job.shotCount || 1
|
const total = job.shotCount || 1
|
||||||
return total <= 1 ? 'image edit' : `${total} passes`
|
return total <= 1 ? 'image edit' : `${total} passes`
|
||||||
@@ -5223,6 +5225,8 @@ async function refreshStudioQueue() {
|
|||||||
} else if (running.id && running.liveJobId === editJobId.value) {
|
} else if (running.id && running.liveJobId === editJobId.value) {
|
||||||
outputStudioJobId.value = running.id
|
outputStudioJobId.value = running.id
|
||||||
}
|
}
|
||||||
|
} else if (running?.kind === 'music') {
|
||||||
|
return
|
||||||
} else if (running?.liveJobId && running.liveJobId !== videoJobId.value && !videoBusy.value && !watchingLiveVideo() && !extendOpen.value) {
|
} else if (running?.liveJobId && running.liveJobId !== videoJobId.value && !videoBusy.value && !watchingLiveVideo() && !extendOpen.value) {
|
||||||
outputStudioJobId.value = running.id
|
outputStudioJobId.value = running.id
|
||||||
videoJobId.value = running.liveJobId
|
videoJobId.value = running.liveJobId
|
||||||
|
|||||||
+84
-2
@@ -3,6 +3,7 @@
|
|||||||
<AppHeader
|
<AppHeader
|
||||||
subtitle="ACE-Step · vocals"
|
subtitle="ACE-Step · vocals"
|
||||||
current="music"
|
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 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">
|
||||||
@@ -121,6 +122,10 @@
|
|||||||
{{ busy ? (status || 'Generating…') : 'Generate' }}
|
{{ busy ? (status || 'Generating…') : 'Generate' }}
|
||||||
</button>
|
</button>
|
||||||
<p v-if="blockReason" class="text-xs text-zinc-400">{{ blockReason }}</p>
|
<p v-if="blockReason" class="text-xs text-zinc-400">{{ blockReason }}</p>
|
||||||
|
<p v-else-if="queued" class="text-xs text-zinc-400">
|
||||||
|
Waiting in the job queue.
|
||||||
|
<NuxtLink to="/queue" class="text-amber-200 hover:text-amber-100">Open Queue</NuxtLink>
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="glass min-w-0 max-w-full space-y-4 rounded-3xl p-5 sm:p-6">
|
<section class="glass min-w-0 max-w-full space-y-4 rounded-3xl p-5 sm:p-6">
|
||||||
@@ -188,10 +193,14 @@ const progress = ref(0)
|
|||||||
const audioUrl = ref('')
|
const audioUrl = ref('')
|
||||||
const trackId = ref('')
|
const trackId = ref('')
|
||||||
const jobId = ref('')
|
const jobId = ref('')
|
||||||
|
const studioJobId = ref('')
|
||||||
|
const queued = ref(false)
|
||||||
|
const queueCount = ref(0)
|
||||||
const downloadName = ref('track.flac')
|
const downloadName = ref('track.flac')
|
||||||
let source: EventSource | null = null
|
let source: EventSource | null = null
|
||||||
let listenGen = 0
|
let listenGen = 0
|
||||||
let settled = false
|
let settled = false
|
||||||
|
let queuePoll: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
const blocked = computed(() => !tags.value.trim() || (!instrumental.value && !lyrics.value.trim()) || !folderId.value)
|
const blocked = computed(() => !tags.value.trim() || (!instrumental.value && !lyrics.value.trim()) || !folderId.value)
|
||||||
const blockReason = computed(() => {
|
const blockReason = computed(() => {
|
||||||
@@ -232,6 +241,55 @@ function stopListen() {
|
|||||||
source = null
|
source = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stopQueuePoll() {
|
||||||
|
if (queuePoll) {
|
||||||
|
clearInterval(queuePoll)
|
||||||
|
queuePoll = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshStudioQueue() {
|
||||||
|
const data = await $fetch<{
|
||||||
|
jobs?: Array<{
|
||||||
|
id: string
|
||||||
|
status: string
|
||||||
|
liveJobId?: string
|
||||||
|
lastError?: string
|
||||||
|
}>
|
||||||
|
waitingCount?: number
|
||||||
|
}>('/api/studio-queue').catch(() => ({ jobs: [] as Array<{ id: string; status: string; liveJobId?: string; lastError?: string }>, waitingCount: 0 }))
|
||||||
|
const rows = data.jobs || []
|
||||||
|
queueCount.value = data.waitingCount || rows.filter(job => job.status === 'waiting').length
|
||||||
|
if (!studioJobId.value) return
|
||||||
|
const row = rows.find(job => job.id === studioJobId.value)
|
||||||
|
if (!row) {
|
||||||
|
if (queued.value && !jobId.value) {
|
||||||
|
queued.value = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (row.status === 'waiting' || (row.status === 'held' && !row.liveJobId)) {
|
||||||
|
queued.value = true
|
||||||
|
busy.value = true
|
||||||
|
status.value = row.status === 'held' ? 'Paused in the job queue' : 'Waiting in the job queue…'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (row.status === 'error') {
|
||||||
|
queued.value = false
|
||||||
|
busy.value = false
|
||||||
|
error.value = row.lastError || 'Generation failed'
|
||||||
|
status.value = error.value
|
||||||
|
stopQueuePoll()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (row.liveJobId && row.liveJobId !== jobId.value) {
|
||||||
|
queued.value = false
|
||||||
|
jobId.value = row.liveJobId
|
||||||
|
status.value = status.value || 'Generating…'
|
||||||
|
listen(row.liveJobId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyEvent(payload: Record<string, any>) {
|
function applyEvent(payload: Record<string, any>) {
|
||||||
if (payload.message) status.value = payload.message
|
if (payload.message) status.value = payload.message
|
||||||
if (typeof payload.progress === 'number') progress.value = payload.progress
|
if (typeof payload.progress === 'number') progress.value = payload.progress
|
||||||
@@ -253,6 +311,10 @@ function applyEvent(payload: Record<string, any>) {
|
|||||||
audioUrl.value = `/api/media?${params.toString()}`
|
audioUrl.value = `/api/media?${params.toString()}`
|
||||||
}
|
}
|
||||||
stopListen()
|
stopListen()
|
||||||
|
queued.value = false
|
||||||
|
studioJobId.value = ''
|
||||||
|
stopQueuePoll()
|
||||||
|
queuePoll = setInterval(() => { void refreshStudioQueue() }, 8000)
|
||||||
void notifyGenerationComplete('music')
|
void notifyGenerationComplete('music')
|
||||||
}
|
}
|
||||||
if (payload.type === 'error' || payload.status === 'error') {
|
if (payload.type === 'error' || payload.status === 'error') {
|
||||||
@@ -260,7 +322,10 @@ function applyEvent(payload: Record<string, any>) {
|
|||||||
busy.value = false
|
busy.value = false
|
||||||
error.value = payload.error || payload.message || 'Generation failed'
|
error.value = payload.error || payload.message || 'Generation failed'
|
||||||
status.value = error.value
|
status.value = error.value
|
||||||
|
queued.value = false
|
||||||
stopListen()
|
stopListen()
|
||||||
|
stopQueuePoll()
|
||||||
|
queuePoll = setInterval(() => { void refreshStudioQueue() }, 8000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,12 +358,15 @@ async function generate() {
|
|||||||
await ensureGenerationNotifyPermission()
|
await ensureGenerationNotifyPermission()
|
||||||
busy.value = true
|
busy.value = true
|
||||||
settled = false
|
settled = false
|
||||||
|
queued.value = false
|
||||||
progress.value = 2
|
progress.value = 2
|
||||||
audioUrl.value = ''
|
audioUrl.value = ''
|
||||||
trackId.value = ''
|
trackId.value = ''
|
||||||
|
jobId.value = ''
|
||||||
|
studioJobId.value = ''
|
||||||
status.value = 'Queueing ACE-Step…'
|
status.value = 'Queueing ACE-Step…'
|
||||||
try {
|
try {
|
||||||
const started = await $fetch<{ jobId: string; name?: string }>(
|
const started = await $fetch<{ jobId: string; studioJobId?: string; queued?: boolean; name?: string }>(
|
||||||
'/api/generate/music',
|
'/api/generate/music',
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -314,11 +382,21 @@ async function generate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
jobId.value = started.jobId
|
studioJobId.value = started.studioJobId || ''
|
||||||
if (started.name) downloadName.value = `${started.name}.flac`
|
if (started.name) downloadName.value = `${started.name}.flac`
|
||||||
|
await refreshStudioQueue()
|
||||||
|
stopQueuePoll()
|
||||||
|
queuePoll = setInterval(() => { void refreshStudioQueue() }, 2500)
|
||||||
|
if (started.queued) {
|
||||||
|
queued.value = true
|
||||||
|
status.value = 'Waiting in the job queue…'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jobId.value = started.jobId
|
||||||
listen(started.jobId)
|
listen(started.jobId)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
busy.value = false
|
busy.value = false
|
||||||
|
queued.value = false
|
||||||
error.value = err?.data?.statusMessage || err?.statusMessage || err?.message || 'Could not queue music generation'
|
error.value = err?.data?.statusMessage || err?.statusMessage || err?.message || 'Could not queue music generation'
|
||||||
status.value = error.value
|
status.value = error.value
|
||||||
}
|
}
|
||||||
@@ -330,6 +408,9 @@ watch(instrumental, (on) => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadLibrary()
|
await loadLibrary()
|
||||||
|
await refreshStudioQueue()
|
||||||
|
stopQueuePoll()
|
||||||
|
queuePoll = setInterval(() => { void refreshStudioQueue() }, 8000)
|
||||||
const query = useRoute().query
|
const query = useRoute().query
|
||||||
if (typeof query.track === 'string' && query.track) {
|
if (typeof query.track === 'string' && query.track) {
|
||||||
audioUrl.value = `/api/library/tracks/${query.track}/audio`
|
audioUrl.value = `/api/library/tracks/${query.track}/audio`
|
||||||
@@ -340,5 +421,6 @@ onMounted(async () => {
|
|||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
stopListen()
|
stopListen()
|
||||||
|
stopQueuePoll()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+24
-4
@@ -11,7 +11,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="font-display text-2xl font-bold">Queue</h2>
|
<h2 class="font-display text-2xl font-bold">Queue</h2>
|
||||||
<p class="mt-1 max-w-2xl text-sm text-zinc-400">
|
<p class="mt-1 max-w-2xl text-sm text-zinc-400">
|
||||||
Jobs you submitted from Studio. A long shot script is one job; its shots are the list underneath. Run next inserts a waiting job after the current shot as its own clip.
|
Jobs you submitted from Studio or Music. A long shot script is one job; its shots are the list underneath. Run next inserts a waiting job after the current shot as its own clip.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-2 sm:gap-3">
|
<div class="flex flex-wrap items-center gap-2 sm:gap-3">
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
This batch is paused. It will not continue until you resume.
|
This batch is paused. It will not continue until you resume.
|
||||||
</p>
|
</p>
|
||||||
<p v-else-if="!jobs.length && !orphans.length" class="glass rounded-3xl px-5 py-10 text-center text-sm text-zinc-400">
|
<p v-else-if="!jobs.length && !orphans.length" class="glass rounded-3xl px-5 py-10 text-center text-sm text-zinc-400">
|
||||||
Nothing queued. Generate from Studio — a 30-shot script takes one slot, and you can line up more jobs behind it.
|
Nothing queued. Generate from Studio or Music — a 30-shot script takes one slot, and you can line up more jobs behind it.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
@@ -572,6 +572,22 @@ function progressLine(queue: Queue) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function jobLine(job: StudioJobRow) {
|
function jobLine(job: StudioJobRow) {
|
||||||
|
if (job.kind === 'music') {
|
||||||
|
if (job.status === 'waiting') {
|
||||||
|
return job.cutIn
|
||||||
|
? 'Next after the current shot · ACE-Step'
|
||||||
|
: 'Waiting · ACE-Step'
|
||||||
|
}
|
||||||
|
if (job.status === 'held') {
|
||||||
|
if (jobIsHeldPaused(job)) return 'Paused. This track will not continue until you resume.'
|
||||||
|
return job.holdForCutIn ? 'Paused so another job can cut in' : 'Paused'
|
||||||
|
}
|
||||||
|
if (job.status === 'running') {
|
||||||
|
if (jobIsLatched(job)) return 'Generating track · will pause after this'
|
||||||
|
return 'Generating track'
|
||||||
|
}
|
||||||
|
return job.status
|
||||||
|
}
|
||||||
if (job.kind === 'edit') {
|
if (job.kind === 'edit') {
|
||||||
const passes = job.shotCount || 1
|
const passes = job.shotCount || 1
|
||||||
const passLine = passes > 1 ? `${passes} passes` : 'image edit'
|
const passLine = passes > 1 ? `${passes} passes` : 'image edit'
|
||||||
@@ -790,7 +806,7 @@ function listenJob(job: StudioJobRow) {
|
|||||||
void loadQueues()
|
void loadQueues()
|
||||||
}
|
}
|
||||||
if (payload.type === 'complete' && !(Number(payload.queuedRemaining) > 0)) {
|
if (payload.type === 'complete' && !(Number(payload.queuedRemaining) > 0)) {
|
||||||
void notifyGenerationComplete(job.kind === 'edit' || payload.stillId ? 'image' : 'video')
|
void notifyGenerationComplete(job.kind === 'music' ? 'music' : job.kind === 'edit' || payload.stillId ? 'image' : 'video')
|
||||||
}
|
}
|
||||||
if (payload.type === 'complete' || payload.type === 'error') {
|
if (payload.type === 'complete' || payload.type === 'error') {
|
||||||
source.close()
|
source.close()
|
||||||
@@ -801,6 +817,7 @@ function listenJob(job: StudioJobRow) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function jobKindLabel(job: StudioJobRow) {
|
function jobKindLabel(job: StudioJobRow) {
|
||||||
|
if (job.kind === 'music') return 'MUSIC'
|
||||||
if (job.kind === 'edit') {
|
if (job.kind === 'edit') {
|
||||||
if (job.imagePipeline === 'v2') return 'IMAGE V2'
|
if (job.imagePipeline === 'v2') return 'IMAGE V2'
|
||||||
return (job.shotCount || 1) > 1 ? 'EDIT' : 'IMAGE'
|
return (job.shotCount || 1) > 1 ? 'EDIT' : 'IMAGE'
|
||||||
@@ -824,6 +841,7 @@ function promptSnippet(text?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function shotProgressDetail(job: StudioJobRow) {
|
function shotProgressDetail(job: StudioJobRow) {
|
||||||
|
if (job.kind === 'music') return promptSnippet(job.prompt)
|
||||||
const queue = job.shots
|
const queue = job.shots
|
||||||
if (!queue) return job.kind === 'edit' ? promptSnippet(job.prompt) : ''
|
if (!queue) return job.kind === 'edit' ? promptSnippet(job.prompt) : ''
|
||||||
const total = queue.totalCount || job.shotCount || 1
|
const total = queue.totalCount || job.shotCount || 1
|
||||||
@@ -841,7 +859,9 @@ function askRemoveJob(job: StudioJobRow) {
|
|||||||
name: job.name || 'Untitled job',
|
name: job.name || 'Untitled job',
|
||||||
detail: shotProgressDetail(job),
|
detail: shotProgressDetail(job),
|
||||||
consequence: generating
|
consequence: generating
|
||||||
? 'This stops the job that’s generating on Beast and removes only this row. Other queue items stay. Saved clips stay in the library.'
|
? (job.kind === 'music'
|
||||||
|
? 'This stops the track that’s generating on Beast and removes only this row. Other queue items stay. Saved tracks stay in the library.'
|
||||||
|
: 'This stops the job that’s generating on Beast and removes only this row. Other queue items stay. Saved clips stay in the library.')
|
||||||
: pausedBatch
|
: pausedBatch
|
||||||
? 'This drops the rest of this shot list only. It will not resume, and it will not stop a different job that’s generating. Saved clips stay in the library.'
|
? 'This drops the rest of this shot list only. It will not resume, and it will not stop a different job that’s generating. Saved clips stay in the library.'
|
||||||
: 'This removes only this job from the queue. Other items stay. Saved clips stay in the library.',
|
: 'This removes only this job from the queue. Other items stay. Saved clips stay in the library.',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { startMusicJob } from '~/server/utils/musicChain'
|
import { addStudioJob, kickStudioQueue, listStudioJobs, videoJobsBusy } from '~/server/utils/studioQueue'
|
||||||
import { comfyConfigured } from '~/server/utils/comfy'
|
import { comfyConfigured } from '~/server/utils/comfy'
|
||||||
import {
|
import {
|
||||||
clampLyricsStrength,
|
clampLyricsStrength,
|
||||||
@@ -67,27 +67,54 @@ export default defineEventHandler(async (event) => {
|
|||||||
engine: engine === 'ace-step-1.5' ? 'ace15' : 'ace'
|
engine: engine === 'ace-step-1.5' ? 'ace15' : 'ace'
|
||||||
})
|
})
|
||||||
|
|
||||||
const job = await startMusicJob({
|
const samplerName = String(body.sampler_name || 'euler')
|
||||||
|
const scheduler = String(body.scheduler || 'simple')
|
||||||
|
const familyId = crypto.randomUUID()
|
||||||
|
const studio = await addStudioJob({
|
||||||
ownerKey,
|
ownerKey,
|
||||||
folderId,
|
familyId,
|
||||||
|
kind: 'music',
|
||||||
|
payload: {
|
||||||
|
prompt: tags,
|
||||||
name: clipName,
|
name: clipName,
|
||||||
tags,
|
folderId,
|
||||||
lyrics,
|
aspect: 'audio',
|
||||||
duration,
|
width: 0,
|
||||||
|
height: 0,
|
||||||
steps,
|
steps,
|
||||||
|
turbo: false,
|
||||||
seed,
|
seed,
|
||||||
cfg,
|
cfg,
|
||||||
lyricsStrength,
|
fps: 0,
|
||||||
instrumental,
|
samplerName,
|
||||||
|
scheduler,
|
||||||
|
duration,
|
||||||
|
sound: !instrumental,
|
||||||
|
workflow: 'v1',
|
||||||
|
useIdentityRefs: false,
|
||||||
|
hideThumbnail: false,
|
||||||
folderLocked,
|
folderLocked,
|
||||||
engine,
|
referenceStillIds: [null, null, null, null],
|
||||||
samplerName: String(body.sampler_name || 'euler'),
|
extensions: [],
|
||||||
scheduler: String(body.scheduler || 'simple')
|
queueAutoRun: false,
|
||||||
|
lyrics,
|
||||||
|
instrumental,
|
||||||
|
lyricsStrength,
|
||||||
|
musicEngine: engine
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
await kickStudioQueue()
|
||||||
|
let latest = listStudioJobs(ownerKey).find(item => item.id === studio.id)
|
||||||
|
if (!latest?.liveJobId && !(await videoJobsBusy())) {
|
||||||
|
await kickStudioQueue()
|
||||||
|
latest = listStudioJobs(ownerKey).find(item => item.id === studio.id)
|
||||||
|
}
|
||||||
|
const liveJobId = latest?.liveJobId || ''
|
||||||
|
|
||||||
return {
|
return {
|
||||||
jobId: job.id,
|
jobId: liveJobId || studio.id,
|
||||||
queued: job.status === 'queued',
|
studioJobId: studio.id,
|
||||||
|
queued: !liveJobId,
|
||||||
seed,
|
seed,
|
||||||
duration,
|
duration,
|
||||||
steps,
|
steps,
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
: listJobs().filter((job) => {
|
: listJobs().filter((job) => {
|
||||||
if (job.status === 'complete' || job.status === 'error' || job.status === 'cancelled') return false
|
if (job.status === 'complete' || job.status === 'error' || job.status === 'cancelled') return false
|
||||||
if (kind === 'edit') return job.kind === 'edit'
|
if (kind === 'edit') return job.kind === 'edit'
|
||||||
if (kind === 'video') return job.kind !== 'edit'
|
if (kind === 'music') return job.kind === 'music'
|
||||||
|
if (kind === 'video') return job.kind !== 'edit' && job.kind !== 'music'
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
for (const job of targets) {
|
for (const job of targets) {
|
||||||
@@ -47,7 +48,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
for (const row of listStudioJobs(owner)) {
|
for (const row of listStudioJobs(owner)) {
|
||||||
if (row.status !== 'running' && row.status !== 'held') continue
|
if (row.status !== 'running' && row.status !== 'held') continue
|
||||||
if (kind === 'edit' && row.kind !== 'edit') continue
|
if (kind === 'edit' && row.kind !== 'edit') continue
|
||||||
|
if (kind === 'music' && row.kind !== 'music') continue
|
||||||
if (kind === 'video' && row.kind === 'edit') continue
|
if (kind === 'video' && row.kind === 'edit') continue
|
||||||
|
if (kind === 'video' && row.kind === 'music') continue
|
||||||
await cancelStudioJob(owner, row.id).catch(() => null)
|
await cancelStudioJob(owner, row.id).catch(() => null)
|
||||||
}
|
}
|
||||||
const ok = await interruptComfy()
|
const ok = await interruptComfy()
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ export default defineEventHandler((event) => {
|
|||||||
const byId = new Map(queues.map(queue => [queue.id, queue]))
|
const byId = new Map(queues.map(queue => [queue.id, queue]))
|
||||||
const jobs = active.map((job) => {
|
const jobs = active.map((job) => {
|
||||||
const shots = job.shotQueueId ? byId.get(job.shotQueueId) || null : null
|
const shots = job.shotQueueId ? byId.get(job.shotQueueId) || null : null
|
||||||
const kind = job.kind === 'edit' ? 'edit' : 'video'
|
const kind = job.kind === 'edit' ? 'edit' : job.kind === 'music' ? 'music' : 'video'
|
||||||
const shotSummary = shots ? summarizeQueue(shots) : null
|
const shotSummary = shots ? summarizeQueue(shots) : null
|
||||||
const plannedShots = shotSummary
|
const plannedShots = shotSummary
|
||||||
|
? undefined
|
||||||
|
: kind === 'music'
|
||||||
? undefined
|
? undefined
|
||||||
: kind === 'edit'
|
: kind === 'edit'
|
||||||
? (job.payload.passes?.length
|
? (job.payload.passes?.length
|
||||||
|
|||||||
@@ -44,6 +44,15 @@ function classifyError(message: string) {
|
|||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function settleMusicStudio(job: Job) {
|
||||||
|
try {
|
||||||
|
const { onLiveVideoSettled } = await import('~/server/utils/studioQueue')
|
||||||
|
await onLiveVideoSettled(job)
|
||||||
|
} catch {
|
||||||
|
/* queue kick is best-effort */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function nodeLabel(node: string) {
|
function nodeLabel(node: string) {
|
||||||
if (!node) return ''
|
if (!node) return ''
|
||||||
return NODE_LABELS[node] || ''
|
return NODE_LABELS[node] || ''
|
||||||
@@ -118,6 +127,7 @@ function watchMusicJob(job: Job): Promise<void> {
|
|||||||
job.status = job.status === 'cancelled' ? 'cancelled' : 'error'
|
job.status = job.status === 'cancelled' ? 'cancelled' : 'error'
|
||||||
job.error = classifyError(error)
|
job.error = classifyError(error)
|
||||||
emitJob(job, { type: 'error', error: job.error, message: job.error })
|
emitJob(job, { type: 'error', error: job.error, message: job.error })
|
||||||
|
await settleMusicStudio(job)
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +193,7 @@ function watchMusicJob(job: Job): Promise<void> {
|
|||||||
job.error = `Audio generated but library save failed: ${message}`
|
job.error = `Audio generated but library save failed: ${message}`
|
||||||
emitJob(job, { type: 'error', error: job.error, message: job.error })
|
emitJob(job, { type: 'error', error: job.error, message: job.error })
|
||||||
}
|
}
|
||||||
|
await settleMusicStudio(job)
|
||||||
job.saving = false
|
job.saving = false
|
||||||
resolve()
|
resolve()
|
||||||
return true
|
return true
|
||||||
@@ -344,6 +355,7 @@ export async function startMusicJob(params: MusicJobParams) {
|
|||||||
job.status = 'error'
|
job.status = 'error'
|
||||||
job.error = classifyError(message)
|
job.error = classifyError(message)
|
||||||
emitJob(job, { type: 'error', error: job.error, message: job.error })
|
emitJob(job, { type: 'error', error: job.error, message: job.error })
|
||||||
|
await settleMusicStudio(job)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { imageV2StackSpecials } from '~/utils/imageV2'
|
|||||||
import { allowIdentityRefs, type PermanenceRef } from '~/utils/globalLocks'
|
import { allowIdentityRefs, type PermanenceRef } from '~/utils/globalLocks'
|
||||||
|
|
||||||
export type StudioJobStatus = 'waiting' | 'running' | 'held' | 'complete' | 'error' | 'cancelled'
|
export type StudioJobStatus = 'waiting' | 'running' | 'held' | 'complete' | 'error' | 'cancelled'
|
||||||
export type StudioJobKind = 'video' | 'edit'
|
export type StudioJobKind = 'video' | 'edit' | 'music'
|
||||||
|
|
||||||
export interface StudioJobPayload {
|
export interface StudioJobPayload {
|
||||||
prompt: string
|
prompt: string
|
||||||
@@ -68,6 +68,10 @@ export interface StudioJobPayload {
|
|||||||
maskStillFilename?: string
|
maskStillFilename?: string
|
||||||
refineStrength?: number
|
refineStrength?: number
|
||||||
extendFromClipId?: string
|
extendFromClipId?: string
|
||||||
|
lyrics?: string
|
||||||
|
instrumental?: boolean
|
||||||
|
lyricsStrength?: number
|
||||||
|
musicEngine?: import('~/utils/music').MusicEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StudioJob {
|
export interface StudioJob {
|
||||||
@@ -209,7 +213,9 @@ export function listStudioJobs(owner: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function studioJobKind(job: Pick<StudioJob, 'kind'> | { kind?: string }) {
|
export function studioJobKind(job: Pick<StudioJob, 'kind'> | { kind?: string }) {
|
||||||
return job.kind === 'edit' ? 'edit' : 'video'
|
if (job.kind === 'edit') return 'edit'
|
||||||
|
if (job.kind === 'music') return 'music'
|
||||||
|
return 'video'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function summarizeStudioJob(job: StudioJob) {
|
export function summarizeStudioJob(job: StudioJob) {
|
||||||
@@ -317,8 +323,10 @@ export async function addStudioJob(params: {
|
|||||||
kind?: StudioJobKind
|
kind?: StudioJobKind
|
||||||
}) {
|
}) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const kind = params.kind === 'edit' ? 'edit' : 'video'
|
const kind = params.kind === 'edit' ? 'edit' : params.kind === 'music' ? 'music' : 'video'
|
||||||
const shotCount = kind === 'edit'
|
const shotCount = kind === 'music'
|
||||||
|
? 1
|
||||||
|
: kind === 'edit'
|
||||||
? 1 + (params.payload.passes?.length || 0)
|
? 1 + (params.payload.passes?.length || 0)
|
||||||
: 1 + (params.payload.extensions?.length || 0)
|
: 1 + (params.payload.extensions?.length || 0)
|
||||||
const job: StudioJob = {
|
const job: StudioJob = {
|
||||||
@@ -976,7 +984,48 @@ async function startStudioExtendJob(item: StudioJob) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function startStudioMusicJob(item: StudioJob) {
|
||||||
|
let live: import('~/server/utils/jobs').Job | undefined
|
||||||
|
try {
|
||||||
|
const { startMusicJob } = await import('~/server/utils/musicChain')
|
||||||
|
const payload = item.payload
|
||||||
|
live = await startMusicJob({
|
||||||
|
ownerKey: item.ownerKey,
|
||||||
|
folderId: payload.folderId,
|
||||||
|
name: payload.name,
|
||||||
|
tags: payload.prompt,
|
||||||
|
lyrics: payload.instrumental ? '' : (payload.lyrics || ''),
|
||||||
|
duration: payload.duration,
|
||||||
|
steps: payload.steps,
|
||||||
|
seed: payload.seed || Math.floor(Math.random() * 2_147_483_647),
|
||||||
|
cfg: payload.cfg,
|
||||||
|
lyricsStrength: payload.instrumental ? 0 : (payload.lyricsStrength ?? 0.9),
|
||||||
|
instrumental: payload.instrumental === true,
|
||||||
|
folderLocked: payload.folderLocked,
|
||||||
|
engine: payload.musicEngine || 'ace-step',
|
||||||
|
samplerName: payload.samplerName || 'euler',
|
||||||
|
scheduler: payload.scheduler || 'simple'
|
||||||
|
})
|
||||||
|
await markStudioLive(item.ownerKey, item.id, live.id)
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
|
if (live && (live.status === 'queued' || live.status === 'uploading' || live.status === 'running')) {
|
||||||
|
live.status = 'error'
|
||||||
|
live.error = message
|
||||||
|
}
|
||||||
|
await patchStudioJob(item.ownerKey, item.id, (job) => {
|
||||||
|
job.status = 'error'
|
||||||
|
job.lastError = message
|
||||||
|
}).catch(() => null)
|
||||||
|
kickStudioQueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function startStudioJob(item: StudioJob) {
|
export async function startStudioJob(item: StudioJob) {
|
||||||
|
if (studioJobKind(item) === 'music') {
|
||||||
|
await startStudioMusicJob(item)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (studioJobKind(item) === 'edit') {
|
if (studioJobKind(item) === 'edit') {
|
||||||
await startStudioEditJob(item)
|
await startStudioEditJob(item)
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user