Clarify output image actions and preserve user VRAM policy
This commit is contained in:
@@ -7,6 +7,6 @@ The September 9 investigation found two separate problems:
|
||||
|
||||
New video batches now default to automatic continuation. Its checkbox stays outside the settings drawer. Paused batches show “Run remaining”; the bounded action says “Run only shot N”. Resume restores automatic continuation in both the Studio row and shot queue and passes the refreshed run-all policy to the dispatcher. Explicit one-shot / next-three actions remain bounded. Errors still stop the batch instead of silently skipping failed shots.
|
||||
|
||||
For the observed Windows HostBuffer failure, the connector now prepares Comfy's supported `--disable-dynamic-vram` launch flag when that installed version supports it. This selects estimate-based loading instead of AIMDO dynamic loading. It changes memory management, not the generation graph, handoff frame, refinement, model precision, or source segments. It may change loading speed and memory use; GPU inference has not been tested. `COMFY_DYNAMIC_VRAM=true` on the connector opts back into upstream defaults.
|
||||
The attempted HostBuffer workaround in `badf59d` forced `--disable-dynamic-vram`. The user subsequently reported VRAM allocation failures and a large video slowdown. Their correction in `6fb8727` restores a pass-through memory policy: launch flags are left exactly as the user configured them. The connector must not automatically disable dynamic VRAM, pinned memory, asynchronous offload, or change precision as a retry strategy. The original intermittent HostBuffer fault remains separate from the batch-continuation fix.
|
||||
|
||||
The memory change takes effect only after the connector is reloaded and Comfy is next started. Do not restart either during an active render. No running job or existing queue was changed during diagnosis. The checkbox default applies to new batches; use “Run remaining” on an existing paused batch after deploying the UI fix.
|
||||
A policy change takes effect only after the connector is reloaded and Comfy is next started. A Coolify deployment or replacing the module on disk does not update an existing Node import or Python process. Do not restart either during an active render. The checkbox default applies to new batches; use “Run remaining” on an existing paused batch after deploying the UI fix.
|
||||
|
||||
+37
-58
@@ -1609,6 +1609,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex rounded-2xl border border-amber-300/40 px-4 py-2 text-sm text-amber-100 hover:border-amber-300/70"
|
||||
title="Replace only the input image; keep the current prompt and settings"
|
||||
@click="useEditAsInput"
|
||||
>
|
||||
Use as input still
|
||||
@@ -1617,6 +1618,7 @@
|
||||
type="button"
|
||||
class="inline-flex rounded-2xl border border-amber-300/40 px-4 py-2 text-sm text-amber-100 hover:border-amber-300/70 disabled:opacity-50"
|
||||
:disabled="usingReferenceBusy"
|
||||
title="Replace only the reference photo; keep the input image, prompt, and settings"
|
||||
@click="useOutputAsFluxReference"
|
||||
>
|
||||
{{ usingReferenceBusy ? 'Loading reference…' : 'Use as reference' }}
|
||||
@@ -1631,17 +1633,12 @@
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex rounded-2xl border border-amber-300/40 px-4 py-2 text-sm text-amber-100 hover:border-amber-300/70"
|
||||
@click="sendToEdit"
|
||||
class="inline-flex rounded-2xl border border-amber-300/40 px-4 py-2 text-sm text-amber-100 hover:border-amber-300/70 disabled:opacity-50"
|
||||
title="Replace the input image, prompt, and generation settings with this image's saved setup"
|
||||
:disabled="usingInputSettingsBusy"
|
||||
@click="useOutputWithSettings"
|
||||
>
|
||||
Send to Edit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex rounded-2xl border border-amber-300/40 px-4 py-2 text-sm text-amber-100 hover:border-amber-300/70"
|
||||
@click="sendToRefine"
|
||||
>
|
||||
Send to Refine
|
||||
{{ usingInputSettingsBusy ? 'Loading settings…' : 'Use as input' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="backupStudioEnabled"
|
||||
@@ -6448,7 +6445,7 @@ function applyStillSettings(still: LibraryStill) {
|
||||
persistImageV2LoraStack(imageV2LoraStack.value, v2Engine.value)
|
||||
v2Engine.value = parseImageV2Engine(settings?.engine, 'flux')
|
||||
if (!settings) return
|
||||
if (typeof settings.negative === 'string') v2Negative.value = settings.negative
|
||||
v2Negative.value = settings.negative || ''
|
||||
if (settings.steps != null) v2Steps.value = clampImageSteps(settings.steps, IMAGE_V2_STEPS_DEFAULT)
|
||||
if (settings.cfg != null) v2Cfg.value = clampImageCfg(settings.cfg, IMAGE_V2_CFG_DEFAULT)
|
||||
if (settings.megapixels != null) v2Megapixels.value = clampImageScaleMegapixels(settings.megapixels, 1)
|
||||
@@ -6458,12 +6455,12 @@ function applyStillSettings(still: LibraryStill) {
|
||||
aspect.value = parseImageAspect(settings.aspect)
|
||||
if (mode === 'generate') {
|
||||
const size = generateSizeForAspect(aspect.value)
|
||||
v2Width.value = size.width
|
||||
v2Height.value = size.height
|
||||
v2Width.value = settings.width && settings.width > 0 ? settings.width : size.width
|
||||
v2Height.value = settings.height && settings.height > 0 ? settings.height : size.height
|
||||
}
|
||||
if (mode === 'compose' && settings.task) v2Task.value = settings.task
|
||||
imageV2LoraStack.value = mergeLegacyImageV2Loras(
|
||||
settings.loraStack?.length || settings.loraName ? readLoraStack(settings) : imageV2LoraStack.value,
|
||||
readLoraStack(settings),
|
||||
{
|
||||
engine: v2Engine.value,
|
||||
mode,
|
||||
@@ -6482,12 +6479,10 @@ function applyStillSettings(still: LibraryStill) {
|
||||
if (!settings) return
|
||||
if (settings.steps != null) editSteps.value = clampImageSteps(settings.steps)
|
||||
if (settings.cfg != null) editCfg.value = clampImageCfg(settings.cfg)
|
||||
if (typeof settings.negative === 'string') editNegative.value = settings.negative
|
||||
editNegative.value = settings.negative || ''
|
||||
editScaleToTotalPixels.value = settings.scaleToTotalPixels === true
|
||||
if (settings.scaleMegapixels != null) editScaleMegapixels.value = clampImageScaleMegapixels(settings.scaleMegapixels)
|
||||
if (settings.loraStack?.length || settings.loraName) {
|
||||
imageLoraStack.value = filterLoraStackForStudio(readLoraStack(settings), ltxEnabled.value)
|
||||
}
|
||||
aspect.value = parseImageAspect(settings.aspect)
|
||||
}
|
||||
|
||||
@@ -7303,34 +7298,6 @@ async function ensureRefineCanvas() {
|
||||
}
|
||||
}
|
||||
|
||||
async function sendToEdit() {
|
||||
if (!editResultUrl.value) return
|
||||
clearEditPasses()
|
||||
studioMode.value = 'editv2'
|
||||
outputFocus.value = 'edit'
|
||||
try {
|
||||
await useEditAsInput()
|
||||
setV2Mode('edit')
|
||||
toast('Output is Still A. Write an edit prompt.')
|
||||
} catch {
|
||||
toast('Could not send that still to Edit')
|
||||
}
|
||||
}
|
||||
|
||||
async function sendToRefine() {
|
||||
if (!editResultUrl.value) return
|
||||
clearEditPasses()
|
||||
studioMode.value = 'editv2'
|
||||
outputFocus.value = 'edit'
|
||||
try {
|
||||
await useEditAsInput()
|
||||
setV2Mode('refine')
|
||||
toast('Output is Still A. Paint a mask, then refine.')
|
||||
} catch {
|
||||
toast('Could not send that still to Refine')
|
||||
}
|
||||
}
|
||||
|
||||
async function editImageV2() {
|
||||
if (editV2Blocked.value) {
|
||||
toast(editV2BlockReason.value || 'Load still A first.')
|
||||
@@ -7502,12 +7469,9 @@ async function useOutputAsFluxReference() {
|
||||
const blob = await $fetch<Blob>(editResultUrl.value, { responseType: 'blob' })
|
||||
if (!blob?.size) throw new Error('Empty image')
|
||||
const reference = new File([blob], `${editDownloadName.value || 'reference'}.png`, { type: blob.type || 'image/png' })
|
||||
selectStudioKind('image')
|
||||
setV2Engine('flux')
|
||||
resetImage()
|
||||
v2Mode.value = editPassQueue.value.length ? 'iterate' : 'generate'
|
||||
setEditRef(reference)
|
||||
toast('Added to Flux Klein reference photo. Your prompt will generate a new image.')
|
||||
scrollToStudioInput()
|
||||
toast('Reference photo updated. Your input image, prompt, and settings were kept.')
|
||||
} catch {
|
||||
toast('Could not load that image as a reference')
|
||||
} finally {
|
||||
@@ -7515,21 +7479,36 @@ async function useOutputAsFluxReference() {
|
||||
}
|
||||
}
|
||||
|
||||
const usingInputSettingsBusy = ref(false)
|
||||
|
||||
async function useOutputWithSettings() {
|
||||
if (!editResultUrl.value || usingInputSettingsBusy.value) return
|
||||
usingInputSettingsBusy.value = true
|
||||
const stillId = stillIdFromLibraryUrl(editResultUrl.value) || lastV2StillId.value
|
||||
try {
|
||||
if (!stillId) throw new Error('This image has no saved settings yet.')
|
||||
if (!stills.value.some(item => item.id === stillId)) await loadLibrary()
|
||||
const still = stills.value.find(item => item.id === stillId)
|
||||
if (!still) throw new Error('Saved settings for this image are unavailable.')
|
||||
await useStillAsInput(still)
|
||||
} catch (error: any) {
|
||||
toast(error?.message || 'Could not load this image and its settings')
|
||||
} finally {
|
||||
usingInputSettingsBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function useEditAsInput() {
|
||||
if (!editResultUrl.value) return
|
||||
const stillId = lastV2StillId.value || stillIdFromLibraryUrl(editResultUrl.value)
|
||||
const libraryStill = stillId ? stills.value.find(item => item.id === stillId) : undefined
|
||||
if (libraryStill) {
|
||||
await useStillAsInput(libraryStill)
|
||||
return
|
||||
}
|
||||
const stillId = stillIdFromLibraryUrl(editResultUrl.value) || lastV2StillId.value
|
||||
try {
|
||||
const blob = await $fetch<Blob>(editResultUrl.value, { responseType: 'blob' })
|
||||
if (!blob || !blob.size) throw new Error('empty still')
|
||||
readFile(new File([blob], `${editDownloadName.value || 'edited'}.png`, { type: blob.type || 'image/png' }), {
|
||||
stillId: stillId || undefined
|
||||
stillId: stillId || undefined, keepThumbnailPref: true
|
||||
})
|
||||
toast('Edited still is now the input')
|
||||
scrollToStudioInput()
|
||||
toast('Input image updated. Your prompt and settings were kept.')
|
||||
} catch {
|
||||
toast('Could not load that still as input')
|
||||
}
|
||||
|
||||
@@ -2,10 +2,14 @@ import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { stableMemoryArgs } from '../scripts/comfy-memory-policy.mjs'
|
||||
|
||||
test('Windows uses the supported non-dynamic loader without changing workflow or precision flags', () => {
|
||||
test('memory policy preserves user launch flags and never forces a different VRAM loader', () => {
|
||||
const args = ['main.py', '--enable-dynamic-vram', '--fast-disk', '--enable-manager', '--fp16-vae']
|
||||
assert.deepEqual(stableMemoryArgs(args, { platform: 'win32', cliSource: '--disable-dynamic-vram', dynamicVram: 'false' }), ['main.py', '--enable-manager', '--fp16-vae', '--disable-dynamic-vram'])
|
||||
assert.deepEqual(stableMemoryArgs(args, { platform: 'win32', cliSource: '--disable-dynamic-vram', dynamicVram: 'false' }), args)
|
||||
assert.deepEqual(stableMemoryArgs(args, { platform: 'win32', cliSource: '' }), args)
|
||||
assert.deepEqual(stableMemoryArgs(args, { platform: 'linux', cliSource: '--disable-dynamic-vram' }), args)
|
||||
assert.deepEqual(stableMemoryArgs(args, { platform: 'win32', cliSource: '--disable-dynamic-vram', dynamicVram: 'true' }), args)
|
||||
const defaults = ['main.py', '--enable-manager']
|
||||
assert.deepEqual(stableMemoryArgs(defaults), defaults)
|
||||
assert.notEqual(stableMemoryArgs(defaults), defaults)
|
||||
assert.deepEqual(stableMemoryArgs(['main.py', '--disable-dynamic-vram']), ['main.py', '--disable-dynamic-vram'])
|
||||
})
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import ts from 'typescript'
|
||||
|
||||
const vue = readFileSync(new URL('../pages/index.vue', import.meta.url), 'utf8')
|
||||
const script = vue.split('<script setup lang="ts">')[1].split('</script>')[0]
|
||||
const tree = ts.createSourceFile('index.ts', script, ts.ScriptTarget.Latest, true)
|
||||
function action(name, scope) {
|
||||
const node = tree.statements.find(n => ts.isFunctionDeclaration(n) && n.name?.text === name)
|
||||
assert.ok(node, `${name} exists`)
|
||||
const code = ts.transpileModule(node.getText(tree), { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText
|
||||
return new Function(...Object.keys(scope), `${code}; return ${name}`)(...Object.values(scope))
|
||||
}
|
||||
function fixture() {
|
||||
const current = { id: 'displayed', settings: { steps: 24 } }
|
||||
const state = { input: 'existing input', reference: 'existing reference', prompt: 'current prompt', steps: 19, engine: 'flux', mode: 'generate' }
|
||||
const calls = []
|
||||
return { state, calls, current, scope: {
|
||||
editResultUrl: { value: '/api/library/stills/displayed' }, lastV2StillId: { value: 'older-generation' }, editDownloadName: { value: 'displayed' },
|
||||
stillIdFromLibraryUrl: () => 'displayed', stills: { value: [current, { id: 'older-generation' }] },
|
||||
usingReferenceBusy: { value: false }, usingInputSettingsBusy: { value: false },
|
||||
$fetch: async () => new Blob(['pixels'], { type: 'image/png' }), File,
|
||||
readFile: (file, options) => { state.input = file; calls.push(options) },
|
||||
setEditRef: file => { state.reference = file },
|
||||
useStillAsInput: async still => { calls.push(still.id) },
|
||||
loadLibrary: async () => {}, scrollToStudioInput: () => {}, toast: () => {},
|
||||
selectStudioKind: () => { throw new Error('Must not change workspace') },
|
||||
setV2Engine: () => { throw new Error('Must not change engine') },
|
||||
resetImage: () => { throw new Error('Must not clear input') }
|
||||
} }
|
||||
}
|
||||
test('input-still action loads only pixels and preserves current settings', async () => {
|
||||
const f = fixture()
|
||||
await action('useEditAsInput', f.scope)()
|
||||
assert.ok(f.state.input instanceof File)
|
||||
assert.deepEqual(f.calls, [{ stillId: 'displayed', keepThumbnailPref: true }])
|
||||
assert.equal(f.state.prompt, 'current prompt'); assert.equal(f.state.steps, 19)
|
||||
assert.equal(f.state.reference, 'existing reference')
|
||||
})
|
||||
test('reference action changes only the reference, preserving input, prompt, engine and mode', async () => {
|
||||
const f = fixture()
|
||||
await action('useOutputAsFluxReference', f.scope)()
|
||||
assert.ok(f.state.reference instanceof File)
|
||||
assert.deepEqual({ ...f.state, reference: null }, { input: 'existing input', reference: null, prompt: 'current prompt', steps: 19, engine: 'flux', mode: 'generate' })
|
||||
})
|
||||
test('full input action restores the displayed image settings instead of the previous generation', async () => {
|
||||
const f = fixture()
|
||||
await action('useOutputWithSettings', f.scope)()
|
||||
assert.deepEqual(f.calls, ['displayed'])
|
||||
})
|
||||
Reference in New Issue
Block a user