Let text-to-video generate without a still: Still/Text lives on the Video card, and Text mode skips the required input image.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -38,6 +38,26 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-else class="border-t border-white/10 px-4 py-2 text-xs text-zinc-500">MiniMax H3</p>
|
<p v-else class="border-t border-white/10 px-4 py-2 text-xs text-zinc-500">MiniMax H3</p>
|
||||||
|
<div class="grid grid-cols-2 border-t border-white/10">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="border-r border-white/10 px-3 py-2.5 text-left text-sm transition"
|
||||||
|
:class="engineBtnClass(current === 'video' && videoStart === 'still')"
|
||||||
|
@click="pickStart('still')"
|
||||||
|
>
|
||||||
|
<span class="block font-semibold">Still</span>
|
||||||
|
<span class="text-xs opacity-80">Image to video</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-3 py-2.5 text-left text-sm transition"
|
||||||
|
:class="engineBtnClass(current === 'video' && videoStart === 'text')"
|
||||||
|
@click="pickStart('text')"
|
||||||
|
>
|
||||||
|
<span class="block font-semibold">Text</span>
|
||||||
|
<span class="text-xs opacity-80">No start still</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -123,11 +143,13 @@ const props = withDefaults(defineProps<{
|
|||||||
current: 'video' | 'image' | 'music'
|
current: 'video' | 'image' | 'music'
|
||||||
ltxEnabled?: boolean
|
ltxEnabled?: boolean
|
||||||
videoEngine?: 'minimax' | 'ltx'
|
videoEngine?: 'minimax' | 'ltx'
|
||||||
|
videoStart?: 'still' | 'text'
|
||||||
imageEngine?: 'flux' | 'krea'
|
imageEngine?: 'flux' | 'krea'
|
||||||
musicEngine?: 'ace' | 'yue'
|
musicEngine?: 'ace' | 'yue'
|
||||||
}>(), {
|
}>(), {
|
||||||
ltxEnabled: false,
|
ltxEnabled: false,
|
||||||
videoEngine: 'minimax',
|
videoEngine: 'minimax',
|
||||||
|
videoStart: 'still',
|
||||||
imageEngine: 'flux',
|
imageEngine: 'flux',
|
||||||
musicEngine: 'ace'
|
musicEngine: 'ace'
|
||||||
})
|
})
|
||||||
@@ -135,6 +157,7 @@ const props = withDefaults(defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
select: [kind: 'video' | 'image']
|
select: [kind: 'video' | 'image']
|
||||||
'video-engine': [engine: 'minimax' | 'ltx']
|
'video-engine': [engine: 'minimax' | 'ltx']
|
||||||
|
'video-start': [start: 'still' | 'text']
|
||||||
'image-engine': [engine: 'flux' | 'krea']
|
'image-engine': [engine: 'flux' | 'krea']
|
||||||
'music-engine': [engine: 'ace' | 'yue']
|
'music-engine': [engine: 'ace' | 'yue']
|
||||||
}>()
|
}>()
|
||||||
@@ -149,6 +172,11 @@ function pickVideo(engine: 'minimax' | 'ltx') {
|
|||||||
if (props.current !== 'video') emit('select', 'video')
|
if (props.current !== 'video') emit('select', 'video')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pickStart(start: 'still' | 'text') {
|
||||||
|
emit('video-start', start)
|
||||||
|
if (props.current !== 'video') emit('select', 'video')
|
||||||
|
}
|
||||||
|
|
||||||
function pickImage(engine: 'flux' | 'krea') {
|
function pickImage(engine: 'flux' | 'krea') {
|
||||||
emit('image-engine', engine)
|
emit('image-engine', engine)
|
||||||
if (props.current !== 'image') emit('select', 'image')
|
if (props.current !== 'image') emit('select', 'image')
|
||||||
|
|||||||
+34
-37
@@ -12,9 +12,11 @@
|
|||||||
:current="imageStudio ? 'image' : 'video'"
|
:current="imageStudio ? 'image' : 'video'"
|
||||||
:ltx-enabled="ltxEnabled"
|
:ltx-enabled="ltxEnabled"
|
||||||
:video-engine="videoEngine"
|
:video-engine="videoEngine"
|
||||||
|
:video-start="videoStart"
|
||||||
:image-engine="v2Engine"
|
:image-engine="v2Engine"
|
||||||
@select="selectStudioKind"
|
@select="selectStudioKind"
|
||||||
@video-engine="selectVideoEngine"
|
@video-engine="selectVideoEngine"
|
||||||
|
@video-start="selectVideoStart"
|
||||||
@image-engine="setV2Engine"
|
@image-engine="setV2Engine"
|
||||||
/>
|
/>
|
||||||
<div class="grid min-w-0 gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] lg:items-stretch">
|
<div class="grid min-w-0 gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] lg:items-stretch">
|
||||||
@@ -37,11 +39,13 @@
|
|||||||
: 'Edit: one still plus text. Compose is a separate graph if you need a second still.')
|
: 'Edit: one still plus text. Compose is a separate graph if you need a second still.')
|
||||||
: studioMode === 'edit'
|
: studioMode === 'edit'
|
||||||
? 'Drop the still to edit. An optional second still applies image 2 onto image 1. Files leave the desktop after save.'
|
? 'Drop the still to edit. An optional second still applies image 2 onto image 1. Files leave the desktop after save.'
|
||||||
: 'Drop a still, describe the motion, then send the job. Generate stays available — a long shot list takes one queue slot, and you can line up more while it runs.' }}</p>
|
: (textToVideo
|
||||||
|
? 'Text-to-video: write a prompt and generate. No start still required. Shot 2+ still continues from the last frame.'
|
||||||
|
: 'Drop a still, describe the motion, then send the job. Switch the Video card to Text for text-to-video.') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="!(studioMode === 'editv2' && (v2Mode === 'generate' || (v2Mode === 'iterate' && !file && !inputStillId)))"
|
v-if="!(studioMode === 'editv2' && (v2Mode === 'generate' || (v2Mode === 'iterate' && !file && !inputStillId))) && !(studioMode === 'video' && textToVideo && !file && !inputStillId)"
|
||||||
class="relative cursor-pointer rounded-2xl border border-dashed border-white/15 bg-zinc-950/50 p-4 transition hover:border-amber-300/50"
|
class="relative cursor-pointer rounded-2xl border border-dashed border-white/15 bg-zinc-950/50 p-4 transition hover:border-amber-300/50"
|
||||||
:class="{ 'border-amber-300/70 bg-amber-400/5': dragging }"
|
:class="{ 'border-amber-300/70 bg-amber-400/5': dragging }"
|
||||||
@dragover.prevent="dragging = true"
|
@dragover.prevent="dragging = true"
|
||||||
@@ -73,6 +77,13 @@
|
|||||||
<p class="mt-1 text-sm text-zinc-500">{{ textToVideo ? 'Text-to-video does not need a still. Shot 2+ will still extend from the last frame.' : 'Browse the library or upload a new PNG, JPG, or WEBP' }}</p>
|
<p class="mt-1 text-sm text-zinc-500">{{ textToVideo ? 'Text-to-video does not need a still. Shot 2+ will still extend from the last frame.' : 'Browse the library or upload a new PNG, JPG, or WEBP' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="studioMode === 'video' && textToVideo"
|
||||||
|
class="rounded-2xl border border-dashed border-white/15 bg-zinc-950/40 px-4 py-3 text-sm text-zinc-400"
|
||||||
|
>
|
||||||
|
<button type="button" class="font-medium text-amber-100 hover:underline" @click="openPicker('main')">Optional · add a start still</button>
|
||||||
|
<span class="mt-1 block text-zinc-500">Text-to-video does not need one. Shot 2+ still extends from the last frame.</span>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="studioMode === 'editv2' && v2Mode === 'generate' && (file || inputStillId)"
|
v-else-if="studioMode === 'editv2' && v2Mode === 'generate' && (file || inputStillId)"
|
||||||
class="rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3 text-sm text-zinc-400"
|
class="rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3 text-sm text-zinc-400"
|
||||||
@@ -649,30 +660,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="studioMode === 'video'" class="space-y-3">
|
<div v-if="studioMode === 'video'" class="space-y-3">
|
||||||
<div>
|
<p class="text-xs text-zinc-500">{{ videoModelHint }}</p>
|
||||||
<p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">Start from</p>
|
|
||||||
<div class="grid grid-cols-2 gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rounded-xl border px-3 py-2 text-left text-sm"
|
|
||||||
:class="videoStart === 'still' ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 hover:border-white/20'"
|
|
||||||
@click="videoStart = 'still'"
|
|
||||||
>
|
|
||||||
<span class="block font-semibold">A still</span>
|
|
||||||
<span class="text-xs text-zinc-400">Image to video. Extensions use the last frame.</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rounded-xl border px-3 py-2 text-left text-sm"
|
|
||||||
:class="videoStart === 'text' ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 hover:border-white/20'"
|
|
||||||
@click="videoStart = 'text'"
|
|
||||||
>
|
|
||||||
<span class="block font-semibold">Text only</span>
|
|
||||||
<span class="text-xs text-zinc-400">No start still. Shot 2+ still continues from last frame.</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p class="mt-2 text-xs text-zinc-500">{{ videoModelHint }}</p>
|
|
||||||
</div>
|
|
||||||
<div v-if="showLegacyMinimaxV1 && videoEngine === 'minimax' && videoStart === 'still'">
|
<div v-if="showLegacyMinimaxV1 && videoEngine === 'minimax' && videoStart === 'still'">
|
||||||
<p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">MiniMax graph</p>
|
<p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">MiniMax graph</p>
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
@@ -7468,6 +7456,11 @@ function selectVideoEngine(engine: VideoEngineId) {
|
|||||||
applyEngineDefaults(engine)
|
applyEngineDefaults(engine)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectVideoStart(start: VideoStartId) {
|
||||||
|
videoStart.value = start
|
||||||
|
if (studioMode.value !== 'video') selectStudioKind('video')
|
||||||
|
}
|
||||||
|
|
||||||
function applyVideoWorkflow(raw?: string) {
|
function applyVideoWorkflow(raw?: string) {
|
||||||
const parsed = coerceVideoWorkflow(raw, ltxEnabled.value)
|
const parsed = coerceVideoWorkflow(raw, ltxEnabled.value)
|
||||||
videoEngine.value = videoEngineOf(parsed)
|
videoEngine.value = videoEngineOf(parsed)
|
||||||
@@ -7522,15 +7515,17 @@ async function generate() {
|
|||||||
toast('Choose a library folder before generating.')
|
toast('Choose a library folder before generating.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
if (!textToVideo.value) {
|
||||||
await ensureVideoInputStill()
|
try {
|
||||||
} catch {
|
await ensureVideoInputStill()
|
||||||
toast('Could not load the selected still for image-to-video.')
|
} catch {
|
||||||
return
|
toast('Could not load the selected still for image-to-video.')
|
||||||
}
|
return
|
||||||
if (!textToVideo.value && !file.value && !inputStillId.value) {
|
}
|
||||||
toast('Load a still first. Image-to-video needs an input still.')
|
if (!file.value && !inputStillId.value) {
|
||||||
return
|
toast('Load a still first. Image-to-video needs an input still.')
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const shots = shotScriptMode.value ? parsedShots.value : null
|
const shots = shotScriptMode.value ? parsedShots.value : null
|
||||||
const initialPrompt = (shots?.[0]?.prompt || prompt.value).trim()
|
const initialPrompt = (shots?.[0]?.prompt || prompt.value).trim()
|
||||||
@@ -7549,8 +7544,10 @@ async function generate() {
|
|||||||
try {
|
try {
|
||||||
await ensureGenerationNotifyPermission()
|
await ensureGenerationNotifyPermission()
|
||||||
const body = new FormData()
|
const body = new FormData()
|
||||||
if (file.value) body.append('image', file.value)
|
if (!textToVideo.value) {
|
||||||
if (inputStillId.value) body.append('stillId', inputStillId.value)
|
if (file.value) body.append('image', file.value)
|
||||||
|
if (inputStillId.value) body.append('stillId', inputStillId.value)
|
||||||
|
}
|
||||||
body.append('prompt', composePromptParts(promptPre.value, initialPrompt, promptPost.value) || initialPrompt)
|
body.append('prompt', composePromptParts(promptPre.value, initialPrompt, promptPost.value) || initialPrompt)
|
||||||
body.append('promptMid', initialPrompt)
|
body.append('promptMid', initialPrompt)
|
||||||
body.append('promptPre', promptPre.value.trim())
|
body.append('promptPre', promptPre.value.trim())
|
||||||
|
|||||||
Reference in New Issue
Block a user