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:
Towsty
2026-09-26 10:54:18 -05:00
co-authored by Cursor
parent 2842855c7b
commit 95873744cd
7 changed files with 119 additions and 32 deletions
+8 -3
View File
@@ -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
View File
@@ -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),
+58
View File
@@ -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 }
}