Add Image v2 Generate for Klein text-to-image.

New sibling graph and mode so T2I does not borrow Edit, Compose, or Refine. Stills are ignored; size is width x height only.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-28 22:56:33 -05:00
co-authored by Cursor
parent a2895b8054
commit b35f9af10d
9 changed files with 511 additions and 112 deletions
+46 -5
View File
@@ -1,5 +1,5 @@
export const IMAGE_V2_MODES = ['edit', 'compose', 'refine'] as const
export const IMAGE_V2_TASKS = ['scene', 'identity', 'outfit', 'face_lock', 'refine'] as const
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 type ImageV2Mode = (typeof IMAGE_V2_MODES)[number]
export type ImageV2Task = (typeof IMAGE_V2_TASKS)[number]
@@ -38,8 +38,21 @@ export const IMAGE_V2_REFINE_HAND = {
}
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_ROLE_HEADERS: Record<Exclude<ImageV2Task, 'scene' | 'refine'>, string> = {
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.'
@@ -69,10 +82,36 @@ export function clampImageV2Denoise(raw: unknown, fallback = IMAGE_V2_DENOISE_DE
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)) 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 === 'refine' || mode !== 'compose' || task === 'scene') return body
const header = IMAGE_V2_ROLE_HEADERS[task as Exclude<ImageV2Task, 'scene' | 'refine'>]
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
}
@@ -89,4 +128,6 @@ export type ImageV2PresetSettings = {
megapixels?: number
turbo?: boolean
strength?: number
width?: number
height?: number
}