feat: painting aihubmix support model: gpt-image-1 (#6486)

* update select style

* add openai painting

* support base64 response

* update config

* fix upload preview bug

* fix remix default model

* fix history data

* feat: optimize structure

* fix history data

---------

Co-authored-by: zhaochenxue <zhaochenxue@bixin.cn>
This commit is contained in:
chenxue 2025-05-27 17:01:39 +08:00 committed by GitHub
parent e475ba0f95
commit 4eaf6fdf15
19 changed files with 608 additions and 611 deletions

View File

@ -111,6 +111,7 @@ export enum IpcChannel {
File_WriteWithId = 'file:writeWithId',
File_SaveImage = 'file:saveImage',
File_Base64Image = 'file:base64Image',
File_SaveBase64Image = 'file:saveBase64Image',
File_Download = 'file:download',
File_Copy = 'file:copy',
File_BinaryImage = 'file:binaryImage',

View File

@ -249,6 +249,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.File_WriteWithId, fileManager.writeFileWithId)
ipcMain.handle(IpcChannel.File_SaveImage, fileManager.saveImage)
ipcMain.handle(IpcChannel.File_Base64Image, fileManager.base64Image)
ipcMain.handle(IpcChannel.File_SaveBase64Image, fileManager.saveBase64Image)
ipcMain.handle(IpcChannel.File_Base64File, fileManager.base64File)
ipcMain.handle(IpcChannel.File_Download, fileManager.downloadFile)
ipcMain.handle(IpcChannel.File_Copy, fileManager.copyFile)

View File

@ -268,6 +268,51 @@ class FileStorage {
}
}
public saveBase64Image = async (_: Electron.IpcMainInvokeEvent, base64Data: string): Promise<FileType> => {
try {
if (!base64Data) {
throw new Error('Base64 data is required')
}
// 移除 base64 头部信息(如果存在)
const base64String = base64Data.replace(/^data:.*;base64,/, '')
const buffer = Buffer.from(base64String, 'base64')
const uuid = uuidv4()
const ext = '.png'
const destPath = path.join(this.storageDir, uuid + ext)
logger.info('[FileStorage] Saving base64 image:', {
storageDir: this.storageDir,
destPath,
bufferSize: buffer.length
})
// 确保目录存在
if (!fs.existsSync(this.storageDir)) {
fs.mkdirSync(this.storageDir, { recursive: true })
}
await fs.promises.writeFile(destPath, buffer)
const fileMetadata: FileType = {
id: uuid,
origin_name: uuid + ext,
name: uuid + ext,
path: destPath,
created_at: new Date().toISOString(),
size: buffer.length,
ext: ext.slice(1),
type: getFileType(ext),
count: 1
}
return fileMetadata
} catch (error) {
logger.error('[FileStorage] Failed to save base64 image:', error)
throw error
}
}
public base64File = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<{ data: string; mime: string }> => {
const filePath = path.join(this.storageDir, id)
const buffer = await fs.promises.readFile(filePath)

View File

@ -76,7 +76,9 @@ const api = {
selectFolder: () => ipcRenderer.invoke(IpcChannel.File_SelectFolder),
saveImage: (name: string, data: string) => ipcRenderer.invoke(IpcChannel.File_SaveImage, name, data),
base64Image: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64Image, fileId),
download: (url: string, isUseContentType?: boolean) => ipcRenderer.invoke(IpcChannel.File_Download, url, isUseContentType),
saveBase64Image: (data: string) => ipcRenderer.invoke(IpcChannel.File_SaveBase64Image, data),
download: (url: string, isUseContentType?: boolean) =>
ipcRenderer.invoke(IpcChannel.File_Download, url, isUseContentType),
copy: (fileId: string, destPath: string) => ipcRenderer.invoke(IpcChannel.File_Copy, fileId, destPath),
binaryImage: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_BinaryImage, fileId),
base64File: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64File, fileId),

View File

@ -820,12 +820,12 @@
"seed_desc_tip": "The same seed and prompt can generate similar images, setting -1 will generate different results each time",
"title": "Images",
"magic_prompt_option": "Magic Prompt",
"model": "Model Version",
"model": "Model",
"aspect_ratio": "Aspect Ratio",
"style_type": "Style",
"rendering_speed": "Rendering Speed",
"learn_more": "Learn More",
"paint_course":"tutorial",
"paint_course": "tutorial",
"prompt_placeholder_edit": "Enter your image description, text drawing uses \"double quotes\" to wrap",
"proxy_required": "Currently, you need to open a proxy to view the generated images, it will be supported in the future",
"image_file_required": "Please upload an image first",
@ -846,6 +846,29 @@
"turbo": "Turbo",
"quality": "Quality"
},
"quality_options": {
"auto": "Auto",
"low": "Low",
"medium": "Medium",
"high": "High"
},
"moderation_options": {
"auto": "Auto",
"low": "Low"
},
"background_options": {
"auto": "Auto",
"transparent": "Transparent",
"opaque": "Opaque"
},
"aspect_ratios": {
"square": "Square",
"portrait": "Portrait",
"landscape": "Landscape"
},
"quality": "Quality",
"moderation": "Moderation",
"background": "Background",
"mode": {
"generate": "Draw",
"edit": "Edit",

View File

@ -820,12 +820,12 @@
"seed_desc_tip": "同じシードとプロンプトで類似した画像を生成できますが、-1 に設定すると毎回異なる結果が生成されます",
"title": "画像",
"magic_prompt_option": "プロンプト強化",
"model": "モデルバージョン",
"model": "モデル",
"aspect_ratio": "画幅比例",
"style_type": "スタイル",
"learn_more": "詳しくはこちら",
"prompt_placeholder_edit": "画像の説明を入力します。テキスト描画には '二重引用符' を使用します",
"paint_course":"チュートリアル",
"paint_course": "チュートリアル",
"proxy_required": "現在、プロキシを開く必要があります。これは、将来サポートされる予定です",
"image_file_required": "画像を先にアップロードしてください",
"image_file_retry": "画像を先にアップロードしてください",
@ -844,6 +844,29 @@
"turbo": "高速",
"quality": "高品質"
},
"quality_options": {
"auto": "自動",
"low": "低",
"medium": "中",
"high": "高"
},
"moderation_options": {
"auto": "自動",
"low": "低"
},
"background_options": {
"auto": "自動",
"transparent": "透明",
"opaque": "不透明"
},
"aspect_ratios": {
"square": "正方形",
"portrait": "縦図",
"landscape": "横図"
},
"quality": "品質",
"moderation": "敏感度",
"background": "背景",
"mode": {
"generate": "画像生成",
"edit": "部分編集",

View File

@ -820,13 +820,13 @@
"seed_desc_tip": "Одинаковые сиды и промпты могут генерировать похожие изображения, установка -1 будет создавать разные результаты каждый раз",
"title": "Изображения",
"magic_prompt_option": "Улучшение промпта",
"model": "Версия",
"model": "Модель",
"aspect_ratio": "Пропорции изображения",
"style_type": "Стиль",
"rendering_speed": "Скорость рендеринга",
"learn_more": "Узнать больше",
"prompt_placeholder_edit": "Введите ваше описание изображения, текстовая отрисовка использует двойные кавычки для обертки",
"paint_course":"Руководство / Учебник",
"paint_course": "Руководство / Учебник",
"proxy_required": "Сейчас необходимо открыть прокси для просмотра сгенерированных изображений, в будущем будет поддерживаться прямое соединение",
"image_file_required": "Пожалуйста, сначала загрузите изображение",
"image_file_retry": "Пожалуйста, сначала загрузите изображение",
@ -841,11 +841,34 @@
"3d": "3D",
"anime": "Аниме"
},
"quality_options": {
"auto": "Авто",
"low": "Низкое",
"medium": "Среднее",
"high": "Высокое"
},
"moderation_options": {
"auto": "Авто",
"low": "Низкое"
},
"background_options": {
"auto": "Авто",
"transparent": "Прозрачный",
"opaque": "Непрозрачный"
},
"rendering_speeds": {
"default": "По умолчанию",
"turbo": "Быстро",
"quality": "Качественно"
},
"aspect_ratios": {
"square": "Квадрат",
"portrait": "Портрет",
"landscape": "Пейзаж"
},
"quality": "Качество",
"moderation": "Сенсорность",
"background": "Фон",
"mode": {
"generate": "Рисование",
"edit": "Редактирование",
@ -892,7 +915,6 @@
"seed_tip": "Контролирует случайный характер увеличения изображений для воспроизводимых результатов",
"magic_prompt_option_tip": "Улучшает увеличение изображений с помощью интеллектуального оптимизирования промптов"
},
"rendering_speed": "Скорость рендеринга",
"text_desc_required": "Пожалуйста, сначала введите описание изображения"
},
"prompts": {

View File

@ -820,12 +820,12 @@
"seed_desc_tip": "相同的种子和提示词可以生成相似的图片,设置 -1 每次生成都不一样",
"title": "图片",
"magic_prompt_option": "提示词增强",
"model": "版本",
"model": "模型",
"aspect_ratio": "画幅比例",
"style_type": "风格",
"rendering_speed": "渲染速度",
"learn_more": "了解更多",
"paint_course":"教程",
"paint_course": "教程",
"prompt_placeholder_edit": "输入你的图片描述,文本绘制用 \"双引号\" 包裹",
"proxy_required": "目前需要打开代理才能查看生成图片,后续会支持国内直连",
"image_file_required": "请先上传图片",
@ -846,11 +846,34 @@
"turbo": "快速",
"quality": "高质量"
},
"quality_options": {
"auto": "自动",
"low": "低",
"medium": "中",
"high": "高"
},
"moderation_options": {
"auto": "自动",
"low": "低"
},
"background_options": {
"auto": "自动",
"transparent": "透明",
"opaque": "不透明"
},
"aspect_ratios": {
"square": "方形",
"portrait": "竖图",
"landscape": "横图"
},
"quality": "质量",
"moderation": "敏感度",
"background": "背景",
"mode": {
"generate": "绘图",
"edit": "编辑",
"remix": "混合",
"upscale": "放大"
"upscale": "高清增强"
},
"generate": {
"model_tip": "模型版本V3 为最新版本V2 为之前版本V2A 为快速模型、V_1 为初代模型_TURBO 为加速版本",

View File

@ -821,12 +821,12 @@
"seed_desc_tip": "相同的種子和提示詞可以生成相似的圖片,設置 -1 每次生成都不一樣",
"title": "繪圖",
"magic_prompt_option": "提示詞增強",
"model": "版本",
"model": "模型",
"aspect_ratio": "畫幅比例",
"style_type": "風格",
"learn_more": "了解更多",
"prompt_placeholder_edit": "輸入你的圖片描述,文本繪製用 '雙引號' 包裹",
"paint_course":"教程",
"paint_course": "教程",
"proxy_required": "目前需要打開代理才能查看生成圖片,後續會支持國內直連",
"image_file_required": "請先上傳圖片",
"image_file_retry": "請重新上傳圖片",
@ -846,6 +846,29 @@
"turbo": "快速",
"quality": "高品質"
},
"quality_options": {
"auto": "自動",
"low": "低",
"medium": "中",
"high": "高"
},
"moderation_options": {
"auto": "自動",
"low": "低"
},
"background_options": {
"auto": "自動",
"transparent": "透明",
"opaque": "不透明"
},
"aspect_ratios": {
"square": "方形",
"portrait": "豎圖",
"landscape": "橫圖"
},
"quality": "品質",
"moderation": "敏感度",
"background": "背景",
"mode": {
"generate": "繪圖",
"edit": "編輯",

View File

@ -22,17 +22,16 @@ import { Avatar, Button, Input, InputNumber, Radio, Segmented, Select, Slider, S
import TextArea from 'antd/es/input/TextArea'
import { Info } from 'lucide-react'
import type { FC } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
import { SettingHelpLink, SettingTitle } from '../settings'
import Artboard from './Artboard'
import { type ConfigItem, createModeConfigs } from './config/aihubmixConfig'
import { DEFAULT_PAINTING } from './config/constants'
import PaintingsList from './PaintingsList'
import Artboard from './components/Artboard'
import PaintingsList from './components/PaintingsList'
import { type ConfigItem, createModeConfigs, DEFAULT_PAINTING } from './config/aihubmixConfig'
// 使用函数创建配置项
const modeConfigs = createModeConfigs()
@ -74,12 +73,13 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
{ label: t('paintings.mode.upscale'), value: 'upscale' }
]
const getNewPainting = () => {
const getNewPainting = useCallback(() => {
return {
...DEFAULT_PAINTING,
model: mode === 'generate' ? 'gpt-image-1' : 'V_3',
id: uuid()
}
}
}, [mode])
const textareaRef = useRef<any>(null)
@ -89,6 +89,47 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
updatePainting(mode, updatedPainting)
}
const handleError = (error: unknown) => {
if (error instanceof Error && error.name !== 'AbortError') {
window.modal.error({
content: getErrorMessage(error),
centered: true
})
}
}
const downloadImages = async (urls: string[]) => {
const downloadedFiles = await Promise.all(
urls.map(async (url) => {
try {
if (!url?.trim()) {
console.error('图像URL为空可能是提示词违禁')
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
return null
}
return await window.api.file.download(url)
} catch (error) {
console.error('下载图像失败:', error)
if (
error instanceof Error &&
(error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL'))
) {
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
}
return null
}
})
)
return downloadedFiles.filter((file): file is FileType => file !== null)
}
const onGenerate = async () => {
if (painting.files.length > 0) {
const confirmed = await window.modal.confirm({
@ -129,11 +170,11 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
dispatch(setGenerating(true))
let body: string | FormData = ''
const headers: Record<string, string> = {
let headers: Record<string, string> = {
'Api-Key': aihubmixProvider.apiKey
}
let url = aihubmixProvider.apiHost + `/ideogram/` + mode
// 不使用 AiProvider 的通用规则,而是直接调用自定义接口
try {
if (mode === 'generate') {
if (painting.model === 'V_3') {
@ -214,67 +255,47 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
console.log('V3 API响应:', data)
const urls = data.data.map((item) => item.url)
// Rest of the code for handling image downloads is the same
if (urls.length > 0) {
const downloadedFiles = await Promise.all(
urls.map(async (url) => {
try {
// 检查URL是否为空
if (!url || url.trim() === '') {
console.error('图像URL为空可能是提示词违禁')
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
return null
}
return await window.api.file.download(url)
} catch (error) {
console.error('下载图像失败:', error)
// 检查是否是URL解析错误
if (
error instanceof Error &&
(error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL'))
) {
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
}
return null
}
})
)
const validFiles = downloadedFiles.filter((file): file is FileType => file !== null)
const validFiles = await downloadImages(urls)
await FileManager.addFiles(validFiles)
updatePaintingState({ files: validFiles, urls })
}
return
} catch (error: unknown) {
if (error instanceof Error && error.name !== 'AbortError') {
window.modal.error({
content: getErrorMessage(error),
centered: true
})
}
handleError(error)
} finally {
setIsLoading(false)
dispatch(setGenerating(false))
setAbortController(null)
}
} else {
// Existing V1/V2 API
const requestData = {
image_request: {
let requestData: any = {}
if (painting.model === 'gpt-image-1') {
requestData = {
prompt,
model: painting.model,
aspect_ratio: painting.aspectRatio,
num_images: painting.numImages,
style_type: painting.styleType,
seed: painting.seed ? +painting.seed : undefined,
negative_prompt: painting.negativePrompt || undefined,
magic_prompt_option: painting.magicPromptOption ? 'ON' : 'OFF'
size: painting.size === 'auto' ? undefined : painting.size,
n: painting.n,
quality: painting.quality,
moderation: painting.moderation
}
url = aihubmixProvider.apiHost + `/v1/images/generations`
headers = {
Authorization: `Bearer ${aihubmixProvider.apiKey}`
}
} else {
// Existing V1/V2 API
requestData = {
image_request: {
prompt,
model: painting.model,
aspect_ratio: painting.aspectRatio,
num_images: painting.numImages,
style_type: painting.styleType,
seed: painting.seed ? +painting.seed : undefined,
negative_prompt: painting.negativePrompt || undefined,
magic_prompt_option: painting.magicPromptOption ? 'ON' : 'OFF'
}
}
}
body = JSON.stringify(requestData)
@ -352,37 +373,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
// Handle the downloaded images
if (urls.length > 0) {
const downloadedFiles = await Promise.all(
urls.map(async (url) => {
try {
// 检查URL是否为空
if (!url || url.trim() === '') {
console.error('图像URL为空可能是提示词违禁')
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
return null
}
return await window.api.file.download(url)
} catch (error) {
console.error('下载图像失败:', error)
// 检查是否是URL解析错误
if (
error instanceof Error &&
(error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL'))
) {
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
}
return null
}
})
)
const validFiles = downloadedFiles.filter((file): file is FileType => file !== null)
const validFiles = await downloadImages(urls)
await FileManager.addFiles(validFiles)
updatePaintingState({ files: validFiles, urls })
}
@ -405,119 +396,6 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
form.append('image_file', fileMap[painting.imageFile] as unknown as Blob)
body = form
}
} else if (mode === 'edit') {
if (!painting.imageFile) {
window.modal.error({
content: t('paintings.image_file_required'),
centered: true
})
return
}
if (!fileMap[painting.imageFile]) {
window.modal.error({
content: t('paintings.image_file_retry'),
centered: true
})
return
}
if (painting.model === 'V_3') {
// V3 Edit API
const formData = new FormData()
formData.append('prompt', prompt)
formData.append('rendering_speed', painting.renderingSpeed || 'DEFAULT')
formData.append('num_images', String(painting.numImages || 1))
if (painting.styleType) {
formData.append('style_type', painting.styleType)
}
if (painting.seed) {
formData.append('seed', painting.seed)
}
if (painting.magicPromptOption !== undefined) {
formData.append('magic_prompt', painting.magicPromptOption ? 'ON' : 'OFF')
}
// Add the image file
formData.append('image', fileMap[painting.imageFile] as unknown as Blob)
// Add the mask if available
if (painting.mask) {
formData.append('mask', painting.mask as unknown as Blob)
}
body = formData
// For V3 Edit endpoint
const response = await fetch(`${aihubmixProvider.apiHost}/ideogram/v1/ideogram-v3/edit`, {
method: 'POST',
headers: { 'Api-Key': aihubmixProvider.apiKey },
body
})
if (!response.ok) {
const errorData = await response.json()
console.error('V3 Edit API错误:', errorData)
throw new Error(errorData.error?.message || '图像编辑失败')
}
const data = await response.json()
console.log('V3 Edit API响应:', data)
const urls = data.data.map((item) => item.url)
// Handle the downloaded images
if (urls.length > 0) {
const downloadedFiles = await Promise.all(
urls.map(async (url) => {
try {
// 检查URL是否为空
if (!url || url.trim() === '') {
console.error('图像URL为空可能是提示词违禁')
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
return null
}
return await window.api.file.download(url)
} catch (error) {
console.error('下载图像失败:', error)
// 检查是否是URL解析错误
if (
error instanceof Error &&
(error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL'))
) {
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
}
return null
}
})
)
const validFiles = downloadedFiles.filter((file): file is FileType => file !== null)
await FileManager.addFiles(validFiles)
updatePaintingState({ files: validFiles, urls })
}
return
} else {
// Existing V1/V2 API for edit
const form = new FormData()
const imageRequest: Record<string, any> = {
prompt,
model: painting.model,
style_type: painting.styleType,
num_images: painting.numImages,
seed: painting.seed ? +painting.seed : undefined,
magic_prompt_option: painting.magicPromptOption ? 'ON' : 'OFF'
}
form.append('image_request', JSON.stringify(imageRequest))
form.append('image_file', fileMap[painting.imageFile] as unknown as Blob)
body = form
}
} else if (mode === 'upscale') {
if (!painting.imageFile) {
window.modal.error({
@ -549,9 +427,9 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
}
// 只针对非V3模型使用通用接口
if (!painting.model?.includes('V_3')) {
if (!painting.model?.includes('V_3') || mode === 'upscale') {
// 直接调用自定义接口
const response = await fetch(`${aihubmixProvider.apiHost}/ideogram/${mode}`, { method: 'POST', headers, body })
const response = await fetch(url, { method: 'POST', headers, body })
if (!response.ok) {
const errorData = await response.json()
@ -561,53 +439,27 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
const data = await response.json()
console.log('通用API响应:', data)
const urls = data.data.map((item) => item.url)
const urls = data.data.filter((item) => item.url).map((item) => item.url)
const base64s = data.data.filter((item) => item.b64_json).map((item) => item.b64_json)
if (urls.length > 0) {
const downloadedFiles = await Promise.all(
urls.map(async (url) => {
try {
// 检查URL是否为空
if (!url || url.trim() === '') {
console.error('图像URL为空可能是提示词违禁')
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
return null
}
return await window.api.file.download(url)
} catch (error) {
console.error('下载图像失败:', error)
// 检查是否是URL解析错误
if (
error instanceof Error &&
(error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL'))
) {
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
}
return null
}
const validFiles = await downloadImages(urls)
await FileManager.addFiles(validFiles)
updatePaintingState({ files: validFiles, urls })
}
if (base64s?.length > 0) {
const validFiles = await Promise.all(
base64s.map(async (base64) => {
return await window.api.file.saveBase64Image(base64)
})
)
const validFiles = downloadedFiles.filter((file): file is FileType => file !== null)
await FileManager.addFiles(validFiles)
updatePaintingState({ files: validFiles, urls })
updatePaintingState({ files: validFiles, urls: validFiles.map((file) => file.name) })
}
}
} catch (error: unknown) {
if (error instanceof Error && error.name !== 'AbortError') {
window.modal.error({
content: getErrorMessage(error),
centered: true
})
}
handleError(error)
} finally {
setIsLoading(false)
dispatch(setGenerating(false))
@ -617,43 +469,15 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
const handleRetry = async (painting: PaintingAction) => {
setIsLoading(true)
const downloadedFiles = await Promise.all(
painting.urls.map(async (url) => {
try {
// 检查URL是否为空
if (!url || url.trim() === '') {
console.error('图像URL为空可能是提示词违禁')
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
return null
}
return await window.api.file.download(url)
} catch (error) {
console.error('下载图像失败:', error)
// 检查是否是URL解析错误
if (
error instanceof Error &&
(error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL'))
) {
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
}
setIsLoading(false)
return null
}
})
)
const validFiles = downloadedFiles.filter((file): file is FileType => file !== null)
await FileManager.addFiles(validFiles)
updatePaintingState({ files: validFiles, urls: painting.urls })
setIsLoading(false)
try {
const validFiles = await downloadImages(painting.urls)
await FileManager.addFiles(validFiles)
updatePaintingState({ files: validFiles, urls: painting.urls })
} catch (error) {
handleError(error)
} finally {
setIsLoading(false)
}
}
const onCancel = () => {
@ -754,20 +578,8 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
}
// 渲染配置项的函数
const renderConfigItem = (item: ConfigItem, index: number) => {
const renderConfigForm = (item: ConfigItem) => {
switch (item.type) {
case 'title': {
return (
<SettingTitle key={index} style={{ marginBottom: 5, marginTop: 15 }}>
{t(item.title!)}
{item.tooltip && (
<Tooltip title={t(item.tooltip)}>
<InfoIcon />
</Tooltip>
)}
</SettingTitle>
)
}
case 'select': {
// 处理函数类型的disabled属性
const isDisabled = typeof item.disabled === 'function' ? item.disabled(item, painting) : item.disabled
@ -786,10 +598,11 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
return (
<Select
key={index}
style={{ width: '100%' }}
listHeight={500}
disabled={isDisabled}
value={painting[item.key!] || item.initialValue}
options={selectOptions}
options={selectOptions as any}
onChange={(v) => updatePaintingState({ [item.key!]: v })}
/>
)
@ -809,8 +622,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
return (
<Radio.Group
key={index}
value={painting[item.key!]}
value={painting[item.key!] || item.initialValue}
onChange={(e) => updatePaintingState({ [item.key!]: e.target.value })}>
{radioOptions!.map((option) => (
<Radio.Button key={option.value} value={option.value}>
@ -822,96 +634,82 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
}
case 'slider': {
return (
<SliderContainer key={index}>
<SliderContainer>
<Slider
min={item.min}
max={item.max}
step={item.step}
value={painting[item.key!] as number}
value={(painting[item.key!] || item.initialValue) as number}
onChange={(v) => updatePaintingState({ [item.key!]: v })}
/>
<StyledInputNumber
min={item.min}
max={item.max}
step={item.step}
value={painting[item.key!] as number}
value={(painting[item.key!] || item.initialValue) as number}
onChange={(v) => updatePaintingState({ [item.key!]: v })}
/>
</SliderContainer>
)
}
case 'input': {
// 处理随机种子按钮的特殊情况
if (item.key === 'seed') {
return (
<Input
key={index}
value={painting[item.key] as string}
onChange={(e) => updatePaintingState({ [item.key!]: e.target.value })}
suffix={
<RedoOutlined onClick={handleRandomSeed} style={{ cursor: 'pointer', color: 'var(--color-text-2)' }} />
}
/>
)
}
case 'input':
return (
<Input
key={index}
value={painting[item.key!] as string}
value={(painting[item.key!] || item.initialValue) as string}
onChange={(e) => updatePaintingState({ [item.key!]: e.target.value })}
suffix={item.suffix}
suffix={
item.key === 'seed' ? (
<RedoOutlined onClick={handleRandomSeed} style={{ cursor: 'pointer', color: 'var(--color-text-2)' }} />
) : (
item.suffix
)
}
/>
)
}
case 'inputNumber': {
case 'inputNumber':
return (
<InputNumber
key={index}
min={item.min}
max={item.max}
style={{ width: '100%' }}
value={painting[item.key!] as number}
value={(painting[item.key!] || item.initialValue) as number}
onChange={(v) => updatePaintingState({ [item.key!]: v })}
/>
)
}
case 'textarea': {
case 'textarea':
return (
<TextArea
key={index}
value={painting[item.key!] as string}
value={(painting[item.key!] || item.initialValue) as string}
onChange={(e) => updatePaintingState({ [item.key!]: e.target.value })}
spellCheck={false}
rows={4}
/>
)
}
case 'switch': {
case 'switch':
return (
<HStack key={index}>
<HStack>
<Switch
checked={painting[item.key!] as boolean}
checked={(painting[item.key!] || item.initialValue) as boolean}
onChange={(checked) => updatePaintingState({ [item.key!]: checked })}
/>
</HStack>
)
}
case 'image': {
return (
<ImageUploadButton
key={index}
accept="image/png, image/jpeg, image/gif"
maxCount={1}
showUploadList={false}
listType="picture-card"
onChange={async ({ file }) => {
const path = file.originFileObj?.path || ''
setFileMap({ ...fileMap, [path]: file.originFileObj as unknown as FileType })
beforeUpload={(file) => {
const path = URL.createObjectURL(file)
setFileMap({ ...fileMap, [path]: file as unknown as FileType })
updatePaintingState({ [item.key!]: path })
return false // 阻止默认上传行为
}}>
{painting[item.key!] ? (
<ImagePreview>
<img src={'file://' + painting[item.key!]} alt="预览图" />
<img src={painting[item.key!]} alt="预览图" />
</ImagePreview>
) : (
<ImageSizeImage src={IcImageUp} theme={theme} />
@ -924,6 +722,23 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
}
}
// 渲染配置项的函数
const renderConfigItem = (item: ConfigItem, index: number) => {
return (
<div key={index}>
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
{t(item.title!)}
{item.tooltip && (
<Tooltip title={t(item.tooltip)}>
<InfoIcon />
</Tooltip>
)}
</SettingTitle>
{renderConfigForm(item)}
</div>
)
}
const onSelectPainting = (newPainting: PaintingAction) => {
if (generating) return
setPainting(newPainting)
@ -936,12 +751,13 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
addPainting(mode, newPainting)
setPainting(newPainting)
}
}, [filteredPaintings, mode, addPainting, painting])
}, [filteredPaintings, mode, addPainting, painting, getNewPainting])
useEffect(() => {
const timer = spaceClickTimer.current
return () => {
if (spaceClickTimer.current) {
clearTimeout(spaceClickTimer.current)
if (timer) {
clearTimeout(timer)
}
}
}, [])
@ -985,7 +801,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
</Select>
{/* 使用JSON配置渲染设置项 */}
{modeConfigs[mode].map(renderConfigItem)}
{modeConfigs[mode].filter((item) => (item.condition ? item.condition(painting) : true)).map(renderConfigItem)}
</LeftContainer>
<MainContainer>
{/* 添加功能切换分段控制器 */}

View File

@ -26,7 +26,8 @@ import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
import { SettingHelpLink, SettingTitle } from '../settings'
import Artboard from './Artboard'
import Artboard from './components/Artboard'
import PaintingsList from './components/PaintingsList'
import {
COURSE_URL,
DEFAULT_PAINTING,
@ -34,7 +35,6 @@ import {
STYLE_TYPE_OPTIONS,
TEXT_TO_IMAGES_MODELS
} from './config/DmxapiConfig'
import PaintingsList from './PaintingsList'
const generateRandomSeed = () => Math.floor(Math.random() * 1000000).toString()

View File

@ -3,7 +3,7 @@ import { Route, Routes } from 'react-router-dom'
import AihubmixPage from './AihubmixPage'
import DmxapiPage from './DmxapiPage'
import SiliconPage from './PaintingsPage'
import SiliconPage from './SiliconPage'
const Options = ['aihubmix', 'silicon', 'dmxapi']

View File

@ -35,8 +35,8 @@ import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
import { SettingTitle } from '../settings'
import Artboard from './Artboard'
import PaintingsList from './PaintingsList'
import Artboard from './components/Artboard'
import PaintingsList from './components/PaintingsList'
const IMAGE_SIZES = [
{
@ -88,7 +88,7 @@ const DEFAULT_PAINTING: Painting = {
// let _painting: Painting
const PaintingsPage: FC<{ Options: string[] }> = ({ Options }) => {
const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
const { t } = useTranslation()
const { paintings, addPainting, removePainting, updatePainting } = usePaintings()
const [painting, setPainting] = useState<Painting>(paintings[0] || DEFAULT_PAINTING)
@ -645,4 +645,4 @@ const StyledInputNumber = styled(InputNumber)`
width: 70px;
`
export default PaintingsPage
export default SiliconPage

View File

@ -7,7 +7,7 @@ import React, { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import ImagePreview from '../home/Markdown/ImagePreview'
import ImagePreview from '../../home/Markdown/ImagePreview'
interface ArtboardProps {
painting: Painting
@ -98,8 +98,8 @@ const Artboard: FC<ArtboardProps> = ({
{painting.urls.length > 0 && retry ? (
<div>
<ImageList>
{painting.urls.map((url) => (
<ImageListItem key={url}>{url}</ImageListItem>
{painting.urls.map((url, index) => (
<ImageListItem key={url || index}>{url}</ImageListItem>
))}
</ImageList>
<div>
@ -109,11 +109,11 @@ const Artboard: FC<ArtboardProps> = ({
</Button>
</div>
</div>
) : imageCover ? (
imageCover
) : (
imageCover ?
imageCover:
(<div>{t('paintings.image_placeholder')}</div>)
)}
<div>{t('paintings.image_placeholder')}</div>
)}
</ImagePlaceholder>
)}
{isLoading && (

View File

@ -1,6 +1,14 @@
import type { PaintingAction, PaintingsState } from '@renderer/types'
import type { PaintingAction } from '@renderer/types'
import { ASPECT_RATIOS, RENDERING_SPEED_OPTIONS, STYLE_TYPES, V3_STYLE_TYPES } from './constants'
import {
ASPECT_RATIOS,
BACKGROUND_OPTIONS,
MODERATION_OPTIONS,
QUALITY_OPTIONS,
RENDERING_SPEED_OPTIONS,
STYLE_TYPES,
V3_STYLE_TYPES
} from './constants'
// 配置项类型定义
export type ConfigItem = {
@ -19,7 +27,14 @@ export type ConfigItem = {
title?: string
tooltip?: string
options?:
| Array<{ label: string; value: string | number; icon?: string; onlyV2?: boolean }>
| Array<{
label: string
title?: string
value?: string | number
icon?: string
onlyV2?: boolean
options?: Array<{ label: string; value: string | number; icon?: string; onlyV2?: boolean }>
}>
| ((
config: ConfigItem,
painting: Partial<PaintingAction>
@ -32,189 +47,158 @@ export type ConfigItem = {
disabled?: boolean | ((config: ConfigItem, painting: Partial<PaintingAction>) => boolean)
initialValue?: string | number
required?: boolean
condition?: (painting: PaintingAction) => boolean
}
export type AihubmixMode = keyof PaintingsState
export type AihubmixMode = 'generate' | 'remix' | 'upscale'
// 创建配置项函数
export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
return {
paintings: [],
DMXAPIPaintings: [],
generate: [
{ type: 'title', title: 'paintings.model', tooltip: 'paintings.generate.model_tip' },
{
type: 'select',
key: 'model',
title: 'paintings.model',
tooltip: 'paintings.generate.model_tip',
options: [
{ label: 'ideogram_V_3', value: 'V_3' },
{ label: 'ideogram_V_2', value: 'V_2' },
{ label: 'ideogram_V_2_TURBO', value: 'V_2_TURBO' },
{ label: 'ideogram_V_2A', value: 'V_2A' },
{ label: 'ideogram_V_2A_TURBO', value: 'V_2A_TURBO' },
{ label: 'ideogram_V_1', value: 'V_1' },
{ label: 'ideogram_V_1_TURBO', value: 'V_1_TURBO' }
{
label: 'OpenAI',
title: 'OpenAI',
options: [{ label: 'gpt-image-1', value: 'gpt-image-1' }]
},
{
label: 'ideogram',
title: 'ideogram',
options: [
{ label: 'ideogram_V_3', value: 'V_3' },
{ label: 'ideogram_V_2', value: 'V_2' },
{ label: 'ideogram_V_2_TURBO', value: 'V_2_TURBO' },
{ label: 'ideogram_V_2A', value: 'V_2A' },
{ label: 'ideogram_V_2A_TURBO', value: 'V_2A_TURBO' },
{ label: 'ideogram_V_1', value: 'V_1' },
{ label: 'ideogram_V_1_TURBO', value: 'V_1_TURBO' }
]
}
]
},
{ type: 'title', title: 'paintings.rendering_speed', tooltip: 'paintings.generate.rendering_speed_tip' },
{
type: 'select',
key: 'renderingSpeed',
title: 'paintings.rendering_speed',
tooltip: 'paintings.generate.rendering_speed_tip',
options: RENDERING_SPEED_OPTIONS,
initialValue: 'DEFAULT',
disabled: (_config, painting) => {
const model = painting?.model
return !model || !model.includes('V_3')
}
condition: (painting) => painting.model === 'V_3'
},
{ type: 'title', title: 'paintings.aspect_ratio' },
{
type: 'select',
key: 'aspectRatio',
options: ASPECT_RATIOS.map((size) => ({
label: size.label,
value: size.value,
icon: size.icon
}))
},
{
type: 'title',
title: 'paintings.number_images',
tooltip: 'paintings.generate.number_images_tip'
title: 'paintings.aspect_ratio',
options: ASPECT_RATIOS,
condition: (painting) => Boolean(painting.model?.startsWith('V_'))
},
{
type: 'slider',
key: 'numImages',
title: 'paintings.number_images',
tooltip: 'paintings.generate.number_images_tip',
min: 1,
max: 8
},
{
type: 'title',
title: 'paintings.style_type',
tooltip: 'paintings.generate.style_type_tip'
max: 8,
condition: (painting) => Boolean(painting.model?.startsWith('V_'))
},
{
type: 'select',
key: 'styleType',
title: 'paintings.style_type',
tooltip: 'paintings.generate.style_type_tip',
options: (_config, painting) => {
// 根据模型选择显示不同的样式类型选项
return painting?.model?.includes('V_3') ? V3_STYLE_TYPES : STYLE_TYPES
},
disabled: false
},
{
type: 'title',
title: 'paintings.seed',
tooltip: 'paintings.generate.seed_tip'
disabled: false,
condition: (painting) => Boolean(painting.model?.startsWith('V_'))
},
{
type: 'input',
key: 'seed'
},
{
type: 'title',
title: 'paintings.negative_prompt',
tooltip: 'paintings.generate.negative_prompt_tip'
key: 'seed',
title: 'paintings.seed',
tooltip: 'paintings.generate.seed_tip',
condition: (painting) => Boolean(painting.model?.startsWith('V_'))
},
{
type: 'textarea',
key: 'negativePrompt'
},
{
type: 'title',
title: 'paintings.magic_prompt_option',
tooltip: 'paintings.generate.magic_prompt_option_tip'
key: 'negativePrompt',
title: 'paintings.negative_prompt',
tooltip: 'paintings.generate.negative_prompt_tip',
condition: (painting) => Boolean(painting.model?.startsWith('V_'))
},
{
type: 'switch',
key: 'magicPromptOption'
}
],
edit: [
{ type: 'title', title: 'paintings.edit.image_file' },
{
type: 'image',
key: 'imageFile'
key: 'magicPromptOption',
title: 'paintings.magic_prompt_option',
tooltip: 'paintings.generate.magic_prompt_option_tip',
condition: (painting) => Boolean(painting.model?.startsWith('V_'))
},
{ type: 'title', title: 'paintings.model', tooltip: 'paintings.edit.model_tip' },
{
type: 'select',
key: 'model',
key: 'size',
title: 'paintings.aspect_ratio',
options: [
{ label: 'ideogram_V_3', value: 'V_3' },
{ label: 'ideogram_V_2', value: 'V_2' },
{ label: 'ideogram_V_2_TURBO', value: 'V_2_TURBO' },
{ label: 'ideogram_V_2A', value: 'V_2A' },
{ label: 'ideogram_V_2A_TURBO', value: 'V_2A_TURBO' },
{ label: 'ideogram_V_1', value: 'V_1' },
{ label: 'ideogram_V_1_TURBO', value: 'V_1_TURBO' }
]
},
{ type: 'title', title: 'paintings.rendering_speed', tooltip: 'paintings.edit.rendering_speed_tip' },
{
type: 'select',
key: 'renderingSpeed',
options: RENDERING_SPEED_OPTIONS,
initialValue: 'DEFAULE',
disabled: (_config, painting) => {
const model = painting?.model
return !model || !model.includes('V_3')
}
},
{
type: 'title',
title: 'paintings.number_images',
tooltip: 'paintings.edit.number_images_tip'
{ label: '自动', value: 'auto' },
{ label: '1:1', value: '1024x1024' },
{ label: '3:2', value: '1536x1024' },
{ label: '2:3', value: '1024x1536' }
],
initialValue: '1024x1024',
condition: (painting) => painting.model === 'gpt-image-1'
},
{
type: 'slider',
key: 'numImages',
key: 'n',
title: 'paintings.number_images',
tooltip: 'paintings.generate.number_images_tip',
min: 1,
max: 8
},
{
type: 'title',
title: 'paintings.style_type',
tooltip: 'paintings.edit.style_type_tip'
max: 10,
initialValue: 1,
condition: (painting) => painting.model === 'gpt-image-1'
},
{
type: 'select',
key: 'styleType',
options: (_config, painting) => {
// 根据模型选择显示不同的样式类型选项
return painting?.model?.includes('V_3') ? V3_STYLE_TYPES : STYLE_TYPES
},
disabled: false
key: 'quality',
title: 'paintings.quality',
options: QUALITY_OPTIONS,
initialValue: 'auto',
condition: (painting) => painting.model === 'gpt-image-1'
},
{
type: 'title',
title: 'paintings.seed',
tooltip: 'paintings.edit.seed_tip'
type: 'select',
key: 'moderation',
title: 'paintings.moderation',
options: MODERATION_OPTIONS,
initialValue: 'auto',
condition: (painting) => painting.model === 'gpt-image-1'
},
{
type: 'input',
key: 'seed'
},
{
type: 'title',
title: 'paintings.magic_prompt_option',
tooltip: 'paintings.edit.magic_prompt_option_tip'
},
{
type: 'switch',
key: 'magicPromptOption'
type: 'select',
key: 'background',
title: 'paintings.background',
options: BACKGROUND_OPTIONS,
initialValue: 'auto',
condition: (painting) => painting.model === 'gpt-image-1'
}
],
remix: [
{ type: 'title', title: 'paintings.remix.image_file' },
{
type: 'image',
key: 'imageFile'
key: 'imageFile',
title: 'paintings.remix.image_file'
},
{ type: 'title', title: 'paintings.model', tooltip: 'paintings.remix.model_tip' },
{
type: 'select',
key: 'model',
title: 'paintings.model',
tooltip: 'paintings.remix.model_tip',
options: [
{ label: 'ideogram_V_3', value: 'V_3' },
{ label: 'ideogram_V_2', value: 'V_2' },
@ -225,10 +209,10 @@ export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
{ label: 'ideogram_V_1_TURBO', value: 'V_1_TURBO' }
]
},
{ type: 'title', title: 'paintings.rendering_speed', tooltip: 'paintings.remix.rendering_speed_tip' },
{
type: 'select',
key: 'renderingSpeed',
title: 'paintings.rendering_speed',
options: RENDERING_SPEED_OPTIONS,
initialValue: 'DEFAULT',
disabled: (_config, painting) => {
@ -236,42 +220,32 @@ export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
return !model || !model.includes('V_3')
}
},
{ type: 'title', title: 'paintings.aspect_ratio' },
{
type: 'select',
key: 'aspectRatio',
options: ASPECT_RATIOS.map((size) => ({
label: size.label,
value: size.value,
icon: size.icon
}))
title: 'paintings.aspect_ratio',
options: ASPECT_RATIOS
},
{ type: 'title', title: 'paintings.remix.image_weight' },
{
type: 'slider',
key: 'imageWeight',
title: 'paintings.remix.image_weight',
min: 1,
max: 100
},
{
type: 'title',
title: 'paintings.number_images',
tooltip: 'paintings.remix.number_images_tip'
},
{
type: 'slider',
key: 'numImages',
title: 'paintings.number_images',
tooltip: 'paintings.remix.number_images_tip',
min: 1,
max: 8
},
{
type: 'title',
title: 'paintings.style_type',
tooltip: 'paintings.remix.style_type_tip'
},
{
type: 'select',
key: 'styleType',
title: 'paintings.style_type',
tooltip: 'paintings.remix.style_type_tip',
options: (_config, painting) => {
// 根据模型选择显示不同的样式类型选项
return painting?.model?.includes('V_3') ? V3_STYLE_TYPES : STYLE_TYPES
@ -279,78 +253,92 @@ export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
disabled: false
},
{
type: 'title',
type: 'input',
key: 'seed',
title: 'paintings.seed',
tooltip: 'paintings.remix.seed_tip'
},
{
type: 'input',
key: 'seed'
},
{
type: 'title',
type: 'textarea',
key: 'negativePrompt',
title: 'paintings.negative_prompt',
tooltip: 'paintings.remix.negative_prompt_tip'
},
{
type: 'textarea',
key: 'negativePrompt'
},
{
type: 'title',
type: 'switch',
key: 'magicPromptOption',
title: 'paintings.magic_prompt_option',
tooltip: 'paintings.remix.magic_prompt_option_tip'
},
{
type: 'switch',
key: 'magicPromptOption'
}
],
upscale: [
{ type: 'title', title: 'paintings.upscale.image_file' },
{
type: 'image',
key: 'imageFile',
title: 'paintings.upscale.image_file',
required: true
},
{ type: 'title', title: 'paintings.upscale.resemblance', tooltip: 'paintings.upscale.resemblance_tip' },
{ type: 'slider', key: 'resemblance', min: 1, max: 100 },
{ type: 'title', title: 'paintings.upscale.detail', tooltip: 'paintings.upscale.detail_tip' },
{
type: 'slider',
key: 'detail',
key: 'resemblance',
title: 'paintings.upscale.resemblance',
min: 1,
max: 100
},
{
type: 'title',
title: 'paintings.number_images',
tooltip: 'paintings.upscale.number_images_tip'
type: 'slider',
key: 'detail',
title: 'paintings.upscale.detail',
tooltip: 'paintings.upscale.detail_tip',
min: 1,
max: 100
},
{
type: 'slider',
key: 'numImages',
title: 'paintings.number_images',
tooltip: 'paintings.upscale.number_images_tip',
min: 1,
max: 8
},
{
type: 'title',
type: 'input',
key: 'seed',
title: 'paintings.seed',
tooltip: 'paintings.upscale.seed_tip'
},
{
type: 'input',
key: 'seed'
},
{
type: 'title',
type: 'switch',
key: 'magicPromptOption',
title: 'paintings.magic_prompt_option',
tooltip: 'paintings.upscale.magic_prompt_option_tip'
},
{
type: 'switch',
key: 'magicPromptOption'
}
]
}
}
// 几种默认的绘画配置
export const DEFAULT_PAINTING: PaintingAction = {
id: 'aihubmix_1',
model: 'gpt-image-1',
aspectRatio: 'ASPECT_1_1',
numImages: 1,
styleType: 'AUTO',
prompt: '',
negativePrompt: '',
magicPromptOption: true,
seed: '',
imageWeight: 50,
resemblance: 50,
detail: 50,
imageFile: undefined,
mask: undefined,
files: [],
urls: [],
renderingSpeed: 'DEFAULT',
size: '1024x1024',
background: 'auto',
quality: 'auto',
moderation: 'auto',
n: 1
}

View File

@ -1,87 +1,78 @@
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 type { PaintingAction } from '@renderer/types'
// 几种默认的绘画配置
export const DEFAULT_PAINTING: PaintingAction = {
id: 'aihubmix_1',
model: 'V_3',
aspectRatio: 'ASPECT_1_1',
numImages: 1,
styleType: 'AUTO',
prompt: '',
negativePrompt: '',
magicPromptOption: true,
seed: '',
imageWeight: 50,
resemblance: 50,
detail: 50,
imageFile: undefined,
mask: undefined,
files: [],
urls: [],
renderingSpeed: 'DEFAULT'
}
export const ASPECT_RATIOS = [
{
label: '1:1',
value: 'ASPECT_1_1',
icon: ImageSize1_1
label: 'paintings.aspect_ratios.square',
options: [
{
label: '1:1',
value: 'ASPECT_1_1'
}
]
},
{
label: '3:1',
value: 'ASPECT_3_1',
icon: ImageSize3_2
label: 'paintings.aspect_ratios.landscape',
options: [
{
label: '1:2',
value: 'ASPECT_1_2'
},
{
label: '1:3',
value: 'ASPECT_1_3'
},
{
label: '2:3',
value: 'ASPECT_2_3'
},
{
label: '3:4',
value: 'ASPECT_3_4'
},
{
label: '4:5',
value: 'ASPECT_4_5'
},
{
label: '9:16',
value: 'ASPECT_9_16'
},
{
label: '10:16',
value: 'ASPECT_10_16'
}
]
},
{
label: '1:3',
value: 'ASPECT_1_3',
icon: ImageSize1_2
},
{
label: '3:2',
value: 'ASPECT_3_2',
icon: ImageSize3_2
},
{
label: '2:3',
value: 'ASPECT_2_3',
icon: ImageSize1_2
},
{
label: '4:3',
value: 'ASPECT_4_3',
icon: ImageSize3_4
},
{
label: '3:4',
value: 'ASPECT_3_4',
icon: ImageSize3_4
},
{
label: '16:9',
value: 'ASPECT_16_9',
icon: ImageSize16_9
},
{
label: '9:16',
value: 'ASPECT_9_16',
icon: ImageSize9_16
},
{
label: '16:10',
value: 'ASPECT_16_10',
icon: ImageSize16_9
},
{
label: '10:16',
value: 'ASPECT_10_16',
icon: ImageSize9_16
label: 'paintings.aspect_ratios.landscape',
options: [
{
label: '2:1',
value: 'ASPECT_2_1'
},
{
label: '3:1',
value: 'ASPECT_3_1'
},
{
label: '3:2',
value: 'ASPECT_3_2'
},
{
label: '4:3',
value: 'ASPECT_4_3'
},
{
label: '5:4',
value: 'ASPECT_5_4'
},
{
label: '16:9',
value: 'ASPECT_16_9'
},
{
label: '16:10',
value: 'ASPECT_16_10'
}
]
}
]
@ -132,3 +123,21 @@ export const RENDERING_SPEED_OPTIONS = [
value: 'QUALITY'
}
]
export const QUALITY_OPTIONS = [
{ label: 'paintings.quality_options.auto', value: 'auto' },
{ label: 'paintings.quality_options.low', value: 'low' },
{ label: 'paintings.quality_options.medium', value: 'medium' },
{ label: 'paintings.quality_options.high', value: 'high' }
]
export const MODERATION_OPTIONS = [
{ label: 'paintings.moderation_options.auto', value: 'auto' },
{ label: 'paintings.moderation_options.low', value: 'low' }
]
export const BACKGROUND_OPTIONS = [
{ label: 'paintings.background_options.auto', value: 'auto' },
{ label: 'paintings.background_options.transparent', value: 'transparent' },
{ label: 'paintings.background_options.opaque', value: 'opaque' }
]

View File

@ -39,6 +39,22 @@ class FileManager {
return fileData.data
}
static async addBase64File(file: FileType): Promise<FileType> {
Logger.log(`[FileManager] Adding base64 file: ${JSON.stringify(file)}`)
const base64File = await window.api.file.base64File(file.id + file.ext)
const fileRecord = await db.files.get(base64File.id)
if (fileRecord) {
await db.files.update(fileRecord.id, { ...fileRecord, count: fileRecord.count + 1 })
return fileRecord
}
await db.files.add(base64File)
return base64File
}
static async uploadFile(file: FileType): Promise<FileType> {
Logger.log(`[FileManager] Uploading file: ${JSON.stringify(file)}`)

View File

@ -207,6 +207,11 @@ export interface GeneratePainting extends PaintingParams {
negativePrompt?: string
magicPromptOption?: boolean
renderingSpeed?: string
quality?: string
moderation?: string
n?: number
size?: string
background?: string
}
export interface EditPainting extends PaintingParams {