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:
Towsty
2026-09-06 07:35:42 -05:00
co-authored by Cursor
parent 4dccaac6d3
commit d4b49df05a
2 changed files with 62 additions and 37 deletions
+28
View File
@@ -38,6 +38,26 @@
</button>
</div>
<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
@@ -123,11 +143,13 @@ const props = withDefaults(defineProps<{
current: 'video' | 'image' | 'music'
ltxEnabled?: boolean
videoEngine?: 'minimax' | 'ltx'
videoStart?: 'still' | 'text'
imageEngine?: 'flux' | 'krea'
musicEngine?: 'ace' | 'yue'
}>(), {
ltxEnabled: false,
videoEngine: 'minimax',
videoStart: 'still',
imageEngine: 'flux',
musicEngine: 'ace'
})
@@ -135,6 +157,7 @@ const props = withDefaults(defineProps<{
const emit = defineEmits<{
select: [kind: 'video' | 'image']
'video-engine': [engine: 'minimax' | 'ltx']
'video-start': [start: 'still' | 'text']
'image-engine': [engine: 'flux' | 'krea']
'music-engine': [engine: 'ace' | 'yue']
}>()
@@ -149,6 +172,11 @@ function pickVideo(engine: 'minimax' | 'ltx') {
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') {
emit('image-engine', engine)
if (props.current !== 'image') emit('select', 'image')
+34 -37
View File
@@ -12,9 +12,11 @@
:current="imageStudio ? 'image' : 'video'"
:ltx-enabled="ltxEnabled"
:video-engine="videoEngine"
:video-start="videoStart"
:image-engine="v2Engine"
@select="selectStudioKind"
@video-engine="selectVideoEngine"
@video-start="selectVideoStart"
@image-engine="setV2Engine"
/>
<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.')
: studioMode === 'edit'
? '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
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="{ 'border-amber-300/70 bg-amber-400/5': dragging }"
@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>
</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
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"
@@ -649,30 +660,7 @@
</div>
<div v-if="studioMode === 'video'" class="space-y-3">
<div>
<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>
<p class="text-xs text-zinc-500">{{ videoModelHint }}</p>
<div v-if="showLegacyMinimaxV1 && videoEngine === 'minimax' && videoStart === 'still'">
<p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">MiniMax graph</p>
<div class="grid grid-cols-2 gap-2">
@@ -7468,6 +7456,11 @@ function selectVideoEngine(engine: VideoEngineId) {
applyEngineDefaults(engine)
}
function selectVideoStart(start: VideoStartId) {
videoStart.value = start
if (studioMode.value !== 'video') selectStudioKind('video')
}
function applyVideoWorkflow(raw?: string) {
const parsed = coerceVideoWorkflow(raw, ltxEnabled.value)
videoEngine.value = videoEngineOf(parsed)
@@ -7522,15 +7515,17 @@ async function generate() {
toast('Choose a library folder before generating.')
return
}
try {
await ensureVideoInputStill()
} catch {
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.')
return
if (!textToVideo.value) {
try {
await ensureVideoInputStill()
} catch {
toast('Could not load the selected still for image-to-video.')
return
}
if (!file.value && !inputStillId.value) {
toast('Load a still first. Image-to-video needs an input still.')
return
}
}
const shots = shotScriptMode.value ? parsedShots.value : null
const initialPrompt = (shots?.[0]?.prompt || prompt.value).trim()
@@ -7549,8 +7544,10 @@ async function generate() {
try {
await ensureGenerationNotifyPermission()
const body = new FormData()
if (file.value) body.append('image', file.value)
if (inputStillId.value) body.append('stillId', inputStillId.value)
if (!textToVideo.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('promptMid', initialPrompt)
body.append('promptPre', promptPre.value.trim())