Files
aigen/utils/imageV2.ts
T

286 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
filterLoraStackForImageEngine,
loraIdentityKey,
makeLoraStackItem,
normalizeLoraStack,
type LoraStackItem
} from '~/utils/loras'
export const IMAGE_V2_MODES = ['edit', 'compose', 'refine', 'generate'] as const
export const IMAGE_V2_TASKS = ['scene', 'identity', 'outfit', 'face_lock', 'refine', 't2i'] as const
export const IMAGE_V2_ENGINES = ['flux', 'krea'] as const
export type ImageV2Mode = (typeof IMAGE_V2_MODES)[number]
export type ImageV2Task = (typeof IMAGE_V2_TASKS)[number]
export type ImageV2Engine = (typeof IMAGE_V2_ENGINES)[number]
export const IMAGE_V2_STEPS_DEFAULT = 24
export const IMAGE_V2_CFG_DEFAULT = 4
export const IMAGE_V2_TURBO_STEPS = 8
export const IMAGE_V2_TURBO_CFG = 1
export const IMAGE_V2_SNOFS_MODEL = 0.65
export const IMAGE_V2_SNOFS_CLIP = 0.35
export const IMAGE_V2_CONSISTENCY_MODEL = 0.7
export const IMAGE_V2_CONSISTENCY_CLIP = 0.7
export const IMAGE_V2_STRENGTH_MIN = 0
export const IMAGE_V2_STRENGTH_MAX = 2
export const IMAGE_V2_STRENGTH_STEP = 0.05
export const IMAGE_V2_SNOFS_LORA = 'xaigen-klein_snofs_v1_4.safetensors'
export const IMAGE_V2_CONSISTENCY_LORA = 'Flux2-Klein-9B-consistency-V2.safetensors'
export const IMAGE_V2_DENOISE_DEFAULT = 0.35
export const IMAGE_V2_DENOISE_MIN = 0.15
export const IMAGE_V2_DENOISE_MAX = 0.75
export const IMAGE_V2_DENOISE_STEP = 0.01
export const IMAGE_V2_REFINE_FACE = {
strength: 0.28,
snofsModel: 0.55,
snofsClip: 0.25,
consistencyModel: 0.75,
consistencyClip: 0.8
}
export const IMAGE_V2_REFINE_HAND = {
strength: 0.4,
snofsModel: 0.65,
snofsClip: 0.3,
consistencyModel: 0.7,
consistencyClip: 0.7
}
export const IMAGE_V2_REFINE_HEAVY = { strength: 0.55 }
export const IMAGE_V2_REFINE_FACE_PROMPT = 'same face as the canvas, same glasses, same cheeks and jaw. Change only the face in the masked area.'
export const IMAGE_V2_SIZE_SIDES = [768, 1024, 1280] as const
export const IMAGE_V2_GENERATE_WIDTH = 1024
export const IMAGE_V2_GENERATE_HEIGHT = 1024
export const IMAGE_V2_GENERATE_ASPECTS = {
'1:1': [1024, 1024],
'3:4': [768, 1024],
'4:3': [1024, 768],
'16:9': [1280, 768],
'9:16': [768, 1280]
} as const
export type ImageV2GenerateAspect = keyof typeof IMAGE_V2_GENERATE_ASPECTS
export const IMAGE_V2_GENERATE_SFW = { snofsModel: 0, snofsClip: 0 }
export const IMAGE_V2_GENERATE_SNOFS = { snofsModel: 0.65, snofsClip: 0.35 }
export const IMAGE_V2_KREA_STEPS = 8
export const IMAGE_V2_KREA_CFG = 1
export const IMAGE_V2_KREA_EDIT_DENOISE = 0.75
export const IMAGE_V2_KREA_COMPOSE_DENOISE = 0.7
export const IMAGE_V2_KREA_CLIP = 'qwen3vl_4b_fp8_scaled.safetensors'
export const IMAGE_V2_KREA_CLIP_TYPE = 'krea2'
export const IMAGE_V2_KREA_VAE = 'qwen_image_vae.safetensors'
export const IMAGE_V2_KREA_UNET_CANDIDATES = [
'krea2_turbo_mxfp8.safetensors',
'krea2_turbo_nvfp4.safetensors',
'krea2_turbo_fp8_scaled.safetensors'
] as const
export const IMAGE_V2_KREA_CONCEPT_LORA = 'xaigen-snofs_krea_v1_3D.safetensors'
export const IMAGE_V2_ROLE_HEADERS: Record<Exclude<ImageV2Task, 'scene' | 'refine' | 't2i'>, string> = {
outfit: 'Person, face, body, pose, and background from image 1. Clothing only from image 2. Fit the outfit from image 2 to the body in image 1. Do not copy image 2’s face, body shape, or pose.',
face_lock: 'Body, pose, and scene from image 1. Exact face from image 2.',
identity: 'Same person as image 1. Use image 2 only to reinforce the face. Follow the user’s pose/scene prompt.'
}
export function parseImageV2Mode(raw: unknown): ImageV2Mode | null {
const value = String(raw || '').trim().toLowerCase()
return IMAGE_V2_MODES.includes(value as ImageV2Mode) ? value as ImageV2Mode : null
}
export function parseImageV2Task(raw: unknown, fallback: ImageV2Task = 'scene'): ImageV2Task {
const value = String(raw || '').trim().toLowerCase()
return IMAGE_V2_TASKS.includes(value as ImageV2Task) ? value as ImageV2Task : fallback
}
export function parseImageV2Engine(raw: unknown, fallback: ImageV2Engine = 'flux'): ImageV2Engine {
const value = String(raw || '').trim().toLowerCase()
return IMAGE_V2_ENGINES.includes(value as ImageV2Engine) ? value as ImageV2Engine : fallback
}
export function imageV2WorkflowFile(mode: ImageV2Mode, engine: ImageV2Engine = 'flux') {
if (engine === 'krea') {
if (mode === 'generate') return 'krea_v2_generate.json'
if (mode === 'refine') return 'krea_v2_refine.json'
if (mode === 'compose') return 'krea_v2_compose.json'
return 'krea_v2_edit.json'
}
if (mode === 'generate') return 'klein_v2_generate.json'
if (mode === 'refine') return 'klein_v2_refine.json'
if (mode === 'compose') return 'klein_v2_compose.json'
return 'klein_v2_edit.json'
}
export function clampImageV2Strength(raw: unknown, fallback: number) {
const value = Number(raw)
if (!Number.isFinite(value)) return fallback
const snapped = Math.round(value / IMAGE_V2_STRENGTH_STEP) * IMAGE_V2_STRENGTH_STEP
return Math.min(IMAGE_V2_STRENGTH_MAX, Math.max(IMAGE_V2_STRENGTH_MIN, Math.round(snapped * 100) / 100))
}
export function clampImageV2Denoise(raw: unknown, fallback = IMAGE_V2_DENOISE_DEFAULT) {
const value = Number(raw)
if (!Number.isFinite(value)) return fallback
const snapped = Math.round(value / IMAGE_V2_DENOISE_STEP) * IMAGE_V2_DENOISE_STEP
return Math.min(IMAGE_V2_DENOISE_MAX, Math.max(IMAGE_V2_DENOISE_MIN, Math.round(snapped * 100) / 100))
}
export function snapImageV2Side(raw: unknown, fallback = IMAGE_V2_GENERATE_WIDTH) {
const value = Number(raw)
if (!Number.isFinite(value) || value <= 0) return fallback
return IMAGE_V2_SIZE_SIDES.reduce((best, side) => (
Math.abs(side - value) < Math.abs(best - value) ? side : best
), IMAGE_V2_SIZE_SIDES[0])
}
export function parseImageV2Aspect(raw: unknown): ImageV2GenerateAspect | null {
const value = String(raw || '').trim()
return value in IMAGE_V2_GENERATE_ASPECTS ? value as ImageV2GenerateAspect : null
}
export function clampImageV2Size(width: unknown, height: unknown, aspect?: unknown) {
const preset = parseImageV2Aspect(aspect)
if (preset) {
const [w, h] = IMAGE_V2_GENERATE_ASPECTS[preset]
return { width: w, height: h, aspect: preset }
}
return {
width: snapImageV2Side(width, IMAGE_V2_GENERATE_WIDTH),
height: snapImageV2Side(height, IMAGE_V2_GENERATE_HEIGHT),
aspect: null as ImageV2GenerateAspect | null
}
}
export function composeImageV2Prompt(mode: ImageV2Mode, task: ImageV2Task, prompt: string) {
const body = String(prompt || '').trim()
if (mode === 'generate' || mode === 'refine' || mode !== 'compose' || task === 'scene' || task === 't2i') return body
const header = IMAGE_V2_ROLE_HEADERS[task as Exclude<ImageV2Task, 'scene' | 'refine' | 't2i'>]
return header ? `${header}\n\n${body}` : body
}
export type ImageV2PresetSettings = {
mode?: ImageV2Mode
task?: ImageV2Task
engine?: ImageV2Engine
prompt?: string
negative?: string
snofsModel?: number
snofsClip?: number
consistencyModel?: number
consistencyClip?: number
steps?: number
cfg?: number
megapixels?: number
scaleToTotalPixels?: boolean
turbo?: boolean
strength?: number
width?: number
height?: number
aspect?: string
}
export function imageV2ConceptLoraName(engine: ImageV2Engine, kreaConcept?: string) {
if (engine === 'krea') return String(kreaConcept || IMAGE_V2_KREA_CONCEPT_LORA || '').trim()
return IMAGE_V2_SNOFS_LORA
}
export function isImageV2ConceptLora(name: string, engine?: ImageV2Engine) {
const id = loraIdentityKey(name)
const flux = id === loraIdentityKey(IMAGE_V2_SNOFS_LORA) || /klein_snofs/i.test(id)
const krea = id === loraIdentityKey(IMAGE_V2_KREA_CONCEPT_LORA) || /snofs_krea/i.test(id)
if (engine === 'krea') return krea
if (engine === 'flux') return flux
return flux || krea
}
export function isImageV2ConsistencyLora(name: string) {
const id = loraIdentityKey(name)
return id === loraIdentityKey(IMAGE_V2_CONSISTENCY_LORA) || /flux2-klein-9b-consistency/i.test(id)
}
/** Strengths that are actually on the stack cards. Separate snofs_* fields are legacy only. */
export function imageV2StackSpecials(stack: unknown, engine?: ImageV2Engine) {
const items = normalizeLoraStack(stack)
const snofs = items.find(item => isImageV2ConceptLora(item.name, engine))
const consistency = items.find(item => isImageV2ConsistencyLora(item.name))
return { items, snofs, consistency }
}
export function upsertImageV2Lora(stack: unknown, name: string, model: number, clip: number): LoraStackItem[] {
const item = makeLoraStackItem(name, model, clip)
const current = normalizeLoraStack(stack)
if (!item) return current
const key = loraIdentityKey(name)
const index = current.findIndex(entry => loraIdentityKey(entry.name) === key)
if (index >= 0) {
const next = [...current]
next[index] = { ...next[index], strengthModel: item.strengthModel, strengthClip: item.strengthClip }
return next
}
if (isImageV2ConsistencyLora(name)) {
const conceptAt = current.findIndex(entry => isImageV2ConceptLora(entry.name))
if (conceptAt >= 0) return [...current.slice(0, conceptAt + 1), item, ...current.slice(conceptAt + 1)]
}
return [item, ...current]
}
export function removeImageV2ConceptLora(stack: unknown, engine?: ImageV2Engine) {
return normalizeLoraStack(stack).filter(item => !isImageV2ConceptLora(item.name, engine))
}
export function defaultImageV2LoraStack(opts: {
engine: ImageV2Engine
mode: ImageV2Mode
xaigen: boolean
kreaConcept?: string
}): LoraStackItem[] {
if (opts.engine === 'krea') {
const name = String(opts.kreaConcept || '').trim()
if (!opts.xaigen || !name) return []
const item = makeLoraStackItem(name, IMAGE_V2_SNOFS_MODEL, IMAGE_V2_SNOFS_CLIP)
return item ? [item] : []
}
const out: LoraStackItem[] = []
if (opts.xaigen) {
const concept = makeLoraStackItem(IMAGE_V2_SNOFS_LORA, IMAGE_V2_SNOFS_MODEL, IMAGE_V2_SNOFS_CLIP)
if (concept) out.push(concept)
}
if (opts.mode !== 'generate') {
const consistency = makeLoraStackItem(IMAGE_V2_CONSISTENCY_LORA, IMAGE_V2_CONSISTENCY_MODEL, IMAGE_V2_CONSISTENCY_CLIP)
if (consistency) out.push(consistency)
}
return out
}
export function mergeLegacyImageV2Loras(stack: unknown, opts: {
engine: ImageV2Engine
mode: ImageV2Mode
xaigen: boolean
snofsModel?: number
snofsClip?: number
consistencyModel?: number
consistencyClip?: number
kreaConcept?: string
}): LoraStackItem[] {
let items = filterLoraStackForImageEngine(stack, opts.engine)
if (opts.engine === 'krea') {
const name = String(opts.kreaConcept || '').trim()
const model = Number(opts.snofsModel) || 0
const clip = Number(opts.snofsClip) || 0
if (name && (model > 0 || clip > 0) && !items.some(item => isImageV2ConceptLora(item.name, 'krea'))) {
return upsertImageV2Lora(items, name, model, clip)
}
return items
}
const conceptModel = opts.xaigen ? Number(opts.snofsModel) || 0 : 0
const conceptClip = opts.xaigen ? Number(opts.snofsClip) || 0 : 0
if ((conceptModel > 0 || conceptClip > 0) && !items.some(item => isImageV2ConceptLora(item.name))) {
items = upsertImageV2Lora(items, IMAGE_V2_SNOFS_LORA, conceptModel, conceptClip)
}
const consistencyModel = opts.mode === 'generate' ? 0 : Number(opts.consistencyModel) || 0
const consistencyClip = opts.mode === 'generate' ? 0 : Number(opts.consistencyClip) || 0
if ((consistencyModel > 0 || consistencyClip > 0) && !items.some(item => isImageV2ConsistencyLora(item.name))) {
items = upsertImageV2Lora(items, IMAGE_V2_CONSISTENCY_LORA, consistencyModel, consistencyClip)
}
return items
}