Files
aigen/utils/queuedJob.ts

300 lines
9.2 KiB
TypeScript

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'
/** Full studio payload JSON for reload when the queue row is gone. */
export const LOAD_GENERATION_PAYLOAD_KEY = 'aigen-load-generation-payload'
export type QueuedInspectKind = 'video' | 'edit' | 'music'
export type QueuedInspectSource = 'studio' | 'live'
export type QueuedShotDraft = import('~/utils/imageIterations').ImageIteration & {
prompt: string
duration: number
loraName?: string
loraStack?: LoraStackItem[]
}
export type QueuedInspectPayload = {
upscale?: { sourceId: string; scale: number; target: string; fps: string | number; enhance: string }
yueProfile?: 1 | 3
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?: import('~/utils/imageIterations').ImageIteration[]
passMode?: 'batch' | 'chain'
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
musicEngine?: string
}
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 => ({ ...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(({ duration, ...item }) => ({ ...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, index) => {
const prev = payload.extensions?.[index]
const fields = persistLoraFields(item.loraStack || item.loraName || prev?.loraStack || prev?.loraName)
return {
...prev,
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.promptMid || 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,
engine: payload.musicEngine || payload.engine,
yueProfile: payload.yueProfile
}
}
}
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
}
}
}