Allow Studio 2 MiniMax text-to-video with empty start when locks are off.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -89,11 +89,12 @@ async function prepareGraph(r: any) {
|
||||
const { frameLength } = await import('../videoChain');
|
||||
const { ltxWorkflowEnabled, LTX_DISABLED_MESSAGE } = await import('~/utils/videoModels');
|
||||
if (!ltxWorkflowEnabled()) throw new Error(LTX_DISABLED_MESSAGE);
|
||||
if (!start) throw new Error('Choose a start still for LTX video.');
|
||||
const textToVideo = q.mode === 'video' && !start;
|
||||
if (!start && !textToVideo) throw new Error('Choose a start still for LTX video.');
|
||||
const fps = s.fps || 24;
|
||||
graph = buildWorkflow({
|
||||
workflow: 'ltx',
|
||||
imageName: start,
|
||||
workflow: textToVideo ? 'ltx-t2v' : 'ltx',
|
||||
imageName: start || '',
|
||||
prompt: q.compiledPrompt,
|
||||
width: s.width,
|
||||
height: s.height,
|
||||
@@ -111,7 +112,7 @@ async function prepareGraph(r: any) {
|
||||
useIdentityRefs: false
|
||||
});
|
||||
await ensureComfyLoraNames('video');
|
||||
r.graphId = 'workflow_ltx_video.json';
|
||||
r.graphId = textToVideo ? 'workflow_ltx_video.json#t2v' : 'workflow_ltx_video.json';
|
||||
} else {
|
||||
if (q.engine !== 'minimax')
|
||||
throw new Error('Studio 2 video supports MiniMax H3 and LTX (xAIGen).');
|
||||
|
||||
@@ -52,7 +52,6 @@ export function validateRequest(raw, xaigen = false) {
|
||||
if (video) {
|
||||
if (promptSections.cameraBeats.slice(0,-1).some(b=>b.durationSec == null)) fail('Only the last camera beat can run to the end.')
|
||||
if (promptSections.cameraBeats.reduce((sum,b)=>sum+(b.durationSec || 0),0)>duration) fail('Camera beats exceed the clip duration.')
|
||||
if (raw.lockFace === false && raw.lockOutfit === false && !raw.imageAId && !raw.startClipId) fail('Choose a start still or previous clip when both identity locks are off.')
|
||||
}
|
||||
const frames=Math.ceil((duration*number('fps',24,1,60)-5)/17)*17+5
|
||||
const shotSeeds=queueSeeds(raw.settings,1+(Array.isArray(raw.batch)?raw.batch.length:0))
|
||||
|
||||
@@ -2,9 +2,20 @@ export function nativeVideoGraph(template, request, files, prefix) {
|
||||
const appearance = request.lockFace !== false || request.lockOutfit !== false
|
||||
if (appearance && !files.hero) throw new Error('Identity still is required; never substitute the start frame.')
|
||||
const g=structuredClone(template), s=request.settings
|
||||
const textToVideo = request.mode === 'video' && !appearance && !files.start
|
||||
if (appearance) { g.hero.inputs.image=files.hero; g['155'].inputs['ref_images.ref_image_0']=['hero',0] }
|
||||
else { delete g.hero; delete g['155'].inputs['ref_images.ref_image_0'] }
|
||||
Object.assign(g['155'].inputs,{prompt:request.compiledPrompt,width:s.width,height:s.height,length:Math.ceil((s.duration*(s.fps || 24)-5)/17)*17+5,ref_image_size:'max'})
|
||||
if (textToVideo) {
|
||||
// Studio 1 T2V: MiniMaxH3ImageToVideo with first_frame disconnected (no empty placeholder still).
|
||||
g['155'].class_type = 'MiniMaxH3ImageToVideo'
|
||||
g['155']._meta = { title: 'MiniMax H3 Text to Video' }
|
||||
delete g['155'].inputs['ref_images.ref_image_0']
|
||||
delete g['155'].inputs.ref_image_size
|
||||
delete g['155'].inputs.audio_vae
|
||||
delete g['155'].inputs.first_frame
|
||||
delete g['155'].inputs.last_frame
|
||||
}
|
||||
let conditioning=['155',0]
|
||||
for (const [name,image,frame] of [['start',files.start,0],['end',files.end,-1],...(files.guides||[]).map((f,i)=>[`guide${i}`,f.image,f.frame])]) {
|
||||
if (!image) continue
|
||||
|
||||
@@ -36,6 +36,17 @@ test('appearance roles distinguish wardrobe lock and both-off graph excludes her
|
||||
const graph=nativeVideoGraph(template,r,{hero:'ignored.png',start:'start.png'},'preview')
|
||||
assert.equal(graph.hero,undefined);assert.equal(graph['155'].inputs['ref_images.ref_image_0'],undefined);assert.equal(graph.load_start.inputs.image,'start.png');assert.equal(r.identityStillId,'hero')
|
||||
})
|
||||
test('video with locks off and no assets is MiniMax T2V empty first frame',()=>{
|
||||
const r=validateRequest({mode:'video',engine:'minimax',folderId:'f',lockFace:false,lockOutfit:false,identityStillId:'',imageAId:'',promptSections:{action:'Wide empty street at dawn'}})
|
||||
assert.equal(r.lockFace,false);assert.equal(r.lockOutfit,false);assert.equal(r.imageAId,'');assert.equal(r.startClipId,'')
|
||||
const template=JSON.parse(fs.readFileSync(new URL('../server/assets/studio2_minimax_native.json',import.meta.url)))
|
||||
const graph=nativeVideoGraph(template,r,{hero:'',start:''},'preview/t2v')
|
||||
assert.equal(graph['155'].class_type,'MiniMaxH3ImageToVideo')
|
||||
assert.equal(graph['155'].inputs.first_frame,undefined)
|
||||
assert.equal(graph.load_start,undefined)
|
||||
assert.equal(graph.hero,undefined)
|
||||
assert.throws(()=>validateRequest({mode:'extend',engine:'minimax',folderId:'f',lockFace:false,lockOutfit:false,promptSections:{action:'Continue'}}),e=>/Previous clip|family strip/i.test(e.message))
|
||||
})
|
||||
test('sound/dialogue/music compile without empty invented audio',()=>{
|
||||
const empty=compilePrompt({},true);assert.doesNotMatch(empty,/Soundscape:|<d>/);assert.match(empty,/non_diegetic_music: N\/A/)
|
||||
const text=compilePrompt({soundscapeChips:['footsteps'],dialogue:{mode:'spoken off-screen',line:'Hello'}},true);assert.match(text,/lips remain closed/);assert.match(text,/<Subject 1> \(S1\) says <d>\[English\]Hello<\/d>/)
|
||||
|
||||
Reference in New Issue
Block a user