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">
|
||||
<p class="truncate text-sm font-medium text-zinc-100">{{ job.name }}</p>
|
||||
<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) }}
|
||||
· {{ studioJobShotLine(job) }}
|
||||
</p>
|
||||
@@ -5170,6 +5171,7 @@ function studioJobStatusLabel(job: { status: string; cutIn?: boolean; holdForCut
|
||||
}
|
||||
|
||||
function studioJobShotLine(job: { kind?: string; shotCount: number; completedCount?: number; status: string }) {
|
||||
if (job.kind === 'music') return 'ACE-Step'
|
||||
if (job.kind === 'edit') {
|
||||
const total = job.shotCount || 1
|
||||
return total <= 1 ? 'image edit' : `${total} passes`
|
||||
@@ -5223,6 +5225,8 @@ async function refreshStudioQueue() {
|
||||
} else if (running.id && running.liveJobId === editJobId.value) {
|
||||
outputStudioJobId.value = running.id
|
||||
}
|
||||
} else if (running?.kind === 'music') {
|
||||
return
|
||||
} else if (running?.liveJobId && running.liveJobId !== videoJobId.value && !videoBusy.value && !watchingLiveVideo() && !extendOpen.value) {
|
||||
outputStudioJobId.value = running.id
|
||||
videoJobId.value = running.liveJobId
|
||||
|
||||
+84
-2
@@ -3,6 +3,7 @@
|
||||
<AppHeader
|
||||
subtitle="ACE-Step · vocals"
|
||||
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">
|
||||
@@ -121,6 +122,10 @@
|
||||
{{ busy ? (status || 'Generating…') : 'Generate' }}
|
||||
</button>
|
||||
<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 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 trackId = ref('')
|
||||
const jobId = ref('')
|
||||
const studioJobId = ref('')
|
||||
const queued = ref(false)
|
||||
const queueCount = ref(0)
|
||||
const downloadName = ref('track.flac')
|
||||
let source: EventSource | null = null
|
||||
let listenGen = 0
|
||||
let settled = false
|
||||
let queuePoll: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const blocked = computed(() => !tags.value.trim() || (!instrumental.value && !lyrics.value.trim()) || !folderId.value)
|
||||
const blockReason = computed(() => {
|
||||
@@ -232,6 +241,55 @@ function stopListen() {
|
||||
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>) {
|
||||
if (payload.message) status.value = payload.message
|
||||
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()}`
|
||||
}
|
||||
stopListen()
|
||||
queued.value = false
|
||||
studioJobId.value = ''
|
||||
stopQueuePoll()
|
||||
queuePoll = setInterval(() => { void refreshStudioQueue() }, 8000)
|
||||
void notifyGenerationComplete('music')
|
||||
}
|
||||
if (payload.type === 'error' || payload.status === 'error') {
|
||||
@@ -260,7 +322,10 @@ function applyEvent(payload: Record<string, any>) {
|
||||
busy.value = false
|
||||
error.value = payload.error || payload.message || 'Generation failed'
|
||||
status.value = error.value
|
||||
queued.value = false
|
||||
stopListen()
|
||||
stopQueuePoll()
|
||||
queuePoll = setInterval(() => { void refreshStudioQueue() }, 8000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,12 +358,15 @@ async function generate() {
|
||||
await ensureGenerationNotifyPermission()
|
||||
busy.value = true
|
||||
settled = false
|
||||
queued.value = false
|
||||
progress.value = 2
|
||||
audioUrl.value = ''
|
||||
trackId.value = ''
|
||||
jobId.value = ''
|
||||
studioJobId.value = ''
|
||||
status.value = 'Queueing ACE-Step…'
|
||||
try {
|
||||
const started = await $fetch<{ jobId: string; name?: string }>(
|
||||
const started = await $fetch<{ jobId: string; studioJobId?: string; queued?: boolean; name?: string }>(
|
||||
'/api/generate/music',
|
||||
{
|
||||
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`
|
||||
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)
|
||||
} catch (err: any) {
|
||||
busy.value = false
|
||||
queued.value = false
|
||||
error.value = err?.data?.statusMessage || err?.statusMessage || err?.message || 'Could not queue music generation'
|
||||
status.value = error.value
|
||||
}
|
||||
@@ -330,6 +408,9 @@ watch(instrumental, (on) => {
|
||||
|
||||
onMounted(async () => {
|
||||
await loadLibrary()
|
||||
await refreshStudioQueue()
|
||||
stopQueuePoll()
|
||||
queuePoll = setInterval(() => { void refreshStudioQueue() }, 8000)
|
||||
const query = useRoute().query
|
||||
if (typeof query.track === 'string' && query.track) {
|
||||
audioUrl.value = `/api/library/tracks/${query.track}/audio`
|
||||
@@ -340,5 +421,6 @@ onMounted(async () => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopListen()
|
||||
stopQueuePoll()
|
||||
})
|
||||
</script>
|
||||
|
||||
+24
-4
@@ -11,7 +11,7 @@
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold">Queue</h2>
|
||||
<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>
|
||||
</div>
|
||||
<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.
|
||||
</p>
|
||||
<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>
|
||||
|
||||
<section
|
||||
@@ -572,6 +572,22 @@ function progressLine(queue: Queue) {
|
||||
}
|
||||
|
||||
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') {
|
||||
const passes = job.shotCount || 1
|
||||
const passLine = passes > 1 ? `${passes} passes` : 'image edit'
|
||||
@@ -790,7 +806,7 @@ function listenJob(job: StudioJobRow) {
|
||||
void loadQueues()
|
||||
}
|
||||
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') {
|
||||
source.close()
|
||||
@@ -801,6 +817,7 @@ function listenJob(job: StudioJobRow) {
|
||||
}
|
||||
|
||||
function jobKindLabel(job: StudioJobRow) {
|
||||
if (job.kind === 'music') return 'MUSIC'
|
||||
if (job.kind === 'edit') {
|
||||
if (job.imagePipeline === 'v2') return 'IMAGE V2'
|
||||
return (job.shotCount || 1) > 1 ? 'EDIT' : 'IMAGE'
|
||||
@@ -824,6 +841,7 @@ function promptSnippet(text?: string) {
|
||||
}
|
||||
|
||||
function shotProgressDetail(job: StudioJobRow) {
|
||||
if (job.kind === 'music') return promptSnippet(job.prompt)
|
||||
const queue = job.shots
|
||||
if (!queue) return job.kind === 'edit' ? promptSnippet(job.prompt) : ''
|
||||
const total = queue.totalCount || job.shotCount || 1
|
||||
@@ -841,7 +859,9 @@ function askRemoveJob(job: StudioJobRow) {
|
||||
name: job.name || 'Untitled job',
|
||||
detail: shotProgressDetail(job),
|
||||
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
|
||||
? '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.',
|
||||
|
||||
@@ -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,
|
||||
familyId,
|
||||
kind: 'music',
|
||||
payload: {
|
||||
prompt: tags,
|
||||
name: clipName,
|
||||
tags,
|
||||
lyrics,
|
||||
duration,
|
||||
folderId,
|
||||
aspect: 'audio',
|
||||
width: 0,
|
||||
height: 0,
|
||||
steps,
|
||||
turbo: false,
|
||||
seed,
|
||||
cfg,
|
||||
lyricsStrength,
|
||||
instrumental,
|
||||
fps: 0,
|
||||
samplerName,
|
||||
scheduler,
|
||||
duration,
|
||||
sound: !instrumental,
|
||||
workflow: 'v1',
|
||||
useIdentityRefs: false,
|
||||
hideThumbnail: false,
|
||||
folderLocked,
|
||||
engine,
|
||||
samplerName: String(body.sampler_name || 'euler'),
|
||||
scheduler: String(body.scheduler || 'simple')
|
||||
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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -16,9 +16,11 @@ 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 === 'music'
|
||||
? undefined
|
||||
: kind === 'edit'
|
||||
? (job.payload.passes?.length
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -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,8 +323,10 @@ export async function addStudioJob(params: {
|
||||
kind?: StudioJobKind
|
||||
}) {
|
||||
const now = Date.now()
|
||||
const kind = params.kind === 'edit' ? 'edit' : 'video'
|
||||
const shotCount = kind === 'edit'
|
||||
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 = {
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user