Model camera beats and independent hero identity locks
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import {compilePrompt,validateRequest,restoreInput} from '../shared/studio2/contracts.mjs'
|
||||
import {normalizeBeat,toggleMove} from '../shared/studio2/prompt.mjs'
|
||||
import {bindClip,restoreAsset,resolveSize,extendSelected} from '../shared/studio2/input.mjs'
|
||||
import {nativeVideoGraph} from '../shared/studio2/graphs.mjs'
|
||||
const base={mode:'video',engine:'minimax',folderId:'f',identityStillId:'hero',promptSections:{action:'Walk forward'}}
|
||||
const beats=[normalizeBeat({moves:['tilt-down','dolly-out'],durationSec:2}),normalizeBeat({moves:['pan-left'],durationSec:null})]
|
||||
test('sequential and simultaneous beats compile as one continuous take',()=>{
|
||||
const text=compilePrompt({cameraBeats:beats},true)
|
||||
assert.match(text,/tilt down and dolly out simultaneously/);assert.match(text,/first 2 seconds/);assert.match(text,/Then, pan left/);assert.match(text,/No cuts\. No new setup\./);assert.doesNotMatch(text,/\[Shot|Shot 2|00:02/)
|
||||
})
|
||||
test('hold is exclusive; toggling moves supports simultaneous motion',()=>{
|
||||
const mixed=normalizeBeat({moves:['hold','pan-left']});assert.deepEqual(mixed.moves,['hold'])
|
||||
const moved=toggleMove(toggleMove(mixed,'tilt-down'),'dolly-out');assert.deepEqual(moved.moves,['tilt-down','dolly-out']);assert.deepEqual(toggleMove(moved,'hold').moves,['hold'])
|
||||
})
|
||||
test('Use as input restores camera, sound, dialogue, music, LoRAs, aspect and locks',()=>{
|
||||
const request=validateRequest({...base,lockFace:true,lockOutfit:false,hideInput:true,promptSections:{action:'Walk',cameraBeats:beats,soundscapeChips:['footsteps'],dialogue:{mode:'whisper',line:'Hello',language:'English'},musicChip:'none'},settings:{aspect:'9:16',width:768,height:1344,loraStack:[{name:'example.safetensors',strengthModel:.8,strengthClip:.4}]}})
|
||||
const restored=restoreInput({studio2:request});assert.deepEqual(restored,request);assert.equal(restored.lockOutfit,false);assert.equal(restored.hideInput,true)
|
||||
})
|
||||
test('extend binds completed clip, inherits settings and leaves delta prompt alone',()=>{
|
||||
const form={...base,lockFace:true,lockOutfit:false,settings:{duration:5,width:1344,height:768},promptSections:{action:'Turn',cameraBeats:beats}}
|
||||
const clip={id:'done',kind:'clip',studio2:{...base,lockFace:true,lockOutfit:false,settings:{duration:8,width:768,height:1344,aspect:'9:16',fps:24,steps:8,cfg:1.5,turbo:false,loraStack:[{name:'a',strengthModel:1,strengthClip:.5}]}}}
|
||||
const bound=bindClip(form,clip);assert.equal(bound.startClipId,'done');assert.deepEqual(bound.settings,clip.studio2.settings);assert.equal(bound.promptSections.action,'Turn');assert.equal(bound.identityStillId,'hero');assert.equal(bound.lockOutfit,false)
|
||||
assert.equal(restoreAsset(form,clip).startClipId,'done');assert.equal(bindClip(form,{id:'legacy'}).lockOutfit,false)
|
||||
const overridden={...bound,lockOutfit:false,settings:{...bound.settings,cfg:2.5}}
|
||||
const extend=extendSelected(overridden,{...clip,studio2:{...clip.studio2,lockOutfit:true}})
|
||||
assert.equal(extend.lockOutfit,false);assert.equal(extend.settings.cfg,2.5);assert.equal(extend.startClipId,'done')
|
||||
})
|
||||
test('appearance roles distinguish wardrobe lock and both-off graph excludes hero',()=>{
|
||||
assert.match(compilePrompt({},true,{lockOutfit:false}),/Do not restore an earlier outfit/)
|
||||
assert.match(compilePrompt({},true),/person and clothes fully preserved/)
|
||||
const r=validateRequest({...base,lockFace:false,lockOutfit:false,imageAId:'start'})
|
||||
const template=JSON.parse(fs.readFileSync(new URL('../server/assets/studio2_minimax_native.json',import.meta.url)))
|
||||
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('sound/dialogue/music compile without empty invented audio',()=>{
|
||||
const empty=compilePrompt({});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'}});assert.match(text,/lips remain closed/);assert.match(text,/<Subject 1> \(S1\) says <d>\[English\]Hello<\/d>/)
|
||||
})
|
||||
test('aspect sizing follows source and named presets',()=>{
|
||||
assert.deepEqual(resolveSize('9:16',{width:1344,height:768}),{width:768,height:1344});const size=resolveSize('auto',{width:768,height:1024});assert.equal(size.width/size.height,.75)
|
||||
})
|
||||
Reference in New Issue
Block a user