118 lines
4.5 KiB
TypeScript
118 lines
4.5 KiB
TypeScript
import { loraBasename, loraDisplayName, loraIdentityKey, MINIMAX_TURBO_LORA, normalizeLoraStack, type LoraStackItem } from '~/utils/loras'
|
|
|
|
export const OUTPUT_NAME_MAX = 80
|
|
|
|
export function sanitizeFilenameKey(raw: unknown, max = 32) {
|
|
return String(raw || '')
|
|
.replace(/\.[a-z0-9]{1,8}$/i, '')
|
|
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
.replace(/-+/g, '-')
|
|
.replace(/^-|-$/g, '')
|
|
.slice(0, max)
|
|
}
|
|
|
|
export function sanitizeLoraAlias(raw: unknown) {
|
|
return sanitizeFilenameKey(raw, 24)
|
|
}
|
|
|
|
export function normalizeLoraAliases(raw: unknown): Record<string, string> {
|
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}
|
|
const out: Record<string, string> = {}
|
|
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
const id = loraIdentityKey(key) || String(key || '').trim().toLowerCase()
|
|
const alias = sanitizeLoraAlias(value)
|
|
if (!id || !alias) continue
|
|
out[id] = alias
|
|
}
|
|
return out
|
|
}
|
|
|
|
export function inputStemFromFilename(filename: string) {
|
|
const base = String(filename || '').replace(/^.*[\\/]/, '').replace(/\.[a-z0-9]+$/i, '')
|
|
if (!base) return ''
|
|
if (/^(start|still|padded|last[_-]?frame|refine-canvas|refine-mask|image[12]?|mask|edited)$/i.test(base)) return ''
|
|
if (/^aigen[_-]/i.test(base)) return ''
|
|
if (/^[0-9a-f]{8,}/i.test(base) && /_\d{5}_?$/i.test(base)) return ''
|
|
return sanitizeFilenameKey(base, 40)
|
|
}
|
|
|
|
export function defaultLoraFilenameKey(name: string) {
|
|
const stem = loraDisplayName(name).replace(/^xaigen-?/i, '')
|
|
return sanitizeFilenameKey(stem, 24) || 'lora'
|
|
}
|
|
|
|
export function loraFilenameKey(name: string, aliases: Record<string, string> = {}) {
|
|
const id = loraIdentityKey(name)
|
|
const alias = String(aliases[id] || aliases[loraBasename(name).toLowerCase()] || '').trim()
|
|
if (alias) return sanitizeFilenameKey(alias, 24) || defaultLoraFilenameKey(name)
|
|
return defaultLoraFilenameKey(name)
|
|
}
|
|
|
|
export function isFilenameSystemLora(name: string) {
|
|
const id = loraIdentityKey(name)
|
|
if (id === loraIdentityKey(MINIMAX_TURBO_LORA)) return true
|
|
if (/flux2-klein-9b-consistency/i.test(id)) return true
|
|
if (/ltx-2\.3-22b-distilled/i.test(name)) return true
|
|
return false
|
|
}
|
|
|
|
export function loraKeysForFilename(stack: unknown, aliases: Record<string, string> = {}) {
|
|
const seen = new Set<string>()
|
|
const keys: string[] = []
|
|
for (const item of normalizeLoraStack(stack)) {
|
|
if (isFilenameSystemLora(item.name)) continue
|
|
const key = loraFilenameKey(item.name, aliases)
|
|
const token = key.toLowerCase()
|
|
if (!key || seen.has(token)) continue
|
|
seen.add(token)
|
|
keys.push(key)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
export function engineFilenameKey(engine: string) {
|
|
const value = String(engine || '').trim().toLowerCase()
|
|
if (value === 'krea') return 'krea'
|
|
if (value === 'flux') return 'flux'
|
|
if (value === 'klein') return 'klein'
|
|
if (value === 'ltx') return 'ltx'
|
|
if (value === 'minimax') return 'minimax'
|
|
if (value === 'ace' || value === 'ace-step') return 'ace'
|
|
if (value === 'ace15' || value === 'ace-step-1.5') return 'ace15'
|
|
if (value === 'yue') return 'yue'
|
|
return sanitizeFilenameKey(value, 16) || 'gen'
|
|
}
|
|
|
|
export function buildOutputBaseName(opts: {
|
|
requested?: string
|
|
inputFilename?: string
|
|
engine: string
|
|
loraStack?: LoraStackItem[] | unknown
|
|
aliases?: Record<string, string>
|
|
}) {
|
|
const requested = String(opts.requested || '').trim().replace(/\s+/g, ' ').slice(0, OUTPUT_NAME_MAX)
|
|
if (requested) return requested
|
|
const stem = inputStemFromFilename(opts.inputFilename || '')
|
|
const engine = engineFilenameKey(opts.engine)
|
|
const loras = loraKeysForFilename(opts.loraStack, opts.aliases || {})
|
|
const joined = [stem, engine, ...loras].filter(Boolean).join('-') || engine || 'output'
|
|
return joined.slice(0, OUTPUT_NAME_MAX)
|
|
}
|
|
|
|
export function nextUniqueName(existing: string[], base: string) {
|
|
const trimmed = String(base || '').trim() || 'output'
|
|
const taken = new Set(existing.map(name => String(name || '').trim().toLowerCase()).filter(Boolean))
|
|
if (!taken.has(trimmed.toLowerCase())) return trimmed.slice(0, OUTPUT_NAME_MAX)
|
|
for (let n = 2; n < 10000; n++) {
|
|
const suffix = `-${n}`
|
|
const candidate = `${trimmed.slice(0, OUTPUT_NAME_MAX - suffix.length)}${suffix}`
|
|
if (!taken.has(candidate.toLowerCase())) return candidate
|
|
}
|
|
return `${trimmed.slice(0, 72)}-${Date.now().toString(16).slice(-6)}`
|
|
}
|
|
|
|
export function comfyJobPrefix(jobId?: string) {
|
|
const hex = `${String(jobId || '').replace(/-/g, '')}${crypto.randomUUID().replace(/-/g, '')}`
|
|
return hex.slice(0, 12)
|
|
}
|