Add batch still arrows, hide output only on tap, and cut studio lag.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-31 07:24:32 -05:00
co-authored by Cursor
parent f51fd4b159
commit 6624fb6f93
8 changed files with 367 additions and 61 deletions
+46 -19
View File
@@ -47,31 +47,22 @@ textarea {
}
.glass {
background: rgba(24, 24, 27, 0.72);
background: rgba(24, 24, 27, 0.88);
border: 1px solid rgba(255, 255, 255, 0.06);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.35);
backdrop-filter: blur(18px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.28);
}
@media (hover: hover) and (min-width: 768px) {
.glass {
background: rgba(24, 24, 27, 0.72);
backdrop-filter: blur(8px);
}
}
.progress-glow {
box-shadow: 0 0 24px rgba(251, 191, 36, 0.35);
}
@keyframes sensitive-pulse {
0%, 100% {
box-shadow:
0 0 28px rgba(251, 191, 36, 0.25),
0 0 90px rgba(251, 191, 36, 0.18),
inset 0 0 60px rgba(251, 191, 36, 0.08);
}
50% {
box-shadow:
0 0 48px rgba(251, 191, 36, 0.55),
0 0 140px rgba(244, 114, 182, 0.28),
inset 0 0 80px rgba(251, 191, 36, 0.16);
}
}
.aigen-slider {
-webkit-appearance: none;
appearance: none;
@@ -109,7 +100,6 @@ textarea {
}
.sensitive-glow {
animation: sensitive-pulse 2.2s ease-in-out infinite;
background:
radial-gradient(circle at 50% 45%, rgba(251, 191, 36, 0.22), transparent 42%),
radial-gradient(circle at 50% 55%, rgba(244, 114, 182, 0.16), transparent 55%),
@@ -120,3 +110,40 @@ textarea {
content-visibility: auto;
contain-intrinsic-size: auto 15rem;
}
.studio-library-card {
content-visibility: auto;
contain-intrinsic-size: auto 5.5rem;
}
.family-nav {
display: flex;
height: 2.75rem;
width: 2.75rem;
align-items: center;
justify-content: center;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(9, 9, 11, 0.78);
color: #fde68a;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
}
.family-nav:hover {
border-color: rgba(252, 211, 77, 0.55);
color: #fffbeb;
}
.family-nav-prev {
position: absolute;
left: 0.5rem;
top: 50%;
transform: translateY(-50%);
}
.family-nav-next {
position: absolute;
right: 0.5rem;
top: 50%;
transform: translateY(-50%);
}
+3 -3
View File
@@ -1,5 +1,5 @@
<template>
<header class="sticky top-0 z-20 border-b border-white/5 bg-zinc-950/70 backdrop-blur-xl">
<header class="sticky top-0 z-20 border-b border-white/5 bg-zinc-950/90 sm:bg-zinc-950/80 sm:backdrop-blur-md">
<div
class="mx-auto flex items-center justify-between px-4 py-4 sm:px-6"
:class="wide ? 'max-w-[90rem]' : 'max-w-7xl'"
@@ -30,7 +30,7 @@
</div>
<div
v-if="menuOpen"
class="absolute left-0 top-full z-30 mt-2 w-52 rounded-2xl border border-white/10 bg-zinc-950/95 p-1 shadow-xl shadow-black/40 backdrop-blur-xl"
class="absolute left-0 top-full z-30 mt-2 w-52 rounded-2xl border border-white/10 bg-zinc-950/95 p-1 shadow-xl shadow-black/40"
role="menu"
@click.stop
>
@@ -285,7 +285,7 @@ onMounted(async () => {
healthTimer = setInterval(() => {
if (typeof document !== 'undefined' && document.hidden) return
void pollHealth()
}, 5000)
}, 15000)
})
onBeforeUnmount(() => {
+42
View File
@@ -0,0 +1,42 @@
<template>
<div v-if="count > 1" class="pointer-events-none absolute inset-0 z-20">
<button
type="button"
class="family-nav family-nav-prev pointer-events-auto"
:aria-label="prevLabel"
@click.stop="$emit('step', -1)"
@pointerdown.stop
>
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M15 5 8 12l7 7" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<button
type="button"
class="family-nav family-nav-next pointer-events-auto"
:aria-label="nextLabel"
@click.stop="$emit('step', 1)"
@pointerdown.stop
>
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="m9 5 7 7-7 7" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<span class="pointer-events-none absolute bottom-2 left-1/2 -translate-x-1/2 rounded-full bg-zinc-950/75 px-2 py-0.5 text-[11px] tabular-nums text-amber-100">
{{ index + 1 }} / {{ count }}
</span>
</div>
</template>
<script setup lang="ts">
withDefaults(defineProps<{
count: number
index: number
prevLabel?: string
nextLabel?: string
}>(), {
prevLabel: 'Previous image',
nextLabel: 'Next image'
})
defineEmits<{ step: [delta: number] }>()
</script>
+218 -31
View File
@@ -1361,6 +1361,7 @@
:src="editResultUrl"
alt="Edited still"
class="mx-auto max-h-[min(70dvh,24rem)] w-full object-contain"
decoding="async"
>
<span
v-if="!editAwaitingReveal"
@@ -1394,6 +1395,11 @@
>
Edited still appears here
</div>
<FamilyNav
:count="outputFamilyStills.length"
:index="outputFamilyIndex"
@step="stepOutputFamily"
/>
</div>
<div v-if="editResultUrl && !editLockedSave && !editAwaitingReveal" class="mt-4 flex flex-wrap gap-3">
<a
@@ -1434,12 +1440,12 @@
<div class="flex items-center gap-3">
<NuxtLink to="/library" class="text-xs text-amber-300 hover:text-amber-100">Open full library</NuxtLink>
<button
v-if="libraryEntries.length"
v-if="visibleLibraryEntries.length"
type="button"
class="text-xs text-zinc-400 hover:text-white"
@click="selectedKeys.length === libraryEntries.length ? clearSelection() : selectAllLibrary()"
@click="selectedKeys.length === visibleLibraryEntries.length ? clearSelection() : selectAllLibrary()"
>
{{ selectedKeys.length && selectedKeys.length === libraryEntries.length ? 'Clear selection' : 'Select all' }}
{{ selectedKeys.length && selectedKeys.length === visibleLibraryEntries.length ? 'Clear selection' : 'Select all' }}
</button>
<button
v-if="browseFolder?.protected && browseFolder.unlocked"
@@ -1512,11 +1518,11 @@
<button type="button" class="text-red-300 hover:text-red-200" @click="askRemoveSelected">Delete</button>
<button type="button" class="text-zinc-400 hover:text-white" @click="clearSelection">Clear</button>
</div>
<ul class="mt-4 space-y-3">
<ul class="mt-4 max-h-[min(28rem,50vh)] space-y-3 overflow-y-auto">
<li
v-for="entry in libraryEntries"
v-for="entry in visibleLibraryEntries"
:key="entry.key"
class="cursor-pointer rounded-2xl border p-3"
class="studio-library-card cursor-pointer rounded-2xl border p-3"
:class="libraryEntryClass(entry)"
:aria-current="isLibraryEntryFocused(entry) ? 'true' : undefined"
@click="onLibraryCardClick(entry, $event)"
@@ -1871,6 +1877,10 @@
</template>
</li>
</ul>
<p v-if="libraryCapped" class="mt-3 text-xs text-zinc-500">
Showing {{ visibleLibraryEntries.length }} of {{ libraryEntries.length }}.
<NuxtLink to="/library" class="text-amber-300 hover:text-amber-100">Open full library</NuxtLink>
</p>
</template>
</div>
</section>
@@ -1924,12 +1934,19 @@
aria-label="Edited still fullscreen"
@click.self="closeEditLightbox"
>
<div class="relative max-h-[90vh] max-w-[min(96vw,80rem)]" @click.stop>
<img
:src="editLightboxUrl"
alt="Edited still"
class="max-h-[90vh] max-w-[min(96vw,80rem)] object-contain"
@click.stop
decoding="async"
>
<FamilyNav
:count="outputFamilyStills.length"
:index="outputFamilyIndex"
@step="stepOutputFamily"
/>
</div>
</div>
</Teleport>
@@ -2613,7 +2630,15 @@ let videoSource: EventSource | null = null
let editSource: EventSource | null = null
let videoPollTimer: ReturnType<typeof setInterval> | null = null
let editPollTimer: ReturnType<typeof setInterval> | null = null
let healthTimer: ReturnType<typeof setInterval> | null = null
let healthTimer: ReturnType<typeof setTimeout> | null = null
let studioPollAlive = false
let loraPollAt = 0
let concealPointers = 0
let concealPinch = false
let concealGestureAt = 0
let concealStart: { pointerId: number; x: number; y: number; moved: boolean } | null = null
const CONCEAL_MOVE_PX = 12
const CONCEAL_GESTURE_MS = 500
let videoSettledUi = false
let editSettledUi = false
let videoListenGen = 0
@@ -3077,6 +3102,30 @@ const reviewingStillId = computed(() => {
if (outputPreviewKind.value !== 'edit' || editLockedSave.value) return ''
return stillIdFromLibraryUrl(editResultUrl.value)
})
const STUDIO_LIBRARY_CAP = 36
const visibleLibraryEntries = computed(() => libraryEntries.value.slice(0, STUDIO_LIBRARY_CAP))
const libraryCapped = computed(() => libraryEntries.value.length > STUDIO_LIBRARY_CAP)
const outputFamilyStills = computed(() => {
const id = reviewingStillId.value
if (!id) return [] as LibraryStill[]
const group = stillGroups.value.find(entry => entry.parts.some(part => part.id === id))
let ordered = chronologicalStills(group?.parts || [])
if (ordered.length < 2) {
const current = stills.value.find(still => still.id === id)
if (current?.familyId) {
ordered = chronologicalStills(stills.value.filter(still => still.familyId === current.familyId && still.role !== 'input'))
} else if (current) {
ordered = [current]
}
}
return ordered.length > 1 ? ordered : []
})
const outputFamilyIndex = computed(() => {
const id = reviewingStillId.value
if (!id) return 0
const index = outputFamilyStills.value.findIndex(still => still.id === id)
return index < 0 ? 0 : index
})
type LibraryEntry = (typeof libraryEntries.value)[number]
function isLibraryEntryFocused(entry: LibraryEntry) {
if (entry.kind === 'clip' && reviewingClipId.value) {
@@ -3468,13 +3517,14 @@ async function pollLoras() {
const listed = await $fetch<{ image?: string[]; video?: string[] }>('/api/loras').catch(() => ({ image: [], video: [] }))
const nextVideo = listed.video || []
const nextImage = listed.image || []
if (nextVideo.length) {
const sameNames = (a: string[], b: string[]) => a.length === b.length && a.every((name, i) => name === b[i])
if (nextVideo.length && !sameNames(videoLoras.value, nextVideo)) {
videoLoras.value = nextVideo
rememberLoraNames('aigen-lora-names-video', nextVideo)
} else if (!videoLoras.value.length) {
videoLoras.value = readStoredLoraNames('aigen-lora-names-video')
}
if (nextImage.length) {
if (nextImage.length && !sameNames(imageLoras.value, nextImage)) {
imageLoras.value = nextImage
rememberLoraNames('aigen-lora-names-image', nextImage)
} else if (!imageLoras.value.length) {
@@ -3487,6 +3537,37 @@ async function pollComfyHealth() {
applyComfyHealth(health)
}
async function pollStudioBackground() {
await pollComfyHealth()
if (Date.now() - loraPollAt > 30_000) {
loraPollAt = Date.now()
void pollLoras()
}
void refreshStudioQueue()
if (activeQueueId.value) void refreshLiveQueue()
}
function studioBackgroundBusy() {
return videoBusy.value
|| editBusy.value
|| comfyBusy.value
|| imageComfyBusy.value
|| studioJobs.value.some(job => job.status === 'running' || job.status === 'waiting' || job.status === 'held')
}
function scheduleStudioBackground() {
if (!studioPollAlive) return
const ms = studioBackgroundBusy() ? 8000 : 20000
healthTimer = setTimeout(() => {
if (!studioPollAlive) return
const run = typeof document === 'undefined' || !document.hidden
const done = run ? pollStudioBackground() : Promise.resolve()
void done.finally(() => {
if (studioPollAlive) scheduleStudioBackground()
})
}, ms)
}
async function pokeGpu() {
if (gpuWaking.value) return
gpuWaking.value = true
@@ -3550,7 +3631,7 @@ onMounted(async () => {
if (savedPreset) recommendPresetId.value = savedPreset
} catch { /* ignore */ }
applyInstancePrivacy()
window.addEventListener('click', onSitePointerDown, true)
bindConcealListeners()
const [health, me] = await Promise.all([
$fetch<{ comfy?: { ok?: boolean; busy?: boolean } }>('/api/health').catch(() => ({ comfy: { ok: false, busy: false } })),
$fetch<{ user?: { name?: string; email?: string }; authEnabled?: boolean; authMode?: string; instanceName?: string }>('/api/auth/me').catch(() => ({ user: null })),
@@ -3558,6 +3639,7 @@ onMounted(async () => {
])
applyComfyHealth(health)
void pollLoras()
loraPollAt = Date.now()
void loadGenerationPresets()
await loadDirectorScripts().catch(() => null)
userName.value = me.user?.name || me.user?.email || ''
@@ -3565,25 +3647,22 @@ onMounted(async () => {
instanceName.value = me.instanceName || instanceName.value || 'AIGen'
applyInstancePrivacy()
setTimeout(() => { void loadLibrary(true).catch(() => null) }, 6000)
healthTimer = setInterval(() => {
if (typeof document !== 'undefined' && document.hidden) return
void pollComfyHealth()
void pollLoras()
void refreshStudioQueue()
if (activeQueueId.value) void refreshLiveQueue()
}, 5000)
studioPollAlive = true
scheduleStudioBackground()
await resumeActiveJob()
await refreshStudioQueue()
await refreshLiveQueue()
await loadPromptPresets()
if (route.query.clip || route.query.still) applyIncomingLibraryItem()
else applyIncomingDirectorScript()
window.addEventListener('keydown', onStudioKey)
})
onBeforeUnmount(() => {
window.removeEventListener('click', onSitePointerDown, true)
window.removeEventListener('keydown', onEditLightboxKey)
if (healthTimer) clearInterval(healthTimer)
unbindConcealListeners()
window.removeEventListener('keydown', onStudioKey)
studioPollAlive = false
if (healthTimer) clearTimeout(healthTimer)
healthTimer = null
if (libraryRefreshTimer) clearTimeout(libraryRefreshTimer)
libraryRefreshTimer = null
@@ -3623,11 +3702,6 @@ watch(videoBusy, (busy) => {
if (busy) outputFocus.value = 'video'
})
watch(editLightboxUrl, (value) => {
if (value) window.addEventListener('keydown', onEditLightboxKey)
else window.removeEventListener('keydown', onEditLightboxKey)
})
watch([editResultUrl, editAwaitingReveal, editLockedSave], () => {
if (!editResultUrl.value || editAwaitingReveal.value || editLockedSave.value) closeEditLightbox()
})
@@ -4593,7 +4667,7 @@ function startTimer(kind: 'video' | 'edit', offset = 0) {
const started = Date.now() - offset
const handle = setInterval(() => {
elapsed.value = Date.now() - started
}, 250)
}, 1000)
if (kind === 'edit') editTimer = handle
else videoTimer = handle
}
@@ -4938,11 +5012,124 @@ function closeEditLightbox() {
editLightboxUrl.value = ''
}
function onEditLightboxKey(event: KeyboardEvent) {
if (event.key === 'Escape') closeEditLightbox()
function typingInField(event: KeyboardEvent) {
const target = event.target as HTMLElement | null
if (!target) return false
const tag = target.tagName
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target.isContentEditable
}
function onSitePointerDown(event: Event) {
function onStudioKey(event: KeyboardEvent) {
if (event.key === 'Escape') {
if (editLightboxUrl.value) closeEditLightbox()
return
}
if (typingInField(event)) return
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return
if (outputFamilyStills.value.length < 2) return
event.preventDefault()
stepOutputFamily(event.key === 'ArrowLeft' ? -1 : 1)
}
function showOutputStill(still: LibraryStill) {
editResultUrl.value = `/api/library/stills/${still.id}`
lastV2StillId.value = still.id
editDownloadName.value = clipFileName(stillTitle(still))
if (editLightboxUrl.value) editLightboxUrl.value = `/api/library/stills/${still.id}`
}
function stepOutputFamily(delta: number) {
const family = outputFamilyStills.value
if (family.length < 2) return
const current = outputFamilyIndex.value
const next = family[(current + delta + family.length) % family.length]
if (next) showOutputStill(next)
}
function markConcealGesture() {
concealGestureAt = Date.now()
}
function bindConcealListeners() {
window.addEventListener('pointerdown', onConcealPointerDown, true)
window.addEventListener('pointermove', onConcealPointerMove, true)
window.addEventListener('pointerup', onConcealPointerUp, true)
window.addEventListener('pointercancel', onConcealPointerCancel, true)
window.addEventListener('touchstart', onConcealTouchStart, { capture: true, passive: true })
window.addEventListener('scroll', markConcealGesture, true)
window.addEventListener('gesturestart', markConcealGesture, true)
const viewport = window.visualViewport
if (viewport) {
viewport.addEventListener('resize', markConcealGesture)
viewport.addEventListener('scroll', markConcealGesture)
}
}
function unbindConcealListeners() {
window.removeEventListener('pointerdown', onConcealPointerDown, true)
window.removeEventListener('pointermove', onConcealPointerMove, true)
window.removeEventListener('pointerup', onConcealPointerUp, true)
window.removeEventListener('pointercancel', onConcealPointerCancel, true)
window.removeEventListener('touchstart', onConcealTouchStart, true)
window.removeEventListener('scroll', markConcealGesture, true)
window.removeEventListener('gesturestart', markConcealGesture, true)
const viewport = window.visualViewport
if (viewport) {
viewport.removeEventListener('resize', markConcealGesture)
viewport.removeEventListener('scroll', markConcealGesture)
}
}
function onConcealPointerDown(event: PointerEvent) {
concealPointers += 1
if (concealPointers > 1) {
concealPinch = true
markConcealGesture()
}
if (concealPointers === 1) {
concealStart = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, moved: false }
}
}
function onConcealPointerMove(event: PointerEvent) {
const start = concealStart
if (!start || event.pointerId !== start.pointerId) return
const dx = event.clientX - start.x
const dy = event.clientY - start.y
if ((dx * dx) + (dy * dy) > CONCEAL_MOVE_PX * CONCEAL_MOVE_PX) start.moved = true
}
function onConcealPointerUp(event: PointerEvent) {
concealPointers = Math.max(0, concealPointers - 1)
const start = concealStart
const pinched = concealPinch
if (event.pointerId === start?.pointerId) concealStart = null
if (concealPointers === 0) concealPinch = false
if (!start || event.pointerId !== start.pointerId) return
if (start.moved || pinched) {
markConcealGesture()
return
}
if (Date.now() - concealGestureAt < CONCEAL_GESTURE_MS) return
if (event.pointerType === 'mouse' && event.button !== 0) return
concealFromOutsideTap(event)
}
function onConcealPointerCancel() {
concealPointers = Math.max(0, concealPointers - 1)
concealStart = null
if (concealPointers === 0) concealPinch = false
markConcealGesture()
}
function onConcealTouchStart(event: TouchEvent) {
if (event.touches.length > 1) {
concealPinch = true
markConcealGesture()
}
}
function concealFromOutsideTap(event: Event) {
const target = event.target as Node | null
if (editLightbox.value && target && editLightbox.value.contains(target)) return
if (editLightboxUrl.value) closeEditLightbox()
@@ -5641,7 +5828,7 @@ function clearSelection() {
}
function selectAllLibrary() {
selectedKeys.value = libraryEntries.value.map(entry => entry.key)
selectedKeys.value = visibleLibraryEntries.value.map(entry => entry.key)
lastSelectedKey.value = selectedKeys.value.at(-1) || ''
}
+34 -1
View File
@@ -206,10 +206,17 @@
alt=""
class="aspect-video w-full object-cover"
>
<FamilyNav
:count="focusedNavParts.length"
:index="focusedNavIndex"
:prev-label="focused?.kind === 'clip' ? 'Previous clip' : 'Previous image'"
:next-label="focused?.kind === 'clip' ? 'Next clip' : 'Next image'"
@step="stepFocusedFamily"
/>
<button
v-if="showPrivacyToggles"
type="button"
class="absolute right-2 top-2 z-10 flex h-8 w-8 items-center justify-center rounded-full border border-white/20 bg-black/60 text-zinc-100 hover:border-amber-300/50 hover:text-amber-100"
class="absolute right-2 top-2 z-30 flex h-8 w-8 items-center justify-center rounded-full border border-white/20 bg-black/60 text-zinc-100 hover:border-amber-300/50 hover:text-amber-100"
:title="detailsHidden ? 'Show preview' : 'Hide preview'"
:aria-label="detailsHidden ? 'Show preview' : 'Hide preview'"
@click.stop="detailsHidden = !detailsHidden"
@@ -329,6 +336,8 @@
<script setup lang="ts">
import {
buildLibraryEntries,
chronologicalParts,
chronologicalStills,
formatClipDuration,
formatWhen,
type LibraryClip,
@@ -458,6 +467,17 @@ const focusedParts = computed(() => {
if (focused.value?.kind === 'clip') return focused.value.group?.parts || []
return focused.value?.stillGroup?.parts || []
})
const focusedNavParts = computed(() => {
const parts = focusedParts.value
if (parts.length < 2) return []
if (focused.value?.kind === 'still') return chronologicalStills(parts as LibraryStill[])
return chronologicalParts(parts as LibraryClip[])
})
const focusedNavIndex = computed(() => {
const id = focusedPartId.value
const index = focusedNavParts.value.findIndex(part => part.id === id)
return index < 0 ? 0 : index
})
const detailPrompt = computed(() => {
if (focused.value?.kind === 'clip') return focused.value.group?.head.prompt || ''
return focused.value?.stillGroup?.head.prompt || ''
@@ -559,6 +579,13 @@ function focusPart(id: string) {
focusedPartId.value = id
}
function stepFocusedFamily(delta: number) {
const parts = focusedNavParts.value
if (parts.length < 2) return
const next = parts[(focusedNavIndex.value + delta + parts.length) % parts.length]
if (next) focusPart(next.id)
}
function onScroll() {
if (scrollRaf) return
scrollRaf = requestAnimationFrame(() => {
@@ -806,6 +833,8 @@ async function lockFolder() {
}
function onKey(event: KeyboardEvent) {
const target = event.target as HTMLElement | null
const typing = Boolean(target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT' || target.isContentEditable))
if (event.key === 'Escape') {
if (pendingDelete.value) pendingDelete.value = null
else if (renaming.value) cancelRename()
@@ -816,6 +845,10 @@ function onKey(event: KeyboardEvent) {
selectAll()
}
if (event.key === 'Delete' && selectedKeys.value.length && !renaming.value) askRemoveSelected()
if (!typing && (event.key === 'ArrowLeft' || event.key === 'ArrowRight') && focusedNavParts.value.length > 1) {
event.preventDefault()
stepFocusedFamily(event.key === 'ArrowLeft' ? -1 : 1)
}
}
watch(searchDraft, (value) => {
+1 -1
View File
@@ -1023,7 +1023,7 @@ onMounted(async () => {
pollTimer = setInterval(() => {
if (typeof document !== 'undefined' && document.hidden) return
void loadQueues().catch(() => null)
}, 4000)
}, 8000)
})
function onRemoveKey(event: KeyboardEvent) {
+7 -1
View File
@@ -3,7 +3,11 @@ import { comfyConfigured } from '~/server/utils/comfy'
import { imageComfyConfigured, getBeastImageHost, getSidecarImageHost, sameImageHost } from '~/server/utils/imageComfy'
import { checkImageComfyHttp, fetchQueueAt } from '~/server/utils/imageComfyLifecycle'
let healthCache: { at: number; body: Record<string, unknown> } | null = null
const HEALTH_CACHE_MS = 2500
export default defineEventHandler(async () => {
if (healthCache && Date.now() - healthCache.at < HEALTH_CACHE_MS) return healthCache.body
const videoConfigured = comfyConfigured()
const imageConfigured = imageComfyConfigured()
const beastHost = getBeastImageHost()
@@ -35,7 +39,7 @@ export default defineEventHandler(async () => {
const preferredHost = beastHost || comfy.host || ''
const promptQueueRunning = promptIsBeast ? ((beastQueue.running || queue.running) || 0) : (promptQueue.running || 0)
const promptQueuePending = promptIsBeast ? ((beastQueue.pending || queue.pending) || 0) : (promptQueue.pending || 0)
return {
const body = {
ok: true,
service: useRuntimeConfig().public.instanceName || 'aigen',
comfy: {
@@ -75,4 +79,6 @@ export default defineEventHandler(async () => {
}
}
}
healthCache = { at: Date.now(), body }
return body
})
+11
View File
@@ -93,6 +93,17 @@ export function chronologicalStills(parts: LibraryStill[]) {
})
}
export function stillsInFamily(stills: LibraryStill[], current?: LibraryStill | null) {
if (!current) return [] as LibraryStill[]
if (!current.familyId) return [current]
return chronologicalStills(stills.filter(item => item.familyId === current.familyId && item.role !== 'input'))
}
export function wrappedIndex(length: number, current: number, delta: number) {
if (length <= 0) return 0
return ((current + delta) % length + length) % length
}
export function sortClipParts(a: LibraryClip, b: LibraryClip) {
const durationDelta = (Number(b.duration) || 0) - (Number(a.duration) || 0)
if (durationDelta) return durationDelta