Show generator settings in the library and restore them as input.
Collapse tiles so prompts stay behind the info button. Use as input opens the matching Video or Image v2 mode with the saved sliders. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
import { loraDisplayName, normalizeLoraStack, type LoraStackItem } from '~/utils/loras'
|
||||
import type { ImageV2Mode, ImageV2Task } from '~/utils/imageV2'
|
||||
import type { VideoWorkflowId } from '~/utils/videoModels'
|
||||
|
||||
export type StillGenerationKind = 'image' | 'imagev2'
|
||||
|
||||
export type StillGenerationSettings = {
|
||||
kind: StillGenerationKind
|
||||
mode?: ImageV2Mode
|
||||
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
|
||||
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 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,
|
||||
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),
|
||||
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) : ''
|
||||
return task ? `Image v2 · ${mode} · ${task}` : `Image v2 · ${mode}`
|
||||
}
|
||||
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: { width?: number; height?: number; settings?: StillGenerationSettings | null }) {
|
||||
const settings = still.settings
|
||||
const rows: SettingsRow[] = []
|
||||
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 (still.width && still.height) {
|
||||
rows.push({ label: 'Output', value: `${still.width}×${still.height}` })
|
||||
}
|
||||
if (settings.snofsModel != null || settings.snofsClip != null) {
|
||||
rows.push({ label: 'SNOFS', 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) })
|
||||
}
|
||||
if (settings.mode === 'refine' && settings.strength != null) {
|
||||
rows.push({ label: 'Strength', value: formatNum(settings.strength) })
|
||||
}
|
||||
if (settings.mode !== 'generate' && settings.megapixels != null) {
|
||||
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.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: {
|
||||
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[] = []
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user