Run image edit on Beast with Flux.2 Klein and stop routing jobs to Sidecar.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+91
-52
@@ -6,7 +6,7 @@
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-amber-400 text-zinc-950 font-display font-extrabold">A</div>
|
||||
<div>
|
||||
<h1 class="font-display text-xl font-bold leading-none">{{ instanceName }}</h1>
|
||||
<p class="text-[11px] uppercase tracking-[0.22em] text-zinc-500">MiniMax H3 · Qwen Edit · ComfyUI</p>
|
||||
<p class="text-[11px] uppercase tracking-[0.22em] text-zinc-500">MiniMax H3 · Flux.2 Klein · ComfyUI</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-sm">
|
||||
@@ -20,7 +20,7 @@
|
||||
<span
|
||||
class="items-center gap-2 rounded-full border px-3 py-1"
|
||||
:class="[studioMode === 'edit' ? 'hidden sm:inline-flex' : 'hidden xl:inline-flex opacity-60', imageComfyBadge.className]"
|
||||
:title="imageComfyHost || 'Image edit: Beast, Sidecar overflow'"
|
||||
:title="imageComfyHost || 'Image edit on Beast'"
|
||||
>
|
||||
<span class="h-2 w-2 rounded-full" :class="imageComfyBadge.dot" />
|
||||
{{ imageComfyBadge.label }}
|
||||
@@ -57,7 +57,7 @@
|
||||
</div>
|
||||
<h2 class="font-display text-2xl font-bold">Input</h2>
|
||||
<p class="text-sm text-zinc-400">{{ studioMode === 'edit'
|
||||
? 'Drop a still and describe the edit. Beast handles it when free; Sidecar only if Beast is busy.'
|
||||
? 'Drop the still to edit. An optional second still applies image 2 onto image 1. Both run on Beast; files are removed from the desktop after save.'
|
||||
: 'Drop a still, describe the motion, then relay the job to the local desktop.' }}</p>
|
||||
</div>
|
||||
|
||||
@@ -80,11 +80,52 @@
|
||||
<button type="button" class="relative mt-3 rounded-full bg-zinc-950/80 px-3 py-1 text-xs" @click.stop="resetImage">Reset</button>
|
||||
</div>
|
||||
<div v-else class="flex min-h-44 flex-col items-center justify-center text-center">
|
||||
<p class="font-medium">Choose an image</p>
|
||||
<p class="font-medium">{{ studioMode === 'edit' ? 'Image 1' : 'Choose an image' }}</p>
|
||||
<p class="mt-1 text-sm text-zinc-500">Browse the library or upload a new PNG, JPG, or WEBP</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="studioMode === 'edit'" class="rounded-2xl border border-dashed border-white/15 bg-zinc-950/50 p-4">
|
||||
<div class="mb-2 flex items-center justify-between gap-2">
|
||||
<p class="text-sm font-medium text-zinc-300">Image 2 · optional</p>
|
||||
<button
|
||||
v-if="editRefFile"
|
||||
type="button"
|
||||
class="text-xs text-zinc-400 hover:text-white"
|
||||
@click="clearEditRef"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="relative block w-full overflow-hidden rounded-xl border border-white/10 bg-zinc-950/60 text-left"
|
||||
@click="openPicker('editRef')"
|
||||
>
|
||||
<span
|
||||
v-if="editRefPreview && hideInputPreview"
|
||||
class="relative flex min-h-28 items-center justify-center px-3 py-6 text-center text-sm text-zinc-300"
|
||||
>
|
||||
<span class="sensitive-glow absolute inset-1 rounded-lg" />
|
||||
<span class="relative">Image 2 loaded · preview hidden</span>
|
||||
</span>
|
||||
<img
|
||||
v-else-if="editRefPreview"
|
||||
:src="editRefPreview"
|
||||
alt="Reference still"
|
||||
class="max-h-40 w-full object-contain bg-black/40"
|
||||
>
|
||||
<span v-else class="flex min-h-28 items-center justify-center px-3 py-6 text-center text-sm text-zinc-500">
|
||||
Apply this still onto image 1
|
||||
</span>
|
||||
</button>
|
||||
<p class="mt-2 text-xs text-zinc-500">
|
||||
{{ editRefFile
|
||||
? 'Two-image branch on. Image 2 is applied onto image 1.'
|
||||
: 'One-image branch on. Add a second still to transfer details from image 2.' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<label class="text-sm font-medium text-zinc-300">{{ studioMode === 'edit' ? 'Edit prompt' : 'Motion & scene prompt' }}</label>
|
||||
@@ -428,13 +469,11 @@
|
||||
<summary class="cursor-pointer text-sm font-medium">Image edit settings</summary>
|
||||
<div class="mt-4 space-y-4">
|
||||
<p class="text-xs text-zinc-500">
|
||||
Beast first, Sidecar overflow.
|
||||
Flux.2 Klein 9B on Beast. Sidecar is off.
|
||||
<span class="text-zinc-200">{{ imageComfyBadge.label }}</span>
|
||||
· Qwen Image Edit u2 (Lightning)
|
||||
</p>
|
||||
<p class="text-[11px] text-zinc-500">
|
||||
Beast {{ imageBeastHost || 'unset' }}{{ imageBeastOk ? (imageBeastBusy ? ' · busy' : ' · free') : ' · offline' }}
|
||||
· Sidecar {{ imageSidecarHost || 'unset' }}{{ imageSidecarOk ? (imageSidecarBusy ? ' · busy' : ' · ready') : (imageSidecarConfigured ? ' · offline' : ' · unset') }}
|
||||
Beast {{ imageBeastHost || imageComfyHost || 'unset' }}{{ comfyOk ? (comfyBusy ? ' · busy' : ' · free') : ' · offline' }}
|
||||
</p>
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block text-zinc-400">Negative prompt</span>
|
||||
@@ -443,22 +482,18 @@
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block text-zinc-400">Steps · {{ editSteps }}</span>
|
||||
<input v-model.number="editSteps" type="range" min="1" max="8" step="1" class="w-full">
|
||||
<input v-model.number="editSteps" type="range" min="4" max="40" step="1" class="w-full">
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block text-zinc-400">CFG · {{ editCfg }}</span>
|
||||
<input v-model.number="editCfg" type="range" min="0.5" max="4" step="0.1" class="w-full">
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block text-zinc-400">Denoise · {{ editDenoise }}</span>
|
||||
<input v-model.number="editDenoise" type="range" min="0.05" max="1" step="0.05" class="w-full">
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
<label class="block text-sm sm:col-span-2">
|
||||
<span class="mb-1 block text-zinc-400">Seed</span>
|
||||
<input v-model="seedInput" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm" placeholder="random">
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-zinc-500">This graph encodes the still in the Qwen prompt. Lightning is tuned for 4 steps, CFG 1, denoise 1.</p>
|
||||
<p class="text-xs text-zinc-500">Graph default is 20 steps, CFG 1, euler. A second still switches the two-image branch on.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -515,8 +550,8 @@
|
||||
· step {{ step }}/{{ maxStep || defaultVideoSteps(turbo, videoWorkflow) }}
|
||||
</p>
|
||||
</div>
|
||||
<p v-if="studioMode === 'edit' && videoBusy" class="mt-2 text-xs text-zinc-500">Video generation continues in the background.</p>
|
||||
<p v-else-if="studioMode === 'video' && editBusy" class="mt-2 text-xs text-zinc-500">Image edit continues in the background.</p>
|
||||
<p v-if="studioMode === 'edit' && videoBusy" class="mt-2 text-xs text-zinc-500">Video is using Beast. This edit waits in the Comfy queue.</p>
|
||||
<p v-else-if="studioMode === 'video' && editBusy" class="mt-2 text-xs text-zinc-500">Image edit is using Beast. Video waits until that finishes.</p>
|
||||
|
||||
<div v-show="studioMode === 'video'">
|
||||
<div class="mt-4">
|
||||
@@ -1084,14 +1119,15 @@ const editProgress = ref(0)
|
||||
const editStatusMessage = ref('Waiting for a still.')
|
||||
const editStatusBusy = ref(false)
|
||||
const editStep = ref(0)
|
||||
const editMaxStep = ref(4)
|
||||
const editMaxStep = ref(20)
|
||||
const editElapsedMs = ref(0)
|
||||
const editAwaitingReveal = ref(false)
|
||||
const editLockedSave = ref(false)
|
||||
const editSteps = ref(4)
|
||||
const editSteps = ref(20)
|
||||
const editCfg = ref(1)
|
||||
const editDenoise = ref(1)
|
||||
const editNegative = ref('blurry, low quality, deformed, bad anatomy, watermark, text, extra fingers')
|
||||
const editNegative = ref('')
|
||||
const editRefFile = ref<File | null>(null)
|
||||
const editRefPreview = ref('')
|
||||
const videoJobId = ref('')
|
||||
const editJobId = ref('')
|
||||
const currentClipId = ref('')
|
||||
@@ -1128,13 +1164,14 @@ const renameInput = ref<HTMLInputElement | null>(null)
|
||||
const unlockPassword = ref('')
|
||||
const settingsOpen = ref(false)
|
||||
const pickerOpen = ref(false)
|
||||
const pickerSlot = ref<'main' | number>('main')
|
||||
const pickerSlot = ref<'main' | 'editRef' | number>('main')
|
||||
const videoWorkflow = ref('v1')
|
||||
const useIdentityRefs = ref(false)
|
||||
const identityRefs = ref<(File | null)[]>([null, null, null, null])
|
||||
const identityPreviews = ref(['', '', '', ''])
|
||||
const pickerHeading = computed(() => {
|
||||
if (pickerSlot.value === 'main') return 'Choose an image'
|
||||
if (pickerSlot.value === 'main') return studioMode.value === 'edit' ? 'Choose image 1' : 'Choose an image'
|
||||
if (pickerSlot.value === 'editRef') return 'Choose image 2'
|
||||
return `Choose Picture ${Number(pickerSlot.value) + 2}`
|
||||
})
|
||||
let videoTimer: ReturnType<typeof setInterval> | null = null
|
||||
@@ -1183,43 +1220,29 @@ const comfyBadge = computed(() => {
|
||||
})
|
||||
|
||||
const imageComfyBadge = computed(() => {
|
||||
if (!imageComfyConfigured.value) {
|
||||
if (!imageComfyConfigured.value && !comfyOk.value) {
|
||||
return {
|
||||
label: 'Edit unset',
|
||||
className: 'border-white/10 text-zinc-500',
|
||||
dot: 'bg-zinc-500'
|
||||
}
|
||||
}
|
||||
if (imageBeastOk.value && !imageBeastBusy.value) {
|
||||
if (comfyOk.value && !comfyBusy.value) {
|
||||
return {
|
||||
label: 'Beast · edit ready',
|
||||
label: 'Beast · Klein ready',
|
||||
className: 'border-emerald-400/30 text-emerald-300',
|
||||
dot: 'bg-emerald-400'
|
||||
}
|
||||
}
|
||||
if (imageBeastBusy.value && imageSidecarOk.value && !imageSidecarBusy.value) {
|
||||
if (comfyBusy.value) {
|
||||
return {
|
||||
label: 'Sidecar · overflow ready',
|
||||
className: 'border-amber-400/40 text-amber-200',
|
||||
dot: 'bg-amber-400'
|
||||
}
|
||||
}
|
||||
if (imageSidecarOk.value && !imageSidecarBusy.value) {
|
||||
return {
|
||||
label: 'Sidecar · edit ready',
|
||||
className: 'border-emerald-400/30 text-emerald-300',
|
||||
dot: 'bg-emerald-400'
|
||||
}
|
||||
}
|
||||
if (imageComfyBusy.value) {
|
||||
return {
|
||||
label: 'Edit boxes busy',
|
||||
label: 'Beast · busy',
|
||||
className: 'border-amber-400/40 text-amber-200',
|
||||
dot: 'bg-amber-400'
|
||||
}
|
||||
}
|
||||
return {
|
||||
label: 'Edit Comfy offline',
|
||||
label: 'Beast offline',
|
||||
className: 'border-red-400/30 text-red-300',
|
||||
dot: 'bg-red-400'
|
||||
}
|
||||
@@ -1282,14 +1305,12 @@ const videoGenerateDisabled = computed(() => {
|
||||
const editBlocked = computed(() => {
|
||||
if (editBusy.value) return true
|
||||
if (!file.value || !prompt.value.trim() || !folderId.value) return true
|
||||
if (!imageComfyConfigured.value) return true
|
||||
if (!imageComfyOk.value && !imageSidecarConfigured.value) return true
|
||||
if (!comfyOk.value && !imageComfyOk.value) return true
|
||||
return false
|
||||
})
|
||||
const editBlockReason = computed(() => {
|
||||
if (!imageComfyConfigured.value) return 'Image edit is not configured. Set COMFY_HOST and IMAGE_COMFY_HOST.'
|
||||
if (!imageComfyOk.value && !imageSidecarConfigured.value) return 'Beast is offline and Sidecar is not configured.'
|
||||
if (!imageComfyOk.value && imageSidecarConfigured.value && !imageSidecarOk.value) return 'Beast and Sidecar are both offline.'
|
||||
if (!imageComfyConfigured.value && !comfyOk.value) return 'Image edit is not configured. Set COMFY_HOST.'
|
||||
if (!comfyOk.value && !imageComfyOk.value) return 'Beast is offline.'
|
||||
return ''
|
||||
})
|
||||
const generateLabel = computed(() => {
|
||||
@@ -1310,7 +1331,9 @@ const composedIdentityPrompt = computed(() => {
|
||||
})
|
||||
const promptPlaceholder = computed(() => {
|
||||
if (studioMode.value === 'edit') {
|
||||
return 'Make the lighting warmer. Replace the background with a rainy street at night. Keep the face unchanged.'
|
||||
return editRefFile.value
|
||||
? 'Apply the dress in image 2 to the person in image 1. Preserve the exact features, proportions, pose, and setting of image 1.'
|
||||
: 'Make the lighting warmer. Replace the background with a rainy street at night. Keep the face unchanged.'
|
||||
}
|
||||
if (identityPrompting.value) {
|
||||
return shotScriptMode.value
|
||||
@@ -1676,11 +1699,23 @@ function readFile(next: File, opts?: { keepThumbnailPref?: boolean }) {
|
||||
img.src = preview.value
|
||||
}
|
||||
|
||||
function openPicker(slot: 'main' | number = 'main') {
|
||||
function openPicker(slot: 'main' | 'editRef' | number = 'main') {
|
||||
pickerSlot.value = slot
|
||||
pickerOpen.value = true
|
||||
}
|
||||
|
||||
function setEditRef(next: File) {
|
||||
if (editRefPreview.value) URL.revokeObjectURL(editRefPreview.value)
|
||||
editRefFile.value = next
|
||||
editRefPreview.value = URL.createObjectURL(next)
|
||||
}
|
||||
|
||||
function clearEditRef() {
|
||||
if (editRefPreview.value) URL.revokeObjectURL(editRefPreview.value)
|
||||
editRefFile.value = null
|
||||
editRefPreview.value = ''
|
||||
}
|
||||
|
||||
function setIdentityRef(index: number, next: File) {
|
||||
const prev = identityPreviews.value[index]
|
||||
if (prev) URL.revokeObjectURL(prev)
|
||||
@@ -1706,6 +1741,10 @@ function clearIdentityRef(index: number) {
|
||||
function onPickedImage(next: File, hidden = false) {
|
||||
const slot = pickerSlot.value
|
||||
pickerOpen.value = false
|
||||
if (slot === 'editRef') {
|
||||
setEditRef(next)
|
||||
return
|
||||
}
|
||||
if (slot !== 'main') {
|
||||
setIdentityRef(Number(slot), next)
|
||||
return
|
||||
@@ -2280,17 +2319,17 @@ async function editImage() {
|
||||
editResultUrl.value = ''
|
||||
editAwaitingReveal.value = false
|
||||
editLockedSave.value = false
|
||||
editStatusMessage.value = 'Choosing Beast or Sidecar...'
|
||||
editStatusMessage.value = 'Checking Beast ComfyUI...'
|
||||
editMaxStep.value = editSteps.value
|
||||
startTimer('edit')
|
||||
try {
|
||||
const body = new FormData()
|
||||
body.append('image', file.value)
|
||||
if (editRefFile.value) body.append('image2', editRefFile.value)
|
||||
body.append('prompt', prompt.value.trim())
|
||||
body.append('negative', editNegative.value)
|
||||
body.append('steps', String(editSteps.value))
|
||||
body.append('cfg', String(editCfg.value))
|
||||
body.append('denoise', String(editDenoise.value))
|
||||
body.append('seed', seedInput.value || 'random')
|
||||
body.append('folderId', folderId.value)
|
||||
body.append('hideInput', String(showPrivacyToggles.value && hideInputPreview.value))
|
||||
|
||||
Reference in New Issue
Block a user