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:
Towsty
2026-09-03 00:08:07 -05:00
co-authored by Cursor
parent 9cb8b9bda3
commit 53990192e4
8 changed files with 233 additions and 34 deletions
+45 -18
View File
@@ -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 {
clampLyricsStrength,
@@ -67,27 +67,54 @@ export default defineEventHandler(async (event) => {
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,
folderId,
name: clipName,
tags,
lyrics,
duration,
steps,
seed,
cfg,
lyricsStrength,
instrumental,
folderLocked,
engine,
samplerName: String(body.sampler_name || 'euler'),
scheduler: String(body.scheduler || 'simple')
familyId,
kind: 'music',
payload: {
prompt: tags,
name: clipName,
folderId,
aspect: 'audio',
width: 0,
height: 0,
steps,
turbo: false,
seed,
cfg,
fps: 0,
samplerName,
scheduler,
duration,
sound: !instrumental,
workflow: 'v1',
useIdentityRefs: false,
hideThumbnail: false,
folderLocked,
referenceStillIds: [null, null, null, null],
extensions: [],
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 {
jobId: job.id,
queued: job.status === 'queued',
jobId: liveJobId || studio.id,
studioJobId: studio.id,
queued: !liveJobId,
seed,
duration,
steps,
+4 -1
View File
@@ -33,7 +33,8 @@ export default defineEventHandler(async (event) => {
: listJobs().filter((job) => {
if (job.status === 'complete' || job.status === 'error' || job.status === 'cancelled') return false
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
})
for (const job of targets) {
@@ -47,7 +48,9 @@ export default defineEventHandler(async (event) => {
for (const row of listStudioJobs(owner)) {
if (row.status !== 'running' && row.status !== 'held') 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 === 'music') continue
await cancelStudioJob(owner, row.id).catch(() => null)
}
const ok = await interruptComfy()
+4 -2
View File
@@ -16,11 +16,13 @@ export default defineEventHandler((event) => {
const byId = new Map(queues.map(queue => [queue.id, queue]))
const jobs = active.map((job) => {
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 plannedShots = shotSummary
? undefined
: kind === 'edit'
: kind === 'music'
? undefined
: kind === 'edit'
? (job.payload.passes?.length
? [
{ prompt: job.payload.prompt, duration: 0 },
+12
View File
@@ -44,6 +44,15 @@ function classifyError(message: string) {
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) {
if (!node) return ''
return NODE_LABELS[node] || ''
@@ -118,6 +127,7 @@ function watchMusicJob(job: Job): Promise<void> {
job.status = job.status === 'cancelled' ? 'cancelled' : 'error'
job.error = classifyError(error)
emitJob(job, { type: 'error', error: job.error, message: job.error })
await settleMusicStudio(job)
resolve()
}
@@ -183,6 +193,7 @@ function watchMusicJob(job: Job): Promise<void> {
job.error = `Audio generated but library save failed: ${message}`
emitJob(job, { type: 'error', error: job.error, message: job.error })
}
await settleMusicStudio(job)
job.saving = false
resolve()
return true
@@ -344,6 +355,7 @@ export async function startMusicJob(params: MusicJobParams) {
job.status = 'error'
job.error = classifyError(message)
emitJob(job, { type: 'error', error: job.error, message: job.error })
await settleMusicStudio(job)
}
}
})()
+55 -6
View File
@@ -11,7 +11,7 @@ import { imageV2StackSpecials } from '~/utils/imageV2'
import { allowIdentityRefs, type PermanenceRef } from '~/utils/globalLocks'
export type StudioJobStatus = 'waiting' | 'running' | 'held' | 'complete' | 'error' | 'cancelled'
export type StudioJobKind = 'video' | 'edit'
export type StudioJobKind = 'video' | 'edit' | 'music'
export interface StudioJobPayload {
prompt: string
@@ -68,6 +68,10 @@ export interface StudioJobPayload {
maskStillFilename?: string
refineStrength?: number
extendFromClipId?: string
lyrics?: string
instrumental?: boolean
lyricsStrength?: number
musicEngine?: import('~/utils/music').MusicEngine
}
export interface StudioJob {
@@ -209,7 +213,9 @@ export function listStudioJobs(owner: 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) {
@@ -317,10 +323,12 @@ export async function addStudioJob(params: {
kind?: StudioJobKind
}) {
const now = Date.now()
const kind = params.kind === 'edit' ? 'edit' : 'video'
const shotCount = kind === 'edit'
? 1 + (params.payload.passes?.length || 0)
: 1 + (params.payload.extensions?.length || 0)
const kind = params.kind === 'edit' ? 'edit' : params.kind === 'music' ? 'music' : 'video'
const shotCount = kind === 'music'
? 1
: kind === 'edit'
? 1 + (params.payload.passes?.length || 0)
: 1 + (params.payload.extensions?.length || 0)
const job: StudioJob = {
id: crypto.randomUUID(),
ownerKey: params.ownerKey,
@@ -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) {
if (studioJobKind(item) === 'music') {
await startStudioMusicJob(item)
return
}
if (studioJobKind(item) === 'edit') {
await startStudioEditJob(item)
return