Store the Edit TextEncode prompt on library stills, not a Start still caption.
Edit + Enhance always leads with the keep/<image1> stanza and typed change; PE describe dumps are dropped, and Typed stays promptRaw. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -656,9 +656,10 @@ function applyUseAsHero(){
|
||||
uploadMessage.value=`Using ${assetLabel(selected.value)} as hero.`
|
||||
}
|
||||
async function copySelectedPrompt(){
|
||||
const text=selected.value?.studio2?.promptSections?.action
|
||||
const text=selected.value?.studio2?.prompt
|
||||
|| selected.value?.studio2?.compiledPrompt
|
||||
|| selected.value?.prompt
|
||||
|| selected.value?.studio2?.promptSections?.action
|
||||
|| ''
|
||||
try{
|
||||
if(import.meta.client && navigator.clipboard)await navigator.clipboard.writeText(String(text||''))
|
||||
|
||||
@@ -19,11 +19,14 @@ const rows=computed(()=>{const m=props.metadata||{},s=m.settings||{};const time=
|
||||
{label:'Start frame',value:m.startFrameSource?JSON.stringify(m.startFrameSource):'None'},
|
||||
{label:'Handoff source',value:m.handoffSource || 'None'},{label:'Handoff fitted (resampled)',value:m.fitted==null?'Not recorded':m.fitted?'Yes':'No'},
|
||||
{label:'End still',value:m.endStillId||'None'},{label:'Guide stills',value:m.guides?.length?JSON.stringify(m.guides):'None'},
|
||||
{label:'Compiled prompt',value:m.compiledPrompt},
|
||||
{label:'Prompt (raw)',value:m.promptRaw || null},
|
||||
{label:'Prompt',value:m.prompt || m.compiledPrompt},
|
||||
{label:'Typed',value:m.promptRaw || null},
|
||||
{label:'Task',value:m.task || m.mode || null},
|
||||
{label:'Enhance prompt',value:m.enhancePrompt==null?null:m.enhancePrompt?'On':'Off'},
|
||||
{label:'Enhance',value:m.enhance?.refused?'Enhance skipped (model refused) — used your prompt.':m.enhance?.parse_ok==null?null:m.enhance.parse_ok?'Applied':'Parse incomplete'},
|
||||
{label:'Enhance skip reason',value:m.enhance?.skipReason || null},
|
||||
{label:'Enhance',value:m.enhance?.refused?'Enhance skipped (model refused) — used your prompt.':m.enhance?.skippedAsDescribe?'Enhance skipped (describe rewrite) — keep stanza + typed only.':m.enhance?.used?'Applied':m.enhance?.parse_ok==null?null:m.enhance.parse_ok?'Applied':'Parse incomplete'},
|
||||
{label:'Enhance used',value:m.enhance?.used==null?null:m.enhance.used?'Yes':'No'},
|
||||
{label:'Enhance refused',value:m.enhance?.refused==null?null:m.enhance.refused?'Yes':'No'},
|
||||
{label:'Enhance skip reason',value:m.enhance?.skipReason || (m.enhance?.skippedAsDescribe?'skippedAsDescribe':null)},
|
||||
{label:'Turbo',value:m.turbo==null?null:m.turbo?'On':'Off'},
|
||||
{label:'Turbo LoRA',value:m.lora || null},
|
||||
{label:'Turbo sigmas',value:m.sigmas || null},
|
||||
|
||||
+3
-3
@@ -42,9 +42,9 @@ 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`. After PE, every Edit rewrite is forced to contain `<image1>` (PE used to ban tags for N=1). If the rewrite still drops every `<imageN>` 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.
|
||||
- Edit + Enhance → PE-I2I only (`pe_i2i`) with the same Start still on `image_1`. After PE, the sample prompt is always the keep-identity stanza + typed instruction first (`<image1>` in front). A PE rewrite is appended only when it is an edit directive; T2I-style observer captions (`The image is…`) are dropped (`enhance.skippedAsDescribe`) and the stanza + raw remain. Never let the PE chunk be the entire prompt. Library stores `prompt` (TextEncode string) and `promptRaw` (Typed). 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 stanza). 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). Edit prompt must lead with an operation and `<image1>` — never “The image is a photograph of…”. 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`.
|
||||
- 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).
|
||||
|
||||
@@ -58,6 +58,10 @@ Before emitting JSON, reason through: what the image(s) actually contain (includ
|
||||
|
||||
For every input, the rewritten instruction MUST use <image1>, <image2>, … to refer to each input image. Single-image edits still use <image1>. Never write "the photo", "the image", "the woman in the picture" as a substitute for the tag. The first Start still is always <image1>.
|
||||
|
||||
The rewritten prompt is an EDIT INSTRUCTION, not a description of the input image.
|
||||
Lead with the operation. Mention <image1> in the first sentence.
|
||||
Do not write "The image is a photograph of…". That format is for text-to-image only.
|
||||
|
||||
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.
|
||||
|
||||
## Output Size Determination
|
||||
|
||||
@@ -108,6 +108,7 @@ export interface LibraryStill {
|
||||
role: StillRole
|
||||
name?: string
|
||||
prompt?: string
|
||||
promptRaw?: string
|
||||
promptPre?: string
|
||||
promptPost?: string
|
||||
familyId?: string
|
||||
@@ -1333,6 +1334,7 @@ export async function saveStill(params: {
|
||||
role?: StillRole
|
||||
name?: string
|
||||
prompt?: string
|
||||
promptRaw?: string
|
||||
promptPre?: string
|
||||
promptPost?: string
|
||||
familyId?: string
|
||||
@@ -1354,8 +1356,9 @@ export async function saveStill(params: {
|
||||
const hideInput = typeof params.hideInput === 'boolean' ? params.hideInput === true : existing.hideInput
|
||||
const name = typeof params.name === 'string' ? params.name.trim().slice(0, 80) : existing.name
|
||||
const prompt = typeof params.prompt === 'string' ? params.prompt : existing.prompt
|
||||
const promptRaw = typeof params.promptRaw === 'string' ? params.promptRaw : existing.promptRaw
|
||||
const nextSettings = params.settings ? normalizeStillSettings(params.settings) : undefined
|
||||
if (existing.hideInput !== hideInput || existing.name !== name || existing.prompt !== prompt || nextSettings) {
|
||||
if (existing.hideInput !== hideInput || existing.name !== name || existing.prompt !== prompt || existing.promptRaw !== promptRaw || nextSettings) {
|
||||
await mutate(params.ownerKey, (next) => {
|
||||
const item = next.stills.find(still => still.id === existing.id)
|
||||
if (!item) return
|
||||
@@ -1363,6 +1366,7 @@ export async function saveStill(params: {
|
||||
item.role = role
|
||||
if (name) item.name = name
|
||||
if (prompt) item.prompt = prompt
|
||||
if (params.promptRaw != null) item.promptRaw = params.promptRaw || undefined
|
||||
if (params.promptPre != null) item.promptPre = params.promptPre || undefined
|
||||
if (params.promptPost != null) item.promptPost = params.promptPost || undefined
|
||||
if (params.familyId) item.familyId = params.familyId
|
||||
@@ -1374,6 +1378,7 @@ export async function saveStill(params: {
|
||||
existing.role = role
|
||||
if (name) existing.name = name
|
||||
if (prompt) existing.prompt = prompt
|
||||
if (params.promptRaw != null) existing.promptRaw = params.promptRaw || undefined
|
||||
if (params.promptPre != null) existing.promptPre = params.promptPre || undefined
|
||||
if (params.promptPost != null) existing.promptPost = params.promptPost || undefined
|
||||
if (nextSettings) existing.settings = nextSettings
|
||||
@@ -1395,6 +1400,7 @@ export async function saveStill(params: {
|
||||
role,
|
||||
name: params.name?.trim().slice(0, 80) || undefined,
|
||||
prompt: params.prompt || undefined,
|
||||
promptRaw: params.promptRaw || undefined,
|
||||
promptPre: params.promptPre || undefined,
|
||||
promptPost: params.promptPost || undefined,
|
||||
familyId: params.familyId,
|
||||
|
||||
@@ -105,8 +105,11 @@ async function runQwen21PromptEnhance(r: any, job: Job) {
|
||||
throw new Error('Qwen Edit requires Start still (image_1). Hero is ignored.')
|
||||
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 || '')
|
||||
const peInput = edit ? ensureQwen21EditPrompt(userPrompt) : userPrompt
|
||||
r.request.promptRaw = userPrompt
|
||||
// Edit PE gets the raw edit instruction + Start 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
|
||||
@@ -137,12 +140,14 @@ async function runQwen21PromptEnhance(r: any, job: Job) {
|
||||
saveRecord(r)
|
||||
const history = await waitPromptHistory(r, job, queued.prompt_id)
|
||||
const result = harvestQwen21Pe(history, queued.prompt_id, edit)
|
||||
r.request.promptRaw = userPrompt
|
||||
const refusal = qwen21PeRefusal(userPrompt, result)
|
||||
if (refusal.refused) {
|
||||
// Fail open: keep the job, send the raw brief (Edit keep-identity prefix still applies).
|
||||
r.request.compiledPrompt = edit ? ensureQwen21EditPrompt(userPrompt) : userPrompt
|
||||
// Fail open: keep the job, send the raw brief (Edit keep-identity stanza still applies).
|
||||
const compiled = edit ? ensureQwen21EditPrompt(userPrompt) : userPrompt
|
||||
r.request.compiledPrompt = compiled
|
||||
r.request.prompt = compiled
|
||||
r.request.enhance = {
|
||||
used: false,
|
||||
wh_ratio: '',
|
||||
...(edit ? { ratio_follow: '' } : {}),
|
||||
parse_ok: !!result.parse_ok,
|
||||
@@ -151,20 +156,35 @@ async function runQwen21PromptEnhance(r: any, job: Job) {
|
||||
thinking: result.thinking || '',
|
||||
comfyPromptId: queued.prompt_id,
|
||||
}
|
||||
} else {
|
||||
// PE-I2I: if rewrite dropped every <imageN>, stitch keep-identity onto the rewrite.
|
||||
r.request.compiledPrompt = edit
|
||||
? stitchQwen21PeEditPrompt(result.positive_prompt)
|
||||
: result.positive_prompt
|
||||
} else if (edit) {
|
||||
// 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
|
||||
r.request.enhance = {
|
||||
used: !stitched.skippedAsDescribe,
|
||||
wh_ratio: result.wh_ratio || '',
|
||||
ratio_follow: result.ratio_follow || '',
|
||||
parse_ok: !!result.parse_ok,
|
||||
refused: false,
|
||||
...(stitched.skippedAsDescribe ? { skippedAsDescribe: true } : {}),
|
||||
thinking: result.thinking || '',
|
||||
comfyPromptId: queued.prompt_id,
|
||||
}
|
||||
} else {
|
||||
r.request.compiledPrompt = result.positive_prompt
|
||||
r.request.prompt = result.positive_prompt
|
||||
r.request.enhance = {
|
||||
used: true,
|
||||
wh_ratio: result.wh_ratio || '',
|
||||
...(edit ? { ratio_follow: result.ratio_follow || '' } : {}),
|
||||
parse_ok: !!result.parse_ok,
|
||||
refused: false,
|
||||
thinking: result.thinking || '',
|
||||
comfyPromptId: queued.prompt_id,
|
||||
}
|
||||
}
|
||||
if (edit || q.mode === 'generate') r.request.task = q.mode
|
||||
// Stage flip PE → generate: clear PE prompt id and reset the bar (no leftover 100%).
|
||||
r.pePromptId = ''
|
||||
r.promptId = ''
|
||||
@@ -287,7 +307,12 @@ async function prepareGraph(r: any) {
|
||||
// Start still → image_1 only. Never Hero. 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.
|
||||
if (q.promptRaw == null) q.promptRaw = String(q.compiledPrompt || '')
|
||||
const prompt = ensureQwen21EditPrompt(String(q.compiledPrompt || ''))
|
||||
q.compiledPrompt = prompt
|
||||
q.prompt = prompt
|
||||
q.task = 'edit'
|
||||
graph = structuredClone(turbo ? qwen21EditTurboTemplate : qwen21EditTemplate)
|
||||
graph['10'].inputs.image = a
|
||||
graph['9'].inputs['images.image_1'] = ['10', 0]
|
||||
@@ -325,6 +350,9 @@ async function prepareGraph(r: any) {
|
||||
r.heroReferenceAttached = false
|
||||
} else {
|
||||
// Bench aspect is law for Generate. PE wh_ratio is advisory only (never sizes the canvas).
|
||||
if (q.promptRaw == null) q.promptRaw = String(q.compiledPrompt || '')
|
||||
q.prompt = String(q.compiledPrompt || '')
|
||||
q.task = 'generate'
|
||||
const size = resolveQwen21Size(s.aspect)
|
||||
s.width = size.width
|
||||
s.height = size.height
|
||||
@@ -462,7 +490,21 @@ async function run(r: any, job: Job) {
|
||||
}
|
||||
else if (!asset) {
|
||||
const data = await downloadComfyImage(file), size = imageDimensions(data) || { width: s.width, height: s.height };
|
||||
asset = await saveStill({ ownerKey: r.owner, folderId: q.folderId, filename: file.filename, data, ...size, role: 'output', prompt: q.compiledPrompt, familyId: r.familyId, chainIndex: r.index, settings: s });
|
||||
// prompt = TextEncode string (after PE + stitch); promptRaw = typed brief.
|
||||
const samplePrompt = String(q.prompt || q.compiledPrompt || '')
|
||||
asset = await saveStill({
|
||||
ownerKey: r.owner,
|
||||
folderId: q.folderId,
|
||||
filename: file.filename,
|
||||
data,
|
||||
...size,
|
||||
role: 'output',
|
||||
prompt: samplePrompt,
|
||||
promptRaw: q.promptRaw != null ? String(q.promptRaw) : undefined,
|
||||
familyId: r.familyId,
|
||||
chainIndex: r.index,
|
||||
settings: s,
|
||||
});
|
||||
}
|
||||
if (!asset)
|
||||
throw new Error('Library did not save the output');
|
||||
@@ -470,6 +512,11 @@ async function run(r: any, job: Job) {
|
||||
saveRecord(r);
|
||||
r.savedAt = Date.now();
|
||||
r.wallTime = (r.savedAt - r.startedAt) / 1000;
|
||||
if (q.engine === 'qwen21' && ['generate', 'edit'].includes(q.mode)) {
|
||||
q.task = q.mode
|
||||
q.prompt = String(q.prompt || q.compiledPrompt || '')
|
||||
if (q.promptRaw == null) q.promptRaw = String(q.compiledPrompt || '')
|
||||
}
|
||||
const metadata = { ...structuredClone(q), id: r.id, sourceStillId:r.sourceStillId,heroReferenceAttached:r.heroReferenceAttached,sampleLatent:r.sampleLatent,sampleDenoise:r.sampleDenoise,kind: video ? 'video' : 'image', graphId: r.graphId, promptId: r.promptId, queuedAt: r.queuedAt, startedAt: r.startedAt, savedAt: r.savedAt, gpuSeconds: r.gpuSeconds, wallTime: r.wallTime, outputWidth: asset.width || s.width, outputHeight: asset.height || s.height, purgeResult: 'Left on host', handoffSource: r.handoffSource || null, fitted: video && q.startClipId ? !!r.handoffFitted : null };
|
||||
await attachStudio2Metadata(r.owner, asset.id, metadata);
|
||||
if (!r.outputs.some((o:any)=>o.id===asset.id)) r.outputs.push({id:asset.id,kind:video?'clip':'still',studio2:metadata});
|
||||
|
||||
@@ -1,33 +1,60 @@
|
||||
/** 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>.'
|
||||
'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} Apply only this change:`
|
||||
`${QWEN21_EDIT_KEEP}\nApply only this change:`
|
||||
|
||||
/** User omitted <image1> — prepend the keep-identity instruction once. */
|
||||
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) {
|
||||
const text = String(prompt || '').trim()
|
||||
if (!text) return text
|
||||
if (/<image1>/i.test(text)) return text
|
||||
return `${QWEN21_EDIT_KEEP_CHANGE} ${text}`.trim()
|
||||
if (/Keep the identity, face, body, pose, and likeness of the person in <image1>/i.test(text)) return text
|
||||
return buildQwen21EditKeepStanza(text)
|
||||
}
|
||||
|
||||
/**
|
||||
* After PE-I2I: official PE told N=1 not to use tags, so rewrites often say
|
||||
* "the photo" / "the image" with zero <image1>. Qwen Edit only binds Start still
|
||||
* when the text contains the tag — force it onto every Edit rewrite.
|
||||
*/
|
||||
export function stitchQwen21PeEditPrompt(rewritten) {
|
||||
let text = String(rewritten || '').trim()
|
||||
if (!text) return text
|
||||
// Prefer the tag over natural substitutes (do not invent <image2>).
|
||||
text = text
|
||||
/** 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>')
|
||||
if (/<image1>/i.test(text)) return text
|
||||
return `${QWEN21_EDIT_KEEP} ${text}`.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 }
|
||||
}
|
||||
|
||||
@@ -105,13 +105,27 @@ test('qwen21 supports Generate and Edit with 25/1 defaults and no locks',()=>{
|
||||
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.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')
|
||||
assert.match(stitchQwen21PeEditPrompt('change the jacket on the woman in the photo'),/<image1>/)
|
||||
assert.doesNotMatch(stitchQwen21PeEditPrompt('change the jacket on the woman in the photo'),/\bthe photo\b/i)
|
||||
assert.match(ensureQwen21EditPrompt('put her in a red leather jacket'),/<image1>/)
|
||||
assert.equal(ensureQwen21EditPrompt('Keep the identity, face, body, pose, and likeness of the person in <image1>.\nDo not invent a different person.\nApply only this change: x'),'Keep the identity, face, body, pose, and likeness of the person in <image1>.\nDo not invent a different person.\nApply only this change: x')
|
||||
const stitchedKeep=stitchQwen21PeEditPrompt('put her in a red leather jacket','Change the outfit on the person in <image1> to a red leather jacket')
|
||||
assert.match(stitchedKeep.prompt,/Keep the identity/)
|
||||
assert.match(stitchedKeep.prompt,/<image1>/)
|
||||
assert.match(stitchedKeep.prompt,/red leather jacket/)
|
||||
assert.equal(stitchedKeep.skippedAsDescribe,false)
|
||||
assert.match(stitchedKeep.prompt,/Change the outfit/)
|
||||
const stitchedDescribe=stitchQwen21PeEditPrompt('put her in a red leather jacket','The image is a photograph of a woman standing outdoors in soft light…')
|
||||
assert.equal(stitchedDescribe.skippedAsDescribe,true)
|
||||
assert.match(stitchedDescribe.prompt,/Keep the identity/)
|
||||
assert.match(stitchedDescribe.prompt,/put her in a red leather jacket/)
|
||||
assert.doesNotMatch(stitchedDescribe.prompt,/The image is a photograph/)
|
||||
const stitchedNoTag=stitchQwen21PeEditPrompt('put her in a red leather jacket','Change the jacket on the woman in the photo to red leather')
|
||||
assert.equal(stitchedNoTag.skippedAsDescribe,false)
|
||||
assert.match(stitchedNoTag.prompt,/<image1>/)
|
||||
assert.doesNotMatch(stitchedNoTag.prompt,/\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 <image1>/)
|
||||
assert.match(readFileSync(new URL('../host/qwen21-pe-prompts/system_prompt_edit.txt',import.meta.url),'utf8'),/EDIT INSTRUCTION/)
|
||||
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'),/EDIT INSTRUCTION/)
|
||||
assert.doesNotMatch(readFileSync(new URL('../host/qwen21-pe-prompts/system_prompt_t2i.txt',import.meta.url),'utf8'),/Single-image edits still use <image1>/)
|
||||
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)
|
||||
@@ -144,6 +158,9 @@ test('custom negative chips survive normalizeStyles and bench exposes Load actio
|
||||
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/)
|
||||
assert.match(readFileSync(new URL('../components/studio2/Details.vue',import.meta.url),'utf8'),/\bTyped\b/)
|
||||
assert.match(readFileSync(new URL('../utils/libraryGeneration.ts',import.meta.url),'utf8'),/\bTyped\b/)
|
||||
assert.match(readFileSync(new URL('../utils/libraryGeneration.ts',import.meta.url),'utf8'),/promptRaw/)
|
||||
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')
|
||||
assert.match(peT2i,/QwenImage21_T2IPromptRewrite/);assert.match(peT2i,/pe_t2i\.int8_convrot/)
|
||||
@@ -158,6 +175,8 @@ test('custom negative chips survive normalizeStyles and bench exposes Load actio
|
||||
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,/skippedAsDescribe/)
|
||||
assert.match(runner,/promptRaw/)
|
||||
assert.match(runner,/images\.image_1/)
|
||||
assert.match(runner,/comfyPromptId/)
|
||||
assert.match(runner,/generateComfyPromptId/)
|
||||
|
||||
@@ -41,14 +41,29 @@ export type StillGenerationSettings = {
|
||||
|
||||
export type SettingsRow = { label: string; value: string }
|
||||
|
||||
function pushPromptRows(rows: SettingsRow[], item: { prompt?: string; promptPre?: string; promptPost?: string }) {
|
||||
const parts = restorePromptParts(item.prompt || '', {
|
||||
function pushPromptRows(rows: SettingsRow[], item: {
|
||||
prompt?: string
|
||||
promptRaw?: string
|
||||
promptPre?: string
|
||||
promptPost?: string
|
||||
studio2?: { prompt?: string; promptRaw?: string; compiledPrompt?: string }
|
||||
}) {
|
||||
// Sampled TextEncode string (after PE + stitch). Never a captioner dump of the PNG.
|
||||
const sampled = String(
|
||||
item.prompt
|
||||
|| item.studio2?.prompt
|
||||
|| item.studio2?.compiledPrompt
|
||||
|| ''
|
||||
).trim()
|
||||
const typed = String(item.promptRaw || item.studio2?.promptRaw || '').trim()
|
||||
const parts = restorePromptParts(sampled || item.prompt || '', {
|
||||
pre: item.promptPre,
|
||||
prompt: item.prompt,
|
||||
prompt: sampled || item.prompt,
|
||||
post: item.promptPost
|
||||
})
|
||||
if (parts.pre.trim()) rows.push({ label: 'Pre-text', value: parts.pre.trim() })
|
||||
if (parts.prompt.trim()) rows.push({ label: 'Prompt', value: parts.prompt.trim() })
|
||||
if (typed && typed !== parts.prompt.trim()) rows.push({ label: 'Typed', value: typed })
|
||||
if (parts.post.trim()) rows.push({ label: 'Post-text', value: parts.post.trim() })
|
||||
}
|
||||
|
||||
@@ -172,7 +187,16 @@ export function stillModeLabel(still: { filename?: string; settings?: StillGener
|
||||
return ''
|
||||
}
|
||||
|
||||
export function stillSettingsRows(still: { prompt?: string; promptPre?: string; promptPost?: string; width?: number; height?: number; settings?: StillGenerationSettings | null }) {
|
||||
export function stillSettingsRows(still: {
|
||||
prompt?: string
|
||||
promptRaw?: string
|
||||
promptPre?: string
|
||||
promptPost?: string
|
||||
studio2?: { prompt?: string; promptRaw?: string; compiledPrompt?: string }
|
||||
width?: number
|
||||
height?: number
|
||||
settings?: StillGenerationSettings | null
|
||||
}) {
|
||||
const settings = still.settings
|
||||
const rows: SettingsRow[] = []
|
||||
pushPromptRows(rows, still)
|
||||
|
||||
Reference in New Issue
Block a user