Wire Qwen Edit Start still to image_1 and PE-I2I identity keep.

Edit no longer invents strangers when the prompt omits <image1> or PE drops image tokens; Hero stays Klein-only.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-09-26 14:27:36 -05:00
co-authored by Cursor
parent ecc4c69a8b
commit 30be4bd5b0
6 changed files with 74 additions and 15 deletions
+3 -3
View File
@@ -109,7 +109,7 @@
<label class="inline-flex items-center gap-2"><input v-model="form.lockOutfit" type="checkbox" class="accent-[var(--amber)]">Lock outfit</label> <label class="inline-flex items-center gap-2"><input v-model="form.lockOutfit" type="checkbox" class="accent-[var(--amber)]">Lock outfit</label>
<p class="text-[11px]">hero encode + lock text — not a MiniMax face/clothes split on stills</p> <p class="text-[11px]">hero encode + lock text — not a MiniMax face/clothes split on stills</p>
</div> </div>
<p v-else-if="form.mode==='edit'" class="text-[var(--type-sm)] text-[var(--muted)]">Qwen edit uses source as &lt;image1&gt;, not Klein hero-ref. No locks or LoRA stack.</p> <p v-else-if="form.mode==='edit'" class="text-[var(--type-sm)] text-[var(--muted)]">Qwen Edit uses Start still as &lt;image1&gt;. Hero is Klein-only and is ignored.</p>
<p v-else class="text-[var(--type-sm)] text-[var(--muted)]">Qwen 2.1 — no hero, locks, or LoRA stack.</p> <p v-else class="text-[var(--type-sm)] text-[var(--muted)]">Qwen 2.1 — no hero, locks, or LoRA stack.</p>
<label> <label>
@@ -521,7 +521,7 @@ const editSourceRequired=computed(()=>['edit','compose','refine'].includes(form.
const heroSlotLabel=computed(()=>editSourceRequired.value?'Identity / reference':'Hero still') const heroSlotLabel=computed(()=>editSourceRequired.value?'Identity / reference':'Hero still')
const sourceSlotLabel=computed(()=>{ const sourceSlotLabel=computed(()=>{
if(form.mode==='extend')return 'Previous clip' if(form.mode==='extend')return 'Previous clip'
if(editSourceRequired.value)return 'Edit source' if(editSourceRequired.value)return form.engine==='qwen21'?'Start still':'Edit source'
return 'Start still' return 'Start still'
}) })
const slotPairHint=computed(()=>{ const slotPairHint=computed(()=>{
@@ -529,7 +529,7 @@ const slotPairHint=computed(()=>{
? 'Krea Refine: Edit source is the picture; Mask is the refine region.' ? 'Krea Refine: Edit source is the picture; Mask is the refine region.'
: 'Refine: Edit source is the picture; Mask marks the region to change.' : 'Refine: Edit source is the picture; Mask marks the region to change.'
if(!editSourceRequired.value)return '' if(!editSourceRequired.value)return ''
if(form.engine==='qwen21')return 'Qwen: Edit source is <image1>. Hero is unused unless the graph maps it to <image2>.' if(form.engine==='qwen21')return 'Qwen Edit uses Start still as <image1>. Hero is Klein-only and is ignored.'
return 'Klein: Hero = identity encode. Edit source = the picture you are changing.' return 'Klein: Hero = identity encode. Edit source = the picture you are changing.'
}) })
const queueCards=computed(()=>{ const queueCards=computed(()=>{
+3 -1
View File
@@ -35,12 +35,14 @@ Restart Comfy **only when idle** (`COMFY_CONTROL_URL/status` → `gpu.busy=false
- Engine key: `qwen21` · UI label: **Qwen 2.1** - Engine key: `qwen21` · UI label: **Qwen 2.1**
- **Generate** (T2I) and **Edit** (same checkpoint, second graph). Compose / Iterate / Video / Extend / Music stay disabled unless a graph exists. - **Generate** (T2I) and **Edit** (same checkpoint, second graph). Compose / Iterate / Video / Extend / Music stay disabled unless a graph exists.
- Edit requires Start still → `images.image_1`; prompt slots use `<image1>` / `<image2>`. If the user omits `<image1>`, the runner prepends `Keep the subject in <image1>.` - Edit requires Start still → `images.image_1`; prompt slots use `<image1>` / `<image2>`. If the user omits `<image1>`, the runner prepends `Keep the identity, face, body, and pose of the person in <image1>. Apply only this change:` then their text. Hero is Klein-only and is ignored (never image_1).
- Face / outfit locks stay Klein semantics — they do not drive Qwen slots. - Face / outfit locks stay Klein semantics — they do not drive Qwen slots.
- Sampler defaults: euler / simple / cfg **1** / steps **25**. Edit uses `QwenImage21Cache` (device auto, dtype int8). T2I uses `ModelSamplingAuraFlow` shift **3.1**. - Sampler defaults: euler / simple / cfg **1** / steps **25**. Edit uses `QwenImage21Cache` (device auto, dtype int8). T2I uses `ModelSamplingAuraFlow` shift **3.1**.
- Default Generate canvas follows the bench Aspect control on the Qwen-safe ~1 MP grid (`EmptyLatentImage`): 1:1 → 1024×1024, 16:9 → 1536×864, 9:16 → 864×1536 (and the other table rows). PE `wh_ratio` is advisory only and never sizes the canvas. Do not use native 2K bins on 16 GB. - Default Generate canvas follows the bench Aspect control on the Qwen-safe ~1 MP grid (`EmptyLatentImage`): 1:1 → 1024×1024, 16:9 → 1536×864, 9:16 → 864×1536 (and the other table rows). PE `wh_ratio` is advisory only and never sizes the canvas. Do not use native 2K bins on 16 GB.
- Edit follows `image_1` via the encode node’s latent (resolution long-edge ~1024). Do not inject a picker EmptyLatentImage onto the edit sampler. - Edit follows `image_1` via the encode node’s latent (resolution long-edge ~1024). Do not inject a picker EmptyLatentImage onto the edit sampler.
- Optional **Enhance prompt** (off by default): runs a separate PE-only Comfy graph (`CLIPLoader` + rewrite node), then frees VRAM and queues the existing T2I/Edit graph with the rewritten prompt. Never loads PE CLIP + DiT together on 16 GB. Fail closed if `parse_ok` is false or the rewrite is empty. - Optional **Enhance prompt** (off by default): runs a separate PE-only Comfy graph (`CLIPLoader` + rewrite node), then frees VRAM and queues the existing T2I/Edit graph with the rewritten prompt. Never loads PE CLIP + DiT together on 16 GB. Fail closed if `parse_ok` is false or the rewrite is empty.
- Generate + Enhance → PE-T2I only (`pe_t2i`). No images.
- Edit + Enhance → PE-I2I only (`pe_i2i`) with the same Start still on `image_1`. If the rewrite drops every `<imageN>` token, stitch `Keep the identity…` onto the rewrite before Edit. Never fall back to PE-T2I on Edit.
- PE weights (int8 only): `text_encoders\qwen3.5_9b_qwen_image_2.1_pe_{t2i,i2i}.int8_convrot.safetensors` — do not replace the image TE `qwen3vl_8b_int8_convrot.safetensors`. - PE weights (int8 only): `text_encoders\qwen3.5_9b_qwen_image_2.1_pe_{t2i,i2i}.int8_convrot.safetensors` — do not replace the image TE `qwen3vl_8b_int8_convrot.safetensors`.
- Optional **Turbo** (off by default): Viggle DMD LoRA `Qwen-Image-2.1-viggle-turbo-v0.2.1-6step-lora-r128.safetensors` on the same Q8 GGUF via `ViggleTurboLora` + `ViggleTurboSigmas` (`1.0, 0.9375, 0.875, 0.75, 0.5, 0.25`), 6 steps, CFG 1, empty negative. Not a new engine. Enhance prompt stays compatible and is recommended with Turbo. - Optional **Turbo** (off by default): Viggle DMD LoRA `Qwen-Image-2.1-viggle-turbo-v0.2.1-6step-lora-r128.safetensors` on the same Q8 GGUF via `ViggleTurboLora` + `ViggleTurboSigmas` (`1.0, 0.9375, 0.875, 0.75, 0.5, 0.25`), 6 steps, CFG 1, empty negative. Not a new engine. Enhance prompt stays compatible and is recommended with Turbo.
- Custom node: `ComfyUI-Viggle-Turbo` (`viggle_turbo.py`). Do not merge the LoRA with stock `LoraLoaderModelOnly` (lossy on int8/bf16). - Custom node: `ComfyUI-Viggle-Turbo` (`viggle_turbo.py`). Do not merge the LoRA with stock `LoraLoaderModelOnly` (lossy on int8/bf16).
+19 -9
View File
@@ -17,6 +17,7 @@ import qwen21PeEditTemplate from '../../assets/studio2_qwen21_pe_edit.json';
import { nativeVideoGraph, attachHeroReference, applyResolvedImageSize } from '~/shared/studio2/graphs.mjs'; import { nativeVideoGraph, attachHeroReference, applyResolvedImageSize } from '~/shared/studio2/graphs.mjs';
import { compilePrompt, scopedFile } from '~/shared/studio2/contracts.mjs'; import { compilePrompt, scopedFile } from '~/shared/studio2/contracts.mjs';
import { resolveQwen21Size } from '~/shared/studio2/qwen21-size.mjs'; import { resolveQwen21Size } from '~/shared/studio2/qwen21-size.mjs';
import { ensureQwen21EditPrompt, stitchQwen21PeEditPrompt } from '~/shared/studio2/qwen21-edit.mjs';
import { QWEN21_TURBO_LORA, QWEN21_TURBO_SIGMAS, QWEN21_TURBO_STEPS } from '~/shared/studio2/qwen21-turbo.mjs'; import { QWEN21_TURBO_LORA, QWEN21_TURBO_SIGMAS, QWEN21_TURBO_STEPS } from '~/shared/studio2/qwen21-turbo.mjs';
import { createJob, restoreJob, getJob, emitJob, type Job } from '../jobs'; import { createJob, restoreJob, getJob, emitJob, type Job } from '../jobs';
import { markStudioLive, onLiveVideoSettled, type StudioJob } from '../studioQueue'; import { markStudioLive, onLiveVideoSettled, type StudioJob } from '../studioQueue';
@@ -98,16 +99,18 @@ async function runQwen21PromptEnhance(r: any, job: Job) {
const q = r.request, s = q.settings const q = r.request, s = q.settings
if (!q.enhancePrompt || q.engine !== 'qwen21' || !['generate', 'edit'].includes(q.mode)) return if (!q.enhancePrompt || q.engine !== 'qwen21' || !['generate', 'edit'].includes(q.mode)) return
const edit = q.mode === 'edit' const edit = q.mode === 'edit'
if (edit && !q.imageAId) throw new Error('Choose the source still.') // Edit → PE-I2I only (pe_i2i). Generate → PE-T2I only. Never cross-feed.
if (edit && !q.imageAId)
throw new Error('Qwen Edit requires Start still (image_1). Hero is ignored.')
update(r, 'enhancing') update(r, 'enhancing')
emitJob(job, { type: 'status', message: 'Enhancing prompt…' }) emitJob(job, { type: 'status', message: 'Enhancing prompt…' })
let prompt = String(q.compiledPrompt || '') let prompt = String(q.compiledPrompt || '')
if (edit && !/<image1>/i.test(prompt)) if (edit) prompt = ensureQwen21EditPrompt(prompt)
prompt = `Keep the subject in <image1>. ${prompt}`.trim()
const graph = structuredClone(edit ? qwen21PeEditTemplate : qwen21PeT2iTemplate) const graph = structuredClone(edit ? qwen21PeEditTemplate : qwen21PeT2iTemplate)
graph['9'].inputs.prompt = prompt graph['9'].inputs.prompt = prompt
graph['9'].inputs.seed = s.seed graph['9'].inputs.seed = s.seed
if (edit) { if (edit) {
// Same Start still pixels as Edit image_1 — never Hero.
const a = await upload(r, 'pe-source', readFileSync(stillPath(r.owner, getStill(r.owner, q.imageAId).id))) const a = await upload(r, 'pe-source', readFileSync(stillPath(r.owner, getStill(r.owner, q.imageAId).id)))
graph['10'].inputs.image = a graph['10'].inputs.image = a
graph['9'].inputs.image_1 = ['10', 0] graph['9'].inputs.image_1 = ['10', 0]
@@ -131,7 +134,10 @@ async function runQwen21PromptEnhance(r: any, job: Job) {
if (!result.parse_ok || !result.positive_prompt) if (!result.parse_ok || !result.positive_prompt)
throw new Error('Prompt enhance failed to parse a rewrite. Try again or turn Enhance prompt off.') throw new Error('Prompt enhance failed to parse a rewrite. Try again or turn Enhance prompt off.')
r.request.promptRaw = q.compiledPrompt r.request.promptRaw = q.compiledPrompt
r.request.compiledPrompt = result.positive_prompt // PE-I2I: if rewrite dropped every <imageN>, stitch keep-identity onto the rewrite.
r.request.compiledPrompt = edit
? stitchQwen21PeEditPrompt(result.positive_prompt)
: result.positive_prompt
r.request.enhance = { r.request.enhance = {
wh_ratio: result.wh_ratio || '', wh_ratio: result.wh_ratio || '',
...(edit ? { ratio_follow: result.ratio_follow || '' } : {}), ...(edit ? { ratio_follow: result.ratio_follow || '' } : {}),
@@ -172,7 +178,10 @@ async function prepareGraph(r: any) {
const load = async (id: string, label: string) => id ? upload(r, label, readFileSync(stillPath(r.owner, getStill(r.owner, id).id))) : ''; const load = async (id: string, label: string) => id ? upload(r, label, readFileSync(stillPath(r.owner, getStill(r.owner, id).id))) : '';
const video=['video','extend'].includes(q.mode); const video=['video','extend'].includes(q.mode);
const identityPlan=kleinIdentityPlan(q); const identityPlan=kleinIdentityPlan(q);
const sourceId=identityPlan ? identityPlan.sourceId : q.imageAId; // Qwen Edit: Start still (imageAId) only — never Hero / identityStillId.
const sourceId = q.engine === 'qwen21'
? (q.imageAId || '')
: (identityPlan ? identityPlan.sourceId : q.imageAId);
await resolveRequestSize(r.owner,identityPlan ? {...q,imageAId:sourceId} : q); await resolveRequestSize(r.owner,identityPlan ? {...q,imageAId:sourceId} : q);
saveRecord(r); saveRecord(r);
const a=await load(sourceId,'source'),b=await load(q.imageBId,'compose'),mask=await load(q.maskId,'mask'); const a=await load(sourceId,'source'),b=await load(q.imageBId,'compose'),mask=await load(q.maskId,'mask');
@@ -247,12 +256,13 @@ async function prepareGraph(r: any) {
} }
const negative = turbo ? '' : stylePrompt(q.imageStyles, true) const negative = turbo ? '' : stylePrompt(q.imageStyles, true)
if (q.mode === 'edit') { if (q.mode === 'edit') {
if (!a) throw new Error('Choose the source still.') // Start still → image_1 only. Never Hero. Never T2I / EmptyLatent.
let prompt = String(q.compiledPrompt || '') if (!q.imageAId || !a)
if (!/<image1>/i.test(prompt)) throw new Error('Qwen Edit requires Start still (image_1). Hero is ignored.')
prompt = `Keep the subject in <image1>. ${prompt}`.trim() const prompt = ensureQwen21EditPrompt(String(q.compiledPrompt || ''))
graph = structuredClone(turbo ? qwen21EditTurboTemplate : qwen21EditTemplate) graph = structuredClone(turbo ? qwen21EditTurboTemplate : qwen21EditTemplate)
graph['10'].inputs.image = a graph['10'].inputs.image = a
graph['9'].inputs['images.image_1'] = ['10', 0]
if (b) { if (b) {
graph['11'] = { graph['11'] = {
inputs: { image: b }, inputs: { image: b },
+4 -1
View File
@@ -40,7 +40,10 @@ export function validateRequest(raw, xaigen = false) {
if (Array.isArray(raw.batch) && raw.batch.length>30) fail('A batch supports up to 31 shots including the first prompt.') if (Array.isArray(raw.batch) && raw.batch.length>30) fail('A batch supports up to 31 shots including the first prompt.')
if (Array.isArray(raw.guides) && raw.guides.length>8) fail('Use up to eight guide stills.') if (Array.isArray(raw.guides) && raw.guides.length>8) fail('Use up to eight guide stills.')
if (!raw.folderId) fail('Choose a folder.') if (!raw.folderId) fail('Choose a folder.')
if (['edit','compose','refine'].includes(mode) && !raw.imageAId && !(mode==='edit' && engine==='flux' && raw.identityStillId)) fail('Choose the source still.') if (['edit','compose','refine'].includes(mode) && !raw.imageAId && !(mode==='edit' && engine==='flux' && raw.identityStillId)) {
if (engine === 'qwen21') fail('Qwen Edit requires Start still (image_1). Hero is ignored.')
fail('Choose the source still.')
}
if ((video || ['edit','compose','iterate'].includes(mode)) && engine !== 'qwen21' && (raw.lockFace !== false || raw.lockOutfit !== false) && !raw.identityStillId && (video || !raw.imageAId)) fail('Choose a sticky hero still.') if ((video || ['edit','compose','iterate'].includes(mode)) && engine !== 'qwen21' && (raw.lockFace !== false || raw.lockOutfit !== false) && !raw.identityStillId && (video || !raw.imageAId)) fail('Choose a sticky hero still.')
if (mode === 'compose' && !raw.imageBId) fail('Compose requires a second still.') if (mode === 'compose' && !raw.imageBId) fail('Compose requires a second still.')
if (mode === 'refine' && !raw.maskId) fail('Refine requires a mask.') if (mode === 'refine' && !raw.maskId) fail('Refine requires a mask.')
+26
View File
@@ -0,0 +1,26 @@
/** Qwen Edit / PE-I2I prompt helpers. Start still = <image1>; Hero is never a slot. */
export const QWEN21_EDIT_KEEP =
'Keep the identity, face, body, and pose of the person in <image1>.'
export const QWEN21_EDIT_KEEP_CHANGE =
`${QWEN21_EDIT_KEEP} Apply only this change:`
/** User omitted <image1> — prepend the keep-identity instruction once. */
export function ensureQwen21EditPrompt(prompt) {
const text = String(prompt || '').trim()
if (!text) return text
if (/<image1>/i.test(text)) return text
return `${QWEN21_EDIT_KEEP_CHANGE} ${text}`.trim()
}
/**
* After PE-I2I: if the rewrite dropped every <imageN> token, stitch the
* keep-identity line onto the rewrite (do not discard the rewrite).
*/
export function stitchQwen21PeEditPrompt(rewritten) {
const text = String(rewritten || '').trim()
if (!text) return text
if (/<image\d+>/i.test(text)) return text
return `${QWEN21_EDIT_KEEP} ${text}`.trim()
}
+19 -1
View File
@@ -8,6 +8,7 @@ import {toggleStyle,parseBatch,stylePrompt} from '../shared/studio2/styles.mjs'
import {compilePrompt,validateRequest} from '../shared/studio2/contracts.mjs' import {compilePrompt,validateRequest} from '../shared/studio2/contracts.mjs'
import {cachedLoras} from '../shared/studio2/lora-cache.mjs' import {cachedLoras} from '../shared/studio2/lora-cache.mjs'
import {resolveQwen21Size} from '../shared/studio2/qwen21-size.mjs' import {resolveQwen21Size} from '../shared/studio2/qwen21-size.mjs'
import {ensureQwen21EditPrompt,stitchQwen21PeEditPrompt,QWEN21_EDIT_KEEP_CHANGE} from '../shared/studio2/qwen21-edit.mjs'
test('deleted library files never return from job outputs; selection falls back or clears',()=>{ test('deleted library files never return from job outputs; selection falls back or clears',()=>{
const deleted={id:'gone',folderId:'f'},keep={id:'keep',folderId:'f',createdAt:2};const jobs=[{request:{folderId:'f'},outputs:[deleted]}] const deleted={id:'gone',folderId:'f'},keep={id:'keep',folderId:'f',createdAt:2};const jobs=[{request:{folderId:'f'},outputs:[deleted]}]
@@ -98,9 +99,14 @@ test('qwen21 supports Generate and Edit with 25/1 defaults and no locks',()=>{
const edit=validateRequest({mode:'edit',engine:'qwen21',folderId:'f',imageAId:'s',promptSections:{action:'change the sky'},settings:{aspect:'auto'}}) const edit=validateRequest({mode:'edit',engine:'qwen21',folderId:'f',imageAId:'s',promptSections:{action:'change the sky'},settings:{aspect:'auto'}})
assert.equal(edit.mode,'edit');assert.equal(edit.settings.steps,25);assert.equal(edit.settings.cfg,1) assert.equal(edit.mode,'edit');assert.equal(edit.settings.steps,25);assert.equal(edit.settings.cfg,1)
assert.equal(edit.lockFace,false);assert.deepEqual(edit.settings.loraStack,[]) assert.equal(edit.lockFace,false);assert.deepEqual(edit.settings.loraStack,[])
assert.throws(()=>validateRequest({mode:'edit',engine:'qwen21',folderId:'f',promptSections:{action:'x'}}),/source still/) assert.throws(()=>validateRequest({mode:'edit',engine:'qwen21',folderId:'f',promptSections:{action:'x'}}),/Start still/)
assert.throws(()=>validateRequest({mode:'edit',engine:'qwen21',folderId:'f',identityStillId:'hero',promptSections:{action:'x'}}),/Start still/)
assert.throws(()=>validateRequest({mode:'compose',engine:'qwen21',folderId:'f',imageAId:'s',imageBId:'b',promptSections:{action:'x'}}),/Generate and Edit/) assert.throws(()=>validateRequest({mode:'compose',engine:'qwen21',folderId:'f',imageAId:'s',imageBId:'b',promptSections:{action:'x'}}),/Generate and Edit/)
assert.throws(()=>validateRequest({mode:'iterate',engine:'qwen21',folderId:'f',imageAId:'s',promptSections:{action:'x'}}),/Generate and Edit/) assert.throws(()=>validateRequest({mode:'iterate',engine:'qwen21',folderId:'f',imageAId:'s',promptSections:{action:'x'}}),/Generate and Edit/)
assert.equal(ensureQwen21EditPrompt('put her in a red leather jacket'),`${QWEN21_EDIT_KEEP_CHANGE} put her in a red leather jacket`)
assert.equal(ensureQwen21EditPrompt('change <image1> outfit'),'change <image1> outfit')
assert.match(stitchQwen21PeEditPrompt('a woman in a red jacket'),/<image1>/)
assert.equal(stitchQwen21PeEditPrompt('keep <image1> add jacket'),'keep <image1> add jacket')
}) })
test('hydrateStudio2Job restores LoRA strengths, locked seed, and clears missing stills',()=>{ test('hydrateStudio2Job restores LoRA strengths, locked seed, and clears missing stills',()=>{
const form={mode:'generate',engine:'flux',identityStillId:'',imageAId:'',promptSections:{action:''},imageStyles:{positive:[],negative:[]},settings:{aspect:'auto',seed:null,seedMode:'random',loraStack:[]},guides:[]} const form={mode:'generate',engine:'flux',identityStillId:'',imageAId:'',promptSections:{action:''},imageStyles:{positive:[],negative:[]},settings:{aspect:'auto',seed:null,seedMode:'random',loraStack:[]},guides:[]}
@@ -121,12 +127,24 @@ test('custom negative chips survive normalizeStyles and bench exposes Load actio
assert.match(src,/aspect:'auto'/);assert.match(src,/strip-actions/);assert.match(src,/addNegative/) assert.match(src,/aspect:'auto'/);assert.match(src,/strip-actions/);assert.match(src,/addNegative/)
assert.match(src,/Enhance prompt/);assert.match(src,/enhancePrompt/) assert.match(src,/Enhance prompt/);assert.match(src,/enhancePrompt/)
assert.match(src,/\bTurbo\b/);assert.match(src,/form\.turbo/) assert.match(src,/\bTurbo\b/);assert.match(src,/form\.turbo/)
assert.match(src,/Qwen Edit uses Start still as/)
assert.match(src,/Hero is Klein-only and is ignored/)
const peT2i=readFileSync(new URL('../server/assets/studio2_qwen21_pe_t2i.json',import.meta.url),'utf8') const peT2i=readFileSync(new URL('../server/assets/studio2_qwen21_pe_t2i.json',import.meta.url),'utf8')
const peEdit=readFileSync(new URL('../server/assets/studio2_qwen21_pe_edit.json',import.meta.url),'utf8') const peEdit=readFileSync(new URL('../server/assets/studio2_qwen21_pe_edit.json',import.meta.url),'utf8')
assert.match(peT2i,/QwenImage21_T2IPromptRewrite/);assert.match(peT2i,/pe_t2i\.int8_convrot/) assert.match(peT2i,/QwenImage21_T2IPromptRewrite/);assert.match(peT2i,/pe_t2i\.int8_convrot/)
assert.match(peEdit,/QwenImage21_EditPromptRewrite/);assert.match(peEdit,/pe_i2i\.int8_convrot/) assert.match(peEdit,/QwenImage21_EditPromptRewrite/);assert.match(peEdit,/pe_i2i\.int8_convrot/)
assert.doesNotMatch(peT2i,/UnetLoader|KSampler|VAELoader/) assert.doesNotMatch(peT2i,/UnetLoader|KSampler|VAELoader/)
assert.doesNotMatch(peEdit,/UnetLoader|KSampler|VAELoader/) assert.doesNotMatch(peEdit,/UnetLoader|KSampler|VAELoader/)
assert.match(peEdit,/"image_1":\s*\["10",\s*0\]/)
const editGraph=readFileSync(new URL('../server/assets/studio2_qwen21_edit.json',import.meta.url),'utf8')
assert.match(editGraph,/"images\.image_1":\s*\["10",\s*0\]/)
assert.match(editGraph,/"latent_image":\s*\["9",\s*2\]/)
assert.doesNotMatch(editGraph,/EmptyLatentImage/)
const runner=readFileSync(new URL('../server/utils/studio2/runner.ts',import.meta.url),'utf8')
assert.match(runner,/ensureQwen21EditPrompt/)
assert.match(runner,/stitchQwen21PeEditPrompt/)
assert.match(runner,/images\.image_1/)
assert.doesNotMatch(runner,/Keep the subject in <image1>/)
const t2i=readFileSync(new URL('../server/assets/studio2_qwen21_t2i.json',import.meta.url),'utf8') const t2i=readFileSync(new URL('../server/assets/studio2_qwen21_t2i.json',import.meta.url),'utf8')
assert.match(t2i,/EmptyLatentImage/);assert.match(t2i,/"latent_image":\s*\["16",\s*0\]/) assert.match(t2i,/EmptyLatentImage/);assert.match(t2i,/"latent_image":\s*\["16",\s*0\]/)
const turboT2i=readFileSync(new URL('../server/assets/studio2_qwen21_t2i_turbo.json',import.meta.url),'utf8') const turboT2i=readFileSync(new URL('../server/assets/studio2_qwen21_t2i_turbo.json',import.meta.url),'utf8')