Add native H3 identity graph and Studio 2 prompt contracts
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
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 = {}) {
|
||||
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
|
||||
if (chip && cameraFamilies.includes(chip.family)) sections.cameraChip = { family: chip.family, amplitude: ['subtle','moderate','large'].includes(chip.amplitude) ? chip.amplitude : 'subtle', speed: ['slow','medium','fast'].includes(chip.speed) ? chip.speed : 'slow' }
|
||||
return sections
|
||||
}
|
||||
export function compilePrompt(value, video = false) {
|
||||
const s = sectionsFrom(value), c = s.cameraChip
|
||||
const camera = c ? (c.family === 'static' ? 'Static shot. Camera locked on a tripod.' : `Camera ${c.family}, ${c.amplitude} movement, ${c.speed} speed.`) : s.camera
|
||||
return [s.action, camera && `Camera: ${camera}`, s.soundscape && `Soundscape: ${s.soundscape}`, s.dialogue && `Dialogue: (S1) <d>[English]${s.dialogue}</d>`, s.music && `Music: ${s.music}`, s.locks && `Locks: ${s.locks}`, video ? identityLock : ''].filter(Boolean).join('\n\n')
|
||||
}
|
||||
export function restoreInput(item) {
|
||||
const saved = item.studio2 || item
|
||||
return { ...saved, promptSections: sectionsFrom(saved.promptSections || { action: item.prompt || saved.compiledPrompt || '' }) }
|
||||
}
|
||||
export function validateRequest(raw, xaigen = false) {
|
||||
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 (!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.')
|
||||
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 (!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 }
|
||||
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)) }
|
||||
}
|
||||
export function scopedFile(file, prefix) {
|
||||
const clean = s => String(s || '').replaceAll('\\','/')
|
||||
const path = [clean(file.subfolder),clean(file.filename)].filter(Boolean).join('/')
|
||||
const p=clean(prefix).replace(/\/$/,'')
|
||||
return !!p && !path.split('/').some(s=>s==='..'||s==='.') && !path.startsWith('/') && path.startsWith(p + '/')
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
export function nativeVideoGraph(template, request, files, prefix) {
|
||||
if (!files.hero) throw new Error('Identity still is required; never substitute the start frame.')
|
||||
const g=structuredClone(template), s=request.settings
|
||||
g.hero.inputs.image=files.hero
|
||||
Object.assign(g['155'].inputs,{prompt:request.compiledPrompt,width:s.width,height:s.height,length:Math.ceil((s.duration*24-5)/17)*17+5,ref_image_size:'max'})
|
||||
g['155'].inputs['ref_images.ref_image_0']=['hero',0]
|
||||
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
|
||||
g[`load_${name}`]={class_type:'LoadImage',inputs:{image}}
|
||||
g[name]={class_type:'MiniMaxH3AddGuide',inputs:{positive:conditioning,latent:['155',1],vae:['161',0],image:[`load_${name}`,0],frame_idx:frame}}
|
||||
conditioning=[name,0]
|
||||
}
|
||||
g.guidance.inputs={conditioning,guidance:s.cfg}
|
||||
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_save={class_type:'SaveImage',inputs:{images:['anchor_frame',0],filename_prefix:prefix+'/anchor'}}
|
||||
return g
|
||||
}
|
||||
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]}}
|
||||
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]}}
|
||||
guider.inputs[name]=[`hero_${name}`,0]
|
||||
}
|
||||
return g
|
||||
}
|
||||
Reference in New Issue
Block a user