Queue durable Studio 2 jobs with sticky identity and saved metadata
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
export const identityLock = '<Picture 1> is identity. Face, hair, glasses, outfit fully preserved. Start frame is composition only. Do not copy facial drift from the start frame.'
|
||||
export const cameraFamilies = ['static', 'dolly in', 'dolly out', 'pan left', 'pan right', 'tilt up', 'tilt down', 'zoom in', 'zoom out', 'orbit left', 'orbit right']
|
||||
export function sectionsFrom(value = {}) {
|
||||
value = value || {}
|
||||
if (typeof value === 'string') value = { action: value }
|
||||
const sections = Object.fromEntries(['action','camera','soundscape','dialogue','music','locks'].map(key => [key, String(value[key] || '')]))
|
||||
const chip = value.cameraChip
|
||||
@@ -17,12 +18,16 @@ export function restoreInput(item) {
|
||||
return { ...saved, promptSections: sectionsFrom(saved.promptSections || { action: item.prompt || saved.compiledPrompt || '' }) }
|
||||
}
|
||||
export function validateRequest(raw, xaigen = false) {
|
||||
raw = raw || {}
|
||||
const fail = message => { throw Object.assign(new Error(message), {statusCode:400}) }
|
||||
const mode = raw.mode || 'generate', engine = raw.engine || 'flux'
|
||||
if (!['generate','edit','compose','refine','iterate','video','extend'].includes(mode)) fail('Unknown task.')
|
||||
const video = ['video','extend'].includes(mode)
|
||||
if (!(video ? ['minimax','ltx'] : ['flux','krea']).includes(engine)) fail('Engine does not support this task.')
|
||||
if (engine === 'ltx' && !xaigen) fail('LTX is available only in xAIGen.')
|
||||
if (engine === 'krea' && ['edit','compose','iterate'].includes(mode)) fail('Sticky hero edits require Klein. Krea remains available for Generate and masked Refine; no engine fallback is performed.')
|
||||
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.identityStillId) fail('Choose a sticky hero still.')
|
||||
if (mode === 'compose' && !raw.imageBId) fail('Compose requires a second still.')
|
||||
@@ -32,12 +37,15 @@ export function validateRequest(raw, xaigen = false) {
|
||||
const promptSections = sectionsFrom(raw.promptSections || {action:raw.prompt || ''})
|
||||
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','seed']) if (!Number.isInteger(number(key,key==='steps'?24:1,0,2147483647))) fail(`Invalid ${key}.`)
|
||||
for (const key of ['width','height']) if (number(key,key==='width'?1344:768,256,4096)%32) fail('Dimensions must be multiples of 32.')
|
||||
const frames=Math.ceil((number('duration',5,1,15)*24-5)/17)*17+5
|
||||
return { mode, engine, folderId:String(raw.folderId), identityStillId:raw.identityStillId || '', imageAId:raw.imageAId || '', imageBId:raw.imageBId || '', maskId:raw.maskId || '', startClipId:raw.startClipId || '', endStillId:raw.endStillId || '',
|
||||
startFrameSource: raw.startClipId ? {kind:'previous-last-frame',clipId:raw.startClipId} : {kind:'still',stillId:raw.imageAId || ''},
|
||||
promptSections, compiledPrompt:compilePrompt(promptSections,video),
|
||||
settings: { 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),seed:number('seed',Math.floor(Math.random()*2147483647),0,2147483647),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) 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=>sectionsFrom(typeof s==='string'?{action:s}:s)) }
|
||||
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}) }
|
||||
}
|
||||
export function scopedFile(file, prefix) {
|
||||
const clean = s => String(s || '').replaceAll('\\','/')
|
||||
|
||||
@@ -15,7 +15,7 @@ export function nativeVideoGraph(template, request, files, prefix) {
|
||||
g['171'].inputs.noise_seed=s.seed
|
||||
g['179'].inputs.value=s.steps;g['180'].inputs.value=s.steps;g['181'].inputs.value=s.turbo
|
||||
g['92'].inputs.filename_prefix=prefix+'/video'
|
||||
g.anchor_frame={class_type:'ImageFromBatch',inputs:{image:['164',0],batch_index:-1,length:1}}
|
||||
g.anchor_frame={class_type:'ImageFromBatch',inputs:{image:['164',0],batch_index:g['155'].inputs.length-1,length:1}}
|
||||
g.anchor_save={class_type:'SaveImage',inputs:{images:['anchor_frame',0],filename_prefix:prefix+'/anchor'}}
|
||||
return g
|
||||
}
|
||||
@@ -23,7 +23,8 @@ export function attachHeroReference(g, image) {
|
||||
if (!image) return g
|
||||
const guider = g['18']; if (!guider) throw new Error('Klein reference conditioning is missing.')
|
||||
g.hero_load={class_type:'LoadImage',inputs:{image}}
|
||||
g.hero_encode={class_type:'VAEEncode',inputs:{pixels:['hero_load',0],vae:['6',0]}}
|
||||
g.hero_scale={class_type:'ImageScaleToTotalPixels',inputs:{image:['hero_load',0],upscale_method:'lanczos',megapixels:1,resolution_steps:1}}
|
||||
g.hero_encode={class_type:'VAEEncode',inputs:{pixels:['hero_scale',0],vae:['6',0]}}
|
||||
for (const name of ['positive','negative']) {
|
||||
if (!guider.inputs[name]) continue
|
||||
g[`hero_${name}`]={class_type:'ReferenceLatent',inputs:{conditioning:guider.inputs[name],latent:['hero_encode',0]}}
|
||||
|
||||
Reference in New Issue
Block a user