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 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 }