Let queued video, image, and music jobs open in an inspect modal.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-09-03 00:15:02 -05:00
co-authored by Cursor
parent 53990192e4
commit f05f8f3597
12 changed files with 1257 additions and 37 deletions
+23
View File
@@ -13,6 +13,7 @@ import {
type GenerationPreset,
type GenerationPresetKind,
type ImagePresetSettings,
type MusicPresetSettings,
type VideoPresetSettings
} from '~/utils/generationPresets'
import {
@@ -35,6 +36,13 @@ import {
parseImageV2Task,
type ImageV2PresetSettings
} from '~/utils/imageV2'
import {
clampMusicCfg,
clampMusicDuration,
clampMusicSteps,
sanitizeMusicLyrics,
sanitizeMusicTags
} from '~/utils/music'
import { coerceVideoWorkflow, isXaigenStudio, parseVideoWorkflow } from '~/utils/videoModels'
const writeChains = new Map<string, Promise<unknown>>()
@@ -155,9 +163,24 @@ function sanitizeImageV2Settings(raw: unknown): ImageV2PresetSettings {
}
}
function sanitizeMusicSettings(raw: unknown): MusicPresetSettings {
const rec = raw && typeof raw === 'object' ? raw as Record<string, unknown> : {}
const instrumental = rec.instrumental === true
return {
tags: sanitizeMusicTags(rec.tags),
lyrics: sanitizeMusicLyrics(rec.lyrics, instrumental),
instrumental,
duration: clampMusicDuration(rec.duration),
steps: clampMusicSteps(rec.steps),
seed: typeof rec.seed === 'number' && Number.isFinite(rec.seed) ? Math.max(0, Math.floor(rec.seed)) : undefined,
cfg: rec.cfg == null ? undefined : clampMusicCfg(rec.cfg)
}
}
function sanitizeSettings(kind: GenerationPresetKind, raw: unknown) {
if (kind === 'image') return sanitizeImageSettings(raw)
if (kind === 'imagev2') return sanitizeImageV2Settings(raw)
if (kind === 'music') return sanitizeMusicSettings(raw)
return sanitizeVideoSettings(raw)
}
+118
View File
@@ -0,0 +1,118 @@
import { emitJob, getJob, listJobs, type Job } from '~/server/utils/jobs'
import { listStudioJobs, type StudioJob } from '~/server/utils/studioQueue'
const LIVE_ERROR_KEEP_MS = 60 * 60 * 1000
function liveMusicActive(job: Job) {
if (job.kind !== 'music') return false
if (job.status === 'queued' || job.status === 'uploading' || job.status === 'running') return true
if (job.status === 'error' && Date.now() - job.startedAt < LIVE_ERROR_KEEP_MS) return true
return false
}
export function listLiveMusicJobs(owner: string) {
return listJobs()
.filter(job => liveMusicActive(job) && job.library?.ownerKey === owner)
.sort((a, b) => b.startedAt - a.startedAt)
}
export function summarizeLiveMusicJob(job: Job) {
const lib = job.library
const tags = String(lib?.tags || lib?.prompt || '').trim()
return {
id: job.id,
createdAt: job.startedAt,
updatedAt: job.startedAt,
status: job.status === 'error' ? 'error' : 'running',
kind: 'music' as const,
name: (lib?.name || '').trim() || tags.slice(0, 80) || 'Music',
prompt: tags,
shotCount: 1,
liveJobId: job.id,
source: 'live' as const,
lastError: job.error,
hideThumbnail: true,
folderLocked: lib?.folderLocked === true
}
}
export function inspectStudioJob(job: StudioJob) {
return {
source: 'studio' as const,
id: job.id,
createdAt: job.createdAt,
updatedAt: job.updatedAt,
status: job.status,
kind: job.kind === 'edit' ? 'edit' as const : 'video' as const,
name: job.name,
prompt: job.prompt,
shotCount: job.shotCount,
familyId: job.familyId,
shotQueueId: job.shotQueueId,
liveJobId: job.liveJobId,
lastError: job.lastError,
imagePipeline: job.payload.imagePipeline || 'v1',
cutIn: job.cutIn === true,
holdForCutIn: job.holdForCutIn === true,
pauseAfterCurrent: job.pauseAfterCurrent === true,
pausedByUser: job.pausedByUser === true,
payload: job.kind === 'music'
? { ...job.payload, tags: job.payload.prompt }
: job.payload
}
}
export function inspectLiveMusicJob(job: Job) {
const lib = job.library
const tags = String(lib?.tags || lib?.prompt || '').trim()
return {
source: 'live' as const,
id: job.id,
createdAt: job.startedAt,
updatedAt: job.startedAt,
status: job.status === 'error' ? 'error' : job.status === 'cancelled' ? 'cancelled' : 'running',
kind: 'music' as const,
name: (lib?.name || '').trim() || tags.slice(0, 80) || 'Music',
prompt: tags,
shotCount: 1,
liveJobId: job.id,
lastError: job.error,
imagePipeline: undefined,
payload: {
name: lib?.name || '',
prompt: tags,
tags,
lyrics: lib?.lyrics || '',
instrumental: lib?.instrumental === true,
folderId: lib?.folderId || '',
duration: lib?.duration,
steps: lib?.steps,
seed: lib?.seed,
cfg: lib?.cfg,
engine: lib?.engine,
lyricsStrength: lib?.lyricsStrength,
hideThumbnail: true,
folderLocked: lib?.folderLocked === true
}
}
}
export function findInspectJob(owner: string, id: string) {
const studio = listStudioJobs(owner).find(item => item.id === id)
if (studio) return inspectStudioJob(studio)
const live = getJob(id)
if (live?.kind === 'music' && live.library?.ownerKey === owner) return inspectLiveMusicJob(live)
return null
}
export async function cancelLiveMusicJob(owner: string, id: string) {
const live = getJob(id)
if (!live || live.kind !== 'music' || live.library?.ownerKey !== owner) {
throw createError({ statusCode: 404, statusMessage: 'Queued job not found' })
}
live.status = 'cancelled'
if (live.library) live.library.stopAfterCurrent = true
emitJob(live, { type: 'error', error: 'Job interrupted.', message: 'Job interrupted.' })
const { interruptComfy } = await import('~/server/utils/comfy')
await interruptComfy().catch(() => false)
}