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>
This commit is contained in:
Towsty
2026-09-26 16:01:49 -05:00
co-authored by Cursor
parent 5c2248a3fd
commit 50319e03a9
4 changed files with 49 additions and 15 deletions
+34 -11
View File
@@ -18,6 +18,7 @@ import { nativeVideoGraph, attachHeroReference, applyResolvedImageSize } from '~
import { compilePrompt, scopedFile } from '~/shared/studio2/contracts.mjs';
import { resolveQwen21Size } from '~/shared/studio2/qwen21-size.mjs';
import { ensureQwen21EditPrompt, stitchQwen21PeEditPrompt } from '~/shared/studio2/qwen21-edit.mjs';
import { applyPhotosToRequest, injectQwenEditMentionTags } from '~/shared/studio2/photos.mjs';
import { qwen21PeRefusal } from '~/shared/studio2/qwen21-pe.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';
@@ -96,25 +97,38 @@ async function waitPromptHistory(r: any, job: Job, promptId: string) {
return history
}
function finalizeQwen21EditSample(rawPrompt: string, request: any) {
const photos = Array.isArray(request?.photos) ? request.photos : []
const tagged = injectQwenEditMentionTags(String(rawPrompt || ''), photos)
return ensureQwen21EditPrompt(tagged, { hasImage2: !!request?.imageBId })
}
async function runQwen21PromptEnhance(r: any, job: Job) {
const q = r.request, s = q.settings
if (!q.enhancePrompt || q.engine !== 'qwen21' || !['generate', 'edit'].includes(q.mode)) return
const edit = q.mode === 'edit'
if (Array.isArray(q.photos) && q.photos.length) {
const bound = applyPhotosToRequest(q)
q.imageAId = bound.imageAId
q.imageBId = bound.imageBId
q.identityStillId = bound.identityStillId
q.photos = bound.photos
}
// 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.')
throw new Error('Add a photo and mark it Photo to change.')
update(r, 'enhancing')
emitJob(job, { type: 'status', message: 'Enhancing prompt…' })
// promptRaw = exactly what they typed (compiled from sections before PE).
const userPrompt = String(q.compiledPrompt || '')
r.request.promptRaw = userPrompt
// Edit PE gets the raw edit instruction + Start still — not the keep stanza.
// Edit PE gets the raw edit instruction + canvas still — not the keep stanza.
const peInput = userPrompt
const graph = structuredClone(edit ? qwen21PeEditTemplate : qwen21PeT2iTemplate)
graph['9'].inputs.prompt = peInput
graph['9'].inputs.seed = s.seed
if (edit) {
// Same Start still pixels as Edit image_1 — never Hero.
// Photo to change → image_1. Outfit/extra → image_2 when present.
const a = await upload(r, 'pe-source', readFileSync(stillPath(r.owner, getStill(r.owner, q.imageAId).id)))
graph['10'].inputs.image = a
graph['9'].inputs.image_1 = ['10', 0]
@@ -143,7 +157,7 @@ async function runQwen21PromptEnhance(r: any, job: Job) {
const refusal = qwen21PeRefusal(userPrompt, result)
if (refusal.refused) {
// Fail open: keep the job, send the raw brief (Edit keep-identity stanza still applies).
const compiled = edit ? ensureQwen21EditPrompt(userPrompt) : userPrompt
const compiled = edit ? finalizeQwen21EditSample(userPrompt, q) : userPrompt
r.request.compiledPrompt = compiled
r.request.prompt = compiled
r.request.enhance = {
@@ -160,8 +174,9 @@ async function runQwen21PromptEnhance(r: any, job: Job) {
// Edit only: stanza + raw first; PE chunk only if it is an edit directive (never PE-alone).
// Generate keeps the long observer paragraph as the product — no describe sanitize.
const stitched = stitchQwen21PeEditPrompt(userPrompt, result.positive_prompt)
r.request.compiledPrompt = stitched.prompt
r.request.prompt = stitched.prompt
const compiled = finalizeQwen21EditSample(stitched.prompt, q)
r.request.compiledPrompt = compiled
r.request.prompt = compiled
r.request.enhance = {
used: !stitched.skippedAsDescribe,
wh_ratio: result.wh_ratio || '',
@@ -223,10 +238,18 @@ async function upload(r: any, name: string, data: Buffer): Promise<string> {
}
async function prepareGraph(r: any) {
const q = r.request, s = q.settings, prefix = String(useRuntimeConfig().comfyFilenamePrefix).replace(/\/$/, '') + `/studio2/${r.id}/${r.index}`;
// Photos roles → graph sockets (imageAId / imageBId / identityStillId). Payload is source of truth.
if (Array.isArray(q.photos) && q.photos.length) {
const bound = applyPhotosToRequest(q)
q.imageAId = bound.imageAId
q.imageBId = bound.imageBId
q.identityStillId = bound.identityStillId
q.photos = bound.photos
}
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 identityPlan=kleinIdentityPlan(q);
// Qwen Edit: Start still (imageAId) only — never Hero / identityStillId.
// Qwen Edit: Photo to change (imageAId) → image_1. Never Klein hero-ref.
const sourceId = q.engine === 'qwen21'
? (q.imageAId || '')
: (identityPlan ? identityPlan.sourceId : q.imageAId);
@@ -304,12 +327,12 @@ async function prepareGraph(r: any) {
}
const negative = turbo ? '' : stylePrompt(q.imageStyles, true)
if (q.mode === 'edit') {
// Start still → image_1 only. Never Hero. Never T2I / EmptyLatent.
// Photo to change → image_1. Outfit/extra → image_2. Never T2I / EmptyLatent.
if (!q.imageAId || !a)
throw new Error('Qwen Edit requires Start still (image_1). Hero is ignored.')
// promptRaw stays typed; prompt/compiledPrompt = what TextEncode samples.
throw new Error('Add a photo and mark it Photo to change.')
// promptRaw stays typed; prompt/compiledPrompt = what TextEncode samples (tags injected silently).
if (q.promptRaw == null) q.promptRaw = String(q.compiledPrompt || '')
const prompt = ensureQwen21EditPrompt(String(q.compiledPrompt || ''))
const prompt = finalizeQwen21EditSample(String(q.compiledPrompt || ''), q)
q.compiledPrompt = prompt
q.prompt = prompt
q.task = 'edit'