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 } } }