Files
aigen/server/utils/generationPresets.ts
T
TowstyandCursor ab5477c999 Keep the Klein Concept LoRA on xAIGen only.
AIGen hides the sliders, ignores snofs strengths, and strips the SNOFS node so that file cannot be queued from the public studio.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 07:08:33 -05:00

236 lines
8.9 KiB
TypeScript

import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { ownerRoot } from '~/server/utils/library'
import { filterLoraStackForStudio, normalizeLoraStack } from '~/utils/loras'
import {
clampImageCfg,
clampImageScaleMegapixels,
clampImageSteps,
clampVideoCfg,
normalizePresetName,
parseGenerationPresetKind,
type GenerationPreset,
type GenerationPresetKind,
type ImagePresetSettings,
type VideoPresetSettings
} from '~/utils/generationPresets'
import {
IMAGE_V2_CFG_DEFAULT,
IMAGE_V2_CONSISTENCY_CLIP,
IMAGE_V2_CONSISTENCY_MODEL,
IMAGE_V2_SNOFS_CLIP,
IMAGE_V2_SNOFS_MODEL,
IMAGE_V2_STEPS_DEFAULT,
IMAGE_V2_KREA_CFG,
IMAGE_V2_KREA_STEPS,
IMAGE_V2_DENOISE_DEFAULT,
clampImageV2Denoise,
clampImageV2Size,
clampImageV2Strength,
parseImageV2Engine,
parseImageV2Mode,
parseImageV2Task,
type ImageV2PresetSettings
} from '~/utils/imageV2'
import { coerceVideoWorkflow, isXaigenStudio, parseVideoWorkflow } from '~/utils/videoModels'
const writeChains = new Map<string, Promise<unknown>>()
function presetsPath(owner: string) {
return join(ownerRoot(owner), 'generation-presets.json')
}
function ensureOwner(owner: string) {
mkdirSync(ownerRoot(owner), { recursive: true })
}
function readPresets(owner: string): GenerationPreset[] {
ensureOwner(owner)
const path = presetsPath(owner)
if (!existsSync(path)) return []
try {
const parsed = JSON.parse(readFileSync(path, 'utf8'))
return Array.isArray(parsed) ? parsed.map(normalizeStoredPreset).filter((item): item is GenerationPreset => Boolean(item)) : []
} catch {
return []
}
}
function writePresets(owner: string, presets: GenerationPreset[]) {
ensureOwner(owner)
const path = presetsPath(owner)
const tmp = `${path}.tmp`
writeFileSync(tmp, JSON.stringify(presets, null, 2))
renameSync(tmp, path)
}
function mutate<T>(owner: string, fn: (presets: GenerationPreset[]) => T): Promise<T> {
const prev = writeChains.get(owner) || Promise.resolve()
const run = prev.then(() => {
const presets = readPresets(owner)
const result = fn(presets)
writePresets(owner, presets)
return result
})
writeChains.set(owner, run.then(() => undefined, () => undefined))
return run
}
function clampNum(raw: unknown, fallback: number, min: number, max: number, step = 0.1) {
const value = Number(raw)
if (!Number.isFinite(value)) return fallback
const snapped = Math.round(value / step) * step
return Math.min(max, Math.max(min, Math.round(snapped * 100) / 100))
}
function sanitizeVideoSettings(raw: unknown): VideoPresetSettings {
const rec = raw && typeof raw === 'object' ? raw as Record<string, unknown> : {}
const workflow = coerceVideoWorkflow(parseVideoWorkflow(rec.workflow))
const turbo = rec.turbo !== false
const sampler = String(rec.samplerName || '')
const scheduler = String(rec.scheduler || '')
const aspect = String(rec.aspect || 'auto')
return {
workflow,
turbo,
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',
duration: clampNum(rec.duration, 5, 0.5, 120, 0.1),
aspect: aspect === '16:9' || aspect === '9:16' || aspect === '1:1' || aspect === 'auto' ? aspect : 'auto',
sound: rec.sound !== false
}
}
function sanitizeImageSettings(raw: unknown): ImagePresetSettings {
const rec = raw && typeof raw === 'object' ? raw as Record<string, unknown> : {}
return {
steps: clampImageSteps(rec.steps),
cfg: clampImageCfg(rec.cfg),
negative: String(rec.negative || '').slice(0, 2000),
scaleToTotalPixels: rec.scaleToTotalPixels === true,
scaleMegapixels: clampImageScaleMegapixels(rec.scaleMegapixels)
}
}
function sanitizeImageV2Settings(raw: unknown): ImageV2PresetSettings {
const rec = raw && typeof raw === 'object' ? raw as Record<string, unknown> : {}
const mode = parseImageV2Mode(rec.mode) || 'edit'
const turbo = rec.turbo === true
const engine = mode === 'generate' ? parseImageV2Engine(rec.engine, 'flux') : 'flux'
return {
mode,
engine,
task: mode === 'compose' ? parseImageV2Task(rec.task, 'scene') : mode === 'refine' ? 'refine' : mode === 'generate' ? 't2i' : 'scene',
negative: String(rec.negative || '').slice(0, 2000),
snofsModel: isXaigenStudio() ? clampImageV2Strength(rec.snofsModel ?? rec.snofs_model, IMAGE_V2_SNOFS_MODEL) : 0,
snofsClip: isXaigenStudio() ? clampImageV2Strength(rec.snofsClip ?? rec.snofs_clip, IMAGE_V2_SNOFS_CLIP) : 0,
consistencyModel: clampImageV2Strength(rec.consistencyModel ?? rec.consistency_model, mode === 'generate' ? 0 : IMAGE_V2_CONSISTENCY_MODEL),
consistencyClip: clampImageV2Strength(rec.consistencyClip ?? rec.consistency_clip, mode === 'generate' ? 0 : IMAGE_V2_CONSISTENCY_CLIP),
steps: engine === 'krea' ? clampImageSteps(rec.steps, IMAGE_V2_KREA_STEPS) : turbo ? 8 : clampImageSteps(rec.steps, IMAGE_V2_STEPS_DEFAULT),
cfg: engine === 'krea' ? clampImageCfg(rec.cfg, IMAGE_V2_KREA_CFG) : turbo ? 1 : clampImageCfg(rec.cfg, IMAGE_V2_CFG_DEFAULT),
megapixels: mode === 'generate' ? undefined : clampImageScaleMegapixels(rec.megapixels ?? rec.scaleMegapixels, 1),
turbo,
strength: mode === 'refine' ? clampImageV2Denoise(rec.strength, IMAGE_V2_DENOISE_DEFAULT) : undefined,
width: mode === 'generate' ? clampImageV2Size(rec.width, rec.height, rec.aspect).width : undefined,
height: mode === 'generate' ? clampImageV2Size(rec.width, rec.height, rec.aspect).height : undefined
}
}
function sanitizeSettings(kind: GenerationPresetKind, raw: unknown) {
if (kind === 'image') return sanitizeImageSettings(raw)
if (kind === 'imagev2') return sanitizeImageV2Settings(raw)
return sanitizeVideoSettings(raw)
}
function normalizeStoredPreset(raw: unknown): GenerationPreset | null {
if (!raw || typeof raw !== 'object') return null
const rec = raw as Record<string, unknown>
const name = normalizePresetName(rec.name)
if (!name) return null
const kind = parseGenerationPresetKind(rec.kind)
const id = String(rec.id || '').trim() || crypto.randomUUID()
const createdAt = Number(rec.createdAt) || Date.now()
return {
id,
name,
kind,
createdAt,
updatedAt: Number(rec.updatedAt) || createdAt,
loraStack: normalizeLoraStack(rec.loraStack),
settings: sanitizeSettings(kind, rec.settings)
}
}
function publicPreset(preset: GenerationPreset): GenerationPreset {
const xaigen = isXaigenStudio()
const settings = preset.kind === 'video'
? { ...preset.settings, workflow: coerceVideoWorkflow((preset.settings as VideoPresetSettings).workflow) }
: preset.kind === 'imagev2' && !xaigen
? { ...preset.settings, snofsModel: 0, snofsClip: 0 }
: preset.settings
return {
...preset,
loraStack: filterLoraStackForStudio(preset.loraStack, xaigen),
settings
}
}
export function listGenerationPresets(owner: string, kind?: GenerationPresetKind) {
return readPresets(owner)
.filter(item => !kind || item.kind === kind)
.map(publicPreset)
.sort((a, b) => a.name.localeCompare(b.name) || b.updatedAt - a.updatedAt)
}
export async function saveGenerationPreset(owner: string, input: {
name: string
kind: GenerationPresetKind
loraStack?: unknown
settings?: unknown
overwrite?: boolean
}) {
const name = normalizePresetName(input.name)
if (!name) {
throw createError({ statusCode: 400, statusMessage: 'A preset name is required' })
}
const kind = parseGenerationPresetKind(input.kind)
const xaigen = isXaigenStudio()
const loraStack = filterLoraStackForStudio(input.loraStack, xaigen)
const settings = sanitizeSettings(kind, input.settings)
const now = Date.now()
return mutate(owner, (presets) => {
const existing = presets.find(item => item.kind === kind && item.name.toLowerCase() === name.toLowerCase())
if (existing && !input.overwrite) {
throw createError({ statusCode: 409, statusMessage: `Overwrite preset “${existing.name}”?` })
}
if (existing) {
existing.updatedAt = now
existing.loraStack = loraStack
existing.settings = settings
return publicPreset(existing)
}
const created: GenerationPreset = {
id: crypto.randomUUID(),
name,
kind,
createdAt: now,
updatedAt: now,
loraStack,
settings
}
presets.unshift(created)
return publicPreset(created)
})
}
export async function deleteGenerationPreset(owner: string, id: string) {
return mutate(owner, (presets) => {
const index = presets.findIndex(item => item.id === id)
if (index < 0) throw createError({ statusCode: 404, statusMessage: 'Preset not found' })
const [removed] = presets.splice(index, 1)
return removed
})
}