Let queued video, image, and music jobs open in an inspect modal.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
import { persistLoraFields, readLoraStack, type LoraStackItem } from '~/utils/loras'
|
||||
import {
|
||||
clampMusicDuration,
|
||||
clampMusicSteps,
|
||||
sanitizeMusicLyrics,
|
||||
sanitizeMusicTags
|
||||
} from '~/utils/music'
|
||||
|
||||
export const LOAD_QUEUED_JOB_KEY = 'aigen-load-queued-job'
|
||||
export const LOAD_MUSIC_JOB_KEY = 'aigen-load-music-job'
|
||||
|
||||
export type QueuedInspectKind = 'video' | 'edit' | 'music'
|
||||
export type QueuedInspectSource = 'studio' | 'live'
|
||||
|
||||
export type QueuedShotDraft = {
|
||||
prompt: string
|
||||
duration: number
|
||||
loraName?: string
|
||||
loraStack?: LoraStackItem[]
|
||||
}
|
||||
|
||||
export type QueuedInspectPayload = {
|
||||
prompt?: string
|
||||
promptMid?: string
|
||||
promptPre?: string
|
||||
promptPost?: string
|
||||
name?: string
|
||||
folderId?: string
|
||||
aspect?: string
|
||||
width?: number
|
||||
height?: number
|
||||
steps?: number
|
||||
turbo?: boolean
|
||||
seed?: number
|
||||
cfg?: number
|
||||
fps?: number
|
||||
samplerName?: string
|
||||
scheduler?: string
|
||||
duration?: number
|
||||
sound?: boolean
|
||||
workflow?: string
|
||||
stillId?: string
|
||||
stillFilename?: string
|
||||
hideThumbnail?: boolean
|
||||
folderLocked?: boolean
|
||||
loraName?: string
|
||||
loraStack?: LoraStackItem[]
|
||||
negative?: string
|
||||
passes?: { prompt: string }[]
|
||||
imagePipeline?: 'v1' | 'v2'
|
||||
v2Mode?: string
|
||||
v2Task?: string
|
||||
engine?: string
|
||||
snofsModel?: number
|
||||
snofsClip?: number
|
||||
consistencyModel?: number
|
||||
consistencyClip?: number
|
||||
refineStrength?: number
|
||||
scaleToTotalPixels?: boolean
|
||||
scaleMegapixels?: number
|
||||
extensions?: QueuedShotDraft[]
|
||||
tags?: string
|
||||
lyrics?: string
|
||||
instrumental?: boolean
|
||||
lyricsStrength?: number
|
||||
}
|
||||
|
||||
export type QueuedInspectJob = {
|
||||
source: QueuedInspectSource
|
||||
id: string
|
||||
status: string
|
||||
kind: QueuedInspectKind
|
||||
name: string
|
||||
prompt: string
|
||||
shotCount: number
|
||||
liveJobId?: string
|
||||
shotQueueId?: string
|
||||
lastError?: string
|
||||
imagePipeline?: string
|
||||
payload: QueuedInspectPayload
|
||||
}
|
||||
|
||||
export function queuedJobKindLabel(job: { kind?: string; imagePipeline?: string; shotCount?: number }) {
|
||||
if (job.kind === 'music') return 'Music'
|
||||
if (job.kind === 'edit') {
|
||||
if (job.imagePipeline === 'v2') return 'Image v2'
|
||||
return (job.shotCount || 1) > 1 ? 'Image edit' : 'Image'
|
||||
}
|
||||
return 'Video'
|
||||
}
|
||||
|
||||
export function queuedJobEditable(job: { source?: string; status?: string }) {
|
||||
return job.source === 'studio' && (job.status === 'waiting' || job.status === 'held' || job.status === 'error')
|
||||
}
|
||||
|
||||
export function queuedJobShots(payload: QueuedInspectPayload, kind: string): QueuedShotDraft[] {
|
||||
if (kind === 'edit') {
|
||||
const first = { prompt: payload.prompt || '', duration: 0 }
|
||||
const rest = (payload.passes || []).map(item => ({ prompt: item.prompt || '', duration: 0 }))
|
||||
return rest.length ? [first, ...rest] : [first]
|
||||
}
|
||||
if (kind === 'music') return []
|
||||
const first = {
|
||||
prompt: payload.prompt || '',
|
||||
duration: Number(payload.duration) || 5,
|
||||
loraName: payload.loraName,
|
||||
loraStack: payload.loraStack
|
||||
}
|
||||
const rest = (payload.extensions || []).map(item => ({
|
||||
prompt: item.prompt || '',
|
||||
duration: Number(item.duration) || 5,
|
||||
loraName: item.loraName,
|
||||
loraStack: item.loraStack
|
||||
}))
|
||||
return [first, ...rest]
|
||||
}
|
||||
|
||||
export function applyQueuedShots(kind: string, shots: QueuedShotDraft[], payload: QueuedInspectPayload) {
|
||||
const next = { ...payload }
|
||||
if (kind === 'music') {
|
||||
next.prompt = payload.tags || payload.prompt || ''
|
||||
return next
|
||||
}
|
||||
if (kind === 'edit') {
|
||||
next.prompt = shots[0]?.prompt || ''
|
||||
next.passes = shots.slice(1).map(item => ({ prompt: item.prompt || '' }))
|
||||
return next
|
||||
}
|
||||
next.prompt = shots[0]?.prompt || ''
|
||||
next.duration = Number(shots[0]?.duration) || next.duration || 5
|
||||
next.extensions = shots.slice(1).map((item) => {
|
||||
const fields = persistLoraFields(item.loraStack || item.loraName)
|
||||
return {
|
||||
prompt: item.prompt || '',
|
||||
duration: Number(item.duration) || 5,
|
||||
loraName: fields.loraName,
|
||||
loraStack: fields.loraStack
|
||||
}
|
||||
})
|
||||
return next
|
||||
}
|
||||
|
||||
function line(label: string, value: unknown) {
|
||||
const text = String(value ?? '').trim()
|
||||
if (!text) return ''
|
||||
return `${label}: ${text}`
|
||||
}
|
||||
|
||||
export function queuedJobCopyText(job: QueuedInspectJob) {
|
||||
const payload = job.payload || {}
|
||||
const kind = queuedJobKindLabel(job)
|
||||
const lines = [
|
||||
line('Kind', kind),
|
||||
line('Name', job.name || payload.name),
|
||||
line('Status', job.status),
|
||||
job.lastError ? line('Error', job.lastError) : '',
|
||||
line('Workflow', payload.workflow),
|
||||
line('Mode', payload.v2Mode),
|
||||
line('Engine', payload.engine),
|
||||
line('Task', payload.v2Task),
|
||||
line('Aspect', payload.aspect),
|
||||
payload.duration != null ? line('Duration', `${payload.duration}s`) : '',
|
||||
payload.steps != null ? line('Steps', payload.steps) : '',
|
||||
payload.cfg != null ? line('CFG', payload.cfg) : '',
|
||||
payload.fps != null ? line('FPS', payload.fps) : '',
|
||||
payload.seed != null ? line('Seed', payload.seed) : '',
|
||||
payload.turbo != null ? line('Turbo', payload.turbo ? 'yes' : 'no') : '',
|
||||
payload.sound != null && job.kind !== 'music' ? line('Sound', payload.sound ? 'on' : 'off') : '',
|
||||
line('Sampler', payload.samplerName),
|
||||
line('Scheduler', payload.scheduler),
|
||||
line('Negative', payload.negative)
|
||||
].filter(Boolean)
|
||||
|
||||
const stack = readLoraStack(payload)
|
||||
if (stack.length) {
|
||||
lines.push(`LoRAs: ${stack.map(item => `${item.name} ${item.strengthModel}/${item.strengthClip}`).join(', ')}`)
|
||||
}
|
||||
|
||||
if (job.kind === 'music') {
|
||||
lines.push(line('Tags', payload.tags || job.prompt))
|
||||
if (payload.instrumental) lines.push('Instrumental: yes')
|
||||
else lines.push(line('Lyrics', payload.lyrics))
|
||||
} else {
|
||||
if (payload.promptPre) lines.push(line('Prompt pre', payload.promptPre))
|
||||
lines.push(line('Prompt', payload.prompt || job.prompt))
|
||||
if (payload.promptPost) lines.push(line('Prompt post', payload.promptPost))
|
||||
const shots = queuedJobShots(payload, job.kind)
|
||||
if (shots.length > 1) {
|
||||
lines.push('')
|
||||
shots.forEach((shot, index) => {
|
||||
const label = job.kind === 'edit' ? `Pass ${index + 1}` : `Shot ${index + 1}`
|
||||
lines.push(`${label} (${shot.duration || 0}s): ${shot.prompt}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return lines.filter(Boolean).join('\n')
|
||||
}
|
||||
|
||||
export function queuedJobPresetKind(job: { kind?: string; imagePipeline?: string }): 'video' | 'image' | 'imagev2' | 'music' {
|
||||
if (job.kind === 'music') return 'music'
|
||||
if (job.kind === 'edit') return job.imagePipeline === 'v2' ? 'imagev2' : 'image'
|
||||
return 'video'
|
||||
}
|
||||
|
||||
export function queuedJobPresetBody(job: QueuedInspectJob, name: string) {
|
||||
const payload = job.payload || {}
|
||||
const kind = queuedJobPresetKind(job)
|
||||
const loraStack = readLoraStack(payload)
|
||||
if (kind === 'music') {
|
||||
return {
|
||||
name,
|
||||
kind,
|
||||
overwrite: true,
|
||||
loraStack: [],
|
||||
settings: {
|
||||
tags: sanitizeMusicTags(payload.tags || job.prompt),
|
||||
lyrics: sanitizeMusicLyrics(payload.lyrics, payload.instrumental === true),
|
||||
instrumental: payload.instrumental === true,
|
||||
duration: clampMusicDuration(payload.duration),
|
||||
steps: clampMusicSteps(payload.steps),
|
||||
seed: payload.seed,
|
||||
cfg: payload.cfg
|
||||
}
|
||||
}
|
||||
}
|
||||
if (kind === 'imagev2') {
|
||||
return {
|
||||
name,
|
||||
kind,
|
||||
overwrite: true,
|
||||
loraStack,
|
||||
settings: {
|
||||
mode: payload.v2Mode,
|
||||
engine: payload.engine,
|
||||
task: payload.v2Task,
|
||||
prompt: payload.prompt || job.prompt,
|
||||
negative: payload.negative,
|
||||
snofsModel: payload.snofsModel,
|
||||
snofsClip: payload.snofsClip,
|
||||
consistencyModel: payload.consistencyModel,
|
||||
consistencyClip: payload.consistencyClip,
|
||||
steps: payload.steps,
|
||||
cfg: payload.cfg,
|
||||
megapixels: payload.scaleMegapixels,
|
||||
scaleToTotalPixels: payload.scaleToTotalPixels,
|
||||
turbo: payload.turbo === true,
|
||||
strength: payload.refineStrength,
|
||||
aspect: payload.aspect,
|
||||
width: payload.width,
|
||||
height: payload.height
|
||||
}
|
||||
}
|
||||
}
|
||||
if (kind === 'image') {
|
||||
return {
|
||||
name,
|
||||
kind,
|
||||
overwrite: true,
|
||||
loraStack,
|
||||
settings: {
|
||||
prompt: payload.prompt || job.prompt,
|
||||
steps: payload.steps,
|
||||
cfg: payload.cfg,
|
||||
negative: payload.negative,
|
||||
scaleToTotalPixels: payload.scaleToTotalPixels,
|
||||
scaleMegapixels: payload.scaleMegapixels,
|
||||
aspect: payload.aspect
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
name,
|
||||
kind,
|
||||
overwrite: true,
|
||||
loraStack,
|
||||
settings: {
|
||||
workflow: payload.workflow,
|
||||
turbo: payload.turbo !== false,
|
||||
cfg: payload.cfg,
|
||||
fps: payload.fps,
|
||||
samplerName: payload.samplerName,
|
||||
scheduler: payload.scheduler,
|
||||
duration: payload.duration,
|
||||
aspect: payload.aspect,
|
||||
sound: payload.sound !== false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user