feature/dmxapi_painting_custom_size (#8689)

* 修改生成图片尺寸

* fix:known problem

* fix:Switching but no recovery occurred

* fix:The problem of loading images

* fix:text i18n
This commit is contained in:
Caelan 2025-08-01 20:55:57 +08:00 committed by GitHub
parent 63ae211af1
commit 2ced1b2d71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 189 additions and 119 deletions

View File

@ -1507,6 +1507,7 @@
"image": "New Image"
}
},
"custom_size": "Custom Size",
"edit": {
"image_file": "Edited Image",
"magic_prompt_option_tip": "Intelligently enhances editing prompts",
@ -1629,6 +1630,7 @@
},
"text_desc_required": "Please enter image description first",
"title": "Images",
"top_up": "Top up ",
"translating": "Translating...",
"uploaded_input": "Uploaded input",
"upscale": {

View File

@ -1507,6 +1507,7 @@
"image": "新しい画像"
}
},
"custom_size": "カスタムサイズ",
"edit": {
"image_file": "編集画像",
"magic_prompt_option_tip": "編集効果を向上させるための提示詞を最適化します",
@ -1629,6 +1630,7 @@
},
"text_desc_required": "画像の説明を先に入力してください",
"title": "画像",
"top_up": "チャージする",
"translating": "翻訳中...",
"uploaded_input": "アップロード済みの入力",
"upscale": {

View File

@ -1507,6 +1507,7 @@
"image": "Новое изображение"
}
},
"custom_size": "Пользовательский размер",
"edit": {
"image_file": "Изображение для редактирования",
"magic_prompt_option_tip": "Интеллектуально оптимизирует подсказки для улучшения эффекта редактирования",
@ -1629,6 +1630,7 @@
},
"text_desc_required": "Пожалуйста, сначала введите описание изображения",
"title": "Изображения",
"top_up": "пополнить счёт",
"translating": "Перевод...",
"uploaded_input": "Загруженный ввод",
"upscale": {

View File

@ -1507,6 +1507,7 @@
"image": "新建图片"
}
},
"custom_size": "自定义尺寸",
"edit": {
"image_file": "编辑的图像",
"magic_prompt_option_tip": "智能优化编辑提示词",
@ -1629,6 +1630,7 @@
},
"text_desc_required": "请先输入图片描述",
"title": "图片",
"top_up": "充值",
"translating": "翻译中...",
"uploaded_input": "已上传输入",
"upscale": {

View File

@ -1507,6 +1507,7 @@
"image": "新繪圖"
}
},
"custom_size": "自訂尺寸",
"edit": {
"image_file": "編輯圖像",
"magic_prompt_option_tip": "智能優化編輯提示詞",
@ -1629,6 +1630,7 @@
},
"text_desc_required": "請先輸入圖片描述",
"title": "繪圖",
"top_up": "儲值",
"translating": "翻譯中...",
"uploaded_input": "已上傳輸入",
"upscale": {

View File

@ -1,11 +1,10 @@
import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
import DMXAPIToImg from '@renderer/assets/images/providers/DMXAPI-to-img.webp'
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
import { HStack, VStack } from '@renderer/components/Layout'
import { HStack } from '@renderer/components/Layout'
import Scrollbar from '@renderer/components/Scrollbar'
import { isMac } from '@renderer/config/constant'
import { getProviderLogo } from '@renderer/config/providers'
import { useTheme } from '@renderer/context/ThemeProvider'
import { usePaintings } from '@renderer/hooks/usePaintings'
import { useAllProviders } from '@renderer/hooks/useProvider'
import { useRuntime } from '@renderer/hooks/useRuntime'
@ -16,7 +15,7 @@ import { setGenerating } from '@renderer/store/runtime'
import type { FileMetadata, PaintingsState } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { DmxapiPainting } from '@types'
import { Avatar, Button, Input, Radio, Segmented, Select, Switch, Tooltip } from 'antd'
import { Avatar, Button, Input, InputNumber, Segmented, Select, Switch, Tooltip } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { Info } from 'lucide-react'
import React, { FC, useEffect, useRef, useState } from 'react'
@ -34,9 +33,9 @@ import {
COURSE_URL,
DEFAULT_PAINTING,
GetModelGroup,
IMAGE_SIZES,
MODEOPTIONS,
STYLE_TYPE_OPTIONS
STYLE_TYPE_OPTIONS,
TOP_UP_URL
} from './config/DmxapiConfig'
const generateRandomSeed = () => Math.floor(Math.random() * 1000000).toString()
@ -45,7 +44,6 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
const [mode] = useState<keyof PaintingsState>('DMXAPIPaintings')
const { DMXAPIPaintings, addPainting, removePainting, updatePainting } = usePaintings()
const [painting, setPainting] = useState<DmxapiPainting>(DMXAPIPaintings?.[0] || DEFAULT_PAINTING)
const { theme } = useTheme()
const { t } = useTranslation()
const providers = useAllProviders()
const providerOptions = Options.map((option) => {
@ -88,6 +86,11 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
paths: []
})
// 自定义尺寸相关状态
const [isCustomSize, setIsCustomSize] = useState(false)
const [customWidth, setCustomWidth] = useState<number | undefined>()
const [customHeight, setCustomHeight] = useState<number | undefined>()
const modeOptions = MODEOPTIONS.map((ele) => {
return {
label: t(ele.label),
@ -144,25 +147,45 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
updatePainting('DMXAPIPaintings', updatedPainting)
}
const getNewPainting = (params?: Partial<DmxapiPainting>) => {
clearImages()
const generationMode = params?.generationMode || painting?.generationMode || MODEOPTIONS[0].value
const modelGroups = getModelOptions(generationMode as generationModeType)
// 获取第一个非空分组的第一个模型
let firstModel = ''
const getFirstModelInfo = (v: generationModeType) => {
const modelGroups = getModelOptions(v)
let model = ''
let priceModel = ''
let image_size = ''
for (const provider of Object.keys(modelGroups)) {
if (modelGroups[provider].length > 0) {
firstModel = modelGroups[provider][0].id
if (modelGroups[provider] && modelGroups[provider].length > 0) {
model = modelGroups[provider][0].id
priceModel = modelGroups[provider][0].price
image_size = modelGroups[provider][0].image_sizes[0].value
break
}
}
return {
model,
priceModel,
image_size,
modelGroups
}
}
const getNewPainting = (params?: Partial<DmxapiPainting>) => {
clearImages()
const generationMode = params?.generationMode || painting?.generationMode || MODEOPTIONS[0].value
const { model, priceModel, image_size, modelGroups } = getFirstModelInfo(generationMode)
return {
...DEFAULT_PAINTING,
id: uuid(),
seed: generateRandomSeed(),
generationMode,
model: firstModel,
model,
modelGroups,
priceModel,
image_size,
...params
}
}
@ -180,7 +203,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
const onSelectModel = (modelId: string) => {
const model = allModels.find((m) => m.id === modelId)
if (model) {
updatePaintingState({ model: modelId, priceModel: model.price })
updatePaintingState({ model: modelId, priceModel: model.price, image_size: model.image_sizes[0].value })
}
}
@ -189,8 +212,34 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
}
const onSelectImageSize = (v: string) => {
const size = IMAGE_SIZES.find((i) => i.value === v)
size && updatePaintingState({ image_size: size.value, aspect_ratio: size.label })
if (v === 'custom') {
setIsCustomSize(true)
// 如果有自定义尺寸值,使用它们
if (customWidth && customHeight) {
updatePaintingState({ image_size: `${customWidth}x${customHeight}`, aspect_ratio: 'custom' })
}
} else {
setIsCustomSize(false)
const currentModel = allModels.find((m) => m.id === painting.model)
const size = currentModel?.image_sizes?.find((i) => i.value === v)
size && updatePaintingState({ image_size: size.value, aspect_ratio: size.label })
}
}
const onCustomSizeChange = (value: number | null, type: string) => {
if (value === null) return
if (type === 'width') {
setCustomWidth(value)
if (customHeight) {
updatePaintingState({ image_size: `${value}x${customHeight}`, aspect_ratio: 'custom' })
}
} else if (type === 'height') {
setCustomHeight(value)
if (customWidth) {
updatePaintingState({ image_size: `${customWidth}x${value}`, aspect_ratio: 'custom' })
}
}
}
const onSelectStyleType = (v: string) => {
@ -251,27 +300,21 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
}
const onGenerationModeChange = (v: generationModeType) => {
clearImages()
const newModelGroups = getModelOptions(v)
setModelOptions(newModelGroups)
// 获取第一个非空分组的第一个模型
let firstModel = ''
let priceModel = ''
for (const provider of Object.keys(newModelGroups)) {
if (newModelGroups[provider] && newModelGroups[provider].length > 0) {
firstModel = newModelGroups[provider][0].id
priceModel = newModelGroups[provider][0].price
break
}
if (isLoading) {
return
}
clearImages()
const { model, priceModel, image_size, modelGroups } = getFirstModelInfo(v)
setModelOptions(modelGroups)
// 如果有urls创建新的painting
if (Array.isArray(painting.urls) && painting.urls.length > 0) {
const newPainting = getNewPainting({
generationMode: v,
model: firstModel, // 使用新模式下的第一个模型
priceModel: priceModel
model
})
const addedPainting = addPainting('DMXAPIPaintings', newPainting)
setPainting(addedPainting)
@ -279,12 +322,20 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
// 否则更新当前painting
updatePaintingState({
generationMode: v,
model: firstModel, // 使用新模式下的第一个模型
model: model,
image_size: image_size,
priceModel: priceModel
})
}
}
const createNewPainting = () => {
if (isLoading) {
return
}
setPainting(addPainting('DMXAPIPaintings', getNewPainting()))
}
// 检查提供者状态函数
const checkProviderStatus = () => {
if (!dmxapiProvider.enabled) {
@ -324,10 +375,6 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
'Content-Type': 'application/json'
}
if (painting.aspect_ratio) {
params['aspect_ratio'] = painting.aspect_ratio
}
if (painting.image_size) {
params['size'] = painting.image_size
}
@ -360,7 +407,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
}
if (painting.image_size) {
params['size'] = '1024x1024'
params['size'] = painting.image_size
}
if (painting.style_type) {
@ -562,6 +609,10 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
const onDeletePainting = async (paintingToDelete: DmxapiPainting) => {
if (paintingToDelete.id === painting.id) {
if (isLoading) {
return
}
const currentIndex = DMXAPIPaintings.findIndex((p) => p.id === paintingToDelete.id)
if (currentIndex > 0) {
@ -715,17 +766,21 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoadingModels, dynamicModelGroups]) // 依赖模型加载状态
// 当模型切换时,检查是否支持自定义尺寸
useEffect(() => {
const currentModel = allModels.find((m) => m.id === painting.model)
if (currentModel && !currentModel.is_custom_size && isCustomSize) {
setIsCustomSize(false)
}
}, [painting.model, allModels, isCustomSize])
return (
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
{isMac && (
<NavbarRight style={{ justifyContent: 'flex-end' }}>
<Button
size="small"
className="nodrag"
icon={<PlusOutlined />}
onClick={() => setPainting(addPainting('DMXAPIPaintings', getNewPainting()))}>
<Button size="small" className="nodrag" icon={<PlusOutlined />} onClick={createNewPainting}>
{t('paintings.button.new.image')}
</Button>
</NavbarRight>
@ -735,15 +790,20 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
<LeftContainer>
<ProviderTitleContainer>
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
<SettingHelpLink target="_blank" href={COURSE_URL}>
{t('paintings.paint_course')}
<div>
<SettingHelpLink target="_blank" href={COURSE_URL}>
{t('paintings.paint_course')}
</SettingHelpLink>
<SettingHelpLink target="_blank" href={TOP_UP_URL}>
{t('paintings.top_up')}
</SettingHelpLink>
<ProviderLogo
shape="square"
src={getProviderLogo(dmxapiProvider.id)}
size={16}
style={{ marginLeft: 5 }}
/>
</SettingHelpLink>
</div>
</ProviderTitleContainer>
<Select value={providerOptions[2].value} onChange={handleProviderChange} style={{ marginBottom: 15 }}>
{providerOptions.map((provider) => (
@ -793,23 +853,66 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
})}
</Select>
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.image.size')}</SettingTitle>
<Select
value={isCustomSize ? 'custom' : painting.image_size}
onChange={(value) => onSelectImageSize(value)}
style={{ width: '100%' }}>
{(() => {
const currentModel = allModels.find((m) => m.id === painting.model)
const modelImageSizes = currentModel?.image_sizes || []
// 直接使用模型返回的image_sizes数据包含label和value
return modelImageSizes.map((size) => {
return (
<Select.Option key={size.value} value={size.value}>
<HStack style={{ alignItems: 'center', gap: 8 }}>
<span>{size.label}</span>
</HStack>
</Select.Option>
)
})
})()}
{/* 检查当前模型是否支持自定义尺寸 */}
{allModels.find((m) => m.id === painting.model)?.is_custom_size && (
<Select.Option value="custom" key="custom">
<HStack style={{ alignItems: 'center', gap: 8 }}>
<span>{t('paintings.custom_size')}</span>
</HStack>
</Select.Option>
)}
</Select>
{/* 自定义尺寸输入框 */}
{isCustomSize && allModels.find((m) => m.id === painting.model)?.is_custom_size && (
<div style={{ marginTop: 10 }}>
<HStack style={{ gap: 8, alignItems: 'center' }}>
<InputNumber
placeholder="W"
value={customWidth}
controls={false}
onChange={(value) => onCustomSizeChange(value, 'width')}
min={parseInt(allModels.find((m) => m.id === painting.model)?.min_image_size || '512')}
max={parseInt(allModels.find((m) => m.id === painting.model)?.max_image_size || '2048')}
style={{ width: 80, flex: 1 }}
/>
<span style={{ color: 'var(--color-text-2)', fontSize: '12px' }}>x</span>
<InputNumber
placeholder="H"
value={customHeight}
controls={false}
onChange={(value) => onCustomSizeChange(value, 'height')}
min={parseInt(allModels.find((m) => m.id === painting.model)?.min_image_size || 512)}
max={parseInt(allModels.find((m) => m.id === painting.model)?.max_image_size || 2048)}
style={{ width: 80, flex: 1 }}
/>
<span style={{ color: 'var(--color-text-3)', fontSize: '11px' }}>px</span>
</HStack>
</div>
)}
{painting.generationMode === generationModeType.GENERATION && (
<>
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.image.size')}</SettingTitle>
<Radio.Group
value={painting.image_size}
onChange={(e) => onSelectImageSize(e.target.value)}
style={{ display: 'flex' }}>
{IMAGE_SIZES.map((size) => (
<RadioButton value={size.value} key={size.value}>
<VStack alignItems="center">
<ImageSizeImage src={size.icon} theme={theme} />
<span>{size.label}</span>
</VStack>
</RadioButton>
))}
</Radio.Group>
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
{t('paintings.seed')}
<Tooltip title={t('paintings.seed_desc_tip')}>
@ -896,7 +999,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
selectedPainting={painting}
onSelectPainting={onSelectPainting}
onDeletePainting={onDeletePainting}
onNewPainting={() => setPainting(addPainting('DMXAPIPaintings', getNewPainting()))}
onNewPainting={createNewPainting}
/>
</ContentContainer>
</Container>
@ -991,22 +1094,6 @@ const ToolbarMenu = styled.div`
align-items: center;
gap: 6px;
`
const ImageSizeImage = styled.img<{ theme: string }>`
filter: ${({ theme }) => (theme === 'dark' ? 'invert(100%)' : 'none')};
margin-top: 8px;
`
const RadioButton = styled(Radio.Button)`
width: 30px;
height: 55px;
display: flex;
flex-direction: column;
flex: 1;
justify-content: center;
align-items: center;
`
const InfoIcon = styled(Info)`
margin-left: 5px;
cursor: help;
@ -1078,8 +1165,11 @@ const EmptyImgBox = styled.div`
const EmptyImg = styled.div<{ bgUrl?: string }>`
width: 70vh;
height: 70vh;
background-size: cover;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
background-image: ${(props) => (props.bgUrl ? `url(${props.bgUrl})` : `url(${DMXAPIToImg})`)};
background-color: #ffffff;
`
const LoadTextWrap = styled.div`

View File

@ -1,9 +1,3 @@
import ImageSize1_1 from '@renderer/assets/images/paintings/image-size-1-1.svg'
import ImageSize1_2 from '@renderer/assets/images/paintings/image-size-1-2.svg'
import ImageSize3_2 from '@renderer/assets/images/paintings/image-size-3-2.svg'
import ImageSize3_4 from '@renderer/assets/images/paintings/image-size-3-4.svg'
import ImageSize9_16 from '@renderer/assets/images/paintings/image-size-9-16.svg'
import ImageSize16_9 from '@renderer/assets/images/paintings/image-size-16-9.svg'
import { uuid } from '@renderer/utils'
import { t } from 'i18next'
@ -15,6 +9,13 @@ export type DMXApiModelData = {
provider: string
name: string
price: string
image_sizes: Array<{
label: string
value: string
}>
is_custom_size: boolean
max_image_size?: number
min_image_size?: number
}
// 模型分组类型
@ -54,41 +55,10 @@ export const STYLE_TYPE_OPTIONS = [
{ label: '巴洛克', value: '巴洛克' }
]
export const IMAGE_SIZES = [
{
label: '1:1',
value: '1328x1328',
icon: ImageSize1_1
},
{
label: '1:2',
value: '800x1600',
icon: ImageSize1_2
},
{
label: '3:2',
value: '1584x1056',
icon: ImageSize3_2
},
{
label: '3:4',
value: '1104x1472',
icon: ImageSize3_4
},
{
label: '16:9',
value: '1664x936',
icon: ImageSize16_9
},
{
label: '9:16',
value: '936x1664',
icon: ImageSize9_16
}
]
export const COURSE_URL = 'http://seedream.dmxapi.cn/'
export const TOP_UP_URL = 'https://www.dmxapi.cn/topup'
export const DEFAULT_PAINTING: DmxapiPainting = {
id: uuid(),
urls: [],