Files
aigen/shared/studio2/qwen21-edit.mjs
T
TowstyandCursor 50319e03a9 Map Photos roles to Qwen image_1/image_2 and inject encode tags silently.
Mention phrases become <imageN> only in the TextEncode string; the inspector never shows Comfy tokens.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-26 16:01:49 -05:00

66 lines
2.9 KiB
JavaScript

/** Qwen Edit / PE-I2I prompt helpers. Start still = <image1>; Hero is never a slot. */
export const QWEN21_EDIT_KEEP =
'Keep the identity, face, body, pose, and likeness of the person in <image1>.\nDo not invent a different person.'
export const QWEN21_EDIT_KEEP_CHANGE =
`${QWEN21_EDIT_KEEP}\nApply only this change:`
const EDIT_VERB =
/\b(?:change|replace|put|swap|keep|remove|add|edit|make|turn|convert|transform|modify|update|set|apply|wear|place|swap|switch)\b/i
const DESCRIBE_LEAD =
/^(?:the image is\b|this is a photograph of\b|a vertical photorealistic\b|a photorealistic\b|this (?:image|photo|picture) (?:shows|depicts|features)\b|the (?:photo|picture) (?:shows|depicts|features)\b)/i
/** Fixed identity stanza + the user's raw edit instruction. Always includes <image1>. */
export function buildQwen21EditKeepStanza(rawInstruction) {
const raw = String(rawInstruction || '').trim()
if (!raw) return QWEN21_EDIT_KEEP.trim()
return `${QWEN21_EDIT_KEEP_CHANGE} ${raw}`.trim()
}
/** Enhance off / refusal: ensure the sample prompt is the keep stanza, never PE-only. */
export function ensureQwen21EditPrompt(prompt, options = {}) {
const text = String(prompt || '').trim()
if (!text) return text
let next = /Keep the identity, face, body, pose, and likeness of the person in <image1>/i.test(text)
? text
: buildQwen21EditKeepStanza(text)
if (options.hasImage2 && !/<image2>/i.test(next)) {
next = `${next}\nUse the garment or object in <image2>.`.trim()
}
return next
}
/** PE rewrite looks like a T2I observer caption, not an edit directive. */
export function isQwen21PeEditDescribe(rewritten) {
const text = String(rewritten || '').trim()
if (!text) return true
if (DESCRIBE_LEAD.test(text)) return true
if (!EDIT_VERB.test(text)) return true
return false
}
function retagNaturalImageRefs(text) {
return String(text || '')
.replace(/\bthe woman in the (?:input )?(?:photo|picture|image)\b/gi, 'the person in <image1>')
.replace(/\bthe (?:person|man|subject) in the (?:input )?(?:photo|picture|image)\b/gi, 'the person in <image1>')
.replace(/\bin the (?:input )?(?:photo|picture|image)\b/gi, 'in <image1>')
.replace(/\bthe (?:input )?(?:photo|picture|image)\b/gi, '<image1>')
}
/**
* After PE-I2I: never let the PE chunk be the entire prompt.
* Always lead with the keep/<image1> stanza + raw instruction; append PE only when it is an edit directive.
*/
export function stitchQwen21PeEditPrompt(rawInstruction, rewritten) {
const stanza = buildQwen21EditKeepStanza(rawInstruction)
let pe = retagNaturalImageRefs(String(rewritten || '').trim()).trim()
if (!pe) return { prompt: stanza, skippedAsDescribe: false }
if (isQwen21PeEditDescribe(pe)) {
return { prompt: stanza, skippedAsDescribe: true }
}
// No <image1> in the PE chunk is fine — stanza already has the tag; keep PE as extra detail.
return { prompt: `${stanza}\n\n${pe}`.trim(), skippedAsDescribe: false }
}