diff --git a/docs/qwen21.md b/docs/qwen21.md index 1459cf0..a096ac7 100644 --- a/docs/qwen21.md +++ b/docs/qwen21.md @@ -42,7 +42,7 @@ Restart Comfy **only when idle** (`COMFY_CONTROL_URL/status` → `gpu.busy=false - 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. - 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 `` token, stitch `Keep the identity…` onto the rewrite before Edit. Never fall back to PE-T2I on Edit. + - Edit + Enhance → PE-I2I only (`pe_i2i`) with the same Start still on `image_1`. After PE, every Edit rewrite is forced to contain `` (PE used to ban tags for N=1). If the rewrite still drops every `` token, stitch `Keep the identity…` onto the rewrite before Edit. Never fall back to PE-T2I on Edit. - If PE refuses or returns an empty/gutted rewrite, the job continues with `promptRaw` (Edit still gets the keep-identity prefix). Details shows `Enhance skipped (model refused) — used your prompt.` (`enhance.refused`). - Host PE system prompts live in `host/qwen21-pe-prompts/` and are copied onto the node pack by `scripts/setup-qwen21.ps1` (official steps + Adult appendix). Do not swap Heretic PE weights on 16 GB. - 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`. diff --git a/host/qwen21-pe-prompts/system_prompt_edit.txt b/host/qwen21-pe-prompts/system_prompt_edit.txt index 338c0ba..65b9626 100644 --- a/host/qwen21-pe-prompts/system_prompt_edit.txt +++ b/host/qwen21-pe-prompts/system_prompt_edit.txt @@ -56,7 +56,7 @@ Before emitting JSON, reason through: what the image(s) actually contain (includ ## Image Reference Rules -For Multi-Image Input (N >= 2), the rewritten instruction MUST use ``, ``, ... to refer to each input image. Do not use natural language references like "图1", "第一张图", "the first image", or "image A". This tagging format is mandatory and non-negotiable. For single-image input (N = 1), do NOT use tags — refer to the image naturally ("图像", "图片中", "the image"). +For every input, the rewritten instruction MUST use , , … to refer to each input image. Single-image edits still use . Never write "the photo", "the image", "the woman in the picture" as a substitute for the tag. The first Start still is always . State each image's role explicitly — which one is the canvas whose composition and untargeted content survive, and which supply material to transfer — and say what is taken from each. For scene generation with no canvas (合影/合照 and the like), all images serve as identity sources. Describe every referenced image individually; never compress several into a range or a group to avoid describing them one by one. diff --git a/shared/studio2/qwen21-edit.mjs b/shared/studio2/qwen21-edit.mjs index 6bf5222..dc3c30b 100644 --- a/shared/studio2/qwen21-edit.mjs +++ b/shared/studio2/qwen21-edit.mjs @@ -15,12 +15,19 @@ export function ensureQwen21EditPrompt(prompt) { } /** - * After PE-I2I: if the rewrite dropped every token, stitch the - * keep-identity line onto the rewrite (do not discard the rewrite). + * After PE-I2I: official PE told N=1 not to use tags, so rewrites often say + * "the photo" / "the image" with zero . Qwen Edit only binds Start still + * when the text contains the tag — force it onto every Edit rewrite. */ export function stitchQwen21PeEditPrompt(rewritten) { - const text = String(rewritten || '').trim() + let text = String(rewritten || '').trim() if (!text) return text - if (//i.test(text)) return text + // Prefer the tag over natural substitutes (do not invent ). + text = text + .replace(/\bthe woman in the (?:input )?(?:photo|picture|image)\b/gi, 'the person in ') + .replace(/\bthe (?:person|man|subject) in the (?:input )?(?:photo|picture|image)\b/gi, 'the person in ') + .replace(/\bin the (?:input )?(?:photo|picture|image)\b/gi, 'in ') + .replace(/\bthe (?:input )?(?:photo|picture|image)\b/gi, '') + if (//i.test(text)) return text return `${QWEN21_EDIT_KEEP} ${text}`.trim() } diff --git a/tests/studio2-bench.test.mjs b/tests/studio2-bench.test.mjs index 8e2069b..1ebfe0d 100644 --- a/tests/studio2-bench.test.mjs +++ b/tests/studio2-bench.test.mjs @@ -108,6 +108,11 @@ test('qwen21 supports Generate and Edit with 25/1 defaults and no locks',()=>{ assert.equal(ensureQwen21EditPrompt('change outfit'),'change outfit') assert.match(stitchQwen21PeEditPrompt('a woman in a red jacket'),//) assert.equal(stitchQwen21PeEditPrompt('keep add jacket'),'keep add jacket') + assert.match(stitchQwen21PeEditPrompt('change the jacket on the woman in the photo'),//) + assert.doesNotMatch(stitchQwen21PeEditPrompt('change the jacket on the woman in the photo'),/\bthe photo\b/i) + assert.match(readFileSync(new URL('../host/qwen21-pe-prompts/system_prompt_edit.txt',import.meta.url),'utf8'),/Single-image edits still use /) + assert.doesNotMatch(readFileSync(new URL('../host/qwen21-pe-prompts/system_prompt_edit.txt',import.meta.url),'utf8'),/do NOT use tags/) + assert.doesNotMatch(readFileSync(new URL('../host/qwen21-pe-prompts/system_prompt_t2i.txt',import.meta.url),'utf8'),/Single-image edits still use /) assert.equal(qwen21PeRefusal('corgi in the rain',{positive_prompt:'A photorealistic photograph of a corgi standing in rain…',parse_ok:true}).refused,false) assert.equal(qwen21PeRefusal('nude adult portrait',{positive_prompt:"I cannot assist with that request.",thinking:'',parse_ok:true}).refused,true) assert.equal(qwen21PeRefusal('put her in a red leather jacket',{positive_prompt:'',parse_ok:false}).refused,true)