| 1 | import type { ImageModelConfig, ImageModelProvider } from '@shared/image-generation.js' |
| 2 | |
| 3 | export type ImageSizeOption = { |
| 4 | value: string |
| 5 | label: string |
| 6 | } |
| 7 | |
| 8 | const PROVIDER_SIZE_OPTIONS: Record<ImageModelProvider, ImageSizeOption[]> = { |
| 9 | jimeng: [ |
| 10 | { value: '16:9', label: '16:9 · 1664x936' }, |
| 11 | { value: '1:1', label: '1:1 · 1328x1328' }, |
| 12 | { value: '4:3', label: '4:3 · 1472x1104' } |
| 13 | ], |
| 14 | jimeng4: [ |
| 15 | { value: '16:9', label: '16:9 · 2560x1440' }, |
| 16 | { value: '1:1', label: '1:1 · 2048x2048' }, |
| 17 | { value: '4:3', label: '4:3 · 2304x1728' }, |
| 18 | { value: '9:16', label: '9:16 · 1440x2560' }, |
| 19 | { value: '3:4', label: '3:4 · 1728x2304' } |
| 20 | ], |
| 21 | agnes: [ |
| 22 | { value: '16:9', label: '16:9 · 1024x768' }, |
| 23 | { value: '1:1', label: '1:1 · 1024x1024' }, |
| 24 | { value: '4:3', label: '4:3 · 1024x768' } |
| 25 | ], |
| 26 | siliconflow: [ |
| 27 | { value: '1024x1024', label: '1:1 · 1024x1024' }, |
| 28 | { value: '1280x720', label: '16:9 · 1280x720' }, |
| 29 | { value: '720x1280', label: '9:16 · 720x1280' } |
| 30 | ], |
| 31 | openaiCompatible: [ |
| 32 | { value: '1024x1024', label: '1:1 · 1024x1024' }, |
| 33 | { value: '1536x1024', label: '3:2 · 1536x1024' }, |
| 34 | { value: '1024x1536', label: '2:3 · 1024x1536' }, |
| 35 | { value: 'auto', label: 'auto' } |
| 36 | ], |
| 37 | gemini: [ |
| 38 | { value: '1:1|1K', label: '1:1 · 1K' }, |
| 39 | { value: '16:9|1K', label: '16:9 · 1K' }, |
| 40 | { value: '9:16|1K', label: '9:16 · 1K' }, |
| 41 | { value: '4:3|1K', label: '4:3 · 1K' }, |
| 42 | { value: '3:4|1K', label: '3:4 · 1K' }, |
| 43 | { value: '3:2|1K', label: '3:2 · 1K' }, |
| 44 | { value: '2:3|1K', label: '2:3 · 1K' }, |
| 45 | { value: '21:9|1K', label: '21:9 · 1K' }, |
| 46 | { value: '1:1|2K', label: '1:1 · 2K' }, |
| 47 | { value: '16:9|2K', label: '16:9 · 2K' }, |
| 48 | { value: '9:16|2K', label: '9:16 · 2K' }, |
| 49 | { value: '1:1|4K', label: '1:1 · 4K' }, |
| 50 | { value: '16:9|4K', label: '16:9 · 4K' }, |
| 51 | { value: '9:16|4K', label: '9:16 · 4K' } |
| 52 | ], |
| 53 | seedream: [{ value: '2K', label: '2K' }] |
| 54 | } |
| 55 | |
| 56 | const SILICONFLOW_QWEN_SIZE_OPTIONS: ImageSizeOption[] = [ |
| 57 | { value: '1328x1328', label: '1:1 · 1328x1328' }, |
| 58 | { value: '1664x928', label: '16:9 · 1664x928' }, |
| 59 | { value: '928x1664', label: '9:16 · 928x1664' }, |
| 60 | { value: '1472x1140', label: '4:3 · 1472x1140' }, |
| 61 | { value: '1140x1472', label: '3:4 · 1140x1472' }, |
| 62 | { value: '1584x1056', label: '3:2 · 1584x1056' }, |
| 63 | { value: '1056x1584', label: '2:3 · 1056x1584' } |
| 64 | ] |
| 65 | |
| 66 | const SILICONFLOW_KOLORS_SIZE_OPTIONS: ImageSizeOption[] = [ |
| 67 | { value: '1024x1024', label: '1:1 · 1024x1024' }, |
| 68 | { value: '960x1280', label: '3:4 · 960x1280' }, |
| 69 | { value: '768x1024', label: '3:4 · 768x1024' }, |
| 70 | { value: '720x1440', label: '1:2 · 720x1440' }, |
| 71 | { value: '720x1280', label: '9:16 · 720x1280' } |
| 72 | ] |
| 73 | |
| 74 | const readModelConfigObject = (config?: ImageModelConfig): Record<string, unknown> => { |
| 75 | if (!config?.modelConfig) return {} |
| 76 | try { |
| 77 | const parsed = JSON.parse(config.modelConfig) |
| 78 | return parsed && typeof parsed === 'object' && !Array.isArray(parsed) |
| 79 | ? (parsed as Record<string, unknown>) |
| 80 | : {} |
| 81 | } catch { |
| 82 | return {} |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | const readSizeString = (record: Record<string, unknown>, key: string): string => { |
| 87 | const value = record[key] |
| 88 | return typeof value === 'string' ? value.trim() : '' |
| 89 | } |
| 90 | |
| 91 | const buildSizeOption = (value: string, label?: string): ImageSizeOption | null => { |
| 92 | const normalizedValue = value.trim() |
| 93 | if (!normalizedValue) return null |
| 94 | const normalizedLabel = label?.trim() |
| 95 | return { |
| 96 | value: normalizedValue, |
| 97 | label: normalizedLabel || normalizedValue |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | const normalizeConfiguredSizeOption = (item: unknown): ImageSizeOption | null => { |
| 102 | if (typeof item === 'string') return buildSizeOption(item) |
| 103 | if (!item || typeof item !== 'object' || Array.isArray(item)) return null |
| 104 | const record = item as Record<string, unknown> |
| 105 | const value = |
| 106 | readSizeString(record, 'value') || |
| 107 | readSizeString(record, 'size') || |
| 108 | readSizeString(record, 'ratio') || |
| 109 | readSizeString(record, 'aspectRatio') || |
| 110 | readSizeString(record, 'aspect_ratio') |
| 111 | const width = Number(record.width) |
| 112 | const height = Number(record.height) |
| 113 | const dimensionLabel = |
| 114 | Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0 |
| 115 | ? `${Math.floor(width)}x${Math.floor(height)}` |
| 116 | : '' |
| 117 | const explicitValue = readSizeString(record, 'value') || readSizeString(record, 'size') |
| 118 | return buildSizeOption( |
| 119 | explicitValue || dimensionLabel || value, |
| 120 | readSizeString(record, 'label') || [value, dimensionLabel].filter(Boolean).join(' · ') |
| 121 | ) |
| 122 | } |
| 123 | |
| 124 | const uniqueSizeOptions = (options: ImageSizeOption[]): ImageSizeOption[] => { |
| 125 | const seen = new Set<string>() |
| 126 | return options.filter((option) => { |
| 127 | if (seen.has(option.value)) return false |
| 128 | seen.add(option.value) |
| 129 | return true |
| 130 | }) |
| 131 | } |
| 132 | |
| 133 | const isImageSizeOption = (option: ImageSizeOption | null): option is ImageSizeOption => |
| 134 | Boolean(option) |
| 135 | |
| 136 | export const resolveImageSizeOptions = (config?: ImageModelConfig): ImageSizeOption[] => { |
| 137 | const modelConfig = readModelConfigObject(config) |
| 138 | const configuredOptionsSource = |
| 139 | modelConfig.sizes || |
| 140 | modelConfig.supportedSizes || |
| 141 | modelConfig.aspectRatios || |
| 142 | modelConfig.aspect_ratios || |
| 143 | modelConfig.ratios |
| 144 | const configuredOptions = Array.isArray(configuredOptionsSource) |
| 145 | ? uniqueSizeOptions( |
| 146 | configuredOptionsSource.map(normalizeConfiguredSizeOption).filter(isImageSizeOption) |
| 147 | ) |
| 148 | : [] |
| 149 | if (configuredOptions.length > 0) return configuredOptions |
| 150 | |
| 151 | const fixedSize = readSizeString(modelConfig, 'size') |
| 152 | if (fixedSize) return [{ value: fixedSize, label: fixedSize }] |
| 153 | |
| 154 | const width = Number(modelConfig.width) |
| 155 | const height = Number(modelConfig.height) |
| 156 | if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) { |
| 157 | const size = `${Math.floor(width)}x${Math.floor(height)}` |
| 158 | return [{ value: size, label: size }] |
| 159 | } |
| 160 | |
| 161 | const model = readSizeString(modelConfig, 'model') |
| 162 | if (config?.provider === 'siliconflow') { |
| 163 | if (/qwen\/qwen-image/i.test(model)) return SILICONFLOW_QWEN_SIZE_OPTIONS |
| 164 | if (/kolors/i.test(model)) return SILICONFLOW_KOLORS_SIZE_OPTIONS |
| 165 | } |
| 166 | |
| 167 | if (config?.provider) return PROVIDER_SIZE_OPTIONS[config.provider] |
| 168 | return PROVIDER_SIZE_OPTIONS.jimeng |
| 169 | } |
| 170 |