Sample Klein edits from source and bind Studio 2 extensions correctly
This commit is contained in:
@@ -10,7 +10,15 @@ export function appearanceLock({lockFace = true, lockOutfit = true} = {}) {
|
||||
return 'The start frame is the only appearance reference for person and clothing, as well as pose, place, and camera. Do not use the hero as an appearance reference.'
|
||||
}
|
||||
export function compilePrompt(value, video = false, locks = {}) {
|
||||
return [compileSections(value), video ? appearanceLock(locks) : ''].filter(Boolean).join('\n\n')
|
||||
if (video) return [compileSections(value),appearanceLock(locks)].filter(Boolean).join('\n\n')
|
||||
const hasImage=!!(locks.imageAId || locks.identityStillId)
|
||||
const attached=hasImage && locks.engine !== 'krea'
|
||||
const reference=locks.mode!=='generate' && locks.identityStillId && locks.imageAId && locks.identityStillId!==locks.imageAId ? 'second (hero) reference' : 'image reference'
|
||||
return [sectionsFrom(value).action,
|
||||
attached && locks.lockFace !== false ? `Preserve the person’s face, glasses, hair, and body identity from the ${reference}.` : '',
|
||||
attached && locks.lockOutfit !== false ? `Preserve the wardrobe from the ${reference}.` : '',
|
||||
attached && locks.lockOutfit === false ? 'Clothing may follow the source and requested scene; do not restore the hero outfit.' : ''
|
||||
].filter(Boolean).join('\n\n')
|
||||
}
|
||||
export function restoreInput(item) {
|
||||
const saved = structuredClone(item.studio2 || item)
|
||||
@@ -28,12 +36,13 @@ export function validateRequest(raw, xaigen = false) {
|
||||
if (Array.isArray(raw.batch) && raw.batch.length>30) fail('A batch supports up to 31 shots including the first prompt.')
|
||||
if (Array.isArray(raw.guides) && raw.guides.length>8) fail('Use up to eight guide stills.')
|
||||
if (!raw.folderId) fail('Choose a folder.')
|
||||
if ((video || ['edit','compose','iterate'].includes(mode)) && (raw.lockFace !== false || raw.lockOutfit !== false) && !raw.identityStillId) fail('Choose a sticky hero still.')
|
||||
if ((video || ['edit','compose','iterate'].includes(mode)) && (raw.lockFace !== false || raw.lockOutfit !== false) && !raw.identityStillId && (video || !raw.imageAId)) fail('Choose a sticky hero still.')
|
||||
if (mode === 'compose' && !raw.imageBId) fail('Compose requires a second still.')
|
||||
if (mode === 'refine' && !raw.maskId) fail('Refine requires a mask.')
|
||||
if (['edit','compose','refine'].includes(mode) && !raw.imageAId) fail('Choose the source still.')
|
||||
if (mode === 'extend' && !raw.startClipId) fail('Choose the video to extend.')
|
||||
const promptSections = sectionsFrom(raw.promptSections || {action:raw.prompt || ''})
|
||||
if (['edit','compose','refine'].includes(mode) && !raw.imageAId && !(mode==='edit' && engine==='flux' && raw.identityStillId)) fail('Choose the source still.')
|
||||
if (mode === 'extend' && !raw.startClipId) fail('Select the clip in the family strip or Previous clip.')
|
||||
const allSections = sectionsFrom(raw.promptSections || {action:raw.prompt || ''})
|
||||
const promptSections = video ? allSections : {action:allSections.action}
|
||||
if (!promptSections.action.trim()) fail('Enter an action prompt.')
|
||||
const number=(key, fallback, min, max) => { const n=Number(raw.settings?.[key] ?? fallback); if (!Number.isFinite(n)||n<min||n>max) fail(`Invalid ${key}.`); return n }
|
||||
for (const key of ['steps']) if (!Number.isInteger(number(key,key==='steps'?24:1,0,2147483647))) fail(`Invalid ${key}.`)
|
||||
@@ -51,7 +60,7 @@ export function validateRequest(raw, xaigen = false) {
|
||||
promptSections, compiledPrompt:compilePrompt(promptSections,video,raw),
|
||||
settings: { aspect:['auto','16:9','9:16','1:1'].includes(raw.settings?.aspect)?raw.settings.aspect:'auto', fps:number('fps',24,1,60), width:number('width',1344,256,4096),height:number('height',768,256,4096),steps:number('steps',video?8:24,1,100),cfg:number('cfg',video?1.5:4,0,20),seedMode:raw.settings?.seedMode==='locked'?'locked':'random',seed:shotSeeds[0],duration:number('duration',5,1,15),turbo:raw.settings?.turbo !== false, loraStack:Array.isArray(raw.settings?.loraStack)?raw.settings.loraStack:[] },
|
||||
guides: (Array.isArray(raw.guides)?raw.guides:[]).slice(0,8).map(g=>{ if (!g.stillId || !Number.isInteger(g.frame) || g.frame<1 || g.frame>=frames) fail('Each guide requires a still and positive frame index.'); return {stillId:String(g.stillId),frame:g.frame} }),
|
||||
batch: (Array.isArray(raw.batch)?raw.batch:[]).slice(0,30).map(s=>{const next=sectionsFrom({...promptSections,...(typeof s==='string'?{action:s}:s)});if(!next.action.trim())fail('Every shot needs an action prompt.');return next}) }
|
||||
batch: (Array.isArray(raw.batch)?raw.batch:[]).slice(0,30).map(s=>{const next=sectionsFrom({...promptSections,...(typeof s==='string'?{action:s}:s)});if(!next.action.trim())fail('Every shot needs an action prompt.');return video?next:{action:next.action}}) }
|
||||
}
|
||||
export function scopedFile(file, prefix) {
|
||||
const clean = s => String(s || '').replaceAll('\\','/')
|
||||
|
||||
@@ -34,6 +34,16 @@ export function attachHeroReference(g, image) {
|
||||
}
|
||||
return g
|
||||
}
|
||||
export function sampleKleinSource(graph, denoise = .65) {
|
||||
if (!graph['11'] || !graph['19'] || !graph['17']) throw new Error('Klein source sampling nodes are missing.')
|
||||
graph['19'].inputs.latent_image=['11',0]
|
||||
delete graph['14']
|
||||
const steps=graph['17'].inputs.steps
|
||||
graph['17'].inputs.steps=Math.ceil(steps/denoise)
|
||||
graph.source_sigmas={class_type:'SplitSigmasDenoise',inputs:{sigmas:['17',0],denoise}}
|
||||
graph['19'].inputs.sigmas=['source_sigmas',1]
|
||||
return graph
|
||||
}
|
||||
/** The shared image builders have their own defaults and megapixel scalers.
|
||||
* Studio 2 must send its already resolved canvas size through those nodes. */
|
||||
export function applyResolvedImageSize(graph, {width,height}) {
|
||||
|
||||
@@ -7,21 +7,28 @@ export function resolveSize(aspect, source, fallback = {width:1344,height:768})
|
||||
const factor = Math.min(1344/Math.max(source.width,source.height),768/Math.min(source.width,source.height))
|
||||
return {width:Math.max(256,Math.round(source.width*factor/32)*32),height:Math.max(256,Math.round(source.height*factor/32)*32)}
|
||||
}
|
||||
export function bindClip(form, clip) {
|
||||
export function bindClip(form, clip, xaigen = false) {
|
||||
if (!clip?.id) return form
|
||||
const saved = clip.studio2 || {}, settings = saved.settings || clip.settings || clip
|
||||
const saved = clip.studio2 || clip.job || {}, settings = saved.settings || clip.settings || clip
|
||||
const next = {...form, startClipId:clip.id, settings:{...form.settings}}
|
||||
next.settings.aspect=['auto','16:9','9:16','1:1'].includes(settings.aspect)?settings.aspect:'auto'
|
||||
for (const key of ['duration','aspect','width','height','fps','loraStack','turbo','steps','cfg']) {
|
||||
if (settings[key] != null && key!=='aspect') next.settings[key] = structuredClone(settings[key])
|
||||
}
|
||||
next.engine = saved.engine || clip.engine || 'minimax'
|
||||
next.engine = xaigen && (saved.engine || clip.engine)==='ltx' ? 'ltx' : 'minimax'
|
||||
if (saved.identityStillId) next.identityStillId = saved.identityStillId
|
||||
// Missing legacy metadata must not reset a user's current choice.
|
||||
for (const key of ['lockFace','lockOutfit']) if (typeof saved[key] === 'boolean') next[key] = saved[key]
|
||||
return next
|
||||
}
|
||||
export function restoreAsset(form, item) {
|
||||
if (item.kind==='clip' || item.studio2?.kind==='video') {
|
||||
const next=bindClip(form,item,form.engine==='ltx')
|
||||
const seed=item.studio2?.settings?.seed ?? item.settings?.seed ?? item.seed
|
||||
if (seed != null) next.settings.seed=seed
|
||||
next.settings.seedMode='random'
|
||||
return {...next,mode:'extend'}
|
||||
}
|
||||
const restored = restoreInput(item)
|
||||
const next = {...form,...restored,settings:{...form.settings,...restored.settings}}
|
||||
next.settings.seedMode='random'
|
||||
@@ -31,7 +38,11 @@ export function restoreAsset(form, item) {
|
||||
next.settings.aspect=item.studio2?.settings?.aspect || item.settings?.aspect || 'auto'
|
||||
return next
|
||||
}
|
||||
export function extendSelected(form, clip) {
|
||||
export function extendSelected(form, clip, xaigen = false) {
|
||||
// Switching task on the already bound clip must preserve the user's overrides.
|
||||
return {...(clip?.id && form.startClipId !== clip.id ? bindClip(form,clip) : form),mode:'extend'}
|
||||
const next=clip?.id && form.startClipId !== clip.id ? bindClip(form,clip,xaigen) : form
|
||||
return {...next,mode:'extend',engine:xaigen && next.engine==='ltx'?'ltx':'minimax'}
|
||||
}
|
||||
export function changeSource(form) {
|
||||
return form.mode==='extend' ? form : {...form,startClipId:'',settings:{...form.settings,aspect:'auto'}}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@ export function applyUploadedStill(form, response, target) {
|
||||
if (!still?.id) throw new Error('The upload did not return a saved still. Please try again.')
|
||||
const next={...form,settings:{...form.settings}}
|
||||
if (target==='hero') next.identityStillId=still.id
|
||||
else {next.imageAId=still.id;next.startClipId='';if(next.mode==='extend')next.mode='video';next.settings.aspect='auto'}
|
||||
else {next.imageAId=still.id;if(next.mode!=='extend'){next.startClipId='';next.settings.aspect='auto'}}
|
||||
return next
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user