Add LoRA stacks with per-item strengths and named video/image generation presets so setups survive reload without mixing Klein onto MiniMax. Keep LTX Pink Cherry and xAIGen LoRAs on xAIGen, and run Recommend on Beast.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+68
-2
@@ -200,6 +200,19 @@
|
||||
label="Duration"
|
||||
@update:modelValue="saveSegment(job.shots, segment)"
|
||||
/>
|
||||
<LoraSelect
|
||||
v-if="segment.status !== 'complete' && segment.status !== 'running'"
|
||||
class="mt-3 max-w-md"
|
||||
:model-value="readLoraStack(segment)"
|
||||
:names="videoLoraOptions"
|
||||
label="LoRA"
|
||||
empty-label="Default"
|
||||
add-label="Add LoRA"
|
||||
compact
|
||||
:clip-strength="false"
|
||||
@update:model-value="(value) => { setSegmentStack(segment, value); saveSegment(job.shots, segment) }"
|
||||
/>
|
||||
<p v-else-if="segmentLoraLabel(job.shots, segment)" class="mt-1 text-[11px] text-zinc-500">{{ segmentLoraLabel(job.shots, segment) }}</p>
|
||||
<div v-if="segment.clipId" class="mt-2">
|
||||
<a
|
||||
class="text-xs text-amber-200 hover:text-amber-100"
|
||||
@@ -221,6 +234,7 @@
|
||||
>
|
||||
<p class="text-sm font-medium text-zinc-100">{{ job.kind === 'edit' ? `Pass ${index + 1}` : `Shot ${index + 1}` }} <span class="font-normal text-zinc-500">· queued with this job</span></p>
|
||||
<p class="mt-1 max-h-24 overflow-y-auto whitespace-pre-wrap text-xs text-zinc-400 [overflow-wrap:anywhere]">{{ shot.prompt }}</p>
|
||||
<p v-if="loraStackLabel(readLoraStack(shot))" class="mt-1 text-[11px] text-zinc-500">{{ loraStackLabel(readLoraStack(shot)) }}</p>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
@@ -328,6 +342,19 @@
|
||||
label="Duration"
|
||||
@update:modelValue="saveSegment(queue, segment)"
|
||||
/>
|
||||
<LoraSelect
|
||||
v-if="segment.status !== 'complete' && segment.status !== 'running'"
|
||||
class="mt-3 max-w-md"
|
||||
:model-value="readLoraStack(segment)"
|
||||
:names="videoLoraOptions"
|
||||
label="LoRA"
|
||||
empty-label="Default"
|
||||
add-label="Add LoRA"
|
||||
compact
|
||||
:clip-strength="false"
|
||||
@update:model-value="(value) => { setSegmentStack(segment, value); saveSegment(queue, segment) }"
|
||||
/>
|
||||
<p v-else-if="segmentLoraLabel(queue, segment)" class="mt-1 text-[11px] text-zinc-500">{{ segmentLoraLabel(queue, segment) }}</p>
|
||||
<div v-if="segment.clipId" class="mt-2">
|
||||
<a
|
||||
class="text-xs text-amber-200 hover:text-amber-100"
|
||||
@@ -382,6 +409,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { loraStackLabel, persistLoraFields, readLoraStack, type LoraStackItem } from '~/utils/loras'
|
||||
|
||||
type Segment = {
|
||||
index: number
|
||||
prompt: string
|
||||
@@ -389,6 +418,8 @@ type Segment = {
|
||||
status: 'pending' | 'running' | 'complete' | 'error'
|
||||
clipId?: string
|
||||
error?: string
|
||||
loraName?: string
|
||||
loraStack?: LoraStackItem[]
|
||||
}
|
||||
|
||||
type Queue = {
|
||||
@@ -401,6 +432,8 @@ type Queue = {
|
||||
totalCount: number
|
||||
currentJobId?: string
|
||||
stopAfterCurrent?: boolean
|
||||
loraName?: string
|
||||
loraStack?: LoraStackItem[]
|
||||
segments: Segment[]
|
||||
}
|
||||
|
||||
@@ -422,7 +455,7 @@ type StudioJobRow = {
|
||||
pausedByUser?: boolean
|
||||
lastError?: string
|
||||
shots?: Queue | null
|
||||
plannedShots?: { prompt: string; duration: number }[]
|
||||
plannedShots?: { prompt: string; duration: number; loraName?: string; loraStack?: LoraStackItem[] }[]
|
||||
completedCount?: number
|
||||
}
|
||||
|
||||
@@ -436,6 +469,7 @@ type PendingRemove = {
|
||||
}
|
||||
|
||||
const instanceName = ref(String(useRuntimeConfig().public.instanceName || 'AIGen'))
|
||||
const videoLoras = ref<string[]>([])
|
||||
const authEnabled = ref(false)
|
||||
const jobs = ref<StudioJobRow[]>([])
|
||||
const orphans = ref<Queue[]>([])
|
||||
@@ -450,6 +484,24 @@ const sources = new Map<string, EventSource>()
|
||||
const pendingRemove = ref<PendingRemove | null>(null)
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const videoLoraOptions = computed(() => {
|
||||
const names = [...videoLoras.value]
|
||||
for (const queue of [...jobs.value.map(job => job.shots), ...orphans.value]) {
|
||||
if (!queue) continue
|
||||
if (queue.loraName && !names.includes(queue.loraName)) names.push(queue.loraName)
|
||||
for (const item of queue.loraStack || []) {
|
||||
if (item.name && !names.includes(item.name)) names.push(item.name)
|
||||
}
|
||||
for (const segment of queue.segments) {
|
||||
if (segment.loraName && !names.includes(segment.loraName)) names.push(segment.loraName)
|
||||
for (const item of segment.loraStack || []) {
|
||||
if (item.name && !names.includes(item.name)) names.push(item.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return names
|
||||
})
|
||||
|
||||
const canCutIn = computed(() => jobs.value.some(job => job.status === 'running' || job.status === 'held'))
|
||||
const pauseBusy = computed(() => busyId.value === 'pause')
|
||||
const anythingGenerating = computed(() => (
|
||||
@@ -569,6 +621,17 @@ function segmentCaption(queue: Queue, segment: Segment) {
|
||||
return '· waiting'
|
||||
}
|
||||
|
||||
function setSegmentStack(segment: Segment, stack: LoraStackItem[]) {
|
||||
const fields = persistLoraFields(stack)
|
||||
segment.loraStack = fields.loraStack
|
||||
segment.loraName = fields.loraName
|
||||
}
|
||||
|
||||
function segmentLoraLabel(queue: Queue, segment: Segment) {
|
||||
const stack = readLoraStack(segment).length ? readLoraStack(segment) : readLoraStack(queue)
|
||||
return loraStackLabel(stack)
|
||||
}
|
||||
|
||||
function segmentClass(queue: Queue, segment: Segment) {
|
||||
if (segment.status === 'complete') return 'border-emerald-400/25 bg-emerald-400/5'
|
||||
if (segment.status === 'running') return 'border-amber-300/50 bg-amber-400/10'
|
||||
@@ -949,7 +1012,7 @@ function saveSegment(queue: Queue, segment: Segment) {
|
||||
saveTimer = setTimeout(() => {
|
||||
void $fetch(`/api/queues/${queue.id}`, {
|
||||
method: 'PATCH',
|
||||
body: { segments: [{ index: segment.index, prompt: segment.prompt, duration: segment.duration }] }
|
||||
body: { segments: [{ index: segment.index, prompt: segment.prompt, duration: segment.duration, loraName: segment.loraName || '', loraStack: segment.loraStack || [] }] }
|
||||
}).then(() => {
|
||||
dirty.delete(dirtyKey(queue.id, segment.index))
|
||||
}).catch(() => null)
|
||||
@@ -960,6 +1023,9 @@ onMounted(async () => {
|
||||
const me = await $fetch<{ user?: { name?: string }; authEnabled?: boolean; instanceName?: string }>('/api/auth/me').catch(() => ({ user: null }))
|
||||
authEnabled.value = Boolean(me.authEnabled)
|
||||
instanceName.value = me.instanceName || instanceName.value
|
||||
void $fetch<{ video?: string[] }>('/api/loras').then((listed) => {
|
||||
videoLoras.value = listed.video || []
|
||||
}).catch(() => null)
|
||||
window.addEventListener('keydown', onRemoveKey)
|
||||
await loadQueues().catch((err: any) => {
|
||||
error.value = err?.data?.statusMessage || err?.message || 'Could not load the queue'
|
||||
|
||||
Reference in New Issue
Block a user