Enable Qwen 2.1 on Edit: wire picker, runner, and source-still validation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-09-20 18:13:07 -05:00
co-authored by Cursor
parent 8609ba24a4
commit 52a01e0adc
6 changed files with 98 additions and 36 deletions
+61 -22
View File
@@ -8,7 +8,8 @@ import { queueSeeds } from '~/shared/studio2/seed.mjs';
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs';
import { join } from 'node:path';
import template from '../../assets/studio2_minimax_native.json';
import qwen21Template from '../../assets/studio2_qwen21_t2i.json';
import qwen21Template from '../../assets/studio2_qwen21_t2i.json'
import qwen21EditTemplate from '../../assets/studio2_qwen21_edit.json';
import { nativeVideoGraph, attachHeroReference, applyResolvedImageSize } from '~/shared/studio2/graphs.mjs';
import { compilePrompt, scopedFile } from '~/shared/studio2/contracts.mjs';
import { resolveQwen21Size } from '~/shared/studio2/input.mjs';
@@ -39,7 +40,7 @@ const NEVER_FIT = new Set(['hero', 'start']);
async function upload(r: any, name: string, data: Buffer): Promise<string> {
const prefix = String(useRuntimeConfig().comfyFilenamePrefix).replace(/\/$/, '') + `/studio2/${r.id}/${r.index}`;
let fitted = false;
if (!NEVER_FIT.has(name) && needsFit(data, r.request.settings.width, r.request.settings.height)) {
if (!NEVER_FIT.has(name) && r.request?.engine !== 'qwen21' && needsFit(data, r.request.settings.width, r.request.settings.height)) {
data = await fitStill(data, r.request.settings.width, r.request.settings.height);
fitted = true;
}
@@ -129,26 +130,64 @@ async function prepareGraph(r: any) {
}
}
else if (q.engine === 'qwen21') {
if (q.mode !== 'generate')
throw new Error('Qwen 2.1 is T2I in this build.');
const size = resolveQwen21Size(s.aspect);
s.width = size.width;
s.height = size.height;
graph = structuredClone(qwen21Template);
graph['9'].inputs.prompt = q.compiledPrompt;
graph['9'].inputs.negative_prompt = stylePrompt(q.imageStyles, true);
// TextEncodeQwenImage21 builds the 64-ch empty latent from resolution (square T2I).
graph['9'].inputs.resolution = Math.max(size.width, size.height);
graph['15'].inputs.seed = s.seed;
graph['15'].inputs.steps = s.steps || 25;
graph['15'].inputs.cfg = s.cfg ?? 1;
graph['15'].inputs.sampler_name = 'euler';
graph['15'].inputs.scheduler = 'simple';
graph['21'].inputs.filename_prefix = prefix + '/image';
r.sampleLatent = 'qwen21 textencode latent';
r.sampleDenoise = null;
r.heroReferenceAttached = false;
r.graphId = 'studio2_qwen21_t2i.json';
if (!['generate', 'edit'].includes(q.mode))
throw new Error('Qwen 2.1 supports Generate and Edit.')
if (q.mode === 'edit') {
if (!a) throw new Error('Choose the source still.')
let prompt = String(q.compiledPrompt || '')
if (!/<image1>/i.test(prompt))
prompt = `Keep the subject in <image1>. ${prompt}`.trim()
graph = structuredClone(qwen21EditTemplate)
graph['10'].inputs.image = a
if (b) {
graph['11'] = {
inputs: { image: b },
class_type: 'LoadImage',
_meta: { title: 'image_2 optional' }
}
graph['9'].inputs['images.image_2'] = ['11', 0]
}
graph['9'].inputs.prompt = prompt
graph['9'].inputs.negative_prompt = stylePrompt(q.imageStyles, true)
graph['9'].inputs.resolution = 1024
if (s.aspect && s.aspect !== 'auto') {
graph['16'] = {
inputs: { width: s.width, height: s.height, batch_size: 1 },
class_type: 'EmptyLatentImage',
_meta: { title: 'Qwen edit canvas' }
}
graph['15'].inputs.latent_image = ['16', 0]
}
graph['15'].inputs.seed = s.seed
graph['15'].inputs.steps = s.steps || 25
graph['15'].inputs.cfg = s.cfg ?? 1
graph['15'].inputs.sampler_name = 'euler'
graph['15'].inputs.scheduler = 'simple'
graph['21'].inputs.filename_prefix = prefix + '/image'
r.sampleLatent = s.aspect && s.aspect !== 'auto' ? 'qwen21 empty latent' : 'qwen21 edit latent'
r.sampleDenoise = null
r.heroReferenceAttached = false
r.graphId = 'studio2_qwen21_edit.json'
} else {
const size = resolveQwen21Size(s.aspect)
s.width = size.width
s.height = size.height
graph = structuredClone(qwen21Template)
graph['9'].inputs.prompt = q.compiledPrompt
graph['9'].inputs.negative_prompt = stylePrompt(q.imageStyles, true)
// TextEncodeQwenImage21 builds the 64-ch empty latent from resolution (square T2I).
graph['9'].inputs.resolution = Math.max(size.width, size.height)
graph['15'].inputs.seed = s.seed
graph['15'].inputs.steps = s.steps || 25
graph['15'].inputs.cfg = s.cfg ?? 1
graph['15'].inputs.sampler_name = 'euler'
graph['15'].inputs.scheduler = 'simple'
graph['21'].inputs.filename_prefix = prefix + '/image'
r.sampleLatent = 'qwen21 textencode latent'
r.sampleDenoise = null
r.heroReferenceAttached = false
r.graphId = 'studio2_qwen21_t2i.json'
}
}
else {
const mode = identityPlan?.mode ?? (q.mode === 'iterate' ? (a ? 'edit' : 'generate') : q.mode);