feat: new painting provider: intel ovms (#10570)

* new painting provider: intel ovms

Signed-off-by: Ma, Kejiang <kj.ma@intel.com>
Signed-off-by: Kejiang Ma <kj.ma@intel.com>

* cherryin -> cherryai

Signed-off-by: Kejiang Ma <kj.ma@intel.com>

* ovms painting only valid when ovms is running

Signed-off-by: Kejiang Ma <kj.ma@intel.com>

* fix: painting(ovms) still appear while ovms is not running after rebase

Signed-off-by: Kejiang Ma <kj.ma@intel.com>

* fix warning in PaintingRoute

Signed-off-by: Kejiang Ma <kj.ma@intel.com>

* add ovms_paintings in migrate config 163

---------

Signed-off-by: Ma, Kejiang <kj.ma@intel.com>
Signed-off-by: Kejiang Ma <kj.ma@intel.com>
This commit is contained in:
Kejiang Ma 2025-10-20 15:41:34 +08:00 committed by GitHub
parent 528524b075
commit 749a4f4679
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 868 additions and 15 deletions

View File

@ -14,6 +14,7 @@ export function usePaintings() {
const aihubmix_image_upscale = useAppSelector((state) => state.paintings.aihubmix_image_upscale)
const openai_image_generate = useAppSelector((state) => state.paintings.openai_image_generate)
const openai_image_edit = useAppSelector((state) => state.paintings.openai_image_edit)
const ovms_paintings = useAppSelector((state) => state.paintings.ovms_paintings)
const dispatch = useAppDispatch()
return {
@ -27,6 +28,7 @@ export function usePaintings() {
aihubmix_image_upscale,
openai_image_generate,
openai_image_edit,
ovms_paintings,
addPainting: (namespace: keyof PaintingsState, painting: PaintingAction) => {
dispatch(addPainting({ namespace, painting }))
return painting

View File

@ -0,0 +1,692 @@
import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
import { loggerService } from '@logger'
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
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 { LanguagesEnum } from '@renderer/config/translate'
import { usePaintings } from '@renderer/hooks/usePaintings'
import { useAllProviders } from '@renderer/hooks/useProvider'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { getProviderLabel } from '@renderer/i18n/label'
import FileManager from '@renderer/services/FileManager'
import { translateText } from '@renderer/services/TranslateService'
import { useAppDispatch } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import type { FileMetadata, OvmsPainting } from '@renderer/types'
import { getErrorMessage, uuid } from '@renderer/utils'
import { Avatar, Button, Input, InputNumber, Select, Slider, Switch, Tooltip } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { Info } from 'lucide-react'
import type { FC } 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 './components/Artboard'
import PaintingsList from './components/PaintingsList'
import {
type ConfigItem,
createDefaultOvmsPainting,
createOvmsConfig,
DEFAULT_OVMS_PAINTING,
getOvmsModels,
OVMS_MODELS
} from './config/ovmsConfig'
const logger = loggerService.withContext('OvmsPage')
const OvmsPage: FC<{ Options: string[] }> = ({ Options }) => {
const { addPainting, removePainting, updatePainting, ovms_paintings } = usePaintings()
const ovmsPaintings = useMemo(() => ovms_paintings || [], [ovms_paintings])
const [painting, setPainting] = useState<OvmsPainting>(ovmsPaintings[0] || DEFAULT_OVMS_PAINTING)
const [currentImageIndex, setCurrentImageIndex] = useState(0)
const [isLoading, setIsLoading] = useState(false)
const [abortController, setAbortController] = useState<AbortController | null>(null)
const [spaceClickCount, setSpaceClickCount] = useState(0)
const [isTranslating, setIsTranslating] = useState(false)
const [availableModels, setAvailableModels] = useState<Array<{ label: string; value: string }>>([])
const [ovmsConfig, setOvmsConfig] = useState<ConfigItem[]>([])
const { t } = useTranslation()
const providers = useAllProviders()
const providerOptions = Options.map((option) => {
const provider = providers.find((p) => p.id === option)
if (provider) {
return {
label: getProviderLabel(provider.id),
value: provider.id
}
} else {
return {
label: 'Unknown Provider',
value: undefined
}
}
})
const dispatch = useAppDispatch()
const { generating } = useRuntime()
const navigate = useNavigate()
const location = useLocation()
const { autoTranslateWithSpace } = useSettings()
const spaceClickTimer = useRef<NodeJS.Timeout>(null)
const ovmsProvider = providers.find((p) => p.id === 'ovms')!
const getNewPainting = useCallback(() => {
if (availableModels.length > 0) {
return createDefaultOvmsPainting(availableModels)
}
return {
...DEFAULT_OVMS_PAINTING,
id: uuid()
}
}, [availableModels])
const textareaRef = useRef<any>(null)
// Load available models on component mount
useEffect(() => {
const loadModels = () => {
try {
// Get OVMS provider to access its models
const ovmsProvider = providers.find((p) => p.id === 'ovms')
const providerModels = ovmsProvider?.models || []
// Filter and format models for image generation
const filteredModels = getOvmsModels(providerModels)
setAvailableModels(filteredModels)
setOvmsConfig(createOvmsConfig(filteredModels))
// Update painting if it doesn't have a valid model
if (filteredModels.length > 0 && !filteredModels.some((m) => m.value === painting.model)) {
const defaultPainting = createDefaultOvmsPainting(filteredModels)
setPainting(defaultPainting)
}
} catch (error) {
logger.error(`Failed to load OVMS models: ${error}`)
// Use default config if loading fails
setOvmsConfig(createOvmsConfig())
}
}
loadModels()
}, [providers, painting.model]) // Re-run when providers change
const updatePaintingState = (updates: Partial<OvmsPainting>) => {
const updatedPainting = { ...painting, ...updates }
setPainting(updatedPainting)
updatePainting('ovms_paintings', 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()) {
logger.error('Image URL is empty, possibly due to prohibited prompt')
window.toast.warning(t('message.empty_url'))
return null
}
return await window.api.file.download(url)
} catch (error) {
logger.error(`Failed to download image: ${error}`)
if (
error instanceof Error &&
(error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL'))
) {
window.toast.warning(t('message.empty_url'))
}
return null
}
})
)
return downloadedFiles.filter((file): file is FileMetadata => file !== null)
}
const onGenerate = async () => {
if (painting.files.length > 0) {
const confirmed = await window.modal.confirm({
content: t('paintings.regenerate.confirm'),
centered: true
})
if (!confirmed) return
await FileManager.deleteFiles(painting.files)
}
const prompt = textareaRef.current?.resizableTextArea?.textArea?.value || ''
updatePaintingState({ prompt })
if (!painting.model || !painting.prompt) {
return
}
const controller = new AbortController()
setAbortController(controller)
setIsLoading(true)
dispatch(setGenerating(true))
try {
// Prepare request body for OVMS
const requestBody = {
model: painting.model,
prompt: painting.prompt,
size: painting.size || '512x512',
num_inference_steps: painting.num_inference_steps || 4,
rng_seed: painting.rng_seed || 0
}
logger.info('OVMS API request:', requestBody)
const response = await fetch(`${ovmsProvider.apiHost}images/generations`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody),
signal: controller.signal
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: { message: `HTTP ${response.status}` } }))
logger.error('OVMS API error:', errorData)
throw new Error(errorData.error?.message || 'Image generation failed')
}
const data = await response.json()
logger.info('OVMS API response:', data)
// Handle base64 encoded images
if (data.data && data.data.length > 0) {
const base64s = data.data.filter((item) => item.b64_json).map((item) => item.b64_json)
if (base64s.length > 0) {
const validFiles = await Promise.all(
base64s.map(async (base64) => {
return await window.api.file.saveBase64Image(base64)
})
)
await FileManager.addFiles(validFiles)
updatePaintingState({ files: validFiles, urls: validFiles.map((file) => file.name) })
}
// Handle URL-based images if available
const urls = data.data.filter((item) => item.url).map((item) => item.url)
if (urls.length > 0) {
const validFiles = await downloadImages(urls)
await FileManager.addFiles(validFiles)
updatePaintingState({ files: validFiles, urls })
}
}
} catch (error: unknown) {
handleError(error)
} finally {
setIsLoading(false)
dispatch(setGenerating(false))
setAbortController(null)
}
}
const handleRetry = async (painting: OvmsPainting) => {
setIsLoading(true)
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 = () => {
abortController?.abort()
}
const nextImage = () => {
setCurrentImageIndex((prev) => (prev + 1) % painting.files.length)
}
const prevImage = () => {
setCurrentImageIndex((prev) => (prev - 1 + painting.files.length) % painting.files.length)
}
const handleAddPainting = () => {
const newPainting = addPainting('ovms_paintings', getNewPainting())
updatePainting('ovms_paintings', newPainting)
setPainting(newPainting)
return newPainting
}
const onDeletePainting = (paintingToDelete: OvmsPainting) => {
if (paintingToDelete.id === painting.id) {
const currentIndex = ovmsPaintings.findIndex((p) => p.id === paintingToDelete.id)
if (currentIndex > 0) {
setPainting(ovmsPaintings[currentIndex - 1])
} else if (ovmsPaintings.length > 1) {
setPainting(ovmsPaintings[1])
}
}
removePainting('ovms_paintings', paintingToDelete)
}
const translate = async () => {
if (isTranslating) {
return
}
if (!painting.prompt) {
return
}
try {
setIsTranslating(true)
const translatedText = await translateText(painting.prompt, LanguagesEnum.enUS)
updatePaintingState({ prompt: translatedText })
} catch (error) {
logger.error('Translation failed:', error as Error)
} finally {
setIsTranslating(false)
}
}
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (autoTranslateWithSpace && event.key === ' ') {
setSpaceClickCount((prev) => prev + 1)
if (spaceClickTimer.current) {
clearTimeout(spaceClickTimer.current)
}
spaceClickTimer.current = setTimeout(() => {
setSpaceClickCount(0)
}, 200)
if (spaceClickCount === 2) {
setSpaceClickCount(0)
setIsTranslating(true)
translate()
}
}
}
const handleProviderChange = (providerId: string) => {
const routeName = location.pathname.split('/').pop()
if (providerId !== routeName) {
navigate('../' + providerId, { replace: true })
}
}
// Handle random seed generation
const handleRandomSeed = () => {
const randomSeed = Math.floor(Math.random() * 2147483647)
updatePaintingState({ rng_seed: randomSeed })
return randomSeed
}
// Render configuration form
const renderConfigForm = (item: ConfigItem) => {
switch (item.type) {
case 'select': {
const isDisabled = typeof item.disabled === 'function' ? item.disabled(item, painting) : item.disabled
const selectOptions =
typeof item.options === 'function'
? item.options(item, painting).map((option) => ({
...option,
label: option.label.startsWith('paintings.') ? t(option.label) : option.label
}))
: item.options?.map((option) => ({
...option,
label: option.label.startsWith('paintings.') ? t(option.label) : option.label
}))
return (
<Select
style={{ width: '100%' }}
listHeight={500}
disabled={isDisabled}
value={painting[item.key!] || item.initialValue}
options={selectOptions as any}
onChange={(v) => updatePaintingState({ [item.key!]: v })}
/>
)
}
case 'slider': {
return (
<SliderContainer>
<Slider
min={item.min}
max={item.max}
step={item.step}
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!] || item.initialValue) as number}
onChange={(v) => updatePaintingState({ [item.key!]: v })}
/>
</SliderContainer>
)
}
case 'input':
return (
<Input
value={(painting[item.key!] || item.initialValue) as string}
onChange={(e) => updatePaintingState({ [item.key!]: e.target.value })}
suffix={
item.key === 'rng_seed' ? (
<RedoOutlined onClick={handleRandomSeed} style={{ cursor: 'pointer', color: 'var(--color-text-2)' }} />
) : (
item.suffix
)
}
/>
)
case 'inputNumber':
return (
<InputNumber
min={item.min}
max={item.max}
style={{ width: '100%' }}
value={(painting[item.key!] || item.initialValue) as number}
onChange={(v) => updatePaintingState({ [item.key!]: v })}
/>
)
case 'textarea':
return (
<TextArea
value={(painting[item.key!] || item.initialValue) as string}
onChange={(e) => updatePaintingState({ [item.key!]: e.target.value })}
spellCheck={false}
rows={4}
/>
)
case 'switch':
return (
<HStack>
<Switch
checked={(painting[item.key!] || item.initialValue) as boolean}
onChange={(checked) => updatePaintingState({ [item.key!]: checked })}
/>
</HStack>
)
default:
return null
}
}
// Render configuration item
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: OvmsPainting) => {
if (generating) return
setPainting(newPainting)
setCurrentImageIndex(0)
}
useEffect(() => {
if (ovmsPaintings.length === 0) {
const newPainting = getNewPainting()
addPainting('ovms_paintings', newPainting)
setPainting(newPainting)
}
}, [ovmsPaintings, addPainting, getNewPainting])
useEffect(() => {
const timer = spaceClickTimer.current
return () => {
if (timer) {
clearTimeout(timer)
}
}
}, [])
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={handleAddPainting}>
{t('paintings.button.new.image')}
</Button>
</NavbarRight>
)}
</Navbar>
<ContentContainer id="content-container">
<LeftContainer>
<Scrollbar>
<div style={{ padding: '20px' }}>
<ProviderTitleContainer>
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
<SettingHelpLink
target="_blank"
href="https://docs.openvino.ai/2025/model-server/ovms_demos_image_generation.html">
{t('paintings.learn_more')}
<ProviderLogo
shape="square"
src={getProviderLogo(ovmsProvider.id)}
size={16}
style={{ marginLeft: 5 }}
/>
</SettingHelpLink>
</ProviderTitleContainer>
<Select
value={providerOptions.find((p) => p.value === 'ovms')?.value || 'ovms'}
onChange={handleProviderChange}
style={{ width: '100%', marginBottom: 15 }}>
{providerOptions.map((provider) => (
<Select.Option value={provider.value} key={provider.value}>
<SelectOptionContainer>
<ProviderLogo shape="square" src={getProviderLogo(provider.value || '')} size={16} />
{provider.label}
</SelectOptionContainer>
</Select.Option>
))}
</Select>
{/* Render configuration items using JSON config */}
{ovmsConfig.map(renderConfigItem)}
</div>
</Scrollbar>
</LeftContainer>
<MainContainer>
<Artboard
painting={painting}
isLoading={isLoading}
currentImageIndex={currentImageIndex}
onPrevImage={prevImage}
onNextImage={nextImage}
onCancel={onCancel}
retry={handleRetry}
/>
<InputContainer>
<Textarea
ref={textareaRef}
variant="borderless"
disabled={isLoading}
value={painting.prompt}
spellCheck={false}
onChange={(e) => updatePaintingState({ prompt: e.target.value })}
placeholder={isTranslating ? t('paintings.translating') : t('paintings.prompt_placeholder')}
onKeyDown={handleKeyDown}
/>
<Toolbar>
<ToolbarMenu>
<SendMessageButton
sendMessage={onGenerate}
disabled={isLoading || !painting.model || painting.model === OVMS_MODELS[0]?.value}
/>
</ToolbarMenu>
</Toolbar>
</InputContainer>
</MainContainer>
<PaintingsList
namespace="ovms_paintings"
paintings={ovmsPaintings}
selectedPainting={painting}
onSelectPainting={onSelectPainting}
onDeletePainting={onDeletePainting}
onNewPainting={handleAddPainting}
/>
</ContentContainer>
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
`
const ContentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: row;
height: 100%;
background-color: var(--color-background);
overflow: hidden;
`
const LeftContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
background-color: var(--color-background);
max-width: var(--assistants-width);
border-right: 0.5px solid var(--color-border);
overflow: hidden;
`
const MainContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
background-color: var(--color-background);
`
const InputContainer = styled.div`
display: flex;
flex-direction: column;
min-height: 95px;
max-height: 95px;
position: relative;
border: 1px solid var(--color-border-soft);
transition: all 0.3s ease;
margin: 0 20px 15px 20px;
border-radius: 10px;
`
const Textarea = styled(TextArea)`
padding: 10px;
border-radius: 0;
display: flex;
flex: 1;
resize: none !important;
overflow: auto;
width: auto;
`
const Toolbar = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
justify-content: flex-end;
padding: 0 8px;
padding-bottom: 0;
height: 40px;
`
const ToolbarMenu = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
`
const InfoIcon = styled(Info)`
margin-left: 5px;
cursor: help;
color: var(--color-text-2);
opacity: 0.6;
width: 14px;
height: 16px;
&:hover {
opacity: 1;
}
`
const SliderContainer = styled.div`
display: flex;
align-items: center;
gap: 16px;
.ant-slider {
flex: 1;
}
`
const StyledInputNumber = styled(InputNumber)`
width: 70px;
`
const ProviderLogo = styled(Avatar)`
border: 0.5px solid var(--color-border);
`
const ProviderTitleContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
`
const SelectOptionContainer = styled.div`
display: flex;
align-items: center;
gap: 8px;
`
export default OvmsPage

View File

@ -4,19 +4,20 @@ import { useAllProviders } from '@renderer/hooks/useProvider'
import { useAppDispatch } from '@renderer/store'
import { setDefaultPaintingProvider } from '@renderer/store/settings'
import { PaintingProvider, SystemProviderId } from '@renderer/types'
import { FC, useEffect, useMemo } from 'react'
import { FC, useEffect, useMemo, useState } from 'react'
import { Route, Routes, useParams } from 'react-router-dom'
import AihubmixPage from './AihubmixPage'
import DmxapiPage from './DmxapiPage'
import NewApiPage from './NewApiPage'
import OvmsPage from './OvmsPage'
import SiliconPage from './SiliconPage'
import TokenFluxPage from './TokenFluxPage'
import ZhipuPage from './ZhipuPage'
const logger = loggerService.withContext('PaintingsRoutePage')
const BASE_OPTIONS: SystemProviderId[] = ['zhipu', 'aihubmix', 'silicon', 'dmxapi', 'tokenflux']
const BASE_OPTIONS: SystemProviderId[] = ['zhipu', 'aihubmix', 'silicon', 'dmxapi', 'tokenflux', 'ovms']
const PaintingsRoutePage: FC = () => {
const params = useParams()
@ -26,28 +27,41 @@ const PaintingsRoutePage: FC = () => {
const Options = useMemo(() => {
return [...BASE_OPTIONS, ...providers.filter((p) => isNewApiProvider(p)).map((p) => p.id)]
}, [providers])
const [ovmsStatus, setOvmsStatus] = useState<'not-installed' | 'not-running' | 'running'>('not-running')
useEffect(() => {
const checkStatus = async () => {
const status = await window.api.ovms.getStatus()
setOvmsStatus(status)
}
checkStatus()
}, [])
const validOptions = Options.filter((option) => option !== 'ovms' || ovmsStatus === 'running')
useEffect(() => {
logger.debug(`defaultPaintingProvider: ${provider}`)
if (provider && Options.includes(provider)) {
if (provider && validOptions.includes(provider)) {
dispatch(setDefaultPaintingProvider(provider as PaintingProvider))
}
}, [provider, dispatch, Options])
}, [provider, dispatch, validOptions])
return (
<Routes>
<Route path="*" element={<ZhipuPage Options={Options} />} />
<Route path="/zhipu" element={<ZhipuPage Options={Options} />} />
<Route path="/aihubmix" element={<AihubmixPage Options={Options} />} />
<Route path="/silicon" element={<SiliconPage Options={Options} />} />
<Route path="/dmxapi" element={<DmxapiPage Options={Options} />} />
<Route path="/tokenflux" element={<TokenFluxPage Options={Options} />} />
<Route path="*" element={<ZhipuPage Options={validOptions} />} />
<Route path="/zhipu" element={<ZhipuPage Options={validOptions} />} />
<Route path="/aihubmix" element={<AihubmixPage Options={validOptions} />} />
<Route path="/silicon" element={<SiliconPage Options={validOptions} />} />
<Route path="/dmxapi" element={<DmxapiPage Options={validOptions} />} />
<Route path="/tokenflux" element={<TokenFluxPage Options={validOptions} />} />
<Route path="/ovms" element={<OvmsPage Options={validOptions} />} />
{/* new-api family providers are mounted dynamically below */}
{providers
.filter((p) => isNewApiProvider(p))
.map((p) => (
<Route key={p.id} path={`/${p.id}`} element={<NewApiPage Options={Options} />} />
<Route key={p.id} path={`/${p.id}`} element={<NewApiPage Options={validOptions} />} />
))}
<Route path="/new-api" element={<NewApiPage Options={validOptions} />} />
</Routes>
)
}

View File

@ -0,0 +1,129 @@
import type { PaintingAction } from '@renderer/types'
import { uuid } from '@renderer/utils'
// Configuration item type definition
export type ConfigItem = {
type: 'select' | 'radio' | 'slider' | 'input' | 'switch' | 'inputNumber' | 'textarea' | 'title' | 'description'
key?: keyof PaintingAction | 'commonModel'
title?: string
tooltip?: string
options?:
| Array<{
label: string
title?: string
value?: string | number
icon?: string
}>
| ((
config: ConfigItem,
painting: Partial<PaintingAction>
) => Array<{ label: string; value: string | number; icon?: string }>)
min?: number
max?: number
step?: number
suffix?: React.ReactNode
content?: string
disabled?: boolean | ((config: ConfigItem, painting: Partial<PaintingAction>) => boolean)
initialValue?: string | number | boolean
required?: boolean
condition?: (painting: PaintingAction) => boolean
}
// Size options for OVMS
const SIZE_OPTIONS = [
{ label: '512x512', value: '512x512' },
{ label: '768x768', value: '768x768' },
{ label: '1024x1024', value: '1024x1024' }
]
// Available OVMS models for image generation - will be populated dynamically
export const OVMS_MODELS = [{ label: 'no available model', value: 'none' }]
// Function to get available OVMS models from provider
export const getOvmsModels = (
providerModels?: Array<{ id: string; name: string }>
): Array<{ label: string; value: string }> => {
if (!providerModels || providerModels.length === 0) {
// Fallback to static models if no provider models
return OVMS_MODELS
}
// Filter provider models for image generation (SD, Stable-Diffusion, Stable Diffusion, FLUX)
const imageGenerationModels = providerModels.filter((model) => {
const modelName = model.name.toLowerCase()
return (
modelName.startsWith('sd') ||
modelName.startsWith('stable-diffusion') ||
modelName.startsWith('stable diffusion') ||
modelName.startsWith('flux')
)
})
// Convert to the expected format
const formattedModels = imageGenerationModels.map((model) => ({
label: model.name,
value: model.id
}))
// Return formatted models or fallback to static models
return formattedModels.length > 0 ? formattedModels : OVMS_MODELS
}
// Create configuration function
export const createOvmsConfig = (models?: Array<{ label: string; value: string }>): ConfigItem[] => {
const availableModels = models || OVMS_MODELS
return [
{
type: 'select',
key: 'model',
title: 'paintings.model',
options: availableModels,
initialValue: availableModels[0]?.value || 'Select Model Here'
},
{
type: 'select',
key: 'size',
title: 'paintings.image.size',
options: SIZE_OPTIONS,
initialValue: '512x512'
},
{
type: 'inputNumber',
key: 'num_inference_steps',
title: 'paintings.inference_steps',
tooltip: 'paintings.inference_steps_tip',
min: 1,
max: 100,
initialValue: 4
},
{
type: 'inputNumber',
key: 'rng_seed',
title: 'paintings.seed',
tooltip: 'paintings.seed_tip',
initialValue: 0
}
]
}
// Default painting configuration for OVMS
export const DEFAULT_OVMS_PAINTING: PaintingAction = {
id: uuid(),
model: '',
prompt: '',
size: '512x512',
num_inference_steps: 4,
rng_seed: 0,
files: [],
urls: []
}
// Function to create default painting with dynamic model
export const createDefaultOvmsPainting = (models?: Array<{ label: string; value: string }>): PaintingAction => {
const availableModels = models || OVMS_MODELS
return {
...DEFAULT_OVMS_PAINTING,
id: uuid(),
model: availableModels[0]?.value || 'Select Model Here'
}
}

View File

@ -2315,7 +2315,8 @@ const migrateConfig = {
// @ts-ignore upscale
aihubmix_image_upscale: state?.paintings?.upscale || [],
openai_image_generate: state?.paintings?.openai_image_generate || [],
openai_image_edit: state?.paintings?.openai_image_edit || []
openai_image_edit: state?.paintings?.openai_image_edit || [],
ovms_paintings: []
}
return state
@ -2676,6 +2677,7 @@ const migrateConfig = {
provider.anthropicApiHost = 'https://open.cherryin.net'
}
})
state.paintings.ovms_paintings = []
return state
} catch (error) {
logger.error('migrate 163 error', error as Error)

View File

@ -19,7 +19,9 @@ const initialState: PaintingsState = {
aihubmix_image_upscale: [],
// OpenAI
openai_image_generate: [],
openai_image_edit: []
openai_image_edit: [],
// OVMS
ovms_paintings: []
}
const paintingsSlice = createSlice({

View File

@ -278,7 +278,7 @@ export type PaintingParams = {
providerId?: string
}
export type PaintingProvider = 'zhipu' | 'aihubmix' | 'silicon' | 'dmxapi' | 'new-api'
export type PaintingProvider = 'zhipu' | 'aihubmix' | 'silicon' | 'dmxapi' | 'new-api' | 'ovms'
export interface Painting extends PaintingParams {
model?: string
@ -378,8 +378,18 @@ export interface TokenFluxPainting extends PaintingParams {
status?: 'starting' | 'processing' | 'succeeded' | 'failed' | 'cancelled'
}
export interface OvmsPainting extends PaintingParams {
model?: string
prompt?: string
size?: string
num_inference_steps?: number
rng_seed?: number
safety_check?: boolean
response_format?: 'url' | 'b64_json'
}
export type PaintingAction = Partial<
GeneratePainting & RemixPainting & EditPainting & ScalePainting & DmxapiPainting & TokenFluxPainting
GeneratePainting & RemixPainting & EditPainting & ScalePainting & DmxapiPainting & TokenFluxPainting & OvmsPainting
> &
PaintingParams
@ -400,6 +410,8 @@ export interface PaintingsState {
// OpenAI
openai_image_generate: Partial<GeneratePainting> & PaintingParams[]
openai_image_edit: Partial<EditPainting> & PaintingParams[]
// OVMS
ovms_paintings: OvmsPainting[]
}
export type MinAppType = {