Files
aigen/utils/outputNames.ts

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