Split prompts into pre/prompt/post and tidy the AIGen video form.
Keep wrappers off the library mid-prompt so Use as input and Extend restore the same split, send stills to xAIGen as hidden inputs, and stop the two-column library from growing with the list. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+196
-161
@@ -7,7 +7,7 @@
|
||||
@gpu="pollComfyHealth"
|
||||
/>
|
||||
|
||||
<main class="mx-auto grid min-w-0 max-w-7xl gap-6 px-4 py-6 sm:px-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
|
||||
<main class="mx-auto grid min-w-0 max-w-7xl items-start gap-6 px-4 py-6 sm:px-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
|
||||
<section id="studio-input" class="glass min-w-0 max-w-full rounded-3xl p-5 sm:p-6 space-y-5">
|
||||
<div>
|
||||
<div class="mb-4 grid grid-cols-2 gap-3">
|
||||
@@ -219,12 +219,6 @@
|
||||
<div class="mb-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<label class="text-sm font-medium text-zinc-300">{{ studioMode === 'editv2' && v2Mode === 'generate' ? 'Generate prompt' : studioMode === 'editv2' && v2Mode === 'refine' ? 'Refine prompt' : studioMode === 'edit' || studioMode === 'editv2' ? 'Edit prompt' : 'Motion & scene prompt' }}</label>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<KeepPromptMenu
|
||||
:items="activeKeepPrompts"
|
||||
:heading="keepPromptHeading"
|
||||
align="right"
|
||||
@insert="insertKeepPrompt"
|
||||
/>
|
||||
<button
|
||||
v-if="identityPrompting"
|
||||
type="button"
|
||||
@@ -284,21 +278,15 @@
|
||||
:class="studioMode === 'edit' || studioMode === 'editv2' ? 'md:grid-cols-2' : ''"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<textarea
|
||||
v-model="prompt"
|
||||
:rows="shotScriptMode || identityPrompting ? 12 : 6"
|
||||
class="hidden max-h-[min(28rem,70vh)] w-full overflow-y-auto rounded-2xl border border-white/10 bg-zinc-950/70 px-4 py-3 text-sm outline-none ring-amber-300/40 placeholder:text-zinc-600 [overflow-wrap:anywhere] focus:ring-2 md:block"
|
||||
:placeholder="promptPlaceholder"
|
||||
<PromptPartsEditor
|
||||
v-model:pre="promptPre"
|
||||
v-model:prompt="prompt"
|
||||
v-model:post="promptPost"
|
||||
:prompt-placeholder="promptPlaceholder"
|
||||
:prompt-rows="shotScriptMode || identityPrompting ? 12 : 6"
|
||||
:keep-items="activeKeepPrompts"
|
||||
:keep-heading="keepPromptHeading"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full rounded-2xl border border-white/10 bg-zinc-950/70 px-4 py-3 text-left md:hidden"
|
||||
@click="promptDrawerOpen = true"
|
||||
>
|
||||
<p v-if="prompt.trim()" class="line-clamp-4 whitespace-pre-wrap text-sm text-zinc-200 [overflow-wrap:anywhere]">{{ prompt }}</p>
|
||||
<p v-else class="text-sm text-zinc-600">{{ promptPlaceholder }}</p>
|
||||
<span class="mt-2 inline-block text-xs text-amber-200">{{ prompt.trim() ? 'View / edit prompt' : 'Write prompt' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<label v-if="studioMode === 'edit' || studioMode === 'editv2'" class="block min-w-0 text-sm">
|
||||
<span class="mb-2 block font-medium text-zinc-300">Negative prompt</span>
|
||||
@@ -318,18 +306,6 @@
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="activeKeepPrompts.length" class="mt-2 flex flex-wrap gap-1.5">
|
||||
<button
|
||||
v-for="item in activeKeepPrompts"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="max-w-full truncate rounded-full border border-white/10 px-2.5 py-1 text-xs text-zinc-300 hover:border-amber-300/40 hover:text-amber-100"
|
||||
:title="item.text"
|
||||
@click="insertKeepPrompt(item)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="studioMode === 'editv2' && v2Mode === 'generate'" class="mt-3 text-xs text-zinc-500">No reference image. Prompt only.</p>
|
||||
<div v-if="studioMode === 'editv2' && v2Mode === 'refine'" class="mt-3 space-y-2">
|
||||
<p class="text-xs text-zinc-500">Prompt only what should change in the painted area.</p>
|
||||
@@ -432,11 +408,14 @@
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
<PromptField
|
||||
v-model="item.prompt"
|
||||
rows="3"
|
||||
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 placeholder:text-zinc-600 focus:ring-2"
|
||||
:rows="3"
|
||||
:title="`${imagePassMode === 'batch' ? 'Prompt' : 'Pass'} ${index + 2}`"
|
||||
:placeholder="extraPassPlaceholder"
|
||||
:keep-items="activeKeepPrompts"
|
||||
:keep-heading="keepPromptHeading"
|
||||
:disabled="editBusy"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@@ -458,28 +437,34 @@
|
||||
<p class="mt-3 text-[10px] uppercase tracking-wide text-zinc-500">Concatenated result</p>
|
||||
<p class="mt-1 whitespace-pre-wrap text-xs text-zinc-300 [overflow-wrap:anywhere]">{{ composedIdentityPrompt }}</p>
|
||||
</details>
|
||||
<div v-if="studioMode === 'video'" class="mt-3 space-y-3">
|
||||
<GlobalLocksEditor v-model="globalLocks" />
|
||||
<div class="rounded-xl border border-white/10 bg-zinc-950/40 px-3 py-2.5">
|
||||
<PermanenceRefStrip
|
||||
:refs="familyPermanenceRefs"
|
||||
:name-options="lockBindingKeys"
|
||||
label="Permanence refs · all shots"
|
||||
hint="Name + still"
|
||||
cta="Add permanence refs"
|
||||
add-title="Choose a still to name for every shot"
|
||||
:hidden="hideInputPreview"
|
||||
@add-ref="openPermanencePicker()"
|
||||
@remove="removeFamilyRef"
|
||||
@update-label="updateFamilyRefLabel"
|
||||
/>
|
||||
<p class="mt-2 text-[11px] leading-relaxed text-zinc-500">
|
||||
MiniMax will not see these photos. Continuity is the last frame of the previous clip. The stills only name the lock lines.
|
||||
</p>
|
||||
<p class="mt-1 text-[11px] leading-relaxed text-zinc-500">{{ permanenceComfyNote }}</p>
|
||||
<p v-if="familyRefLabelLine" class="mt-1 text-[11px] text-amber-200/80">{{ familyRefLabelLine }}</p>
|
||||
<details v-if="studioMode === 'video'" class="mt-3 rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3">
|
||||
<summary class="cursor-pointer text-sm font-medium text-zinc-200">
|
||||
Continuity
|
||||
<span class="ml-2 font-normal text-xs text-zinc-500">Locks and named stills. Last-frame I2V carries the picture.</span>
|
||||
</summary>
|
||||
<div class="mt-3 space-y-3">
|
||||
<GlobalLocksEditor v-model="globalLocks" embedded />
|
||||
<div class="rounded-xl border border-white/10 bg-zinc-950/40 px-3 py-2.5">
|
||||
<PermanenceRefStrip
|
||||
:refs="familyPermanenceRefs"
|
||||
:name-options="lockBindingKeys"
|
||||
label="Named stills · all shots"
|
||||
hint="Name + still"
|
||||
cta="Add a named still"
|
||||
add-title="Choose a still to name for every shot"
|
||||
:hidden="hideInputPreview"
|
||||
@add-ref="openPermanencePicker()"
|
||||
@remove="removeFamilyRef"
|
||||
@update-label="updateFamilyRefLabel"
|
||||
/>
|
||||
<p class="mt-2 text-[11px] leading-relaxed text-zinc-500">
|
||||
MiniMax does not see these photos. Shot 2+ starts from the previous clip’s last frame. Names only go in the lock line.
|
||||
</p>
|
||||
<p class="mt-1 text-[11px] leading-relaxed text-zinc-500">{{ permanenceComfyNote }}</p>
|
||||
<p v-if="familyRefLabelLine" class="mt-1 text-[11px] text-amber-200/80">{{ familyRefLabelLine }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<div v-if="shotScriptMode && studioMode === 'video'" class="mt-3 space-y-2">
|
||||
<p v-if="!parsedShots.length" class="text-xs text-zinc-500">Add a first shot, then <span class="text-zinc-300">shot 2</span> for an extension.</p>
|
||||
<div
|
||||
@@ -532,16 +517,7 @@
|
||||
class="rounded-2xl border border-white/10 bg-zinc-950/50 p-4"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-sm font-medium text-zinc-200">Extension #{{ index + 1 }}</p>
|
||||
<KeepPromptMenu
|
||||
:items="keepPrompts.video"
|
||||
heading="Video prompts"
|
||||
align="left"
|
||||
:disabled="videoBusy"
|
||||
@insert="insertKeepIntoExtension(item.id, $event)"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-sm font-medium text-zinc-200">Extension #{{ index + 1 }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs text-red-300 hover:text-red-200"
|
||||
@@ -551,11 +527,14 @@
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
<PromptField
|
||||
v-model="item.prompt"
|
||||
rows="3"
|
||||
class="max-h-32 w-full overflow-y-auto rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 placeholder:text-zinc-600 [overflow-wrap:anywhere] focus:ring-2 md:max-h-none"
|
||||
:rows="3"
|
||||
:title="`Extension #${index + 1}`"
|
||||
placeholder="Motion prompt for this extension…"
|
||||
:keep-items="keepPrompts.video"
|
||||
keep-heading="Video prompts"
|
||||
:disabled="videoBusy"
|
||||
/>
|
||||
<DurationField v-model="item.duration" class="mt-3" label="Duration" />
|
||||
<LoraSelect
|
||||
@@ -737,11 +716,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="videoEngine === 'minimax' && videoStart === 'still' && minimaxGraph === 'v2'" class="rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3 space-y-3">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-zinc-200">Character stills</p>
|
||||
<p class="mt-0.5 text-xs text-zinc-500">Optional. Extra views of the same person — face, hair, body. Shot 2+ still starts from the last frame (clothes and pose stay). Empty slots are left out. A named extra person turns these off.</p>
|
||||
</div>
|
||||
<details v-if="videoEngine === 'minimax' && videoStart === 'still' && minimaxGraph === 'v2'" class="rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3" :open="identityRefs.some(Boolean)">
|
||||
<summary class="cursor-pointer text-sm font-medium text-zinc-200">
|
||||
Character stills
|
||||
<span class="ml-2 font-normal text-xs text-zinc-500">Optional extra views of the same person. Off unless you need them.</span>
|
||||
</summary>
|
||||
<div class="mt-3 space-y-3">
|
||||
<p class="text-xs text-zinc-500">Face, hair, body. Shot 2+ still starts from the last frame (clothes and pose stay). Empty slots are left out. A named extra person turns these off.</p>
|
||||
<div v-if="!extraPersonLock" class="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
<div
|
||||
v-for="slot in 4"
|
||||
@@ -782,7 +763,8 @@
|
||||
</div>
|
||||
<p v-if="extraPersonLock" class="text-xs text-zinc-500">Character stills stay off while a second named person is attached. Shot 2+ starts from the last frame.</p>
|
||||
<p v-else class="text-xs text-zinc-500">Picture 1 is the start still on shot 1 and the last frame after that. Extra stills are Picture 2–5. They do not put the start-still outfit back on.</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div v-if="studioMode === 'video'" class="max-w-md space-y-2">
|
||||
@@ -1046,20 +1028,15 @@
|
||||
<p class="text-xs text-zinc-500">Graph default is 20 steps, CFG 1, euler. CFG 0–20 (0.1 steps), steps 1–50. A second still switches the two-image branch on. Scale to 1 MP runs Scale Image to Total Pixels (lanczos) on both stills before Klein.</p>
|
||||
</div>
|
||||
|
||||
<details v-if="studioMode === 'video'" class="rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3">
|
||||
<summary class="cursor-pointer text-sm font-medium">Generation settings</summary>
|
||||
<div class="mt-4 space-y-4">
|
||||
<div>
|
||||
<AspectRatioPick v-model="aspect" heading="Aspect ratio / resolution" :options="videoAspects" />
|
||||
<p class="mt-2 text-xs text-zinc-500">Output {{ outputSize.width }} × {{ outputSize.height }}{{ aspect === 'auto' ? (file ? ' · matching the still' : ' · 16:9 until a still is loaded') : '' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">Steps</p>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button type="button" class="rounded-xl border px-3 py-2 text-sm" :class="turbo ? 'border-amber-300 bg-amber-400/10' : 'border-white/10'" @click="turbo = true">{{ ltxVideo ? '8 Distilled LoRA' : (videoWorkflow === 'v2' ? '6 Turbo · LoRA on' : '8 Turbo · LoRA on') }}</button>
|
||||
<button type="button" class="rounded-xl border px-3 py-2 text-sm" :class="!turbo ? 'border-amber-300 bg-amber-400/10' : 'border-white/10'" @click="turbo = false">{{ ltxVideo ? '12 Quality · LoRA on' : '20 High-Fidelity · LoRA bypass' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="studioMode === 'video'" class="space-y-4">
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<DurationField
|
||||
v-model="duration"
|
||||
label="Duration"
|
||||
:hint="shotScriptMode
|
||||
? 'This length applies to the initial shot and every extension in the script.'
|
||||
: 'Type any length, or hover the wheel and scroll. Shift-scroll jumps by 1s. Drag up and down.'"
|
||||
/>
|
||||
<div class="text-sm">
|
||||
<Thumbwheel
|
||||
v-model="cfg"
|
||||
@@ -1069,9 +1046,48 @@
|
||||
:step="CFG_STEP"
|
||||
@update:model-value="cfgTouched = true"
|
||||
/>
|
||||
<p class="mt-1 text-[11px] text-zinc-500">{{ ltxVideo ? 'Default 3.5 for PinkCherry' : (turbo ? 'Default 1.5 with Turbo LoRA' : 'Default 4.0 with High-Fidelity') }}. Hover and scroll ±0.1, Shift-scroll ±1. Drag the wheel up and down.</p>
|
||||
<p class="mt-1 text-[11px] text-zinc-500">{{ ltxVideo ? 'Default 3.5 for PinkCherry' : (turbo ? 'Default 1.5 with Turbo LoRA' : 'Default 4.0 with High-Fidelity') }}.</p>
|
||||
</div>
|
||||
</div>
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block text-zinc-400">Seed</span>
|
||||
<input v-model="seedInput" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2" placeholder="random">
|
||||
</label>
|
||||
<div>
|
||||
<p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">Sampler & scheduler</p>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<label class="text-sm">
|
||||
<span class="mb-1 block text-zinc-400">Sampler</span>
|
||||
<select v-model="samplerName" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm">
|
||||
<option value="res_multistep">res_multistep</option>
|
||||
<option value="euler">euler</option>
|
||||
<option value="dpmpp_2m">dpmpp_2m</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="text-sm">
|
||||
<span class="mb-1 block text-zinc-400">Scheduler</span>
|
||||
<select v-model="scheduler" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm">
|
||||
<option value="simple">simple</option>
|
||||
<option value="ddim_uniform">ddim_uniform</option>
|
||||
<option value="sgm_uniform">sgm_uniform</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<details class="rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3">
|
||||
<summary class="cursor-pointer text-sm font-medium">Aspect, steps, frame rate</summary>
|
||||
<div class="mt-4 space-y-4">
|
||||
<div>
|
||||
<AspectRatioPick v-model="aspect" heading="Aspect ratio / resolution" :options="videoAspects" />
|
||||
<p class="mt-2 text-xs text-zinc-500">Output {{ outputSize.width }} × {{ outputSize.height }}{{ aspect === 'auto' ? (file ? ' · matching the still' : ' · 16:9 until a still is loaded') : '' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">Steps</p>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button type="button" class="rounded-xl border px-3 py-2 text-sm" :class="turbo ? 'border-amber-300 bg-amber-400/10' : 'border-white/10'" @click="turbo = true">{{ ltxVideo ? '8 Distilled LoRA' : (videoWorkflow === 'v2' ? '6 Turbo · LoRA on' : '8 Turbo · LoRA on') }}</button>
|
||||
<button type="button" class="rounded-xl border px-3 py-2 text-sm" :class="!turbo ? 'border-amber-300 bg-amber-400/10' : 'border-white/10'" @click="turbo = false">{{ ltxVideo ? '12 Quality · LoRA on' : '20 High-Fidelity · LoRA bypass' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<label class="text-sm">
|
||||
<span class="mb-1 block text-zinc-400">Frame rate</span>
|
||||
<select v-model.number="fps" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm">
|
||||
@@ -1081,41 +1097,9 @@
|
||||
</select>
|
||||
<p class="mt-1 text-[11px] text-zinc-500">{{ frameCount }} frames · duration × FPS</p>
|
||||
</label>
|
||||
<DurationField
|
||||
v-model="duration"
|
||||
label="Duration"
|
||||
:hint="shotScriptMode
|
||||
? 'This length applies to the initial shot and every extension in the script.'
|
||||
: 'Type any length, or hover the wheel and scroll. Shift-scroll jumps by 1s. Drag up and down.'"
|
||||
/>
|
||||
</div>
|
||||
<label class="text-sm">
|
||||
<span class="mb-1 block text-zinc-400">Seed</span>
|
||||
<input v-model="seedInput" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2" placeholder="random">
|
||||
</label>
|
||||
<div>
|
||||
<p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">Sampler & scheduler</p>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<label class="text-sm">
|
||||
<span class="mb-1 block text-zinc-400">Sampler</span>
|
||||
<select v-model="samplerName" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm">
|
||||
<option value="res_multistep">res_multistep</option>
|
||||
<option value="euler">euler</option>
|
||||
<option value="dpmpp_2m">dpmpp_2m</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="text-sm">
|
||||
<span class="mb-1 block text-zinc-400">Scheduler</span>
|
||||
<select v-model="scheduler" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm">
|
||||
<option value="simple">simple</option>
|
||||
<option value="ddim_uniform">ddim_uniform</option>
|
||||
<option value="sgm_uniform">sgm_uniform</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<label
|
||||
v-if="studioMode === 'video' && queuedExtensionCount > 0"
|
||||
@@ -1228,8 +1212,8 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="min-w-0 max-w-full space-y-5">
|
||||
<div ref="outputPanel" class="glass min-w-0 max-w-full rounded-3xl p-5 sm:p-6">
|
||||
<section class="min-w-0 max-w-full space-y-5 lg:sticky lg:top-4 lg:flex lg:max-h-[calc(100dvh-5.5rem)] lg:flex-col lg:overflow-hidden">
|
||||
<div ref="outputPanel" class="glass min-w-0 max-w-full rounded-3xl p-5 sm:p-6 lg:shrink-0">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 class="font-display text-2xl font-bold">Output</h2>
|
||||
@@ -1428,21 +1412,16 @@
|
||||
</p>
|
||||
</div>
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 flex items-center gap-2 text-zinc-400">
|
||||
Extension prompt
|
||||
<KeepPromptMenu
|
||||
:items="keepPrompts.video"
|
||||
heading="Video prompts"
|
||||
align="left"
|
||||
:disabled="lastFrameLoading || videoBusy"
|
||||
@insert="insertKeepExtend"
|
||||
/>
|
||||
</span>
|
||||
<textarea
|
||||
<span class="mb-1 block text-zinc-400">Extension prompt</span>
|
||||
<PromptField
|
||||
v-model="extendPrompt"
|
||||
rows="4"
|
||||
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 placeholder:text-zinc-600 focus:ring-2"
|
||||
:rows="4"
|
||||
title="Extension prompt"
|
||||
placeholder="Continue the motion from this frame…"
|
||||
:keep-items="keepPrompts.video"
|
||||
keep-heading="Video prompts"
|
||||
:disabled="lastFrameLoading || videoBusy"
|
||||
textarea-class=""
|
||||
/>
|
||||
</label>
|
||||
<DurationField
|
||||
@@ -1572,7 +1551,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass rounded-3xl p-5 sm:p-6">
|
||||
<div class="glass rounded-3xl p-5 sm:p-6 lg:flex lg:min-h-0 lg:flex-1 lg:flex-col">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<h2 class="font-display text-xl font-bold">Library</h2>
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -1603,6 +1582,7 @@
|
||||
</form>
|
||||
|
||||
<template v-else>
|
||||
<div class="lg:flex lg:min-h-0 lg:flex-1 lg:flex-col">
|
||||
<div class="mt-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex min-w-0 flex-wrap gap-1.5">
|
||||
<button
|
||||
@@ -1656,7 +1636,7 @@
|
||||
<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 max-h-[min(28rem,50vh)] space-y-3 overflow-y-auto">
|
||||
<ul class="mt-4 max-h-[min(28rem,50vh)] space-y-3 overflow-y-auto lg:min-h-0 lg:max-h-none lg:flex-1">
|
||||
<li
|
||||
v-for="entry in visibleLibraryEntries"
|
||||
:key="entry.key"
|
||||
@@ -1932,6 +1912,13 @@
|
||||
:disabled="keepingInputBusy"
|
||||
@click="saveStillAsInput(entry.stillGroup.head)"
|
||||
>Save as input</button>
|
||||
<button
|
||||
v-if="backupStudioEnabled"
|
||||
type="button"
|
||||
class="text-xs text-amber-300"
|
||||
:disabled="backupBusyId === entry.stillGroup.head.id"
|
||||
@click="sendStillToBackup(entry.stillGroup.head)"
|
||||
>{{ backupBusyId === entry.stillGroup.head.id ? 'Sending…' : 'Send to Backup on AIGen only' }}</button>
|
||||
<button type="button" class="text-xs text-zinc-400" @click="toggleDetails(entry.key)">
|
||||
{{ expandedDetails[entry.key] ? 'Hide details' : 'Details' }}
|
||||
</button>
|
||||
@@ -2008,6 +1995,13 @@
|
||||
:disabled="keepingInputBusy"
|
||||
@click="saveStillAsInput(item)"
|
||||
>Save as input</button>
|
||||
<button
|
||||
v-if="backupStudioEnabled"
|
||||
type="button"
|
||||
class="text-xs text-amber-300"
|
||||
:disabled="backupBusyId === item.id"
|
||||
@click="sendStillToBackup(item)"
|
||||
>{{ backupBusyId === item.id ? 'Sending…' : 'Send to Backup on AIGen only' }}</button>
|
||||
<button type="button" class="text-xs text-zinc-400" @click="toggleDetails(item.id)">
|
||||
{{ expandedDetails[item.id] ? 'Hide details' : 'Details' }}
|
||||
</button>
|
||||
@@ -2045,20 +2039,12 @@
|
||||
Showing {{ visibleLibraryEntries.length }} of {{ libraryEntries.length }}.
|
||||
<NuxtLink to="/library" class="text-amber-300 hover:text-amber-100">Open full library</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<PromptDrawer
|
||||
:open="promptDrawerOpen"
|
||||
v-model:text="prompt"
|
||||
:title="studioMode === 'edit' || studioMode === 'editv2' ? 'Edit prompt' : (shotScriptMode ? 'Shot script' : 'Prompt')"
|
||||
:placeholder="promptPlaceholder"
|
||||
:keep-items="activeKeepPrompts"
|
||||
:keep-heading="keepPromptHeading"
|
||||
@update:open="promptDrawerOpen = $event"
|
||||
/>
|
||||
<ImageBatchModal
|
||||
:open="imageBatchOpen"
|
||||
v-model:text="imageBatchDraft"
|
||||
@@ -2155,6 +2141,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { composePromptParts, restorePromptParts } from '~/utils/promptParts'
|
||||
import { parseEditPasses, looksLikeShotScript } from '~/utils/parseRecommend'
|
||||
import { parseImageBatchScript, parseShotScript } from '~/utils/parseShots'
|
||||
import type { DirectorScript } from '~/utils/directorScripts'
|
||||
@@ -2296,6 +2283,8 @@ interface LibraryStill {
|
||||
role?: 'input' | 'output'
|
||||
name?: string
|
||||
prompt?: string
|
||||
promptPre?: string
|
||||
promptPost?: string
|
||||
familyId?: string
|
||||
parentStillId?: string
|
||||
chainIndex?: number
|
||||
@@ -2333,6 +2322,8 @@ interface LibraryClip {
|
||||
permanenceRefs?: PermanenceRef[]
|
||||
loraName?: string
|
||||
loraStack?: LoraStackItem[]
|
||||
promptPre?: string
|
||||
promptPost?: string
|
||||
}
|
||||
|
||||
interface RetryDraft {
|
||||
@@ -2343,6 +2334,8 @@ interface RetryDraft {
|
||||
stillFilename?: string
|
||||
name?: string
|
||||
prompt: string
|
||||
promptPre?: string
|
||||
promptPost?: string
|
||||
aspect: string
|
||||
width: number
|
||||
height: number
|
||||
@@ -2439,7 +2432,10 @@ const inputStillRenameEl = ref<HTMLInputElement | null>(null)
|
||||
const preview = ref('')
|
||||
const dragging = ref(false)
|
||||
const prompt = ref('')
|
||||
const promptDrawerOpen = ref(false)
|
||||
const promptPre = ref('')
|
||||
const promptPost = ref('')
|
||||
const backupBusyId = ref('')
|
||||
const backupStudioEnabled = computed(() => Boolean(runtimeConfig.public.backupStudioEnabled))
|
||||
const shotScriptMode = ref(false)
|
||||
const directorScripts = ref<DirectorScript[]>([])
|
||||
const directorScriptId = ref('')
|
||||
@@ -2983,7 +2979,7 @@ const canUseGeneratedStill = computed(() => Boolean(
|
||||
))
|
||||
const videoBlockReason = computed(() => {
|
||||
if (!textToVideo.value && !hasInputStill.value) return 'Load a still first. Image-to-video needs an input still.'
|
||||
if (!prompt.value.trim()) return 'Write a motion prompt first.'
|
||||
if (![promptPre.value, prompt.value, promptPost.value].some(part => part.trim())) return 'Write a motion prompt first.'
|
||||
if (!folderId.value) return 'Choose a library folder before generating.'
|
||||
if (!queueReady.value) {
|
||||
return shotScriptMode.value ? 'Each shot in the script needs a prompt.' : 'Each queued extension needs a prompt.'
|
||||
@@ -3135,7 +3131,7 @@ const extraIdentityPictures = computed(() => identityRefs.value.flatMap((item, i
|
||||
function composedShotPreview(text: string, index: number, shotN?: number) {
|
||||
const composed = composeShotPrompt({
|
||||
globalLocks: globalLocks.value,
|
||||
prompt: text,
|
||||
prompt: composePromptParts(promptPre.value, text, promptPost.value),
|
||||
shotIndex: index,
|
||||
familyRefs: familyPermanenceRefs.value,
|
||||
shotRefs: shotPermanenceRefs.value[shotN ?? index + 1]
|
||||
@@ -4095,6 +4091,21 @@ function insertKeepPrompt(item: KeepPrompt) {
|
||||
prompt.value = insertKeepSnippet(prompt.value, item.text)
|
||||
}
|
||||
|
||||
function applyPromptRestore(saved: string, stored?: { prompt?: string; promptPre?: string; promptPost?: string } | null) {
|
||||
const parts = restorePromptParts(saved, {
|
||||
pre: stored?.promptPre,
|
||||
prompt: stored?.prompt ?? saved,
|
||||
post: stored?.promptPost
|
||||
}, {
|
||||
pre: promptPre.value,
|
||||
prompt: prompt.value,
|
||||
post: promptPost.value
|
||||
})
|
||||
promptPre.value = parts.pre
|
||||
prompt.value = parts.prompt
|
||||
promptPost.value = parts.post
|
||||
}
|
||||
|
||||
function insertKeepExtend(item: KeepPrompt) {
|
||||
extendPrompt.value = insertKeepSnippet(extendPrompt.value, item.text)
|
||||
}
|
||||
@@ -4118,11 +4129,9 @@ function insertLoraTrigger(phrase: string, target: 'main' | 'extend' | { shot: n
|
||||
}
|
||||
if (typeof target === 'object' && 'shot' in target) {
|
||||
prompt.value = insertActivationInShotScript(prompt.value, target.shot, token)
|
||||
promptDrawerOpen.value = true
|
||||
return
|
||||
}
|
||||
prompt.value = insertActivationPhrase(prompt.value, token)
|
||||
promptDrawerOpen.value = true
|
||||
}
|
||||
|
||||
function setShotLoraStack(shotN: number, stack: LoraStackItem[]) {
|
||||
@@ -4680,6 +4689,8 @@ function resetImage() {
|
||||
function clearVideoForm() {
|
||||
resetImage()
|
||||
prompt.value = ''
|
||||
promptPre.value = ''
|
||||
promptPost.value = ''
|
||||
clipName.value = ''
|
||||
extensionQueue.value = []
|
||||
shotScriptMode.value = false
|
||||
@@ -5115,6 +5126,8 @@ async function restoreStudioJob(id: string) {
|
||||
referenceStillIds?: Array<string | null>
|
||||
extensions?: { prompt: string; duration: number; permanenceRefs?: PermanenceRef[]; loraName?: string; loraStack?: LoraStackItem[] }[]
|
||||
queueAutoRun?: boolean
|
||||
promptPre?: string
|
||||
promptPost?: string
|
||||
globalLocks?: string
|
||||
permanenceRefs?: PermanenceRef[]
|
||||
shotPermanenceRefs?: PermanenceRef[][]
|
||||
@@ -5131,9 +5144,9 @@ async function restoreStudioJob(id: string) {
|
||||
closeExtend()
|
||||
const extensions = payload.extensions || []
|
||||
applyVideoWorkflow(payload.workflow)
|
||||
prompt.value = extensions.length
|
||||
applyPromptRestore(extensions.length
|
||||
? [payload.prompt.trim(), ...extensions.map((item, index) => `shot ${index + 2}\n${item.prompt.trim()}`)].join('\n\n')
|
||||
: (payload.prompt || '')
|
||||
: (payload.prompt || ''), payload)
|
||||
clipName.value = payload.name || ''
|
||||
seedInput.value = String(payload.seed || '')
|
||||
restoreAdvanced({
|
||||
@@ -5516,7 +5529,7 @@ async function rerun(item: LibraryClip, collection = false) {
|
||||
const parts = familyPartsFor(item)
|
||||
const target = collection ? parts[0] || item : item
|
||||
const restoreAll = collection && parts.length > 1
|
||||
prompt.value = restoreAll ? familyScript(parts) : target.prompt
|
||||
applyPromptRestore(restoreAll ? familyScript(parts) : target.prompt, target)
|
||||
clipName.value = restoreAll
|
||||
? (familyBaseName(collectionTitle({ head: target, parts })) || target.name || '')
|
||||
: (target.name || '')
|
||||
@@ -5574,7 +5587,7 @@ async function rerun(item: LibraryClip, collection = false) {
|
||||
|
||||
async function loadDraft(draft: RetryDraft) {
|
||||
activeDraftId = draft.id
|
||||
prompt.value = draft.prompt
|
||||
applyPromptRestore(draft.prompt, draft)
|
||||
clipName.value = draft.name || ''
|
||||
seedInput.value = String(draft.seed)
|
||||
restoreAdvanced(draft)
|
||||
@@ -5711,6 +5724,7 @@ async function openExtend(item?: LibraryClip) {
|
||||
}
|
||||
currentClipId.value = id
|
||||
const clip = clips.value.find(entry => entry.id === id)
|
||||
if (clip) applyPromptRestore(clip.prompt, clip)
|
||||
extendPrompt.value = clip?.prompt || prompt.value
|
||||
extendDuration.value = clampDuration(duration.value)
|
||||
extendLoraStack.value = readLoraStack(clip)
|
||||
@@ -5750,6 +5764,8 @@ async function generateExtension() {
|
||||
body: {
|
||||
clipId: id,
|
||||
prompt: extendPrompt.value.trim(),
|
||||
promptPre: promptPre.value.trim() || undefined,
|
||||
promptPost: promptPost.value.trim() || undefined,
|
||||
duration: extendDuration.value,
|
||||
loraStack: extendLoraStack.value
|
||||
}
|
||||
@@ -5812,7 +5828,7 @@ function applyStillSettings(still: LibraryStill) {
|
||||
const settings = still.settings
|
||||
folderId.value = still.folderId
|
||||
browseFolderId.value = still.folderId
|
||||
if (still.prompt) prompt.value = still.prompt
|
||||
if (still.prompt || still.promptPre || still.promptPost) applyPromptRestore(still.prompt || '', still)
|
||||
if (dest === 'imagev2') {
|
||||
studioMode.value = 'editv2'
|
||||
outputFocus.value = 'edit'
|
||||
@@ -5889,6 +5905,19 @@ function stillUseAsInputMessage(still: LibraryStill) {
|
||||
return 'Loaded as input. It stays in Output, not the upload library.'
|
||||
}
|
||||
|
||||
async function sendStillToBackup(still: Pick<LibraryStill, 'id'>) {
|
||||
if (!backupStudioEnabled.value || backupBusyId.value) return
|
||||
backupBusyId.value = still.id
|
||||
try {
|
||||
await $fetch(`/api/library/stills/${still.id}/backup`, { method: 'POST' })
|
||||
toast('Sent to Backup as a hidden input.')
|
||||
} catch (error: any) {
|
||||
toast(error?.data?.statusMessage || 'Could not send that still to Backup')
|
||||
} finally {
|
||||
backupBusyId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function saveStillAsInput(still: Pick<LibraryStill, 'id'>) {
|
||||
if (keepingInputBusy.value) return
|
||||
keepingInputBusy.value = true
|
||||
@@ -6470,6 +6499,8 @@ async function editImage() {
|
||||
body.append('image', file.value)
|
||||
if (editRefFile.value) body.append('image2', editRefFile.value)
|
||||
body.append('prompt', prompt.value.trim())
|
||||
if (promptPre.value.trim()) body.append('promptPre', promptPre.value.trim())
|
||||
if (promptPost.value.trim()) body.append('promptPost', promptPost.value.trim())
|
||||
body.append('aspect', aspect.value)
|
||||
body.append('negative', editNegative.value)
|
||||
body.append('steps', String(clampImageSteps(editSteps.value)))
|
||||
@@ -6707,6 +6738,8 @@ async function editImageV2() {
|
||||
body.append('aspect', aspect.value)
|
||||
body.append('engine', v2Engine.value)
|
||||
body.append('prompt', prompt.value.trim())
|
||||
if (promptPre.value.trim()) body.append('promptPre', promptPre.value.trim())
|
||||
if (promptPost.value.trim()) body.append('promptPost', promptPost.value.trim())
|
||||
body.append('negative', v2Negative.value)
|
||||
body.append('snofs_model', String(imageV2EngineLoras().find(item => isImageV2ConceptLora(item.name))?.strengthModel ?? 0))
|
||||
body.append('snofs_clip', String(imageV2EngineLoras().find(item => isImageV2ConceptLora(item.name))?.strengthClip ?? 0))
|
||||
@@ -6909,6 +6942,8 @@ async function generate() {
|
||||
if (file.value) body.append('image', file.value)
|
||||
if (inputStillId.value) body.append('stillId', inputStillId.value)
|
||||
body.append('prompt', initialPrompt)
|
||||
if (promptPre.value.trim()) body.append('promptPre', promptPre.value.trim())
|
||||
if (promptPost.value.trim()) body.append('promptPost', promptPost.value.trim())
|
||||
body.append('aspect', aspect.value)
|
||||
body.append('width', String(outputSize.value.width))
|
||||
body.append('height', String(outputSize.value.height))
|
||||
|
||||
Reference in New Issue
Block a user