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:
Towsty
2026-09-26 15:14:20 -05:00
co-authored by Cursor
parent c46e827c68
commit 58c5458c87
9 changed files with 177 additions and 46 deletions
+7 -1
View File
@@ -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,
+58 -11
View File
@@ -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});