Separate Studio 2 source and canvas and apply size and seed choices
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { queueSeeds } from './seed.mjs'
|
||||
import { sectionsFrom, compileSections } from './prompt.mjs'
|
||||
export { sectionsFrom } from './prompt.mjs'
|
||||
export const cameraFamilies = ['static','dolly in','dolly out','pan left','pan right','tilt up','tilt down','zoom in','zoom out','orbit left','orbit right']
|
||||
@@ -35,7 +36,7 @@ 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 ['steps']) 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 duration=number('duration',5,1,15)
|
||||
if (video) {
|
||||
@@ -44,10 +45,11 @@ export function validateRequest(raw, xaigen = false) {
|
||||
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
|
||||
return { mode, engine, lockFace:raw.lockFace !== false, lockOutfit:raw.lockOutfit !== false, hideInput:!!raw.hideInput, folderId:String(raw.folderId), identityStillId:raw.identityStillId || '', imageAId:raw.imageAId || '', imageBId:raw.imageBId || '', maskId:raw.maskId || '', startClipId:raw.startClipId || '', endStillId:raw.endStillId || '',
|
||||
const shotSeeds=queueSeeds(raw.settings,1+(Array.isArray(raw.batch)?raw.batch.length:0))
|
||||
return { mode, engine, shotSeeds, lockFace:raw.lockFace !== false, lockOutfit:raw.lockOutfit !== false, hideInput:!!raw.hideInput, 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,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),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:[] },
|
||||
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}) }
|
||||
}
|
||||
|
||||
@@ -34,3 +34,17 @@ export function attachHeroReference(g, image) {
|
||||
}
|
||||
return g
|
||||
}
|
||||
/** 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}) {
|
||||
for (const node of Object.values(graph)) {
|
||||
if (node.class_type === 'ImageScaleToTotalPixels') {
|
||||
node.class_type='ImageScale'
|
||||
node.inputs={image:node.inputs.image,upscale_method:'lanczos',width,height,crop:'disabled'}
|
||||
} else {
|
||||
if (typeof node.inputs?.width === 'number') node.inputs.width=width
|
||||
if (typeof node.inputs?.height === 'number') node.inputs.height=height
|
||||
}
|
||||
}
|
||||
return graph
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ export function bindClip(form, clip) {
|
||||
if (!clip?.id) return form
|
||||
const saved = clip.studio2 || {}, 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) next.settings[key] = structuredClone(settings[key])
|
||||
if (settings[key] != null && key!=='aspect') next.settings[key] = structuredClone(settings[key])
|
||||
}
|
||||
next.engine = saved.engine || clip.engine || 'minimax'
|
||||
if (saved.identityStillId) next.identityStillId = saved.identityStillId
|
||||
@@ -23,7 +24,12 @@ export function bindClip(form, clip) {
|
||||
export function restoreAsset(form, item) {
|
||||
const restored = restoreInput(item)
|
||||
const next = {...form,...restored,settings:{...form.settings,...restored.settings}}
|
||||
return item.kind === 'clip' || item.studio2?.kind === 'video' ? {...bindClip(next,item),mode:'extend'} : next
|
||||
next.settings.seedMode='random'
|
||||
if (item.kind === 'clip' || item.studio2?.kind === 'video') return {...bindClip(next,item),mode:'extend'}
|
||||
next.imageAId=item.id
|
||||
next.startClipId=''
|
||||
next.settings.aspect=item.studio2?.settings?.aspect || item.settings?.aspect || 'auto'
|
||||
return next
|
||||
}
|
||||
export function extendSelected(form, clip) {
|
||||
// Switching task on the already bound clip must preserve the user's overrides.
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
let previous
|
||||
export function randomSeed() {
|
||||
const bytes = new Uint32Array(1)
|
||||
globalThis.crypto.getRandomValues(bytes)
|
||||
let seed = 2 + bytes[0] % 2147483646
|
||||
if (seed === previous) seed = seed === 2147483647 ? 2 : seed + 1
|
||||
previous = seed
|
||||
return seed
|
||||
}
|
||||
export function queueSeeds(settings = {}, count = 1) {
|
||||
if (settings.seedMode === 'locked') {
|
||||
const seed=Number(settings.seed)
|
||||
if (!Number.isInteger(seed) || seed<0 || seed>2147483647) throw Object.assign(new Error('Invalid seed.'),{statusCode:400})
|
||||
return Array(count).fill(seed)
|
||||
}
|
||||
return Array.from({length:count},()=>randomSeed())
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export function folderAssets(library, jobs, folderId) {
|
||||
const assets=new Map()
|
||||
for (const kind of ['still','clip']) for (const asset of library[kind==='still'?'stills':'clips'] || []) {
|
||||
if (asset.folderId===folderId) assets.set(asset.id,{...asset,kind})
|
||||
}
|
||||
for (const job of jobs) if (job.request.folderId===folderId) for (const asset of job.outputs || []) assets.set(asset.id,{...assets.get(asset.id),...asset})
|
||||
return [...assets.values()].sort((a,b)=>(a.createdAt || a.studio2?.savedAt || 0)-(b.createdAt || b.studio2?.savedAt || 0))
|
||||
}
|
||||
export function newestOutput(jobs, seen, folderId) {
|
||||
const fresh=jobs.filter(j=>j.request.folderId===folderId).flatMap(j=>j.outputs || []).filter(a=>!seen.has(a.id))
|
||||
return fresh.sort((a,b)=>(a.studio2?.savedAt || 0)-(b.studio2?.savedAt || 0)).at(-1) || null
|
||||
}
|
||||
export function selectResult(state, asset) { return {...state,selected:asset} }
|
||||
export function assetLabel(asset) { return asset?.name || asset?.originalName || (asset ? `Still ${asset.id.slice(0,8)}` : 'None') }
|
||||
Reference in New Issue
Block a user