Keep a generation log so wiped or failed jobs can be re-queued.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+26
-9
@@ -2227,7 +2227,7 @@ import {
|
||||
VIDEO_CFG_STEP,
|
||||
type GenerationPreset
|
||||
} from '~/utils/generationPresets'
|
||||
import { LOAD_QUEUED_JOB_KEY } from '~/utils/queuedJob'
|
||||
import { LOAD_GENERATION_PAYLOAD_KEY, LOAD_QUEUED_JOB_KEY } from '~/utils/queuedJob'
|
||||
import {
|
||||
clipSettingsRows,
|
||||
inferStillPipeline,
|
||||
@@ -4400,6 +4400,19 @@ function applyIncomingDirectorScript() {
|
||||
}
|
||||
|
||||
async function applyIncomingQueuedJob() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(LOAD_GENERATION_PAYLOAD_KEY)
|
||||
if (raw) {
|
||||
sessionStorage.removeItem(LOAD_GENERATION_PAYLOAD_KEY)
|
||||
const loaded = JSON.parse(raw) as { kind?: string; payload?: Record<string, any> }
|
||||
if (loaded?.payload) {
|
||||
await restoreStudioPayload(loaded.kind || 'video', loaded.payload)
|
||||
toast('Saved generation loaded into Input.')
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch { /* fall through to queue id */ }
|
||||
|
||||
let id = ''
|
||||
try {
|
||||
id = sessionStorage.getItem(LOAD_QUEUED_JOB_KEY) || ''
|
||||
@@ -5376,7 +5389,16 @@ async function restoreStudioEditJob(payload: {
|
||||
|
||||
async function restoreStudioJob(id: string) {
|
||||
const job = await $fetch<{
|
||||
payload?: {
|
||||
payload?: Parameters<typeof restoreStudioPayload>[1]
|
||||
kind?: string
|
||||
imagePipeline?: string
|
||||
}>(`/api/studio-queue/${id}`)
|
||||
const payload = job.payload
|
||||
if (!payload) throw new Error('That job has no saved inputs')
|
||||
await restoreStudioPayload(job.kind || (payload.imagePipeline === 'v2' ? 'edit' : 'video'), payload)
|
||||
}
|
||||
|
||||
async function restoreStudioPayload(kind: string, payload: {
|
||||
prompt?: string
|
||||
name?: string
|
||||
folderId?: string
|
||||
@@ -5423,13 +5445,8 @@ async function restoreStudioJob(id: string) {
|
||||
scaleMegapixels?: number
|
||||
steps?: number
|
||||
referenceStillId?: string
|
||||
}
|
||||
kind?: string
|
||||
imagePipeline?: string
|
||||
}>(`/api/studio-queue/${id}`)
|
||||
const payload = job.payload
|
||||
if (!payload) throw new Error('That job has no saved inputs')
|
||||
if (job.kind === 'edit' || payload.imagePipeline === 'v2') {
|
||||
}) {
|
||||
if (kind === 'edit' || payload.imagePipeline === 'v2') {
|
||||
await restoreStudioEditJob(payload)
|
||||
return
|
||||
}
|
||||
|
||||
+23
-1
@@ -244,7 +244,7 @@ import {
|
||||
} from '~/utils/music'
|
||||
import { ensureGenerationNotifyPermission, notifyGenerationComplete } from '~/utils/generationNotify'
|
||||
import type { LibraryFolder } from '~/utils/libraryBrowse'
|
||||
import { LOAD_MUSIC_JOB_KEY } from '~/utils/queuedJob'
|
||||
import { LOAD_GENERATION_PAYLOAD_KEY, LOAD_MUSIC_JOB_KEY } from '~/utils/queuedJob'
|
||||
import type { GenerationPreset, MusicPresetSettings } from '~/utils/generationPresets'
|
||||
|
||||
const tagsPlaceholder = DEFAULT_MUSIC_TAGS
|
||||
@@ -566,6 +566,28 @@ watch(instrumental, (on) => {
|
||||
})
|
||||
|
||||
function applyIncomingMusic() {
|
||||
try {
|
||||
const genRaw = sessionStorage.getItem(LOAD_GENERATION_PAYLOAD_KEY)
|
||||
if (genRaw) {
|
||||
const loaded = JSON.parse(genRaw) as { kind?: string; payload?: Record<string, any> }
|
||||
if (loaded?.kind === 'music' && loaded.payload) {
|
||||
sessionStorage.removeItem(LOAD_GENERATION_PAYLOAD_KEY)
|
||||
const payload = loaded.payload
|
||||
if (typeof payload.prompt === 'string') tags.value = payload.prompt
|
||||
if (typeof payload.lyrics === 'string') lyrics.value = payload.lyrics
|
||||
instrumental.value = payload.instrumental === true
|
||||
if (typeof payload.duration === 'number') duration.value = payload.duration
|
||||
if (typeof payload.steps === 'number') steps.value = payload.steps
|
||||
if (payload.seed != null) seed.value = String(payload.seed)
|
||||
if (typeof payload.name === 'string') name.value = payload.name
|
||||
if (payload.folderId && folders.value.some(folder => folder.id === payload.folderId)) folderId.value = payload.folderId
|
||||
if (payload.musicEngine || payload.engine) applyEngine(parseMusicEngine(payload.musicEngine || payload.engine))
|
||||
status.value = 'Loaded from generation log'
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch { /* fall through */ }
|
||||
|
||||
try {
|
||||
const raw = sessionStorage.getItem(LOAD_MUSIC_JOB_KEY)
|
||||
if (!raw) return
|
||||
|
||||
+149
-6
@@ -374,6 +374,58 @@
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section v-if="logEntries.length" class="glass min-w-0 max-w-full rounded-3xl p-5 sm:p-6 space-y-4">
|
||||
<div>
|
||||
<h2 class="font-display text-xl font-semibold text-zinc-100">Recent generations</h2>
|
||||
<p class="mt-1 text-sm text-zinc-400">
|
||||
Kept even after the queue clears or a zombie job is wiped. Re-queue to run again, or load the prompt into Studio.
|
||||
</p>
|
||||
</div>
|
||||
<ul class="space-y-2">
|
||||
<li
|
||||
v-for="entry in logEntries"
|
||||
:key="entry.id"
|
||||
class="flex flex-wrap items-start justify-between gap-3 rounded-2xl border border-white/10 bg-zinc-950/50 px-4 py-3"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-[11px] uppercase tracking-[0.18em] text-zinc-500">
|
||||
{{ logKindLabel(entry) }} · {{ logStatusLabel(entry.status) }}
|
||||
<span class="normal-case tracking-normal text-zinc-600"> · {{ formatLogWhen(entry.updatedAt || entry.createdAt) }}</span>
|
||||
</p>
|
||||
<p class="mt-1 truncate text-sm font-medium text-zinc-100">{{ entry.name || 'Untitled' }}</p>
|
||||
<p class="mt-0.5 line-clamp-2 text-xs text-zinc-400">{{ promptSnippet(entry.prompt) }}</p>
|
||||
<p v-if="entry.lastError" class="mt-1 text-xs text-red-300">{{ entry.lastError }}</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full border border-amber-300/40 px-3 py-1.5 text-xs text-amber-100 hover:border-amber-200 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="Boolean(busyId)"
|
||||
@click="requeueLog(entry)"
|
||||
>
|
||||
Re-queue
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full border border-sky-300/40 px-3 py-1.5 text-xs text-sky-100 hover:border-sky-200 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="Boolean(busyId)"
|
||||
@click="loadLog(entry)"
|
||||
>
|
||||
Load
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full border border-white/10 px-3 py-1.5 text-xs text-zinc-400 hover:text-red-200 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="Boolean(busyId)"
|
||||
@click="dismissLog(entry)"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Teleport to="body">
|
||||
@@ -420,6 +472,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { insertActivationPhrase, loraIdentityKey, loraMatchesUse, loraStackLabel, persistLoraFields, readLoraStack, type LoraStackItem } from '~/utils/loras'
|
||||
import { LOAD_GENERATION_PAYLOAD_KEY } from '~/utils/queuedJob'
|
||||
|
||||
type Segment = {
|
||||
index: number
|
||||
@@ -483,8 +536,22 @@ const videoLoras = ref<string[]>([])
|
||||
const loraTriggers = ref<Record<string, string>>({})
|
||||
const loraAliases = ref<Record<string, string>>({})
|
||||
const inspectJobId = ref('')
|
||||
type GenerationLogEntry = {
|
||||
id: string
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
kind: string
|
||||
name: string
|
||||
prompt: string
|
||||
status: string
|
||||
lastError?: string
|
||||
payload: Record<string, any>
|
||||
imagePipeline?: string
|
||||
}
|
||||
|
||||
const jobs = ref<StudioJobRow[]>([])
|
||||
const orphans = ref<Queue[]>([])
|
||||
const logEntries = ref<GenerationLogEntry[]>([])
|
||||
const waitingCount = ref(0)
|
||||
const pendingCount = ref(0)
|
||||
const queuePaused = ref(false)
|
||||
@@ -717,12 +784,15 @@ function mergeQueues(incoming: Queue[]) {
|
||||
}
|
||||
|
||||
async function loadQueues() {
|
||||
const data = await $fetch<{
|
||||
jobs?: StudioJobRow[]
|
||||
orphans?: Queue[]
|
||||
waitingCount?: number
|
||||
paused?: boolean
|
||||
}>('/api/studio-queue?full=1')
|
||||
const [data, logData] = await Promise.all([
|
||||
$fetch<{
|
||||
jobs?: StudioJobRow[]
|
||||
orphans?: Queue[]
|
||||
waitingCount?: number
|
||||
paused?: boolean
|
||||
}>('/api/studio-queue?full=1'),
|
||||
$fetch<{ entries?: GenerationLogEntry[] }>('/api/generation-log').catch(() => ({ entries: [] as GenerationLogEntry[] }))
|
||||
])
|
||||
const nextJobs = (data.jobs || []).map((job) => (
|
||||
job.shots ? { ...job, shots: mergeQueues([job.shots])[0] } : job
|
||||
))
|
||||
@@ -730,6 +800,7 @@ async function loadQueues() {
|
||||
orphans.value = mergeQueues(data.orphans || [])
|
||||
waitingCount.value = data.waitingCount || 0
|
||||
queuePaused.value = data.paused === true
|
||||
logEntries.value = logData.entries || []
|
||||
pendingCount.value = [
|
||||
...nextJobs.flatMap(job => job.shots ? [job.shots] : []),
|
||||
...orphans.value
|
||||
@@ -821,6 +892,78 @@ function jobKindLabel(job: StudioJobRow) {
|
||||
return 'VIDEO'
|
||||
}
|
||||
|
||||
function logKindLabel(entry: GenerationLogEntry) {
|
||||
if (entry.kind === 'music') return 'MUSIC'
|
||||
if (entry.kind === 'edit') {
|
||||
if (entry.payload?.imagePipeline === 'v2') return 'IMAGE V2'
|
||||
return 'IMAGE'
|
||||
}
|
||||
return 'VIDEO'
|
||||
}
|
||||
|
||||
function logStatusLabel(status: string) {
|
||||
if (status === 'complete') return 'done'
|
||||
if (status === 'error') return 'failed'
|
||||
if (status === 'cancelled') return 'cancelled'
|
||||
if (status === 'running') return 'running'
|
||||
return 'queued'
|
||||
}
|
||||
|
||||
function formatLogWhen(ts: number) {
|
||||
if (!ts) return ''
|
||||
try {
|
||||
return new Date(ts).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
async function requeueLog(entry: GenerationLogEntry) {
|
||||
busyId.value = entry.id
|
||||
error.value = ''
|
||||
try {
|
||||
await $fetch(`/api/generation-log/${entry.id}/requeue`, { method: 'POST' })
|
||||
await loadQueues()
|
||||
} catch (err: any) {
|
||||
error.value = err?.data?.statusMessage || err?.statusMessage || err?.message || 'Could not re-queue that generation'
|
||||
} finally {
|
||||
busyId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLog(entry: GenerationLogEntry) {
|
||||
busyId.value = entry.id
|
||||
error.value = ''
|
||||
try {
|
||||
sessionStorage.setItem(LOAD_GENERATION_PAYLOAD_KEY, JSON.stringify({
|
||||
kind: entry.kind,
|
||||
payload: entry.payload
|
||||
}))
|
||||
if (entry.kind === 'music') {
|
||||
await navigateTo('/music')
|
||||
} else {
|
||||
await navigateTo('/')
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err?.message || 'Could not load that generation'
|
||||
} finally {
|
||||
busyId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function dismissLog(entry: GenerationLogEntry) {
|
||||
busyId.value = entry.id
|
||||
error.value = ''
|
||||
try {
|
||||
await $fetch(`/api/generation-log/${entry.id}`, { method: 'DELETE' })
|
||||
logEntries.value = logEntries.value.filter(item => item.id !== entry.id)
|
||||
} catch (err: any) {
|
||||
error.value = err?.data?.statusMessage || err?.statusMessage || err?.message || 'Could not dismiss that entry'
|
||||
} finally {
|
||||
busyId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function jobThumb(job: StudioJobRow) {
|
||||
if (!job.stillId || job.hideThumbnail || job.folderLocked) return ''
|
||||
return `/api/library/stills/${job.stillId}`
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { deleteGenerationLogEntry } from '~/server/utils/generationLog'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
const id = String(getRouterParam(event, 'id') || '')
|
||||
await deleteGenerationLogEntry(owner, id)
|
||||
return { ok: true }
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
import { getGenerationLogEntry } from '~/server/utils/generationLog'
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
const id = String(getRouterParam(event, 'id') || '')
|
||||
return getGenerationLogEntry(owner, id)
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
import { requeueGenerationLog } from '~/server/utils/generationLog'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
const id = String(getRouterParam(event, 'id') || '')
|
||||
return await requeueGenerationLog(owner, id)
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
import { listGenerationLog } from '~/server/utils/generationLog'
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
const limit = Number(getQuery(event).limit || 40)
|
||||
return {
|
||||
entries: listGenerationLog(owner, Number.isFinite(limit) ? limit : 40)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,159 @@
|
||||
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { ownerRoot } from '~/server/utils/library'
|
||||
import type { StudioJobKind, StudioJobPayload } from '~/server/utils/studioQueue'
|
||||
|
||||
export type GenerationLogStatus = 'queued' | 'running' | 'complete' | 'error' | 'cancelled'
|
||||
|
||||
export type GenerationLogEntry = {
|
||||
id: string
|
||||
studioJobId?: string
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
kind: StudioJobKind
|
||||
name: string
|
||||
prompt: string
|
||||
status: GenerationLogStatus
|
||||
lastError?: string
|
||||
payload: StudioJobPayload
|
||||
}
|
||||
|
||||
const MAX_ENTRIES = 80
|
||||
const writeChains = new Map<string, Promise<unknown>>()
|
||||
|
||||
function logPath(owner: string) {
|
||||
return join(ownerRoot(owner), 'generation-log.json')
|
||||
}
|
||||
|
||||
function ensureOwner(owner: string) {
|
||||
mkdirSync(ownerRoot(owner), { recursive: true })
|
||||
}
|
||||
|
||||
function readLog(owner: string): GenerationLogEntry[] {
|
||||
ensureOwner(owner)
|
||||
const path = logPath(owner)
|
||||
if (!existsSync(path)) return []
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(path, 'utf8'))
|
||||
return Array.isArray(parsed) ? parsed.filter(item => item && typeof item === 'object' && item.id && item.payload) : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function writeLog(owner: string, entries: GenerationLogEntry[]) {
|
||||
ensureOwner(owner)
|
||||
const path = logPath(owner)
|
||||
const tmp = `${path}.tmp`
|
||||
writeFileSync(tmp, JSON.stringify(entries, null, 2))
|
||||
renameSync(tmp, path)
|
||||
}
|
||||
|
||||
function mutate<T>(owner: string, fn: (entries: GenerationLogEntry[]) => T): Promise<T> {
|
||||
const prev = writeChains.get(owner) || Promise.resolve()
|
||||
const run = prev.then(() => {
|
||||
const entries = readLog(owner)
|
||||
const result = fn(entries)
|
||||
writeLog(owner, entries)
|
||||
return result
|
||||
})
|
||||
writeChains.set(owner, run.then(() => undefined, () => undefined))
|
||||
return run
|
||||
}
|
||||
|
||||
function snippetName(payload: StudioJobPayload, kind: StudioJobKind) {
|
||||
const named = String(payload.name || '').trim()
|
||||
if (named) return named.slice(0, 80)
|
||||
if (kind === 'music') return String(payload.prompt || 'Track').slice(0, 80)
|
||||
return String(payload.prompt || 'Untitled').slice(0, 80)
|
||||
}
|
||||
|
||||
export function listGenerationLog(owner: string, limit = 40) {
|
||||
return readLog(owner).slice(0, Math.max(1, Math.min(MAX_ENTRIES, limit)))
|
||||
}
|
||||
|
||||
export function getGenerationLogEntry(owner: string, id: string) {
|
||||
const entry = readLog(owner).find(item => item.id === id)
|
||||
if (!entry) throw createError({ statusCode: 404, statusMessage: 'Generation log entry not found' })
|
||||
return entry
|
||||
}
|
||||
|
||||
export async function recordGenerationQueued(params: {
|
||||
ownerKey: string
|
||||
studioJobId: string
|
||||
kind: StudioJobKind
|
||||
payload: StudioJobPayload
|
||||
}) {
|
||||
const now = Date.now()
|
||||
return mutate(params.ownerKey, (entries) => {
|
||||
const existing = entries.find(item => item.studioJobId === params.studioJobId)
|
||||
if (existing) {
|
||||
existing.updatedAt = now
|
||||
existing.status = 'queued'
|
||||
existing.payload = structuredClone(params.payload)
|
||||
existing.name = snippetName(params.payload, params.kind)
|
||||
existing.prompt = params.payload.prompt || existing.prompt
|
||||
existing.kind = params.kind
|
||||
existing.lastError = undefined
|
||||
entries.splice(entries.indexOf(existing), 1)
|
||||
entries.unshift(existing)
|
||||
return existing
|
||||
}
|
||||
const entry: GenerationLogEntry = {
|
||||
id: crypto.randomUUID(),
|
||||
studioJobId: params.studioJobId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
kind: params.kind,
|
||||
name: snippetName(params.payload, params.kind),
|
||||
prompt: params.payload.prompt || '',
|
||||
status: 'queued',
|
||||
payload: structuredClone(params.payload)
|
||||
}
|
||||
entries.unshift(entry)
|
||||
entries.splice(MAX_ENTRIES)
|
||||
return entry
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateGenerationLogByStudioJob(
|
||||
owner: string,
|
||||
studioJobId: string,
|
||||
patch: { status?: GenerationLogStatus; lastError?: string; payload?: StudioJobPayload }
|
||||
) {
|
||||
return mutate(owner, (entries) => {
|
||||
const entry = entries.find(item => item.studioJobId === studioJobId)
|
||||
if (!entry) return null
|
||||
entry.updatedAt = Date.now()
|
||||
if (patch.status) entry.status = patch.status
|
||||
if (patch.lastError !== undefined) entry.lastError = patch.lastError || undefined
|
||||
if (patch.payload) {
|
||||
entry.payload = structuredClone(patch.payload)
|
||||
entry.name = snippetName(patch.payload, entry.kind)
|
||||
entry.prompt = patch.payload.prompt || entry.prompt
|
||||
}
|
||||
return structuredClone(entry)
|
||||
})
|
||||
}
|
||||
|
||||
export async function requeueGenerationLog(owner: string, id: string) {
|
||||
const entry = getGenerationLogEntry(owner, id)
|
||||
const { addStudioJob, kickStudioQueue } = await import('~/server/utils/studioQueue')
|
||||
const studio = await addStudioJob({
|
||||
ownerKey: owner,
|
||||
familyId: crypto.randomUUID(),
|
||||
kind: entry.kind,
|
||||
payload: structuredClone(entry.payload)
|
||||
})
|
||||
await kickStudioQueue()
|
||||
return { studioJobId: studio.id, entry: listGenerationLog(owner, 1)[0] || getGenerationLogEntry(owner, id) }
|
||||
}
|
||||
|
||||
export async function deleteGenerationLogEntry(owner: string, id: string) {
|
||||
return mutate(owner, (entries) => {
|
||||
const index = entries.findIndex(item => item.id === id)
|
||||
if (index < 0) throw createError({ statusCode: 404, statusMessage: 'Generation log entry not found' })
|
||||
const [removed] = entries.splice(index, 1)
|
||||
return removed
|
||||
})
|
||||
}
|
||||
@@ -389,17 +389,34 @@ export async function addStudioJob(params: {
|
||||
jobs.push(job)
|
||||
return job
|
||||
})
|
||||
void import('~/server/utils/generationLog')
|
||||
.then(({ recordGenerationQueued }) => recordGenerationQueued({
|
||||
ownerKey: params.ownerKey,
|
||||
studioJobId: job.id,
|
||||
kind,
|
||||
payload: params.payload
|
||||
}))
|
||||
.catch(() => null)
|
||||
return job
|
||||
}
|
||||
|
||||
export async function patchStudioJob(owner: string, id: string, patch: (job: StudioJob) => void) {
|
||||
return mutate(owner, (jobs) => {
|
||||
const updated = await mutate(owner, (jobs) => {
|
||||
const job = jobs.find(item => item.id === id)
|
||||
if (!job) throw createError({ statusCode: 404, statusMessage: 'Queued job not found' })
|
||||
patch(job)
|
||||
job.updatedAt = Date.now()
|
||||
return structuredClone(job)
|
||||
})
|
||||
if (updated.status === 'error' || updated.status === 'running' || updated.status === 'cancelled' || updated.status === 'complete') {
|
||||
void import('~/server/utils/generationLog')
|
||||
.then(({ updateGenerationLogByStudioJob }) => updateGenerationLogByStudioJob(owner, id, {
|
||||
status: updated.status === 'running' ? 'running' : updated.status,
|
||||
lastError: updated.lastError
|
||||
}))
|
||||
.catch(() => null)
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
function canCancelStudioStatus(status: StudioJobStatus) {
|
||||
@@ -460,6 +477,9 @@ export async function cancelStudioJob(owner: string, id: string) {
|
||||
}
|
||||
}
|
||||
kickStudioQueue()
|
||||
void import('~/server/utils/generationLog')
|
||||
.then(({ updateGenerationLogByStudioJob }) => updateGenerationLogByStudioJob(owner, id, { status: 'cancelled' }))
|
||||
.catch(() => null)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -598,7 +618,7 @@ export async function markStudioHeld(owner: string, id: string) {
|
||||
}
|
||||
|
||||
export async function markStudioSettled(owner: string, liveJobId: string, status: 'complete' | 'error' | 'cancelled', error?: string) {
|
||||
return mutate(owner, (jobs) => {
|
||||
const settled = await mutate(owner, (jobs) => {
|
||||
const job = jobs.find(item => item.liveJobId === liveJobId)
|
||||
if (!job) return null
|
||||
if (job.holdForCutIn && status === 'complete') {
|
||||
@@ -615,6 +635,15 @@ export async function markStudioSettled(owner: string, liveJobId: string, status
|
||||
job.updatedAt = Date.now()
|
||||
return structuredClone(job)
|
||||
})
|
||||
if (settled) {
|
||||
void import('~/server/utils/generationLog')
|
||||
.then(({ updateGenerationLogByStudioJob }) => updateGenerationLogByStudioJob(owner, settled.id, {
|
||||
status: settled.status === 'held' ? 'queued' : status,
|
||||
lastError: error
|
||||
}))
|
||||
.catch(() => null)
|
||||
}
|
||||
return settled
|
||||
}
|
||||
|
||||
function pickCutIn(jobs: StudioJob[]) {
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
|
||||
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'
|
||||
|
||||
Reference in New Issue
Block a user