247 lines
11 KiB
TypeScript
247 lines
11 KiB
TypeScript
import { loraDisplayName, normalizeLoraStack, type LoraStackItem } from '~/utils/loras'
|
||
import type { ImageV2Engine, ImageV2Mode, ImageV2Task } from '~/utils/imageV2'
|
||
import { isXaigenStudio, type VideoWorkflowId } from '~/utils/videoModels'
|
||
|
||
export type StillGenerationKind = 'image' | 'imagev2'
|
||
|
||
export type StillGenerationSettings = {
|
||
kind: StillGenerationKind
|
||
mode?: ImageV2Mode
|
||
engine?: ImageV2Engine
|
||
task?: ImageV2Task
|
||
negative?: string
|
||
snofsModel?: number
|
||
snofsClip?: number
|
||
consistencyModel?: number
|
||
consistencyClip?: number
|
||
steps?: number
|
||
cfg?: number
|
||
megapixels?: number
|
||
turbo?: boolean
|
||
strength?: number
|
||
width?: number
|
||
height?: number
|
||
aspect?: string
|
||
seed?: number
|
||
scaleToTotalPixels?: boolean
|
||
scaleMegapixels?: number
|
||
loraName?: string
|
||
loraStack?: LoraStackItem[]
|
||
sourceStillId?: string
|
||
referenceStillId?: string
|
||
}
|
||
|
||
export type SettingsRow = { label: string; value: string }
|
||
|
||
function finiteNumber(raw: unknown): number | undefined {
|
||
const value = Number(raw)
|
||
return Number.isFinite(value) ? value : undefined
|
||
}
|
||
|
||
function formatNum(raw: unknown, digits = 2) {
|
||
const value = Number(raw)
|
||
if (!Number.isFinite(value)) return ''
|
||
const rounded = Number(value.toFixed(digits))
|
||
return Number.isInteger(rounded) ? String(rounded) : String(rounded)
|
||
}
|
||
|
||
function formatPair(model?: number, clip?: number) {
|
||
if (model == null && clip == null) return ''
|
||
return `${formatNum(model ?? 0)} / ${formatNum(clip ?? 0)}`
|
||
}
|
||
|
||
function formatLoras(stack?: LoraStackItem[], name?: string) {
|
||
const items = normalizeLoraStack(stack?.length ? stack : name)
|
||
if (!items.length) return ''
|
||
return items.map((item) => {
|
||
const label = loraDisplayName(item.name)
|
||
if (item.strengthModel === item.strengthClip) return `${label} ${formatNum(item.strengthModel)}`
|
||
return `${label} ${formatNum(item.strengthModel)} / ${formatNum(item.strengthClip)}`
|
||
}).join(', ')
|
||
}
|
||
|
||
export function normalizeStillSettings(raw: unknown): StillGenerationSettings | undefined {
|
||
if (!raw || typeof raw !== 'object') return undefined
|
||
const value = raw as Record<string, unknown>
|
||
const mode = value.mode === 'compose' || value.mode === 'refine' || value.mode === 'generate' || value.mode === 'edit'
|
||
? value.mode
|
||
: undefined
|
||
const engine = value.engine === 'krea' || value.engine === 'flux' ? value.engine : undefined
|
||
const kind: StillGenerationKind | undefined = value.kind === 'imagev2' || mode
|
||
? 'imagev2'
|
||
: value.kind === 'image'
|
||
? 'image'
|
||
: undefined
|
||
if (!kind) return undefined
|
||
const task = value.task === 'identity' || value.task === 'outfit' || value.task === 'face_lock' || value.task === 'refine' || value.task === 't2i' || value.task === 'scene'
|
||
? value.task
|
||
: undefined
|
||
const stack = normalizeLoraStack(value.loraStack || value.loraName)
|
||
return {
|
||
kind,
|
||
mode,
|
||
engine,
|
||
task,
|
||
negative: typeof value.negative === 'string' ? value.negative : undefined,
|
||
snofsModel: finiteNumber(value.snofsModel),
|
||
snofsClip: finiteNumber(value.snofsClip),
|
||
consistencyModel: finiteNumber(value.consistencyModel),
|
||
consistencyClip: finiteNumber(value.consistencyClip),
|
||
steps: finiteNumber(value.steps),
|
||
cfg: finiteNumber(value.cfg),
|
||
megapixels: finiteNumber(value.megapixels),
|
||
turbo: typeof value.turbo === 'boolean' ? value.turbo : undefined,
|
||
strength: finiteNumber(value.strength),
|
||
width: finiteNumber(value.width),
|
||
height: finiteNumber(value.height),
|
||
aspect: typeof value.aspect === 'string' ? value.aspect : undefined,
|
||
seed: finiteNumber(value.seed),
|
||
scaleToTotalPixels: typeof value.scaleToTotalPixels === 'boolean' ? value.scaleToTotalPixels : undefined,
|
||
scaleMegapixels: finiteNumber(value.scaleMegapixels),
|
||
loraName: typeof value.loraName === 'string' ? value.loraName : undefined,
|
||
loraStack: stack.length ? stack : undefined,
|
||
sourceStillId: typeof value.sourceStillId === 'string' ? value.sourceStillId : undefined,
|
||
referenceStillId: typeof value.referenceStillId === 'string' ? value.referenceStillId : undefined
|
||
}
|
||
}
|
||
|
||
export function inferStillPipeline(still: { filename?: string; settings?: StillGenerationSettings | null }) {
|
||
if (still.settings?.kind === 'imagev2' || still.settings?.mode) return 'imagev2' as const
|
||
if (still.settings?.kind === 'image') return 'image' as const
|
||
if (/aigen_v2_/i.test(String(still.filename || ''))) return 'imagev2' as const
|
||
if (/aigen[_-]?edit/i.test(String(still.filename || ''))) return 'image' as const
|
||
return null
|
||
}
|
||
|
||
function composeTaskLabel(task?: ImageV2Task) {
|
||
if (task === 'outfit') return 'Outfit'
|
||
if (task === 'identity') return 'Identity'
|
||
if (task === 'face_lock') return 'Face lock'
|
||
if (task === 'scene') return 'Scene'
|
||
return ''
|
||
}
|
||
|
||
export function stillModeLabel(still: { filename?: string; settings?: StillGenerationSettings | null }) {
|
||
const settings = still.settings
|
||
if (settings?.kind === 'imagev2' || settings?.mode) {
|
||
const mode = settings.mode === 'generate'
|
||
? 'Generate'
|
||
: settings.mode === 'compose'
|
||
? 'Compose'
|
||
: settings.mode === 'refine'
|
||
? 'Refine'
|
||
: 'Edit'
|
||
const task = settings.mode === 'compose' ? composeTaskLabel(settings.task) : ''
|
||
const engine = settings.engine === 'krea' ? 'Krea' : settings.engine === 'flux' ? 'Flux' : ''
|
||
return [engine ? `Image v2 · ${mode} · ${engine}` : `Image v2 · ${mode}`, task].filter(Boolean).join(' · ')
|
||
}
|
||
if (settings?.kind === 'image') return 'Image edit'
|
||
if (/aigen_v2_/i.test(String(still.filename || ''))) return 'Image v2'
|
||
if (/aigen[_-]?edit/i.test(String(still.filename || ''))) return 'Image edit'
|
||
return ''
|
||
}
|
||
|
||
export function stillSettingsRows(still: { prompt?: string; width?: number; height?: number; settings?: StillGenerationSettings | null }) {
|
||
const settings = still.settings
|
||
const rows: SettingsRow[] = []
|
||
if (still.prompt?.trim()) rows.push({ label: 'Prompt', value: still.prompt.trim() })
|
||
const mode = stillModeLabel(still)
|
||
if (mode) rows.push({ label: 'Mode', value: mode })
|
||
if (!settings) return rows
|
||
if (settings.kind === 'imagev2') {
|
||
if (settings.mode === 'generate' && settings.width && settings.height) {
|
||
rows.push({ label: 'Size', value: `${settings.width}×${settings.height}` })
|
||
} else if (settings.aspect && settings.aspect !== 'auto') {
|
||
rows.push({ label: 'Aspect', value: `${settings.aspect} fill` })
|
||
} else if (still.width && still.height) {
|
||
rows.push({ label: 'Output', value: `${still.width}×${still.height}` })
|
||
}
|
||
if (settings.engine) {
|
||
rows.push({ label: 'Engine', value: settings.engine === 'krea' ? 'Krea' : 'Flux' })
|
||
}
|
||
if (isXaigenStudio() && (settings.snofsModel != null || settings.snofsClip != null)) {
|
||
rows.push({ label: 'Concept LoRA', value: formatPair(settings.snofsModel, settings.snofsClip) })
|
||
}
|
||
if (settings.mode !== 'generate' && (settings.consistencyModel != null || settings.consistencyClip != null)) {
|
||
rows.push({ label: 'Consistency', value: formatPair(settings.consistencyModel, settings.consistencyClip) })
|
||
}
|
||
const loras = formatLoras(settings.loraStack, settings.loraName)
|
||
if (loras) rows.push({ label: 'LoRA', value: loras })
|
||
if (settings.mode === 'refine' && settings.strength != null) {
|
||
rows.push({ label: 'Strength', value: formatNum(settings.strength) })
|
||
}
|
||
if (settings.mode !== 'generate' && settings.scaleToTotalPixels !== false && settings.megapixels != null && settings.megapixels > 0) {
|
||
rows.push({ label: 'Scale to MP', value: formatNum(settings.megapixels) })
|
||
}
|
||
} else {
|
||
if (still.width && still.height) rows.push({ label: 'Output', value: `${still.width}×${still.height}` })
|
||
if (settings.aspect && settings.aspect !== 'auto') {
|
||
rows.push({ label: 'Aspect', value: `${settings.aspect} fill` })
|
||
}
|
||
if (settings.scaleToTotalPixels) {
|
||
rows.push({ label: 'Scale to MP', value: formatNum(settings.scaleMegapixels ?? 1) })
|
||
}
|
||
const loras = formatLoras(settings.loraStack, settings.loraName)
|
||
if (loras) rows.push({ label: 'LoRA', value: loras })
|
||
}
|
||
if (settings.steps != null) rows.push({ label: 'Steps', value: String(Math.round(settings.steps)) })
|
||
if (settings.cfg != null) rows.push({ label: 'CFG', value: formatNum(settings.cfg) })
|
||
if (settings.seed != null) rows.push({ label: 'Seed', value: String(Math.round(settings.seed)) })
|
||
if (settings.turbo === true) rows.push({ label: 'Turbo', value: 'On' })
|
||
if (settings.negative?.trim()) rows.push({ label: 'Negative', value: settings.negative.trim() })
|
||
return rows
|
||
}
|
||
|
||
function videoWorkflowLabel(workflow?: string) {
|
||
if (workflow === 'v2') return 'MiniMax I2V v2'
|
||
if (workflow === 't2v') return 'MiniMax T2V'
|
||
if (workflow === 'ltx') return 'LTX Pink Cherry'
|
||
if (workflow === 'ltx-t2v') return 'LTX T2V'
|
||
if (workflow === 'v1') return 'MiniMax I2V'
|
||
return workflow || ''
|
||
}
|
||
|
||
export function clipSettingsRows(clip: {
|
||
prompt?: string
|
||
width?: number
|
||
height?: number
|
||
duration?: number
|
||
steps?: number
|
||
turbo?: boolean
|
||
seed?: number
|
||
cfg?: number
|
||
fps?: number
|
||
samplerName?: string
|
||
scheduler?: string
|
||
aspect?: string
|
||
sound?: boolean
|
||
workflow?: VideoWorkflowId | string
|
||
loraName?: string
|
||
loraStack?: LoraStackItem[]
|
||
}) {
|
||
const rows: SettingsRow[] = []
|
||
if (clip.prompt?.trim()) rows.push({ label: 'Prompt', value: clip.prompt.trim() })
|
||
const workflow = videoWorkflowLabel(clip.workflow)
|
||
if (workflow) rows.push({ label: 'Workflow', value: workflow })
|
||
if (clip.width && clip.height) rows.push({ label: 'Size', value: `${clip.width}×${clip.height}` })
|
||
if (clip.aspect && clip.aspect !== 'auto') rows.push({ label: 'Aspect', value: clip.aspect })
|
||
if (typeof clip.duration === 'number' && clip.duration > 0) {
|
||
const rounded = Math.round(clip.duration * 10) / 10
|
||
rows.push({ label: 'Duration', value: Number.isInteger(rounded) ? `${rounded}s` : `${rounded.toFixed(1)}s` })
|
||
}
|
||
if (clip.steps != null) {
|
||
rows.push({ label: 'Steps', value: clip.turbo ? `${clip.steps} · Turbo` : String(clip.steps) })
|
||
} else if (clip.turbo) {
|
||
rows.push({ label: 'Turbo', value: 'On' })
|
||
}
|
||
if (clip.cfg != null) rows.push({ label: 'CFG', value: formatNum(clip.cfg) })
|
||
if (clip.fps != null) rows.push({ label: 'FPS', value: String(clip.fps) })
|
||
if (clip.samplerName) rows.push({ label: 'Sampler', value: clip.samplerName })
|
||
if (clip.scheduler) rows.push({ label: 'Scheduler', value: clip.scheduler })
|
||
if (typeof clip.sound === 'boolean') rows.push({ label: 'Sound', value: clip.sound ? 'On' : 'Off' })
|
||
if (clip.seed != null) rows.push({ label: 'Seed', value: String(clip.seed) })
|
||
const loras = formatLoras(clip.loraStack, clip.loraName)
|
||
if (loras) rows.push({ label: 'LoRA', value: loras })
|
||
return rows
|
||
}
|