Add a Scale to 1 MP toggle on image edit before Klein.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+44
-3
@@ -621,6 +621,25 @@
|
|||||||
<p class="text-xs text-zinc-500">Flux.2 Klein 9B. Shares the desktop GPU with video.</p>
|
<p class="text-xs text-zinc-500">Flux.2 Klein 9B. Shares the desktop GPU with video.</p>
|
||||||
<div class="max-w-md space-y-2">
|
<div class="max-w-md space-y-2">
|
||||||
<LoraSelect v-model="imageLoraStack" :names="imageLoraOptions" label="LoRA" empty-label="None" compact />
|
<LoraSelect v-model="imageLoraStack" :names="imageLoraOptions" label="LoRA" empty-label="None" compact />
|
||||||
|
<label class="flex cursor-pointer items-center gap-2 text-xs text-zinc-400">
|
||||||
|
<span class="relative inline-flex h-5 w-9 shrink-0 items-center">
|
||||||
|
<input v-model="editScaleToTotalPixels" type="checkbox" class="peer sr-only" role="switch" aria-label="Scale to 1 MP">
|
||||||
|
<span class="absolute inset-0 rounded-full bg-zinc-700 transition peer-checked:bg-amber-400" />
|
||||||
|
<span class="absolute left-0.5 top-0.5 h-4 w-4 rounded-full bg-white transition peer-checked:translate-x-4" />
|
||||||
|
</span>
|
||||||
|
<span>Scale to 1 MP</span>
|
||||||
|
<input
|
||||||
|
v-if="editScaleToTotalPixels"
|
||||||
|
v-model.number="editScaleMegapixels"
|
||||||
|
type="number"
|
||||||
|
:min="IMAGE_SCALE_MP_MIN"
|
||||||
|
:max="IMAGE_SCALE_MP_MAX"
|
||||||
|
:step="IMAGE_SCALE_MP_STEP"
|
||||||
|
class="w-16 rounded-lg border border-white/10 bg-zinc-950 px-2 py-1 text-xs outline-none ring-amber-300/40 focus:ring-2"
|
||||||
|
aria-label="Megapixels"
|
||||||
|
@change="onEditScaleMegapixels"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
<GenerationPresetBar
|
<GenerationPresetBar
|
||||||
:presets="imageGenerationPresets"
|
:presets="imageGenerationPresets"
|
||||||
v-model:selected-id="imagePresetId"
|
v-model:selected-id="imagePresetId"
|
||||||
@@ -660,7 +679,7 @@
|
|||||||
<input v-model="seedInput" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm" placeholder="random">
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-zinc-500">Graph default is 20 steps, CFG 1, euler. CFG 0–20 (0.1 steps), steps 1–50. A second still switches the two-image branch on.</p>
|
<p class="text-xs text-zinc-500">Graph default is 20 steps, CFG 1, euler. CFG 0–20 (0.1 steps), steps 1–50. A second still switches the two-image branch on. Scale to 1 MP runs Scale Image to Total Pixels (lanczos) on both stills before Klein.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details v-if="studioMode === 'video'" class="rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3">
|
<details v-if="studioMode === 'video'" class="rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3">
|
||||||
@@ -1627,11 +1646,15 @@ import {
|
|||||||
} from '~/utils/loras'
|
} from '~/utils/loras'
|
||||||
import {
|
import {
|
||||||
clampImageCfg,
|
clampImageCfg,
|
||||||
|
clampImageScaleMegapixels,
|
||||||
clampImageSteps,
|
clampImageSteps,
|
||||||
clampVideoCfg,
|
clampVideoCfg,
|
||||||
IMAGE_CFG_MAX,
|
IMAGE_CFG_MAX,
|
||||||
IMAGE_CFG_MIN,
|
IMAGE_CFG_MIN,
|
||||||
IMAGE_CFG_STEP,
|
IMAGE_CFG_STEP,
|
||||||
|
IMAGE_SCALE_MP_MAX,
|
||||||
|
IMAGE_SCALE_MP_MIN,
|
||||||
|
IMAGE_SCALE_MP_STEP,
|
||||||
IMAGE_STEPS_MAX,
|
IMAGE_STEPS_MAX,
|
||||||
IMAGE_STEPS_MIN,
|
IMAGE_STEPS_MIN,
|
||||||
VIDEO_CFG_MAX,
|
VIDEO_CFG_MAX,
|
||||||
@@ -1895,6 +1918,8 @@ const editLockedSave = ref(false)
|
|||||||
const editSteps = ref(20)
|
const editSteps = ref(20)
|
||||||
const editCfg = ref(1)
|
const editCfg = ref(1)
|
||||||
const editNegative = ref('')
|
const editNegative = ref('')
|
||||||
|
const editScaleToTotalPixels = ref(false)
|
||||||
|
const editScaleMegapixels = ref(1)
|
||||||
const editRefFile = ref<File | null>(null)
|
const editRefFile = ref<File | null>(null)
|
||||||
const editRefPreview = ref('')
|
const editRefPreview = ref('')
|
||||||
const editChainStep = ref(1)
|
const editChainStep = ref(1)
|
||||||
@@ -2891,6 +2916,10 @@ function onEditCfgNumber() {
|
|||||||
editCfg.value = clampImageCfg(editCfg.value)
|
editCfg.value = clampImageCfg(editCfg.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onEditScaleMegapixels() {
|
||||||
|
editScaleMegapixels.value = clampImageScaleMegapixels(editScaleMegapixels.value)
|
||||||
|
}
|
||||||
|
|
||||||
function onCfgSlider(event: Event) {
|
function onCfgSlider(event: Event) {
|
||||||
cfgTouched.value = true
|
cfgTouched.value = true
|
||||||
cfg.value = clampCfg(Number((event.target as HTMLInputElement).value))
|
cfg.value = clampCfg(Number((event.target as HTMLInputElement).value))
|
||||||
@@ -2967,7 +2996,9 @@ function currentPresetSnapshot() {
|
|||||||
settings: {
|
settings: {
|
||||||
steps: clampImageSteps(editSteps.value),
|
steps: clampImageSteps(editSteps.value),
|
||||||
cfg: clampImageCfg(editCfg.value),
|
cfg: clampImageCfg(editCfg.value),
|
||||||
negative: editNegative.value
|
negative: editNegative.value,
|
||||||
|
scaleToTotalPixels: editScaleToTotalPixels.value,
|
||||||
|
scaleMegapixels: clampImageScaleMegapixels(editScaleMegapixels.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2993,10 +3024,18 @@ function applyGenerationPreset(preset: GenerationPreset) {
|
|||||||
const skipped = preset.loraStack.length - stack.length
|
const skipped = preset.loraStack.length - stack.length
|
||||||
if (preset.kind === 'image') {
|
if (preset.kind === 'image') {
|
||||||
imageLoraStack.value = stack
|
imageLoraStack.value = stack
|
||||||
const settings = preset.settings as { steps?: number; cfg?: number; negative?: string }
|
const settings = preset.settings as {
|
||||||
|
steps?: number
|
||||||
|
cfg?: number
|
||||||
|
negative?: string
|
||||||
|
scaleToTotalPixels?: boolean
|
||||||
|
scaleMegapixels?: number
|
||||||
|
}
|
||||||
if (typeof settings.steps === 'number') editSteps.value = clampImageSteps(settings.steps)
|
if (typeof settings.steps === 'number') editSteps.value = clampImageSteps(settings.steps)
|
||||||
if (typeof settings.cfg === 'number') editCfg.value = clampImageCfg(settings.cfg)
|
if (typeof settings.cfg === 'number') editCfg.value = clampImageCfg(settings.cfg)
|
||||||
if (typeof settings.negative === 'string') editNegative.value = settings.negative
|
if (typeof settings.negative === 'string') editNegative.value = settings.negative
|
||||||
|
editScaleToTotalPixels.value = settings.scaleToTotalPixels === true
|
||||||
|
editScaleMegapixels.value = clampImageScaleMegapixels(settings.scaleMegapixels)
|
||||||
imagePresetId.value = preset.id
|
imagePresetId.value = preset.id
|
||||||
imagePresetName.value = preset.name
|
imagePresetName.value = preset.name
|
||||||
} else {
|
} else {
|
||||||
@@ -4645,6 +4684,8 @@ async function editImage() {
|
|||||||
body.append('negative', editNegative.value)
|
body.append('negative', editNegative.value)
|
||||||
body.append('steps', String(clampImageSteps(editSteps.value)))
|
body.append('steps', String(clampImageSteps(editSteps.value)))
|
||||||
body.append('cfg', String(clampImageCfg(editCfg.value)))
|
body.append('cfg', String(clampImageCfg(editCfg.value)))
|
||||||
|
body.append('scaleToTotalPixels', String(editScaleToTotalPixels.value))
|
||||||
|
body.append('scaleMegapixels', String(clampImageScaleMegapixels(editScaleMegapixels.value)))
|
||||||
body.append('seed', seedInput.value || 'random')
|
body.append('seed', seedInput.value || 'random')
|
||||||
body.append('folderId', folderId.value)
|
body.append('folderId', folderId.value)
|
||||||
body.append('hideInput', String(showPrivacyToggles.value && hideInputPreview.value))
|
body.append('hideInput', String(showPrivacyToggles.value && hideInputPreview.value))
|
||||||
|
|||||||
+14
-2
@@ -1,8 +1,8 @@
|
|||||||
import { addStudioJob, kickStudioQueue, listStudioJobs } from '~/server/utils/studioQueue'
|
import { addStudioJob, kickStudioQueue, listStudioJobs } from '~/server/utils/studioQueue'
|
||||||
import { parsePostedLoraStack, listStudioLoras, persistLoraFields } from '~/server/utils/loras'
|
import { parsePostedLoraStack, listStudioLoras, persistLoraFields } from '~/server/utils/loras'
|
||||||
import { comfyConfigured } from '~/server/utils/comfy'
|
import { assertImageScaleToTotalPixelsNode, comfyConfigured } from '~/server/utils/comfy'
|
||||||
import { imageDimensions } from '~/server/utils/resolution'
|
import { imageDimensions } from '~/server/utils/resolution'
|
||||||
import { clampImageCfg, clampImageSteps } from '~/utils/generationPresets'
|
import { clampImageCfg, clampImageScaleMegapixels, clampImageSteps, parseImageScaleToTotalPixels } from '~/utils/generationPresets'
|
||||||
|
|
||||||
function parsePasses(raw: string | undefined) {
|
function parsePasses(raw: string | undefined) {
|
||||||
if (!raw) return [] as { prompt: string }[]
|
if (!raw) return [] as { prompt: string }[]
|
||||||
@@ -66,6 +66,16 @@ export default defineEventHandler(async (event) => {
|
|||||||
const hideThumbnail = fields.hideThumbnail === 'true'
|
const hideThumbnail = fields.hideThumbnail === 'true'
|
||||||
const steps = clampImageSteps(fields.steps)
|
const steps = clampImageSteps(fields.steps)
|
||||||
const cfg = clampImageCfg(fields.cfg)
|
const cfg = clampImageCfg(fields.cfg)
|
||||||
|
const scaleToTotalPixels = parseImageScaleToTotalPixels(fields.scaleToTotalPixels)
|
||||||
|
const scaleMegapixels = clampImageScaleMegapixels(fields.scaleMegapixels)
|
||||||
|
if (scaleToTotalPixels) {
|
||||||
|
try {
|
||||||
|
await assertImageScaleToTotalPixelsNode()
|
||||||
|
} catch (error) {
|
||||||
|
const status = Number((error as { statusCode?: number })?.statusCode || 0)
|
||||||
|
if (status === 400) throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
const seed = fields.seed && fields.seed !== 'random'
|
const seed = fields.seed && fields.seed !== 'random'
|
||||||
? Number(fields.seed)
|
? Number(fields.seed)
|
||||||
: Math.floor(Math.random() * 2_147_483_647)
|
: Math.floor(Math.random() * 2_147_483_647)
|
||||||
@@ -130,6 +140,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
passes,
|
passes,
|
||||||
referenceStillId: savedRef?.id,
|
referenceStillId: savedRef?.id,
|
||||||
referenceStillFilename: savedRef?.filename,
|
referenceStillFilename: savedRef?.filename,
|
||||||
|
scaleToTotalPixels,
|
||||||
|
scaleMegapixels,
|
||||||
...persistLoraFields(loraStack)
|
...persistLoraFields(loraStack)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -405,21 +405,6 @@
|
|||||||
"title": "Empty Flux 2 Latent"
|
"title": "Empty Flux 2 Latent"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"92:85": {
|
|
||||||
"inputs": {
|
|
||||||
"upscale_method": "lanczos",
|
|
||||||
"megapixels": 1,
|
|
||||||
"resolution_steps": 1,
|
|
||||||
"image": [
|
|
||||||
"81",
|
|
||||||
0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"class_type": "ImageScaleToTotalPixels",
|
|
||||||
"_meta": {
|
|
||||||
"title": "Scale Image to Total Pixels"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"92:111": {
|
"92:111": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"clip_name": "qwen_3_8b_fp8mixed.safetensors",
|
"clip_name": "qwen_3_8b_fp8mixed.safetensors",
|
||||||
@@ -562,7 +547,7 @@
|
|||||||
"92:129": {
|
"92:129": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"pixels": [
|
"pixels": [
|
||||||
"92:85",
|
"81",
|
||||||
0
|
0
|
||||||
],
|
],
|
||||||
"vae": [
|
"vae": [
|
||||||
|
|||||||
@@ -51,6 +51,34 @@ export async function comfyFetch(path: string, init?: RequestInit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** true = installed, false = missing, null = Comfy unreachable / object_info failed */
|
||||||
|
export async function comfyHasClassType(classType: string): Promise<boolean | null> {
|
||||||
|
try {
|
||||||
|
const res = await comfyFetch(`/object_info/${encodeURIComponent(classType)}`, {
|
||||||
|
signal: AbortSignal.timeout(8000)
|
||||||
|
})
|
||||||
|
if (res.status === 404) return false
|
||||||
|
if (!res.ok) return null
|
||||||
|
const body = await res.json().catch(() => null)
|
||||||
|
if (!body || typeof body !== 'object') return false
|
||||||
|
const rec = body as Record<string, unknown>
|
||||||
|
if (rec[classType] && typeof rec[classType] === 'object') return true
|
||||||
|
return String(rec.name || rec.class_type || '') === classType
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IMAGE_SCALE_TO_TOTAL_PIXELS = 'ImageScaleToTotalPixels'
|
||||||
|
export const IMAGE_SCALE_MISSING_MESSAGE = 'Scale Image to Total Pixels (ImageScaleToTotalPixels) is not installed on Beast. Turn Scale to 1 MP off, or install the node.'
|
||||||
|
|
||||||
|
export async function assertImageScaleToTotalPixelsNode() {
|
||||||
|
const present = await comfyHasClassType(IMAGE_SCALE_TO_TOTAL_PIXELS)
|
||||||
|
if (present === false) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: IMAGE_SCALE_MISSING_MESSAGE })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function uploadImage(file: { filename: string; data: Buffer; type?: string }, jobId?: string) {
|
export async function uploadImage(file: { filename: string; data: Buffer; type?: string }, jobId?: string) {
|
||||||
const body = new FormData()
|
const body = new FormData()
|
||||||
const blob = new Blob([new Uint8Array(file.data)], { type: file.type || 'application/octet-stream' })
|
const blob = new Blob([new Uint8Array(file.data)], { type: file.type || 'application/octet-stream' })
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ownerRoot } from '~/server/utils/library'
|
|||||||
import { filterLoraStackForStudio, normalizeLoraStack } from '~/utils/loras'
|
import { filterLoraStackForStudio, normalizeLoraStack } from '~/utils/loras'
|
||||||
import {
|
import {
|
||||||
clampImageCfg,
|
clampImageCfg,
|
||||||
|
clampImageScaleMegapixels,
|
||||||
clampImageSteps,
|
clampImageSteps,
|
||||||
clampVideoCfg,
|
clampVideoCfg,
|
||||||
normalizePresetName,
|
normalizePresetName,
|
||||||
@@ -89,7 +90,9 @@ function sanitizeImageSettings(raw: unknown): ImagePresetSettings {
|
|||||||
return {
|
return {
|
||||||
steps: clampImageSteps(rec.steps),
|
steps: clampImageSteps(rec.steps),
|
||||||
cfg: clampImageCfg(rec.cfg),
|
cfg: clampImageCfg(rec.cfg),
|
||||||
negative: String(rec.negative || '').slice(0, 2000)
|
negative: String(rec.negative || '').slice(0, 2000),
|
||||||
|
scaleToTotalPixels: rec.scaleToTotalPixels === true,
|
||||||
|
scaleMegapixels: clampImageScaleMegapixels(rec.scaleMegapixels)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createJob, emitJob, type Job } from '~/server/utils/jobs'
|
import { createJob, emitJob, type Job } from '~/server/utils/jobs'
|
||||||
import { ensureComfyReady } from '~/server/utils/comfyLifecycle'
|
import { ensureComfyReady } from '~/server/utils/comfyLifecycle'
|
||||||
import { getComfyHost, uploadImage, queuePrompt, purgeComfyArtifacts } from '~/server/utils/comfy'
|
import { assertImageScaleToTotalPixelsNode, getComfyHost, uploadImage, queuePrompt, purgeComfyArtifacts } from '~/server/utils/comfy'
|
||||||
import { withImageComfyHost, waitForImageEdit, downloadEditedImage } from '~/server/utils/imageComfy'
|
import { withImageComfyHost, waitForImageEdit, downloadEditedImage } from '~/server/utils/imageComfy'
|
||||||
import { buildEditWorkflow } from '~/server/utils/imageWorkflow'
|
import { buildEditWorkflow } from '~/server/utils/imageWorkflow'
|
||||||
import { ensureComfyLoraNames } from '~/server/utils/loras'
|
import { ensureComfyLoraNames } from '~/server/utils/loras'
|
||||||
@@ -21,6 +21,8 @@ export type EditRunParams = {
|
|||||||
cfg: number
|
cfg: number
|
||||||
loraName?: string
|
loraName?: string
|
||||||
loraStack?: import('~/utils/loras').LoraStackItem[]
|
loraStack?: import('~/utils/loras').LoraStackItem[]
|
||||||
|
scaleToTotalPixels?: boolean
|
||||||
|
scaleMegapixels?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runEdit(job: Job, params: EditRunParams) {
|
export async function runEdit(job: Job, params: EditRunParams) {
|
||||||
@@ -93,6 +95,7 @@ export async function runEdit(job: Job, params: EditRunParams) {
|
|||||||
progress: 12
|
progress: 12
|
||||||
})
|
})
|
||||||
await ensureComfyLoraNames('image')
|
await ensureComfyLoraNames('image')
|
||||||
|
if (params.scaleToTotalPixels) await assertImageScaleToTotalPixelsNode()
|
||||||
const graph = buildEditWorkflow({
|
const graph = buildEditWorkflow({
|
||||||
prompt,
|
prompt,
|
||||||
negative: params.negative,
|
negative: params.negative,
|
||||||
@@ -102,7 +105,9 @@ export async function runEdit(job: Job, params: EditRunParams) {
|
|||||||
seed,
|
seed,
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
filenamePrefix: `aigen_edit_${job.id.slice(0, 8)}_p${index + 1}`,
|
filenamePrefix: `aigen_edit_${job.id.slice(0, 8)}_p${index + 1}`,
|
||||||
loraStack: params.loraStack || params.loraName
|
loraStack: params.loraStack || params.loraName,
|
||||||
|
scaleToTotalPixels: params.scaleToTotalPixels,
|
||||||
|
scaleMegapixels: params.scaleMegapixels
|
||||||
})
|
})
|
||||||
const queued = await queuePrompt(graph, job.clientId)
|
const queued = await queuePrompt(graph, job.clientId)
|
||||||
job.promptId = queued.prompt_id
|
job.promptId = queued.prompt_id
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import template from '../assets/workflow_flux2_klein_edit.json'
|
import template from '../assets/workflow_flux2_klein_edit.json'
|
||||||
|
import { IMAGE_SCALE_TO_TOTAL_PIXELS } from '~/server/utils/comfy'
|
||||||
import { applyOptionalLoraLoaders } from '~/server/utils/loras'
|
import { applyOptionalLoraLoaders } from '~/server/utils/loras'
|
||||||
|
import { clampImageScaleMegapixels } from '~/utils/generationPresets'
|
||||||
import type { LoraStackItem } from '~/utils/loras'
|
import type { LoraStackItem } from '~/utils/loras'
|
||||||
|
|
||||||
const IMAGE1 = '76'
|
const IMAGE1 = '76'
|
||||||
@@ -16,6 +18,7 @@ const DUAL_NEGATIVE = '92:87'
|
|||||||
const DUAL_NOISE = '92:105'
|
const DUAL_NOISE = '92:105'
|
||||||
const DUAL_SCHEDULER = '92:115'
|
const DUAL_SCHEDULER = '92:115'
|
||||||
const DUAL_CFG = '92:114'
|
const DUAL_CFG = '92:114'
|
||||||
|
const LOAD_IMAGE = 'LoadImage'
|
||||||
|
|
||||||
type WorkflowNode = { class_type: string; inputs: Record<string, unknown>; _meta?: { title?: string } }
|
type WorkflowNode = { class_type: string; inputs: Record<string, unknown>; _meta?: { title?: string } }
|
||||||
type WorkflowGraph = Record<string, WorkflowNode>
|
type WorkflowGraph = Record<string, WorkflowNode>
|
||||||
@@ -31,6 +34,8 @@ export interface EditParams {
|
|||||||
filenamePrefix?: string
|
filenamePrefix?: string
|
||||||
loraName?: string
|
loraName?: string
|
||||||
loraStack?: LoraStackItem[]
|
loraStack?: LoraStackItem[]
|
||||||
|
scaleToTotalPixels?: boolean
|
||||||
|
scaleMegapixels?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildEditWorkflow(params: EditParams) {
|
export function buildEditWorkflow(params: EditParams) {
|
||||||
@@ -65,11 +70,63 @@ export function buildEditWorkflow(params: EditParams) {
|
|||||||
if (dual ? singleNode : dualNode) delete graph[id]
|
if (dual ? singleNode : dualNode) delete graph[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyScaleToTotalPixels(graph, Boolean(params.scaleToTotalPixels), params.scaleMegapixels)
|
||||||
|
|
||||||
// User LoRAs only. Empty stack bypasses the optional LoraLoader (no snofs default).
|
// User LoRAs only. Empty stack bypasses the optional LoraLoader (no snofs default).
|
||||||
applyOptionalLoraLoaders(graph, params.loraStack || params.loraName, 'image')
|
applyOptionalLoraLoaders(graph, params.loraStack || params.loraName, 'image')
|
||||||
return graph
|
return graph
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function linkSource(value: unknown): string | null {
|
||||||
|
return Array.isArray(value) && typeof value[0] === 'string' ? value[0] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripScaleNodes(graph: WorkflowGraph) {
|
||||||
|
for (const [id, node] of Object.entries(graph)) {
|
||||||
|
if (node.class_type !== IMAGE_SCALE_TO_TOTAL_PIXELS) continue
|
||||||
|
const src = Array.isArray(node.inputs.image) ? node.inputs.image : null
|
||||||
|
for (const [otherId, other] of Object.entries(graph)) {
|
||||||
|
if (otherId === id) continue
|
||||||
|
for (const [key, value] of Object.entries(other.inputs)) {
|
||||||
|
if (linkSource(value) !== id || !Array.isArray(value)) continue
|
||||||
|
other.inputs[key] = src ? [src[0], value[1] === 0 ? src[1] : value[1]] : value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete graph[id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertScaleNodes(graph: WorkflowGraph, megapixels: number) {
|
||||||
|
const loaders = Object.entries(graph).filter(([, node]) => node.class_type === LOAD_IMAGE)
|
||||||
|
for (const [id] of loaders) {
|
||||||
|
const scaleId = `scale:${id}`
|
||||||
|
if (graph[scaleId]) continue
|
||||||
|
graph[scaleId] = {
|
||||||
|
class_type: IMAGE_SCALE_TO_TOTAL_PIXELS,
|
||||||
|
inputs: {
|
||||||
|
upscale_method: 'lanczos',
|
||||||
|
megapixels,
|
||||||
|
resolution_steps: 1,
|
||||||
|
image: [id, 0]
|
||||||
|
},
|
||||||
|
_meta: { title: 'Scale Image to Total Pixels' }
|
||||||
|
}
|
||||||
|
for (const [otherId, other] of Object.entries(graph)) {
|
||||||
|
if (otherId === scaleId) continue
|
||||||
|
for (const [key, value] of Object.entries(other.inputs)) {
|
||||||
|
if (!Array.isArray(value) || value[0] !== id || value[1] !== 0) continue
|
||||||
|
other.inputs[key] = [scaleId, 0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyScaleToTotalPixels(graph: WorkflowGraph, enabled: boolean, megapixels?: number) {
|
||||||
|
stripScaleNodes(graph)
|
||||||
|
if (!enabled) return
|
||||||
|
insertScaleNodes(graph, clampImageScaleMegapixels(megapixels))
|
||||||
|
}
|
||||||
|
|
||||||
function setImage(graph: WorkflowGraph, id: string, name: string) {
|
function setImage(graph: WorkflowGraph, id: string, name: string) {
|
||||||
if (graph[id]) graph[id].inputs.image = name
|
if (graph[id]) graph[id].inputs.image = name
|
||||||
}
|
}
|
||||||
@@ -85,6 +142,8 @@ function setInput(graph: WorkflowGraph, id: string, key: string, value: unknown)
|
|||||||
export const EDIT_NODE_LABELS: Record<string, string> = {
|
export const EDIT_NODE_LABELS: Record<string, string> = {
|
||||||
'76': 'Loading image 1',
|
'76': 'Loading image 1',
|
||||||
'81': 'Loading image 2',
|
'81': 'Loading image 2',
|
||||||
|
'scale:76': 'Scaling image 1',
|
||||||
|
'scale:81': 'Scaling image 2',
|
||||||
'75:70': 'Loading Flux.2 Klein',
|
'75:70': 'Loading Flux.2 Klein',
|
||||||
'75:71': 'Loading CLIP',
|
'75:71': 'Loading CLIP',
|
||||||
'75:72': 'Loading VAE',
|
'75:72': 'Loading VAE',
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ export interface StudioJobPayload {
|
|||||||
passes?: { prompt: string }[]
|
passes?: { prompt: string }[]
|
||||||
referenceStillId?: string
|
referenceStillId?: string
|
||||||
referenceStillFilename?: string
|
referenceStillFilename?: string
|
||||||
|
scaleToTotalPixels?: boolean
|
||||||
|
scaleMegapixels?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StudioJob {
|
export interface StudioJob {
|
||||||
@@ -656,6 +658,8 @@ async function startStudioEditJob(item: StudioJob) {
|
|||||||
steps: payload.steps,
|
steps: payload.steps,
|
||||||
seed: job.library?.seed || payload.seed,
|
seed: job.library?.seed || payload.seed,
|
||||||
cfg: payload.cfg,
|
cfg: payload.cfg,
|
||||||
|
scaleToTotalPixels: payload.scaleToTotalPixels === true,
|
||||||
|
scaleMegapixels: payload.scaleMegapixels,
|
||||||
...persistLoraFields(payload.loraStack || payload.loraName)
|
...persistLoraFields(payload.loraStack || payload.loraName)
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export type ImagePresetSettings = {
|
|||||||
steps?: number
|
steps?: number
|
||||||
cfg?: number
|
cfg?: number
|
||||||
negative?: string
|
negative?: string
|
||||||
|
scaleToTotalPixels?: boolean
|
||||||
|
scaleMegapixels?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IMAGE_STEPS_MIN = 1
|
export const IMAGE_STEPS_MIN = 1
|
||||||
@@ -26,6 +28,10 @@ export const IMAGE_STEPS_MAX = 50
|
|||||||
export const IMAGE_CFG_MIN = 0
|
export const IMAGE_CFG_MIN = 0
|
||||||
export const IMAGE_CFG_MAX = 20
|
export const IMAGE_CFG_MAX = 20
|
||||||
export const IMAGE_CFG_STEP = 0.1
|
export const IMAGE_CFG_STEP = 0.1
|
||||||
|
export const IMAGE_SCALE_MP_MIN = 0.01
|
||||||
|
export const IMAGE_SCALE_MP_MAX = 16
|
||||||
|
export const IMAGE_SCALE_MP_STEP = 0.01
|
||||||
|
export const IMAGE_SCALE_MP_DEFAULT = 1
|
||||||
|
|
||||||
export const VIDEO_CFG_MIN = 0
|
export const VIDEO_CFG_MIN = 0
|
||||||
export const VIDEO_CFG_MAX = 20
|
export const VIDEO_CFG_MAX = 20
|
||||||
@@ -44,6 +50,17 @@ export function clampImageCfg(raw: unknown, fallback = 1) {
|
|||||||
return Math.min(IMAGE_CFG_MAX, Math.max(IMAGE_CFG_MIN, Math.round(snapped * 10) / 10))
|
return Math.min(IMAGE_CFG_MAX, Math.max(IMAGE_CFG_MIN, Math.round(snapped * 10) / 10))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clampImageScaleMegapixels(raw: unknown, fallback = IMAGE_SCALE_MP_DEFAULT) {
|
||||||
|
const value = Number(raw)
|
||||||
|
if (!Number.isFinite(value)) return fallback
|
||||||
|
const snapped = Math.round(value / IMAGE_SCALE_MP_STEP) * IMAGE_SCALE_MP_STEP
|
||||||
|
return Math.min(IMAGE_SCALE_MP_MAX, Math.max(IMAGE_SCALE_MP_MIN, Math.round(snapped * 100) / 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseImageScaleToTotalPixels(raw: unknown) {
|
||||||
|
return raw === true || raw === 'true' || raw === '1' || raw === 1
|
||||||
|
}
|
||||||
|
|
||||||
export function clampVideoCfg(raw: unknown, fallback = 1.5) {
|
export function clampVideoCfg(raw: unknown, fallback = 1.5) {
|
||||||
const value = Number(raw)
|
const value = Number(raw)
|
||||||
if (!Number.isFinite(value)) return fallback
|
if (!Number.isFinite(value)) return fallback
|
||||||
|
|||||||
Reference in New Issue
Block a user