Flatten image edit controls and let CFG/steps be typed past the old slider caps.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+108
-68
@@ -173,21 +173,37 @@
|
|||||||
<p v-else-if="identityPrompting" class="mb-2 text-xs text-zinc-500">
|
<p v-else-if="identityPrompting" class="mb-2 text-xs text-zinc-500">
|
||||||
Write camera, action, and audio. That is the dynamic action prompt. We concatenate the <span class="font-medium text-zinc-300"><Picture 1></span> identity prefix in front of it and send the result to <span class="font-medium text-zinc-300">Reference to Video</span>, not Image to Video.
|
Write camera, action, and audio. That is the dynamic action prompt. We concatenate the <span class="font-medium text-zinc-300"><Picture 1></span> identity prefix in front of it and send the result to <span class="font-medium text-zinc-300">Reference to Video</span>, not Image to Video.
|
||||||
</p>
|
</p>
|
||||||
<textarea
|
<div
|
||||||
v-model="prompt"
|
class="grid gap-3"
|
||||||
:rows="shotScriptMode || identityPrompting ? 12 : 6"
|
:class="studioMode === 'edit' ? 'md:grid-cols-2' : ''"
|
||||||
class="hidden max-h-[min(28rem,70vh)] w-full overflow-y-auto rounded-2xl border border-white/10 bg-zinc-950/70 px-4 py-3 text-sm outline-none ring-amber-300/40 placeholder:text-zinc-600 [overflow-wrap:anywhere] focus:ring-2 md:block"
|
|
||||||
:placeholder="promptPlaceholder"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="w-full rounded-2xl border border-white/10 bg-zinc-950/70 px-4 py-3 text-left md:hidden"
|
|
||||||
@click="promptDrawerOpen = true"
|
|
||||||
>
|
>
|
||||||
<p v-if="prompt.trim()" class="line-clamp-4 whitespace-pre-wrap text-sm text-zinc-200 [overflow-wrap:anywhere]">{{ prompt }}</p>
|
<div class="min-w-0">
|
||||||
<p v-else class="text-sm text-zinc-600">{{ promptPlaceholder }}</p>
|
<textarea
|
||||||
<span class="mt-2 inline-block text-xs text-amber-200">{{ prompt.trim() ? 'View / edit prompt' : 'Write prompt' }}</span>
|
v-model="prompt"
|
||||||
</button>
|
:rows="shotScriptMode || identityPrompting ? 12 : 6"
|
||||||
|
class="hidden max-h-[min(28rem,70vh)] w-full overflow-y-auto rounded-2xl border border-white/10 bg-zinc-950/70 px-4 py-3 text-sm outline-none ring-amber-300/40 placeholder:text-zinc-600 [overflow-wrap:anywhere] focus:ring-2 md:block"
|
||||||
|
:placeholder="promptPlaceholder"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full rounded-2xl border border-white/10 bg-zinc-950/70 px-4 py-3 text-left md:hidden"
|
||||||
|
@click="promptDrawerOpen = true"
|
||||||
|
>
|
||||||
|
<p v-if="prompt.trim()" class="line-clamp-4 whitespace-pre-wrap text-sm text-zinc-200 [overflow-wrap:anywhere]">{{ prompt }}</p>
|
||||||
|
<p v-else class="text-sm text-zinc-600">{{ promptPlaceholder }}</p>
|
||||||
|
<span class="mt-2 inline-block text-xs text-amber-200">{{ prompt.trim() ? 'View / edit prompt' : 'Write prompt' }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<label v-if="studioMode === 'edit'" class="block min-w-0 text-sm">
|
||||||
|
<span class="mb-2 block font-medium text-zinc-300">Negative prompt</span>
|
||||||
|
<textarea
|
||||||
|
v-model="editNegative"
|
||||||
|
rows="2"
|
||||||
|
class="min-h-[4.5rem] w-full rounded-2xl border border-white/10 bg-zinc-950/70 px-4 py-3 text-sm outline-none ring-amber-300/40 placeholder:text-zinc-600 [overflow-wrap:anywhere] focus:ring-2 md:h-[calc(100%-1.75rem)] md:min-h-[8.5rem]"
|
||||||
|
placeholder="What to avoid…"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<p v-if="shotScriptMode && studioMode === 'video' && parsedShots.length" class="mt-2 text-xs text-zinc-500 md:hidden">
|
<p v-if="shotScriptMode && studioMode === 'video' && parsedShots.length" class="mt-2 text-xs text-zinc-500 md:hidden">
|
||||||
{{ parsedShots.length }} shot{{ parsedShots.length === 1 ? '' : 's' }} in this script. Open the prompt to scroll through them.
|
{{ parsedShots.length }} shot{{ parsedShots.length === 1 ? '' : 's' }} in this script. Open the prompt to scroll through them.
|
||||||
</p>
|
</p>
|
||||||
@@ -601,16 +617,50 @@
|
|||||||
@remove="deleteGenerationPreset"
|
@remove="deleteGenerationPreset"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="max-w-md space-y-2">
|
<div v-else class="space-y-3">
|
||||||
<LoraSelect v-model="imageLoraStack" :names="imageLoraOptions" label="LoRA" empty-label="None" compact />
|
<p class="text-xs text-zinc-500">Flux.2 Klein 9B. Shares the desktop GPU with video.</p>
|
||||||
<GenerationPresetBar
|
<div class="max-w-md space-y-2">
|
||||||
:presets="imageGenerationPresets"
|
<LoraSelect v-model="imageLoraStack" :names="imageLoraOptions" label="LoRA" empty-label="None" compact />
|
||||||
v-model:selected-id="imagePresetId"
|
<GenerationPresetBar
|
||||||
v-model:name="imagePresetName"
|
:presets="imageGenerationPresets"
|
||||||
@load="loadGenerationPreset"
|
v-model:selected-id="imagePresetId"
|
||||||
@save="saveGenerationPreset"
|
v-model:name="imagePresetName"
|
||||||
@remove="deleteGenerationPreset"
|
@load="loadGenerationPreset"
|
||||||
/>
|
@save="saveGenerationPreset"
|
||||||
|
@remove="deleteGenerationPreset"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block text-zinc-400">Steps</span>
|
||||||
|
<input
|
||||||
|
v-model.number="editSteps"
|
||||||
|
type="number"
|
||||||
|
:min="IMAGE_STEPS_MIN"
|
||||||
|
:max="IMAGE_STEPS_MAX"
|
||||||
|
step="1"
|
||||||
|
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 focus:ring-2"
|
||||||
|
@change="onEditStepsNumber"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block text-zinc-400">CFG</span>
|
||||||
|
<input
|
||||||
|
v-model.number="editCfg"
|
||||||
|
type="number"
|
||||||
|
:min="IMAGE_CFG_MIN"
|
||||||
|
:max="IMAGE_CFG_MAX"
|
||||||
|
:step="IMAGE_CFG_STEP"
|
||||||
|
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 focus:ring-2"
|
||||||
|
@change="onEditCfgNumber"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<label class="col-span-2 block text-sm">
|
||||||
|
<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">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>
|
||||||
</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">
|
||||||
@@ -660,11 +710,11 @@
|
|||||||
:min="CFG_MIN"
|
:min="CFG_MIN"
|
||||||
:max="CFG_MAX"
|
:max="CFG_MAX"
|
||||||
:step="CFG_STEP"
|
:step="CFG_STEP"
|
||||||
class="w-20 rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 focus:ring-2"
|
class="w-24 rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 focus:ring-2"
|
||||||
@change="onCfgNumber"
|
@change="onCfgNumber"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-[11px] text-zinc-500">{{ ltxVideo ? 'Default 3.5 for PinkCherry' : (turbo ? 'Default 1.5 with Turbo LoRA' : 'Default 4.0 with High-Fidelity') }}</p>
|
<p class="mt-1 text-[11px] text-zinc-500">{{ ltxVideo ? 'Default 3.5 for PinkCherry' : (turbo ? 'Default 1.5 with Turbo LoRA' : 'Default 4.0 with High-Fidelity') }}. Type 0–20 (0.1 steps); the box is the source of truth.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<label class="text-sm">
|
<label class="text-sm">
|
||||||
@@ -688,7 +738,6 @@
|
|||||||
<span class="mb-1 block text-zinc-400">Seed</span>
|
<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" placeholder="random">
|
<input v-model="seedInput" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2" placeholder="random">
|
||||||
</label>
|
</label>
|
||||||
<LoraSelect v-model="videoLoraStack" :names="videoLoraOptions" label="LoRA" empty-label="None" :clip-strength="false" />
|
|
||||||
<div>
|
<div>
|
||||||
<p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">Sampler & scheduler</p>
|
<p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">Sampler & scheduler</p>
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
@@ -713,35 +762,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details v-else class="rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3">
|
|
||||||
<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">
|
|
||||||
Flux.2 Klein 9B. Shares the desktop GPU with video.
|
|
||||||
</p>
|
|
||||||
<label class="block text-sm">
|
|
||||||
<span class="mb-1 block text-zinc-400">Negative prompt</span>
|
|
||||||
<textarea v-model="editNegative" rows="2" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm" />
|
|
||||||
</label>
|
|
||||||
<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="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 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>
|
|
||||||
<LoraSelect v-model="imageLoraStack" :names="imageLoraOptions" label="LoRA" empty-label="None" />
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<label
|
<label
|
||||||
v-if="studioMode === 'video' && queuedExtensionCount > 0"
|
v-if="studioMode === 'video' && queuedExtensionCount > 0"
|
||||||
class="flex cursor-pointer items-start gap-3 rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3"
|
class="flex cursor-pointer items-start gap-3 rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3"
|
||||||
@@ -1605,7 +1625,20 @@ import {
|
|||||||
readLoraStack,
|
readLoraStack,
|
||||||
type LoraStackItem
|
type LoraStackItem
|
||||||
} from '~/utils/loras'
|
} from '~/utils/loras'
|
||||||
import type { GenerationPreset } from '~/utils/generationPresets'
|
import {
|
||||||
|
clampImageCfg,
|
||||||
|
clampImageSteps,
|
||||||
|
clampVideoCfg,
|
||||||
|
IMAGE_CFG_MAX,
|
||||||
|
IMAGE_CFG_MIN,
|
||||||
|
IMAGE_CFG_STEP,
|
||||||
|
IMAGE_STEPS_MAX,
|
||||||
|
IMAGE_STEPS_MIN,
|
||||||
|
VIDEO_CFG_MAX,
|
||||||
|
VIDEO_CFG_MIN,
|
||||||
|
VIDEO_CFG_STEP,
|
||||||
|
type GenerationPreset
|
||||||
|
} from '~/utils/generationPresets'
|
||||||
import {
|
import {
|
||||||
coerceVideoWorkflow,
|
coerceVideoWorkflow,
|
||||||
composeVideoWorkflow,
|
composeVideoWorkflow,
|
||||||
@@ -1800,9 +1833,9 @@ const videoPresetId = ref('')
|
|||||||
const imagePresetId = ref('')
|
const imagePresetId = ref('')
|
||||||
const videoPresetName = ref('')
|
const videoPresetName = ref('')
|
||||||
const imagePresetName = ref('')
|
const imagePresetName = ref('')
|
||||||
const CFG_MIN = 1
|
const CFG_MIN = VIDEO_CFG_MIN
|
||||||
const CFG_MAX = 10
|
const CFG_MAX = VIDEO_CFG_MAX
|
||||||
const CFG_STEP = 0.5
|
const CFG_STEP = VIDEO_CFG_STEP
|
||||||
const CFG_TURBO = 1.5
|
const CFG_TURBO = 1.5
|
||||||
const CFG_QUALITY = 4
|
const CFG_QUALITY = 4
|
||||||
const cfg = ref(CFG_TURBO)
|
const cfg = ref(CFG_TURBO)
|
||||||
@@ -2847,8 +2880,15 @@ function clampDuration(value: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clampCfg(value: number) {
|
function clampCfg(value: number) {
|
||||||
if (!Number.isFinite(value)) return turbo.value ? CFG_TURBO : CFG_QUALITY
|
return clampVideoCfg(value, turbo.value ? CFG_TURBO : CFG_QUALITY)
|
||||||
return Math.min(CFG_MAX, Math.max(CFG_MIN, Math.round(value / CFG_STEP) * CFG_STEP))
|
}
|
||||||
|
|
||||||
|
function onEditStepsNumber() {
|
||||||
|
editSteps.value = clampImageSteps(editSteps.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEditCfgNumber() {
|
||||||
|
editCfg.value = clampImageCfg(editCfg.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCfgSlider(event: Event) {
|
function onCfgSlider(event: Event) {
|
||||||
@@ -2925,8 +2965,8 @@ function currentPresetSnapshot() {
|
|||||||
kind: 'image' as const,
|
kind: 'image' as const,
|
||||||
loraStack: filterLoraStackForStudio(imageLoraStack.value, ltxEnabled.value),
|
loraStack: filterLoraStackForStudio(imageLoraStack.value, ltxEnabled.value),
|
||||||
settings: {
|
settings: {
|
||||||
steps: editSteps.value,
|
steps: clampImageSteps(editSteps.value),
|
||||||
cfg: editCfg.value,
|
cfg: clampImageCfg(editCfg.value),
|
||||||
negative: editNegative.value
|
negative: editNegative.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2954,8 +2994,8 @@ function applyGenerationPreset(preset: GenerationPreset) {
|
|||||||
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 }
|
||||||
if (typeof settings.steps === 'number') editSteps.value = settings.steps
|
if (typeof settings.steps === 'number') editSteps.value = clampImageSteps(settings.steps)
|
||||||
if (typeof settings.cfg === 'number') editCfg.value = 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
|
||||||
imagePresetId.value = preset.id
|
imagePresetId.value = preset.id
|
||||||
imagePresetName.value = preset.name
|
imagePresetName.value = preset.name
|
||||||
@@ -4603,8 +4643,8 @@ async function editImage() {
|
|||||||
if (editRefFile.value) body.append('image2', editRefFile.value)
|
if (editRefFile.value) body.append('image2', editRefFile.value)
|
||||||
body.append('prompt', prompt.value.trim())
|
body.append('prompt', prompt.value.trim())
|
||||||
body.append('negative', editNegative.value)
|
body.append('negative', editNegative.value)
|
||||||
body.append('steps', String(editSteps.value))
|
body.append('steps', String(clampImageSteps(editSteps.value)))
|
||||||
body.append('cfg', String(editCfg.value))
|
body.append('cfg', String(clampImageCfg(editCfg.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))
|
||||||
|
|||||||
+3
-14
@@ -2,18 +2,7 @@ import { addStudioJob, kickStudioQueue, listStudioJobs } from '~/server/utils/st
|
|||||||
import { parsePostedLoraStack, listStudioLoras, persistLoraFields } from '~/server/utils/loras'
|
import { parsePostedLoraStack, listStudioLoras, persistLoraFields } from '~/server/utils/loras'
|
||||||
import { comfyConfigured } from '~/server/utils/comfy'
|
import { comfyConfigured } from '~/server/utils/comfy'
|
||||||
import { imageDimensions } from '~/server/utils/resolution'
|
import { imageDimensions } from '~/server/utils/resolution'
|
||||||
|
import { clampImageCfg, clampImageSteps } from '~/utils/generationPresets'
|
||||||
function parseSteps(raw: unknown) {
|
|
||||||
const value = Number(raw)
|
|
||||||
if (!Number.isFinite(value)) return 20
|
|
||||||
return Math.min(40, Math.max(1, Math.round(value)))
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseCfg(raw: unknown) {
|
|
||||||
const value = Number(raw)
|
|
||||||
if (!Number.isFinite(value)) return 1
|
|
||||||
return Math.min(8, Math.max(0.5, Math.round(value * 10) / 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
function parsePasses(raw: string | undefined) {
|
function parsePasses(raw: string | undefined) {
|
||||||
if (!raw) return [] as { prompt: string }[]
|
if (!raw) return [] as { prompt: string }[]
|
||||||
@@ -75,8 +64,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
const folderLocked = Boolean(destFolder?.protected && !destFolder.unlocked)
|
const folderLocked = Boolean(destFolder?.protected && !destFolder.unlocked)
|
||||||
const hideInput = fields.hideInput === 'true'
|
const hideInput = fields.hideInput === 'true'
|
||||||
const hideThumbnail = fields.hideThumbnail === 'true'
|
const hideThumbnail = fields.hideThumbnail === 'true'
|
||||||
const steps = parseSteps(fields.steps)
|
const steps = clampImageSteps(fields.steps)
|
||||||
const cfg = parseCfg(fields.cfg)
|
const cfg = clampImageCfg(fields.cfg)
|
||||||
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)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { join } from 'node:path'
|
|||||||
import { composeShotPrompt } from '~/utils/globalLocks'
|
import { composeShotPrompt } from '~/utils/globalLocks'
|
||||||
import { parsePostedLoraStack, listStudioLoras, persistLoraFields } from '~/server/utils/loras'
|
import { parsePostedLoraStack, listStudioLoras, persistLoraFields } from '~/server/utils/loras'
|
||||||
import { readLoraStack } from '~/utils/loras'
|
import { readLoraStack } from '~/utils/loras'
|
||||||
|
import { clampVideoCfg } from '~/utils/generationPresets'
|
||||||
import { defaultVideoSteps, isLtxWorkflow, LTX_DISABLED_MESSAGE, ltxWorkflowEnabled, parseVideoWorkflow } from '~/utils/videoModels'
|
import { defaultVideoSteps, isLtxWorkflow, LTX_DISABLED_MESSAGE, ltxWorkflowEnabled, parseVideoWorkflow } from '~/utils/videoModels'
|
||||||
|
|
||||||
const SAMPLERS = new Set(['res_multistep', 'euler', 'dpmpp_2m'])
|
const SAMPLERS = new Set(['res_multistep', 'euler', 'dpmpp_2m'])
|
||||||
@@ -24,11 +25,7 @@ function parseFps(raw: unknown) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseCfg(raw: unknown, turbo: boolean) {
|
function parseCfg(raw: unknown, turbo: boolean) {
|
||||||
const fallback = turbo ? 1.5 : 4
|
return clampVideoCfg(raw, turbo ? 1.5 : 4)
|
||||||
const value = Number(raw)
|
|
||||||
if (!Number.isFinite(value)) return fallback
|
|
||||||
const snapped = Math.round(value * 2) / 2
|
|
||||||
return Math.min(10, Math.max(1, snapped))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseSampler(raw: unknown) {
|
function parseSampler(raw: unknown) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { addStudioJob, kickStudioQueue, listStudioJobs } from '~/server/utils/studioQueue'
|
import { addStudioJob, kickStudioQueue, listStudioJobs } from '~/server/utils/studioQueue'
|
||||||
import { listStudioLoras, parsePostedLoraStack, parseShotLoraStacks, persistLoraFields } from '~/server/utils/loras'
|
import { listStudioLoras, parsePostedLoraStack, parseShotLoraStacks, persistLoraFields } from '~/server/utils/loras'
|
||||||
import type { LoraStackItem } from '~/utils/loras'
|
import type { LoraStackItem } from '~/utils/loras'
|
||||||
|
import { clampVideoCfg } from '~/utils/generationPresets'
|
||||||
import { defaultVideoSteps, isLtxWorkflow, isTextToVideo, LTX_DISABLED_MESSAGE, ltxWorkflowEnabled, parseVideoWorkflow } from '~/utils/videoModels'
|
import { defaultVideoSteps, isLtxWorkflow, isTextToVideo, LTX_DISABLED_MESSAGE, ltxWorkflowEnabled, parseVideoWorkflow } from '~/utils/videoModels'
|
||||||
import { allowIdentityRefs, normalizePermanenceRefs, resolveGlobalLocks, type PermanenceRef } from '~/utils/globalLocks'
|
import { allowIdentityRefs, normalizePermanenceRefs, resolveGlobalLocks, type PermanenceRef } from '~/utils/globalLocks'
|
||||||
|
|
||||||
@@ -66,10 +67,7 @@ function parseFps(raw: string | undefined) {
|
|||||||
|
|
||||||
function parseCfg(raw: string | undefined, turbo: boolean, ltx = false) {
|
function parseCfg(raw: string | undefined, turbo: boolean, ltx = false) {
|
||||||
const fallback = ltx ? 3.5 : (turbo ? 1.5 : 4)
|
const fallback = ltx ? 3.5 : (turbo ? 1.5 : 4)
|
||||||
const value = Number(raw)
|
return clampVideoCfg(raw, fallback)
|
||||||
if (!Number.isFinite(value)) return fallback
|
|
||||||
const snapped = Math.round(value * 2) / 2
|
|
||||||
return Math.min(10, Math.max(1, snapped))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseSampler(raw: string | undefined, ltx = false) {
|
function parseSampler(raw: string | undefined, ltx = false) {
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { join } from 'node:path'
|
|||||||
import { ownerRoot } from '~/server/utils/library'
|
import { ownerRoot } from '~/server/utils/library'
|
||||||
import { filterLoraStackForStudio, normalizeLoraStack } from '~/utils/loras'
|
import { filterLoraStackForStudio, normalizeLoraStack } from '~/utils/loras'
|
||||||
import {
|
import {
|
||||||
|
clampImageCfg,
|
||||||
|
clampImageSteps,
|
||||||
|
clampVideoCfg,
|
||||||
normalizePresetName,
|
normalizePresetName,
|
||||||
parseGenerationPresetKind,
|
parseGenerationPresetKind,
|
||||||
type GenerationPreset,
|
type GenerationPreset,
|
||||||
@@ -54,12 +57,6 @@ function mutate<T>(owner: string, fn: (presets: GenerationPreset[]) => T): Promi
|
|||||||
return run
|
return run
|
||||||
}
|
}
|
||||||
|
|
||||||
function clampInt(raw: unknown, fallback: number, min: number, max: number) {
|
|
||||||
const value = Number(raw)
|
|
||||||
if (!Number.isFinite(value)) return fallback
|
|
||||||
return Math.min(max, Math.max(min, Math.round(value)))
|
|
||||||
}
|
|
||||||
|
|
||||||
function clampNum(raw: unknown, fallback: number, min: number, max: number, step = 0.1) {
|
function clampNum(raw: unknown, fallback: number, min: number, max: number, step = 0.1) {
|
||||||
const value = Number(raw)
|
const value = Number(raw)
|
||||||
if (!Number.isFinite(value)) return fallback
|
if (!Number.isFinite(value)) return fallback
|
||||||
@@ -77,7 +74,7 @@ function sanitizeVideoSettings(raw: unknown): VideoPresetSettings {
|
|||||||
return {
|
return {
|
||||||
workflow,
|
workflow,
|
||||||
turbo,
|
turbo,
|
||||||
cfg: clampNum(rec.cfg, turbo ? 1.5 : 4, 1, 10, 0.5),
|
cfg: clampVideoCfg(rec.cfg, turbo ? 1.5 : 4),
|
||||||
fps: rec.fps === 12 || rec.fps === 30 || rec.fps === 24 ? Number(rec.fps) : 24,
|
fps: rec.fps === 12 || rec.fps === 30 || rec.fps === 24 ? Number(rec.fps) : 24,
|
||||||
samplerName: sampler === 'euler' || sampler === 'dpmpp_2m' || sampler === 'res_multistep' ? sampler : 'res_multistep',
|
samplerName: sampler === 'euler' || sampler === 'dpmpp_2m' || sampler === 'res_multistep' ? sampler : 'res_multistep',
|
||||||
scheduler: scheduler === 'ddim_uniform' || scheduler === 'sgm_uniform' || scheduler === 'simple' ? scheduler : 'simple',
|
scheduler: scheduler === 'ddim_uniform' || scheduler === 'sgm_uniform' || scheduler === 'simple' ? scheduler : 'simple',
|
||||||
@@ -90,8 +87,8 @@ function sanitizeVideoSettings(raw: unknown): VideoPresetSettings {
|
|||||||
function sanitizeImageSettings(raw: unknown): ImagePresetSettings {
|
function sanitizeImageSettings(raw: unknown): ImagePresetSettings {
|
||||||
const rec = raw && typeof raw === 'object' ? raw as Record<string, unknown> : {}
|
const rec = raw && typeof raw === 'object' ? raw as Record<string, unknown> : {}
|
||||||
return {
|
return {
|
||||||
steps: clampInt(rec.steps, 20, 1, 40),
|
steps: clampImageSteps(rec.steps),
|
||||||
cfg: clampNum(rec.cfg, 1, 0.5, 4, 0.1),
|
cfg: clampImageCfg(rec.cfg),
|
||||||
negative: String(rec.negative || '').slice(0, 2000)
|
negative: String(rec.negative || '').slice(0, 2000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,36 @@ export type ImagePresetSettings = {
|
|||||||
negative?: string
|
negative?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const IMAGE_STEPS_MIN = 1
|
||||||
|
export const IMAGE_STEPS_MAX = 50
|
||||||
|
export const IMAGE_CFG_MIN = 0
|
||||||
|
export const IMAGE_CFG_MAX = 20
|
||||||
|
export const IMAGE_CFG_STEP = 0.1
|
||||||
|
|
||||||
|
export const VIDEO_CFG_MIN = 0
|
||||||
|
export const VIDEO_CFG_MAX = 20
|
||||||
|
export const VIDEO_CFG_STEP = 0.1
|
||||||
|
|
||||||
|
export function clampImageSteps(raw: unknown, fallback = 20) {
|
||||||
|
const value = Number(raw)
|
||||||
|
if (!Number.isFinite(value)) return fallback
|
||||||
|
return Math.min(IMAGE_STEPS_MAX, Math.max(IMAGE_STEPS_MIN, Math.round(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clampImageCfg(raw: unknown, fallback = 1) {
|
||||||
|
const value = Number(raw)
|
||||||
|
if (!Number.isFinite(value)) return fallback
|
||||||
|
const snapped = Math.round(value / IMAGE_CFG_STEP) * IMAGE_CFG_STEP
|
||||||
|
return Math.min(IMAGE_CFG_MAX, Math.max(IMAGE_CFG_MIN, Math.round(snapped * 10) / 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clampVideoCfg(raw: unknown, fallback = 1.5) {
|
||||||
|
const value = Number(raw)
|
||||||
|
if (!Number.isFinite(value)) return fallback
|
||||||
|
const snapped = Math.round(value / VIDEO_CFG_STEP) * VIDEO_CFG_STEP
|
||||||
|
return Math.min(VIDEO_CFG_MAX, Math.max(VIDEO_CFG_MIN, Math.round(snapped * 10) / 10))
|
||||||
|
}
|
||||||
|
|
||||||
export type GenerationPreset = {
|
export type GenerationPreset = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
Reference in New Issue
Block a user