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 { if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {} const out: Record = {} for (const [key, value] of Object.entries(raw as Record)) { 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 = {}) { 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 = {}) { const seen = new Set() 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 }) { 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) }