Size Qwen 2.1 Generate from the bench aspect table.
Wire EmptyLatentImage to the official ~1 MP grid so 16:9 lands at 1536×864, keep PE wh_ratio advisory, and leave Edit on the encode latent from image_1. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import {normalizeStyles,stylePrompt} from './styles.mjs'
|
||||
import { queueSeeds } from './seed.mjs'
|
||||
import { sectionsFrom, compileSections } from './prompt.mjs'
|
||||
import { QWEN21_ASPECT_IDS, resolveQwen21Size } from './qwen21-size.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']
|
||||
export const identityLock = '<Picture 1> / hero is identity for face, hair, glasses, body, and wardrobe. Keep the person and clothes fully preserved. Start frame is composition, pose, place, and camera only. Do not copy facial drift from the start frame.'
|
||||
@@ -50,8 +51,10 @@ export function validateRequest(raw, xaigen = false) {
|
||||
const qwen = engine === 'qwen21'
|
||||
const defaultSteps = video ? 8 : qwen ? 25 : 24
|
||||
const defaultCfg = video ? 1.5 : qwen ? 1 : 4
|
||||
const defaultWidth = qwen ? 1024 : 1344
|
||||
const defaultHeight = qwen ? 1024 : 768
|
||||
const aspect = QWEN21_ASPECT_IDS.has(raw.settings?.aspect) ? raw.settings.aspect : 'auto'
|
||||
const qwenSize = qwen && mode === 'generate' ? resolveQwen21Size(aspect) : null
|
||||
const defaultWidth = qwenSize ? qwenSize.width : qwen ? 1024 : 1344
|
||||
const defaultHeight = qwenSize ? qwenSize.height : qwen ? 1024 : 768
|
||||
for (const key of ['steps']) if (!Number.isInteger(number(key,key==='steps'?defaultSteps:1,0,2147483647))) fail(`Invalid ${key}.`)
|
||||
for (const key of ['width','height']) if (number(key,key==='width'?defaultWidth:defaultHeight,256,4096)%32) fail('Dimensions must be multiples of 32.')
|
||||
const duration=number('duration',5,1,15)
|
||||
@@ -62,10 +65,12 @@ export function validateRequest(raw, xaigen = false) {
|
||||
const frames=Math.ceil((duration*number('fps',24,1,60)-5)/17)*17+5
|
||||
const shotSeeds=queueSeeds(raw.settings,1+(Array.isArray(raw.batch)?raw.batch.length:0))
|
||||
const enhancePrompt = qwen && !video && !!raw.enhancePrompt
|
||||
const width = qwenSize ? qwenSize.width : number('width',defaultWidth,256,4096)
|
||||
const height = qwenSize ? qwenSize.height : number('height',defaultHeight,256,4096)
|
||||
return { mode, engine, shotSeeds, imageStyles:video?{positive:[],negative:[]}:normalizeStyles(raw.imageStyles), hideOutput:!!raw.hideOutput, enhancePrompt, lockFace:qwen ? false : raw.lockFace !== false, lockOutfit:qwen ? false : raw.lockOutfit !== false, hideInput:!!raw.hideInput, folderId:String(raw.folderId), identityStillId:qwen ? '' : (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,engine,lockFace:qwen?false:raw.lockFace,lockOutfit:qwen?false:raw.lockOutfit}),
|
||||
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',defaultWidth,256,4096),height:number('height',defaultHeight,256,4096),steps:number('steps',defaultSteps,1,100),cfg:number('cfg',defaultCfg,0,20),seedMode:raw.settings?.seedMode==='locked'?'locked':'random',seed:shotSeeds[0],duration:number('duration',5,1,15),turbo:raw.settings?.turbo !== false, loraStack:qwen?[]:(Array.isArray(raw.settings?.loraStack)?raw.settings.loraStack:[]) },
|
||||
settings: { aspect, fps:number('fps',24,1,60), width, height, steps:number('steps',defaultSteps,1,100),cfg:number('cfg',defaultCfg,0,20),seedMode:raw.settings?.seedMode==='locked'?'locked':'random',seed:shotSeeds[0],duration:number('duration',5,1,15),turbo:raw.settings?.turbo !== false, loraStack:qwen?[]:(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 video?next:{action:next.action}}) }
|
||||
}
|
||||
|
||||
+21
-14
@@ -1,18 +1,22 @@
|
||||
import {restoreLoras,loraUse} from './lora-memory.mjs'
|
||||
import { restoreInput, sectionsFrom } from './contracts.mjs'
|
||||
import { normalizeStyles } from './styles.mjs'
|
||||
export const aspectOptions = [{id:'auto',label:'Automatic',hint:'Match start or hero still'},{id:'16:9',label:'16:9',hint:'1344 × 768 · padded'},{id:'9:16',label:'9:16',hint:'768 × 1344 · padded'},{id:'1:1',label:'1:1',hint:'960 × 960 · padded'}]
|
||||
export function resolveQwen21Size(aspect, source) {
|
||||
const presets = {'16:9':[1024,576],'9:16':[576,1024],'1:1':[1024,1024]}
|
||||
if (presets[aspect]) return {width:presets[aspect][0],height:presets[aspect][1]}
|
||||
if (source?.width && source?.height) {
|
||||
const ratio = source.width / source.height
|
||||
const width = Math.max(32, Math.round(Math.sqrt(1024 * 1024 * ratio) / 32) * 32)
|
||||
const height = Math.max(32, Math.round(Math.sqrt(1024 * 1024 / ratio) / 32) * 32)
|
||||
return {width, height}
|
||||
}
|
||||
return {width:1024,height:1024}
|
||||
}
|
||||
import { QWEN21_ASPECT_IDS as ASPECT_IDS, resolveQwen21Size } from './qwen21-size.mjs'
|
||||
export { resolveQwen21Size, QWEN21_SIZE_TABLE } from './qwen21-size.mjs'
|
||||
|
||||
export const aspectOptions = [
|
||||
{id:'auto',label:'Automatic',hint:'Match start or hero still'},
|
||||
{id:'16:9',label:'16:9',hint:'Widescreen'},
|
||||
{id:'9:16',label:'9:16',hint:'Tall'},
|
||||
{id:'1:1',label:'1:1',hint:'Square'},
|
||||
{id:'4:3',label:'4:3',hint:'Standard'},
|
||||
{id:'3:4',label:'3:4',hint:'Portrait standard'},
|
||||
{id:'3:2',label:'3:2',hint:'Photo'},
|
||||
{id:'2:3',label:'2:3',hint:'Portrait photo'},
|
||||
{id:'21:9',label:'21:9',hint:'Ultrawide'},
|
||||
{id:'9:21',label:'9:21',hint:'Ultra-tall'}
|
||||
]
|
||||
|
||||
export function resolveSize(aspect, source, fallback = {width:1344,height:768}, engine = '') {
|
||||
if (engine === 'qwen21') return resolveQwen21Size(aspect, source)
|
||||
const presets = {'16:9':[1344,768],'9:16':[768,1344],'1:1':[960,960]}
|
||||
@@ -21,12 +25,15 @@ 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 { ASPECT_IDS }
|
||||
|
||||
export function bindClip(form, clip, xaigen = false, memory) {
|
||||
if (!clip?.id) return form
|
||||
const saved = clip.studio2 || clip.job || {}
|
||||
const settings = saved.settings || clip.settings || {}
|
||||
const next = {...form, startClipId:clip.id, mode:'extend', settings:{...form.settings}}
|
||||
next.settings.aspect=['auto','16:9','9:16','1:1'].includes(settings.aspect)?settings.aspect:(form.settings?.aspect || 'auto')
|
||||
next.settings.aspect=ASPECT_IDS.has(settings.aspect)?settings.aspect:(form.settings?.aspect || 'auto')
|
||||
for (const key of ['duration','width','height','fps','loraStack','turbo','steps','cfg']) {
|
||||
if (settings[key] == null) continue
|
||||
try { next.settings[key] = typeof structuredClone === 'function' ? structuredClone(settings[key]) : JSON.parse(JSON.stringify(settings[key])) }
|
||||
@@ -96,7 +103,7 @@ export function hydrateStudio2Job(form, saved, { stillIds = new Set(), clipIds =
|
||||
if (['video','extend'].includes(mode)) engine = xaigen && engine === 'ltx' ? 'ltx' : 'minimax'
|
||||
else if (engine === 'minimax' || engine === 'ltx') engine = 'flux'
|
||||
const settings = structuredClone(src.settings || {})
|
||||
const aspect = ['auto','16:9','9:16','1:1'].includes(settings.aspect) ? settings.aspect : 'auto'
|
||||
const aspect = ASPECT_IDS.has(settings.aspect) ? settings.aspect : 'auto'
|
||||
const loraStack = Array.isArray(settings.loraStack) ? settings.loraStack.map(item => ({
|
||||
name: item.name,
|
||||
strengthModel: Number(item.strengthModel ?? item.strength ?? 1),
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/** Qwen-safe ~1 MP grid (÷32). Do not use native 2K bins on 16 GB. */
|
||||
export const QWEN21_SIZE_TABLE = {
|
||||
'1:1': [1024, 1024],
|
||||
'16:9': [1536, 864],
|
||||
'9:16': [864, 1536],
|
||||
'4:3': [1152, 864],
|
||||
'3:4': [864, 1152],
|
||||
'3:2': [1248, 832],
|
||||
'2:3': [832, 1248],
|
||||
'21:9': [1536, 672],
|
||||
'9:21': [672, 1536]
|
||||
}
|
||||
|
||||
export const QWEN21_ASPECT_IDS = new Set(['auto', ...Object.keys(QWEN21_SIZE_TABLE)])
|
||||
|
||||
function parseAspectRatio(aspect) {
|
||||
const m = String(aspect || '').trim().match(/^(\d+(?:\.\d+)?)\s*:\s*(\d+(?:\.\d+)?)$/)
|
||||
if (!m) return null
|
||||
const w = Number(m[1]), h = Number(m[2])
|
||||
if (!(w > 0 && h > 0)) return null
|
||||
return w / h
|
||||
}
|
||||
|
||||
function nearestQwen21Aspect(aspect) {
|
||||
const target = parseAspectRatio(aspect)
|
||||
if (target == null) return null
|
||||
let best = '1:1', bestDiff = Infinity
|
||||
for (const [key, [w, h]] of Object.entries(QWEN21_SIZE_TABLE)) {
|
||||
const diff = Math.abs(w / h - target)
|
||||
if (diff < bestDiff) { bestDiff = diff; best = key }
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
export function resolveQwen21Size(aspect, source) {
|
||||
const key = String(aspect || 'auto').trim()
|
||||
if (QWEN21_SIZE_TABLE[key]) {
|
||||
const [width, height] = QWEN21_SIZE_TABLE[key]
|
||||
return { width, height }
|
||||
}
|
||||
if (key === 'auto') {
|
||||
if (source?.width && source?.height) {
|
||||
const nearest = nearestQwen21Aspect(`${source.width}:${source.height}`)
|
||||
if (nearest) {
|
||||
const [width, height] = QWEN21_SIZE_TABLE[nearest]
|
||||
return { width, height }
|
||||
}
|
||||
}
|
||||
return { width: 1024, height: 1024 }
|
||||
}
|
||||
const nearest = nearestQwen21Aspect(key)
|
||||
if (nearest) {
|
||||
const [width, height] = QWEN21_SIZE_TABLE[nearest]
|
||||
return { width, height }
|
||||
}
|
||||
console.warn(`[qwen21] unknown aspect "${key}"; using 1024×1024`)
|
||||
return { width: 1024, height: 1024 }
|
||||
}
|
||||
Reference in New Issue
Block a user