diff --git a/pages/index.vue b/pages/index.vue
index fec4ca1..aff8698 100644
--- a/pages/index.vue
+++ b/pages/index.vue
@@ -173,21 +173,37 @@
Write camera, action, and audio. That is the dynamic action prompt. We concatenate the <Picture 1> identity prefix in front of it and send the result to Reference to Video , not Image to Video.
-
-
- {{ prompt }}
- {{ promptPlaceholder }}
- {{ prompt.trim() ? 'View / edit prompt' : 'Write prompt' }}
-
+
+
+
+ {{ prompt }}
+ {{ promptPlaceholder }}
+ {{ prompt.trim() ? 'View / edit prompt' : 'Write prompt' }}
+
+
+
+ Negative prompt
+
+
+
{{ parsedShots.length }} shot{{ parsedShots.length === 1 ? '' : 's' }} in this script. Open the prompt to scroll through them.
@@ -601,16 +617,50 @@
@remove="deleteGenerationPreset"
/>
-
-
-
+
@@ -660,11 +710,11 @@
:min="CFG_MIN"
:max="CFG_MAX"
: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"
>
- {{ ltxVideo ? 'Default 3.5 for PinkCherry' : (turbo ? 'Default 1.5 with Turbo LoRA' : 'Default 4.0 with High-Fidelity') }}
+ {{ 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.
@@ -688,7 +738,6 @@
Seed
-
Sampler & scheduler
@@ -713,35 +762,6 @@
-
- Image edit settings
-
-
- Flux.2 Klein 9B. Shares the desktop GPU with video.
-
-
- Negative prompt
-
-
-
-
- Steps · {{ editSteps }}
-
-
-
- CFG · {{ editCfg }}
-
-
-
- Seed
-
-
-
-
-
Graph default is 20 steps, CFG 1, euler. A second still switches the two-image branch on.
-
-
-
{
const folderLocked = Boolean(destFolder?.protected && !destFolder.unlocked)
const hideInput = fields.hideInput === 'true'
const hideThumbnail = fields.hideThumbnail === 'true'
- const steps = parseSteps(fields.steps)
- const cfg = parseCfg(fields.cfg)
+ const steps = clampImageSteps(fields.steps)
+ const cfg = clampImageCfg(fields.cfg)
const seed = fields.seed && fields.seed !== 'random'
? Number(fields.seed)
: Math.floor(Math.random() * 2_147_483_647)
diff --git a/server/api/extend.post.ts b/server/api/extend.post.ts
index a94df07..0f4c2d9 100644
--- a/server/api/extend.post.ts
+++ b/server/api/extend.post.ts
@@ -3,6 +3,7 @@ import { join } from 'node:path'
import { composeShotPrompt } from '~/utils/globalLocks'
import { parsePostedLoraStack, listStudioLoras, persistLoraFields } from '~/server/utils/loras'
import { readLoraStack } from '~/utils/loras'
+import { clampVideoCfg } from '~/utils/generationPresets'
import { defaultVideoSteps, isLtxWorkflow, LTX_DISABLED_MESSAGE, ltxWorkflowEnabled, parseVideoWorkflow } from '~/utils/videoModels'
const SAMPLERS = new Set(['res_multistep', 'euler', 'dpmpp_2m'])
@@ -24,11 +25,7 @@ function parseFps(raw: unknown) {
}
function parseCfg(raw: unknown, turbo: boolean) {
- const fallback = 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))
+ return clampVideoCfg(raw, turbo ? 1.5 : 4)
}
function parseSampler(raw: unknown) {
diff --git a/server/api/generate.post.ts b/server/api/generate.post.ts
index 91ea93a..a43f8cd 100644
--- a/server/api/generate.post.ts
+++ b/server/api/generate.post.ts
@@ -1,6 +1,7 @@
import { addStudioJob, kickStudioQueue, listStudioJobs } from '~/server/utils/studioQueue'
import { listStudioLoras, parsePostedLoraStack, parseShotLoraStacks, persistLoraFields } from '~/server/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 { 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) {
const fallback = ltx ? 3.5 : (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))
+ return clampVideoCfg(raw, fallback)
}
function parseSampler(raw: string | undefined, ltx = false) {
diff --git a/server/utils/generationPresets.ts b/server/utils/generationPresets.ts
index 4dddce5..d30d50d 100644
--- a/server/utils/generationPresets.ts
+++ b/server/utils/generationPresets.ts
@@ -3,6 +3,9 @@ import { join } from 'node:path'
import { ownerRoot } from '~/server/utils/library'
import { filterLoraStackForStudio, normalizeLoraStack } from '~/utils/loras'
import {
+ clampImageCfg,
+ clampImageSteps,
+ clampVideoCfg,
normalizePresetName,
parseGenerationPresetKind,
type GenerationPreset,
@@ -54,12 +57,6 @@ function mutate(owner: string, fn: (presets: GenerationPreset[]) => T): Promi
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) {
const value = Number(raw)
if (!Number.isFinite(value)) return fallback
@@ -77,7 +74,7 @@ function sanitizeVideoSettings(raw: unknown): VideoPresetSettings {
return {
workflow,
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,
samplerName: sampler === 'euler' || sampler === 'dpmpp_2m' || sampler === 'res_multistep' ? sampler : 'res_multistep',
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 {
const rec = raw && typeof raw === 'object' ? raw as Record : {}
return {
- steps: clampInt(rec.steps, 20, 1, 40),
- cfg: clampNum(rec.cfg, 1, 0.5, 4, 0.1),
+ steps: clampImageSteps(rec.steps),
+ cfg: clampImageCfg(rec.cfg),
negative: String(rec.negative || '').slice(0, 2000)
}
}
diff --git a/utils/generationPresets.ts b/utils/generationPresets.ts
index c2923d7..6acdfb7 100644
--- a/utils/generationPresets.ts
+++ b/utils/generationPresets.ts
@@ -21,6 +21,36 @@ export type ImagePresetSettings = {
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 = {
id: string
name: string