Compare commits
76 Commits
copilot/fi
...
v1.7.0-sor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a62418f41 | ||
|
|
58c5df9284 | ||
|
|
c20394f460 | ||
|
|
8518734e48 | ||
|
|
29a01ef49a | ||
|
|
5e4b516402 | ||
|
|
1c89262929 | ||
|
|
b68a0ffaba | ||
|
|
41041fa296 | ||
|
|
66b88aec74 | ||
|
|
f54e583f34 | ||
|
|
1e1bfafb88 | ||
|
|
63459e3ec4 | ||
|
|
de10a7fd6c | ||
|
|
dced99ce57 | ||
|
|
0cafdeb540 | ||
|
|
258666e382 | ||
|
|
8a45fe70d0 | ||
|
|
d8363b5591 | ||
|
|
397a24b833 | ||
|
|
ca53e5f0c7 | ||
|
|
c50a574982 | ||
|
|
c3c125f3a3 | ||
|
|
eba370210f | ||
|
|
697ef22ab6 | ||
|
|
33582a460b | ||
|
|
d5078baa20 | ||
|
|
ae54d5d9b9 | ||
|
|
7bde37680e | ||
|
|
942c239d14 | ||
|
|
83114ee0c1 | ||
|
|
0dd894c911 | ||
|
|
e0cb39d00d | ||
|
|
12323375a5 | ||
|
|
788b170f98 | ||
|
|
42015b51e3 | ||
|
|
9997188f5e | ||
|
|
1fd7b0b667 | ||
|
|
1467493e1d | ||
|
|
f61cadd5b5 | ||
|
|
377b2b796f | ||
|
|
36df06db75 | ||
|
|
a901943675 | ||
|
|
953f0f4a2f | ||
|
|
8b875935d0 | ||
|
|
2f9b174095 | ||
|
|
d80eac2fbe | ||
|
|
5776512bf6 | ||
|
|
fd1a3faa69 | ||
|
|
82ad9e15e2 | ||
|
|
46221985bd | ||
|
|
d982c659d3 | ||
|
|
dad9425b44 | ||
|
|
dc19c17526 | ||
|
|
85c8d5fca2 | ||
|
|
4cf4c1e946 | ||
|
|
00221471b8 | ||
|
|
6d22a635f2 | ||
|
|
014247f983 | ||
|
|
7fe4524415 | ||
|
|
0ada5656ad | ||
|
|
c7c6561b77 | ||
|
|
590d69cfba | ||
|
|
9487eaf091 | ||
|
|
1235362c82 | ||
|
|
5db5d69cec | ||
|
|
9931856a1f | ||
|
|
833d2d9276 | ||
|
|
a1fde0db38 | ||
|
|
612d3756cf | ||
|
|
05ad98bb20 | ||
|
|
1c53222582 | ||
|
|
c6a0ad3fc0 | ||
|
|
ab2aa8380f | ||
|
|
45bdea5301 | ||
|
|
0f14b1625f |
BIN
.yarn/patches/openai-npm-5.12.2-30b075401c.patch
vendored
BIN
.yarn/patches/openai-npm-5.12.2-30b075401c.patch
vendored
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "CherryStudio",
|
"name": "CherryStudio",
|
||||||
"version": "1.7.0-alpha.5",
|
"version": "1.7.0-sora.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A powerful AI assistant for producer.",
|
"description": "A powerful AI assistant for producer.",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
@@ -124,6 +124,7 @@
|
|||||||
"@cherrystudio/embedjs-ollama": "^0.1.31",
|
"@cherrystudio/embedjs-ollama": "^0.1.31",
|
||||||
"@cherrystudio/embedjs-openai": "^0.1.31",
|
"@cherrystudio/embedjs-openai": "^0.1.31",
|
||||||
"@cherrystudio/extension-table-plus": "workspace:^",
|
"@cherrystudio/extension-table-plus": "workspace:^",
|
||||||
|
"@cherrystudio/openai": "6.3.0-fork.1",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
@@ -295,7 +296,6 @@
|
|||||||
"motion": "^12.10.5",
|
"motion": "^12.10.5",
|
||||||
"notion-helper": "^1.3.22",
|
"notion-helper": "^1.3.22",
|
||||||
"npx-scope-finder": "^1.2.0",
|
"npx-scope-finder": "^1.2.0",
|
||||||
"openai": "patch:openai@npm%3A5.12.2#~/.yarn/patches/openai-npm-5.12.2-30b075401c.patch",
|
|
||||||
"oxlint": "^1.22.0",
|
"oxlint": "^1.22.0",
|
||||||
"oxlint-tsgolint": "^0.2.0",
|
"oxlint-tsgolint": "^0.2.0",
|
||||||
"p-queue": "^8.1.0",
|
"p-queue": "^8.1.0",
|
||||||
@@ -375,8 +375,8 @@
|
|||||||
"file-stream-rotator@npm:^0.6.1": "patch:file-stream-rotator@npm%3A0.6.1#~/.yarn/patches/file-stream-rotator-npm-0.6.1-eab45fb13d.patch",
|
"file-stream-rotator@npm:^0.6.1": "patch:file-stream-rotator@npm%3A0.6.1#~/.yarn/patches/file-stream-rotator-npm-0.6.1-eab45fb13d.patch",
|
||||||
"libsql@npm:^0.4.4": "patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch",
|
"libsql@npm:^0.4.4": "patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch",
|
||||||
"node-abi": "4.12.0",
|
"node-abi": "4.12.0",
|
||||||
"openai@npm:^4.77.0": "patch:openai@npm%3A5.12.2#~/.yarn/patches/openai-npm-5.12.2-30b075401c.patch",
|
"openai@npm:^4.77.0": "npm:@cherrystudio/openai@6.3.0-fork.1",
|
||||||
"openai@npm:^4.87.3": "patch:openai@npm%3A5.12.2#~/.yarn/patches/openai-npm-5.12.2-30b075401c.patch",
|
"openai@npm:^4.87.3": "npm:@cherrystudio/openai@6.3.0-fork.1",
|
||||||
"pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",
|
"pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",
|
||||||
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch",
|
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch",
|
||||||
"undici": "6.21.2",
|
"undici": "6.21.2",
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* 该脚本用于少量自动翻译所有baseLocale以外的文本。待翻译文案必须以[to be translated]开头
|
* 该脚本用于少量自动翻译所有baseLocale以外的文本。待翻译文案必须以[to be translated]开头
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
import OpenAI from '@cherrystudio/openai'
|
||||||
import cliProgress from 'cli-progress'
|
import cliProgress from 'cli-progress'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import OpenAI from 'openai'
|
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
|
||||||
const localesDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
|
const localesDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
* API_KEY=sk-xxxx BASE_URL=xxxx MODEL=xxxx ts-node scripts/update-i18n.ts
|
* API_KEY=sk-xxxx BASE_URL=xxxx MODEL=xxxx ts-node scripts/update-i18n.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import OpenAI from '@cherrystudio/openai'
|
||||||
import cliProgress from 'cli-progress'
|
import cliProgress from 'cli-progress'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import OpenAI from 'openai'
|
|
||||||
|
|
||||||
type I18NValue = string | { [key: string]: I18NValue }
|
type I18NValue = string | { [key: string]: I18NValue }
|
||||||
type I18N = { [key: string]: I18NValue }
|
type I18N = { [key: string]: I18NValue }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { ChatCompletionCreateParams } from '@cherrystudio/openai/resources'
|
||||||
import express, { Request, Response } from 'express'
|
import express, { Request, Response } from 'express'
|
||||||
import { ChatCompletionCreateParams } from 'openai/resources'
|
|
||||||
|
|
||||||
import { loggerService } from '../../services/LoggerService'
|
import { loggerService } from '../../services/LoggerService'
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import OpenAI from '@cherrystudio/openai'
|
||||||
|
import { ChatCompletionCreateParams, ChatCompletionCreateParamsStreaming } from '@cherrystudio/openai/resources'
|
||||||
import { Provider } from '@types'
|
import { Provider } from '@types'
|
||||||
import OpenAI from 'openai'
|
|
||||||
import { ChatCompletionCreateParams, ChatCompletionCreateParamsStreaming } from 'openai/resources'
|
|
||||||
|
|
||||||
import { loggerService } from '../../services/LoggerService'
|
import { loggerService } from '../../services/LoggerService'
|
||||||
import { ModelValidationError, validateModelId } from '../utils'
|
import { ModelValidationError, validateModelId } from '../utils'
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import OpenAI from '@cherrystudio/openai'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { fileStorage } from '@main/services/FileStorage'
|
import { fileStorage } from '@main/services/FileStorage'
|
||||||
import { FileListResponse, FileMetadata, FileUploadResponse, Provider } from '@types'
|
import { FileListResponse, FileMetadata, FileUploadResponse, Provider } from '@types'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import OpenAI from 'openai'
|
|
||||||
|
|
||||||
import { CacheService } from '../CacheService'
|
import { CacheService } from '../CacheService'
|
||||||
import { BaseFileService } from './BaseFileService'
|
import { BaseFileService } from './BaseFileService'
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
|
|||||||
import SettingsPage from './pages/settings/SettingsPage'
|
import SettingsPage from './pages/settings/SettingsPage'
|
||||||
import AssistantPresetsPage from './pages/store/assistants/presets/AssistantPresetsPage'
|
import AssistantPresetsPage from './pages/store/assistants/presets/AssistantPresetsPage'
|
||||||
import TranslatePage from './pages/translate/TranslatePage'
|
import TranslatePage from './pages/translate/TranslatePage'
|
||||||
|
import { VideoPage } from './pages/video/VideoPage'
|
||||||
|
|
||||||
const Router: FC = () => {
|
const Router: FC = () => {
|
||||||
const { navbarPosition } = useNavbarPosition()
|
const { navbarPosition } = useNavbarPosition()
|
||||||
@@ -40,6 +41,7 @@ const Router: FC = () => {
|
|||||||
<Route path="/code" element={<CodeToolsPage />} />
|
<Route path="/code" element={<CodeToolsPage />} />
|
||||||
<Route path="/settings/*" element={<SettingsPage />} />
|
<Route path="/settings/*" element={<SettingsPage />} />
|
||||||
<Route path="/launchpad" element={<LaunchpadPage />} />
|
<Route path="/launchpad" element={<LaunchpadPage />} />
|
||||||
|
<Route path="/video" element={<VideoPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,8 +12,15 @@ import { loggerService } from '@logger'
|
|||||||
import { getEnableDeveloperMode } from '@renderer/hooks/useSettings'
|
import { getEnableDeveloperMode } from '@renderer/hooks/useSettings'
|
||||||
import { addSpan, endSpan } from '@renderer/services/SpanManagerService'
|
import { addSpan, endSpan } from '@renderer/services/SpanManagerService'
|
||||||
import { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
|
import { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
|
||||||
import type { Assistant, GenerateImageParams, Model, Provider } from '@renderer/types'
|
import type { Assistant, GenerateImageParams, Model, Provider, RetrieveVideoContentParams } from '@renderer/types'
|
||||||
import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes'
|
import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes'
|
||||||
|
import {
|
||||||
|
CreateVideoParams,
|
||||||
|
CreateVideoResult,
|
||||||
|
RetrieveVideoContentResult,
|
||||||
|
RetrieveVideoParams,
|
||||||
|
RetrieveVideoResult
|
||||||
|
} from '@renderer/types/video'
|
||||||
import { buildClaudeCodeSystemModelMessage } from '@shared/anthropic'
|
import { buildClaudeCodeSystemModelMessage } from '@shared/anthropic'
|
||||||
import { type ImageModel, type LanguageModel, type Provider as AiSdkProvider, wrapLanguageModel } from 'ai'
|
import { type ImageModel, type LanguageModel, type Provider as AiSdkProvider, wrapLanguageModel } from 'ai'
|
||||||
|
|
||||||
@@ -500,6 +507,27 @@ export default class ModernAiProvider {
|
|||||||
return images
|
return images
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We manually implement this method before aisdk supports it well
|
||||||
|
*/
|
||||||
|
public async createVideo(params: CreateVideoParams): Promise<CreateVideoResult> {
|
||||||
|
return this.legacyProvider.createVideo(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We manually implement this method before aisdk supports it well
|
||||||
|
*/
|
||||||
|
public async retrieveVideo(params: RetrieveVideoParams): Promise<RetrieveVideoResult> {
|
||||||
|
return this.legacyProvider.retrieveVideo(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We manually implement this method before aisdk supports it well
|
||||||
|
*/
|
||||||
|
public async retrieveVideoContent(params: RetrieveVideoContentParams): Promise<RetrieveVideoContentResult> {
|
||||||
|
return this.legacyProvider.retrieveVideoContent(params)
|
||||||
|
}
|
||||||
|
|
||||||
public getBaseURL(): string {
|
public getBaseURL(): string {
|
||||||
return this.legacyProvider.getBaseURL()
|
return this.legacyProvider.getBaseURL()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import OpenAI from '@cherrystudio/openai'
|
||||||
import { Provider } from '@renderer/types'
|
import { Provider } from '@renderer/types'
|
||||||
import { OpenAISdkParams, OpenAISdkRawOutput } from '@renderer/types/sdk'
|
import { OpenAISdkParams, OpenAISdkRawOutput } from '@renderer/types/sdk'
|
||||||
import OpenAI from 'openai'
|
|
||||||
|
|
||||||
import { OpenAIAPIClient } from '../openai/OpenAIApiClient'
|
import { OpenAIAPIClient } from '../openai/OpenAIApiClient'
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import OpenAI, { AzureOpenAI } from '@cherrystudio/openai'
|
||||||
|
import {
|
||||||
|
ChatCompletionContentPart,
|
||||||
|
ChatCompletionContentPartRefusal,
|
||||||
|
ChatCompletionTool
|
||||||
|
} from '@cherrystudio/openai/resources'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
|
import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
|
||||||
import {
|
import {
|
||||||
@@ -78,8 +84,6 @@ import {
|
|||||||
} from '@renderer/utils/mcp-tools'
|
} from '@renderer/utils/mcp-tools'
|
||||||
import { findFileBlocks, findImageBlocks } from '@renderer/utils/messageUtils/find'
|
import { findFileBlocks, findImageBlocks } from '@renderer/utils/messageUtils/find'
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
import OpenAI, { AzureOpenAI } from 'openai'
|
|
||||||
import { ChatCompletionContentPart, ChatCompletionContentPartRefusal, ChatCompletionTool } from 'openai/resources'
|
|
||||||
|
|
||||||
import { GenericChunk } from '../../middleware/schemas'
|
import { GenericChunk } from '../../middleware/schemas'
|
||||||
import { RequestTransformer, ResponseChunkTransformer, ResponseChunkTransformerContext } from '../types'
|
import { RequestTransformer, ResponseChunkTransformer, ResponseChunkTransformerContext } from '../types'
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import OpenAI, { AzureOpenAI } from '@cherrystudio/openai'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import {
|
import {
|
||||||
isClaudeReasoningModel,
|
isClaudeReasoningModel,
|
||||||
@@ -24,7 +25,6 @@ import {
|
|||||||
ReasoningEffortOptionalParams
|
ReasoningEffortOptionalParams
|
||||||
} from '@renderer/types/sdk'
|
} from '@renderer/types/sdk'
|
||||||
import { formatApiHost } from '@renderer/utils/api'
|
import { formatApiHost } from '@renderer/utils/api'
|
||||||
import OpenAI, { AzureOpenAI } from 'openai'
|
|
||||||
|
|
||||||
import { BaseApiClient } from '../BaseApiClient'
|
import { BaseApiClient } from '../BaseApiClient'
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import OpenAI, { AzureOpenAI } from '@cherrystudio/openai'
|
||||||
|
import { ResponseInput } from '@cherrystudio/openai/resources/responses/responses'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { GenericChunk } from '@renderer/aiCore/legacy/middleware/schemas'
|
import { GenericChunk } from '@renderer/aiCore/legacy/middleware/schemas'
|
||||||
import { CompletionsContext } from '@renderer/aiCore/legacy/middleware/types'
|
import { CompletionsContext } from '@renderer/aiCore/legacy/middleware/types'
|
||||||
@@ -34,6 +36,7 @@ import {
|
|||||||
OpenAIResponseSdkTool,
|
OpenAIResponseSdkTool,
|
||||||
OpenAIResponseSdkToolCall
|
OpenAIResponseSdkToolCall
|
||||||
} from '@renderer/types/sdk'
|
} from '@renderer/types/sdk'
|
||||||
|
import { CreateVideoParams, RetrieveVideoContentParams, RetrieveVideoParams } from '@renderer/types/video'
|
||||||
import { addImageFileToContents } from '@renderer/utils/formats'
|
import { addImageFileToContents } from '@renderer/utils/formats'
|
||||||
import {
|
import {
|
||||||
isSupportedToolUse,
|
isSupportedToolUse,
|
||||||
@@ -45,8 +48,6 @@ import { findFileBlocks, findImageBlocks } from '@renderer/utils/messageUtils/fi
|
|||||||
import { MB } from '@shared/config/constant'
|
import { MB } from '@shared/config/constant'
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import OpenAI, { AzureOpenAI } from 'openai'
|
|
||||||
import { ResponseInput } from 'openai/resources/responses/responses'
|
|
||||||
|
|
||||||
import { RequestTransformer, ResponseChunkTransformer } from '../types'
|
import { RequestTransformer, ResponseChunkTransformer } from '../types'
|
||||||
import { OpenAIAPIClient } from './OpenAIApiClient'
|
import { OpenAIAPIClient } from './OpenAIApiClient'
|
||||||
@@ -152,6 +153,21 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient<
|
|||||||
return await sdk.responses.create(payload, options)
|
return await sdk.responses.create(payload, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async createVideo(params: CreateVideoParams): Promise<OpenAI.Videos.Video> {
|
||||||
|
const sdk = await this.getSdkInstance()
|
||||||
|
return sdk.videos.create(params.params, params.options)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async retrieveVideo(params: RetrieveVideoParams): Promise<OpenAI.Videos.Video> {
|
||||||
|
const sdk = await this.getSdkInstance()
|
||||||
|
return sdk.videos.retrieve(params.videoId, params.options)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async retrieveVideoContent(params: RetrieveVideoContentParams): Promise<Response> {
|
||||||
|
const sdk = await this.getSdkInstance()
|
||||||
|
return sdk.videos.downloadContent(params.videoId, params.query, params.options)
|
||||||
|
}
|
||||||
|
|
||||||
private async handlePdfFile(file: FileMetadata): Promise<OpenAI.Responses.ResponseInputFile | undefined> {
|
private async handlePdfFile(file: FileMetadata): Promise<OpenAI.Responses.ResponseInputFile | undefined> {
|
||||||
if (file.size > 32 * MB) return undefined
|
if (file.size > 32 * MB) return undefined
|
||||||
try {
|
try {
|
||||||
@@ -343,7 +359,14 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient<
|
|||||||
}
|
}
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'function_call_output':
|
case 'function_call_output':
|
||||||
sum += estimateTextTokens(message.output)
|
if (typeof message.output === 'string') {
|
||||||
|
sum += estimateTextTokens(message.output)
|
||||||
|
} else {
|
||||||
|
sum += message.output
|
||||||
|
.filter((item) => item.type === 'input_text')
|
||||||
|
.map((item) => estimateTextTokens(item.text))
|
||||||
|
.reduce((prev, cur) => prev + cur, 0)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case 'function_call':
|
case 'function_call':
|
||||||
sum += estimateTextTokens(message.arguments)
|
sum += estimateTextTokens(message.arguments)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import OpenAI from '@cherrystudio/openai'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { isSupportedModel } from '@renderer/config/models'
|
import { isSupportedModel } from '@renderer/config/models'
|
||||||
import { objectKeys, Provider } from '@renderer/types'
|
import { objectKeys, Provider } from '@renderer/types'
|
||||||
import OpenAI from 'openai'
|
|
||||||
|
|
||||||
import { OpenAIAPIClient } from '../openai/OpenAIApiClient'
|
import { OpenAIAPIClient } from '../openai/OpenAIApiClient'
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import OpenAI from '@cherrystudio/openai'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { isSupportedModel } from '@renderer/config/models'
|
import { isSupportedModel } from '@renderer/config/models'
|
||||||
import { Model, Provider } from '@renderer/types'
|
import { Model, Provider } from '@renderer/types'
|
||||||
import OpenAI from 'openai'
|
|
||||||
|
|
||||||
import { OpenAIAPIClient } from '../openai/OpenAIApiClient'
|
import { OpenAIAPIClient } from '../openai/OpenAIApiClient'
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Anthropic from '@anthropic-ai/sdk'
|
import Anthropic from '@anthropic-ai/sdk'
|
||||||
|
import OpenAI from '@cherrystudio/openai'
|
||||||
import { Assistant, MCPTool, MCPToolResponse, Model, ToolCallResponse } from '@renderer/types'
|
import { Assistant, MCPTool, MCPToolResponse, Model, ToolCallResponse } from '@renderer/types'
|
||||||
import { Provider } from '@renderer/types'
|
import { Provider } from '@renderer/types'
|
||||||
import {
|
import {
|
||||||
@@ -13,7 +14,6 @@ import {
|
|||||||
SdkTool,
|
SdkTool,
|
||||||
SdkToolCall
|
SdkToolCall
|
||||||
} from '@renderer/types/sdk'
|
} from '@renderer/types/sdk'
|
||||||
import OpenAI from 'openai'
|
|
||||||
|
|
||||||
import { CompletionsParams, GenericChunk } from '../middleware/schemas'
|
import { CompletionsParams, GenericChunk } from '../middleware/schemas'
|
||||||
import { CompletionsContext } from '../middleware/types'
|
import { CompletionsContext } from '../middleware/types'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import OpenAI from '@cherrystudio/openai'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { Provider } from '@renderer/types'
|
import { Provider } from '@renderer/types'
|
||||||
import { GenerateImageParams } from '@renderer/types'
|
import { GenerateImageParams } from '@renderer/types'
|
||||||
import OpenAI from 'openai'
|
|
||||||
|
|
||||||
import { OpenAIAPIClient } from '../openai/OpenAIApiClient'
|
import { OpenAIAPIClient } from '../openai/OpenAIApiClient'
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,15 @@ import { isDedicatedImageGenerationModel, isFunctionCallingModel } from '@render
|
|||||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||||
import { withSpanResult } from '@renderer/services/SpanManagerService'
|
import { withSpanResult } from '@renderer/services/SpanManagerService'
|
||||||
import { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
|
import { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
|
||||||
import type { GenerateImageParams, Model, Provider } from '@renderer/types'
|
import type { GenerateImageParams, Model, Provider, RetrieveVideoContentParams } from '@renderer/types'
|
||||||
import type { RequestOptions, SdkModel } from '@renderer/types/sdk'
|
import type { RequestOptions, SdkModel } from '@renderer/types/sdk'
|
||||||
|
import {
|
||||||
|
CreateVideoParams,
|
||||||
|
CreateVideoResult,
|
||||||
|
RetrieveVideoContentResult,
|
||||||
|
RetrieveVideoParams,
|
||||||
|
RetrieveVideoResult
|
||||||
|
} from '@renderer/types/video'
|
||||||
import { isSupportedToolUse } from '@renderer/utils/mcp-tools'
|
import { isSupportedToolUse } from '@renderer/utils/mcp-tools'
|
||||||
|
|
||||||
import { AihubmixAPIClient } from './clients/aihubmix/AihubmixAPIClient'
|
import { AihubmixAPIClient } from './clients/aihubmix/AihubmixAPIClient'
|
||||||
@@ -179,6 +186,42 @@ export default class AiProvider {
|
|||||||
return this.apiClient.generateImage(params)
|
return this.apiClient.generateImage(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async createVideo(params: CreateVideoParams): Promise<CreateVideoResult> {
|
||||||
|
if (this.apiClient instanceof OpenAIResponseAPIClient && params.type === 'openai') {
|
||||||
|
const video = await this.apiClient.createVideo(params)
|
||||||
|
return {
|
||||||
|
type: 'openai',
|
||||||
|
video
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Video generation is not supported by this provider')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async retrieveVideo(params: RetrieveVideoParams): Promise<RetrieveVideoResult> {
|
||||||
|
if (this.apiClient instanceof OpenAIResponseAPIClient && params.type === 'openai') {
|
||||||
|
const video = await this.apiClient.retrieveVideo(params)
|
||||||
|
return {
|
||||||
|
type: 'openai',
|
||||||
|
video
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Video generation is not supported by this provider')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async retrieveVideoContent(params: RetrieveVideoContentParams): Promise<RetrieveVideoContentResult> {
|
||||||
|
if (this.apiClient instanceof OpenAIResponseAPIClient && params.type === 'openai') {
|
||||||
|
const response = await this.apiClient.retrieveVideoContent(params)
|
||||||
|
return {
|
||||||
|
type: 'openai',
|
||||||
|
response
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Video generation is not supported by this provider')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public getBaseURL(): string {
|
public getBaseURL(): string {
|
||||||
return this.apiClient.getBaseURL()
|
return this.apiClient.getBaseURL()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import OpenAI from '@cherrystudio/openai'
|
||||||
|
import { toFile } from '@cherrystudio/openai/uploads'
|
||||||
import { isDedicatedImageGenerationModel } from '@renderer/config/models'
|
import { isDedicatedImageGenerationModel } from '@renderer/config/models'
|
||||||
import FileManager from '@renderer/services/FileManager'
|
import FileManager from '@renderer/services/FileManager'
|
||||||
import { ChunkType } from '@renderer/types/chunk'
|
import { ChunkType } from '@renderer/types/chunk'
|
||||||
import { findImageBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
|
import { findImageBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
|
||||||
import { defaultTimeout } from '@shared/config/constant'
|
import { defaultTimeout } from '@shared/config/constant'
|
||||||
import OpenAI from 'openai'
|
|
||||||
import { toFile } from 'openai/uploads'
|
|
||||||
|
|
||||||
import { BaseApiClient } from '../../clients/BaseApiClient'
|
import { BaseApiClient } from '../../clients/BaseApiClient'
|
||||||
import { CompletionsParams, CompletionsResult, GenericChunk } from '../schemas'
|
import { CompletionsParams, CompletionsResult, GenericChunk } from '../schemas'
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
* 处理文件内容提取、文件格式转换、文件上传等逻辑
|
* 处理文件内容提取、文件格式转换、文件上传等逻辑
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type OpenAI from '@cherrystudio/openai'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||||
import type { FileMetadata, Message, Model } from '@renderer/types'
|
import type { FileMetadata, Message, Model } from '@renderer/types'
|
||||||
@@ -10,7 +11,6 @@ import { FileTypes } from '@renderer/types'
|
|||||||
import { FileMessageBlock } from '@renderer/types/newMessage'
|
import { FileMessageBlock } from '@renderer/types/newMessage'
|
||||||
import { findFileBlocks } from '@renderer/utils/messageUtils/find'
|
import { findFileBlocks } from '@renderer/utils/messageUtils/find'
|
||||||
import type { FilePart, TextPart } from 'ai'
|
import type { FilePart, TextPart } from 'ai'
|
||||||
import type OpenAI from 'openai'
|
|
||||||
|
|
||||||
import { getAiSdkProviderId } from '../provider/factory'
|
import { getAiSdkProviderId } from '../provider/factory'
|
||||||
import { getFileSizeLimit, supportsImageInput, supportsLargeFileUpload, supportsPdfInput } from './modelCapabilities'
|
import { getFileSizeLimit, supportsImageInput, supportsLargeFileUpload, supportsPdfInput } from './modelCapabilities'
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
Sparkle,
|
Sparkle,
|
||||||
Sun,
|
Sun,
|
||||||
Terminal,
|
Terminal,
|
||||||
|
Video,
|
||||||
X
|
X
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useCallback, useEffect, useMemo } from 'react'
|
import { useCallback, useEffect, useMemo } from 'react'
|
||||||
@@ -106,6 +107,8 @@ const getTabIcon = (
|
|||||||
return <Settings size={14} />
|
return <Settings size={14} />
|
||||||
case 'code':
|
case 'code':
|
||||||
return <Terminal size={14} />
|
return <Terminal size={14} />
|
||||||
|
case 'video':
|
||||||
|
return <Video size={14} />
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ export const ToastPortal = () => {
|
|||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
classNames: {
|
classNames: {
|
||||||
// This setting causes the 'hero-toast' class to be applied twice to the toast element. This is weird and I don't know why, but it works.
|
// This setting causes the 'hero-toast' class to be applied twice to the toast element. This is weird and I don't know why, but it works.
|
||||||
base: 'hero-toast'
|
// `w-auto` would not overwrite default style, which set the width to a fixed value and causes text overflow.
|
||||||
|
base: 'hero-toast w-auto! max-w-[50vw]'
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ import {
|
|||||||
Palette,
|
Palette,
|
||||||
Settings,
|
Settings,
|
||||||
Sparkle,
|
Sparkle,
|
||||||
Sun
|
Sun,
|
||||||
|
Video
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -139,7 +140,8 @@ const MainMenus: FC = () => {
|
|||||||
knowledge: <FileSearch size={18} className="icon" />,
|
knowledge: <FileSearch size={18} className="icon" />,
|
||||||
files: <Folder size={18} className="icon" />,
|
files: <Folder size={18} className="icon" />,
|
||||||
notes: <NotepadText size={18} className="icon" />,
|
notes: <NotepadText size={18} className="icon" />,
|
||||||
code_tools: <Code size={18} className="icon" />
|
code_tools: <Code size={18} className="icon" />,
|
||||||
|
video: <Video size={18} className="icon" />
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathMap = {
|
const pathMap = {
|
||||||
@@ -151,7 +153,8 @@ const MainMenus: FC = () => {
|
|||||||
knowledge: '/knowledge',
|
knowledge: '/knowledge',
|
||||||
files: '/files',
|
files: '/files',
|
||||||
code_tools: '/code',
|
code_tools: '/code',
|
||||||
notes: '/notes'
|
notes: '/notes',
|
||||||
|
video: '/video'
|
||||||
}
|
}
|
||||||
|
|
||||||
return sidebarIcons.visible.map((icon) => {
|
return sidebarIcons.visible.map((icon) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import OpenAI from '@cherrystudio/openai'
|
||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
import { getLowerBaseModelName } from '@renderer/utils'
|
import { getLowerBaseModelName } from '@renderer/utils'
|
||||||
import OpenAI from 'openai'
|
|
||||||
|
|
||||||
import { WEB_SEARCH_PROMPT_FOR_OPENROUTER } from '../prompts'
|
import { WEB_SEARCH_PROMPT_FOR_OPENROUTER } from '../prompts'
|
||||||
import { getWebSearchTools } from '../tools'
|
import { getWebSearchTools } from '../tools'
|
||||||
|
|||||||
149
src/renderer/src/config/models/video.ts
Normal file
149
src/renderer/src/config/models/video.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { SystemProviderId, Video } from '@renderer/types'
|
||||||
|
|
||||||
|
// Hard-encoded for now. We may implement a function to filter video generation model from provider.models.
|
||||||
|
export const videoModelsMap = {
|
||||||
|
openai: ['sora-2', 'sora-2-pro'] as const
|
||||||
|
} as const satisfies Partial<Record<SystemProviderId, string[]>>
|
||||||
|
|
||||||
|
// Mock data for testing
|
||||||
|
export const mockVideos: Video[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'openai',
|
||||||
|
status: 'downloaded',
|
||||||
|
prompt: 'A beautiful sunset over the ocean with waves crashing',
|
||||||
|
thumbnail: 'https://picsum.photos/200/200?random=1',
|
||||||
|
fileId: 'file-001',
|
||||||
|
providerId: 'openai',
|
||||||
|
name: 'video-001',
|
||||||
|
metadata: {
|
||||||
|
id: 'video-001',
|
||||||
|
object: 'video',
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
completed_at: Math.floor(Date.now() / 1000),
|
||||||
|
expires_at: null,
|
||||||
|
error: null,
|
||||||
|
model: 'sora-2',
|
||||||
|
progress: 100,
|
||||||
|
remixed_from_video_id: null,
|
||||||
|
seconds: '4',
|
||||||
|
size: '1280x720',
|
||||||
|
status: 'completed'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: 'openai',
|
||||||
|
status: 'in_progress',
|
||||||
|
prompt: 'A cat playing with a ball of yarn in slow motion',
|
||||||
|
progress: 65,
|
||||||
|
providerId: 'openai',
|
||||||
|
name: 'video-002',
|
||||||
|
metadata: {
|
||||||
|
id: 'video-002',
|
||||||
|
object: 'video',
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
completed_at: null,
|
||||||
|
expires_at: null,
|
||||||
|
error: null,
|
||||||
|
model: 'sora-2-pro',
|
||||||
|
progress: 65,
|
||||||
|
remixed_from_video_id: null,
|
||||||
|
seconds: '8',
|
||||||
|
size: '1792x1024',
|
||||||
|
status: 'in_progress'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: 'openai',
|
||||||
|
status: 'queued',
|
||||||
|
prompt: 'Time-lapse of flowers blooming in a garden',
|
||||||
|
providerId: 'openai',
|
||||||
|
name: 'video-003',
|
||||||
|
metadata: {
|
||||||
|
id: 'video-003',
|
||||||
|
object: 'video',
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
completed_at: null,
|
||||||
|
expires_at: null,
|
||||||
|
error: null,
|
||||||
|
model: 'sora-2',
|
||||||
|
progress: 0,
|
||||||
|
remixed_from_video_id: null,
|
||||||
|
seconds: '12',
|
||||||
|
size: '1280x720',
|
||||||
|
status: 'queued'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
type: 'openai',
|
||||||
|
prompt: 'Birds flying in formation against blue sky',
|
||||||
|
status: 'downloading',
|
||||||
|
progress: 80,
|
||||||
|
thumbnail: 'https://picsum.photos/200/200?random=4',
|
||||||
|
providerId: 'openai',
|
||||||
|
name: 'video-004',
|
||||||
|
metadata: {
|
||||||
|
id: 'video-004',
|
||||||
|
object: 'video',
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
completed_at: Math.floor(Date.now() / 1000),
|
||||||
|
expires_at: null,
|
||||||
|
error: null,
|
||||||
|
model: 'sora-2-pro',
|
||||||
|
progress: 100,
|
||||||
|
remixed_from_video_id: null,
|
||||||
|
seconds: '8',
|
||||||
|
size: '1792x1024',
|
||||||
|
status: 'completed'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
type: 'openai',
|
||||||
|
status: 'failed',
|
||||||
|
error: { code: '400', message: 'Video generation failed' },
|
||||||
|
prompt: 'Mountain landscape with snow peaks and forest',
|
||||||
|
providerId: 'openai',
|
||||||
|
name: 'video-005',
|
||||||
|
metadata: {
|
||||||
|
id: 'video-005',
|
||||||
|
object: 'video',
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
completed_at: Math.floor(Date.now() / 1000),
|
||||||
|
expires_at: null,
|
||||||
|
error: { code: '400', message: 'Video generation failed' },
|
||||||
|
model: 'sora-2',
|
||||||
|
progress: 0,
|
||||||
|
remixed_from_video_id: null,
|
||||||
|
seconds: '4',
|
||||||
|
size: '1280x720',
|
||||||
|
status: 'failed'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6',
|
||||||
|
type: 'openai',
|
||||||
|
status: 'completed',
|
||||||
|
thumbnail: 'https://picsum.photos/200/200?random=6',
|
||||||
|
prompt: 'City street at night with neon lights reflecting on wet pavement',
|
||||||
|
providerId: 'openai',
|
||||||
|
name: 'video-006',
|
||||||
|
metadata: {
|
||||||
|
id: 'video-006',
|
||||||
|
object: 'video',
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
completed_at: Math.floor(Date.now() / 1000),
|
||||||
|
expires_at: null,
|
||||||
|
error: null,
|
||||||
|
model: 'sora-2-pro',
|
||||||
|
progress: 100,
|
||||||
|
remixed_from_video_id: null,
|
||||||
|
seconds: '12',
|
||||||
|
size: '1024x1792',
|
||||||
|
status: 'completed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { ChatCompletionTool } from '@cherrystudio/openai/resources'
|
||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
import { ChatCompletionTool } from 'openai/resources'
|
|
||||||
|
|
||||||
import { WEB_SEARCH_PROMPT_FOR_ZHIPU } from './prompts'
|
import { WEB_SEARCH_PROMPT_FOR_ZHIPU } from './prompts'
|
||||||
|
|
||||||
|
|||||||
65
src/renderer/src/hooks/video/useAddOpenAIVideo.ts
Normal file
65
src/renderer/src/hooks/video/useAddOpenAIVideo.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import OpenAI from '@cherrystudio/openai'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
|
import { useVideos } from './useVideos'
|
||||||
|
|
||||||
|
export const useAddOpenAIVideo = (providerId: string) => {
|
||||||
|
const { addVideo } = useVideos(providerId)
|
||||||
|
|
||||||
|
const addOpenAIVideo = useCallback(
|
||||||
|
(video: OpenAI.Videos.Video, prompt: string) => {
|
||||||
|
switch (video.status) {
|
||||||
|
case 'queued':
|
||||||
|
addVideo({
|
||||||
|
id: video.id,
|
||||||
|
name: video.id,
|
||||||
|
providerId,
|
||||||
|
status: video.status,
|
||||||
|
type: 'openai',
|
||||||
|
metadata: video,
|
||||||
|
prompt
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'in_progress':
|
||||||
|
addVideo({
|
||||||
|
id: video.id,
|
||||||
|
name: video.id,
|
||||||
|
providerId,
|
||||||
|
status: 'in_progress',
|
||||||
|
type: 'openai',
|
||||||
|
progress: video.progress,
|
||||||
|
metadata: video,
|
||||||
|
prompt
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'completed':
|
||||||
|
addVideo({
|
||||||
|
id: video.id,
|
||||||
|
name: video.id,
|
||||||
|
providerId,
|
||||||
|
status: 'completed',
|
||||||
|
type: 'openai',
|
||||||
|
metadata: video,
|
||||||
|
prompt,
|
||||||
|
thumbnail: null
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'failed':
|
||||||
|
addVideo({
|
||||||
|
id: video.id,
|
||||||
|
name: video.id,
|
||||||
|
providerId,
|
||||||
|
status: 'failed',
|
||||||
|
type: 'openai',
|
||||||
|
error: video.error,
|
||||||
|
metadata: video,
|
||||||
|
prompt
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[addVideo]
|
||||||
|
)
|
||||||
|
|
||||||
|
return addOpenAIVideo
|
||||||
|
}
|
||||||
43
src/renderer/src/hooks/video/useOpenAIVideo.ts
Normal file
43
src/renderer/src/hooks/video/useOpenAIVideo.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { retrieveVideo } from '@renderer/services/ApiService'
|
||||||
|
import { SystemProviderIds } from '@renderer/types'
|
||||||
|
import useSWR, { SWRConfiguration, useSWRConfig } from 'swr'
|
||||||
|
|
||||||
|
import { useProvider } from '../useProvider'
|
||||||
|
import { useVideo } from './useVideo'
|
||||||
|
|
||||||
|
export const useOpenAIVideo = (id: string) => {
|
||||||
|
const providerId = SystemProviderIds.openai
|
||||||
|
const { provider: openai } = useProvider(providerId)
|
||||||
|
const fetcher = async () => {
|
||||||
|
return retrieveVideo({
|
||||||
|
type: 'openai',
|
||||||
|
videoId: id,
|
||||||
|
provider: openai
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const video = useVideo(providerId, id)
|
||||||
|
let options: SWRConfiguration = {}
|
||||||
|
switch (video?.status) {
|
||||||
|
case 'queued':
|
||||||
|
case 'in_progress':
|
||||||
|
options = {
|
||||||
|
refreshInterval: 3000
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
options = {
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnMount: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { data, isLoading, error } = useSWR(`video/openai/${id}`, fetcher, options)
|
||||||
|
const { mutate } = useSWRConfig()
|
||||||
|
const revalidate = () => mutate(`video/openai/${id}`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
video: data,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
revalidate
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/renderer/src/hooks/video/useVideo.ts
Normal file
7
src/renderer/src/hooks/video/useVideo.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { useVideos } from './useVideos'
|
||||||
|
|
||||||
|
export const useVideo = (providerId: string, id: string) => {
|
||||||
|
const { videos } = useVideos(providerId)
|
||||||
|
const video = videos.find((v) => v.id === id)
|
||||||
|
return video
|
||||||
|
}
|
||||||
150
src/renderer/src/hooks/video/useVideos.ts
Normal file
150
src/renderer/src/hooks/video/useVideos.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import { loggerService } from '@logger'
|
||||||
|
import { retrieveVideo, retrieveVideoContent } from '@renderer/services/ApiService'
|
||||||
|
import { getProviderById } from '@renderer/services/ProviderService'
|
||||||
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
|
import {
|
||||||
|
addVideoAction,
|
||||||
|
removeVideoAction,
|
||||||
|
setVideoAction,
|
||||||
|
setVideosAction,
|
||||||
|
updateVideoAction
|
||||||
|
} from '@renderer/store/video'
|
||||||
|
import { Video } from '@renderer/types/video'
|
||||||
|
import { useCallback, useEffect, useRef } from 'react'
|
||||||
|
import useSWR from 'swr'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('useVideo')
|
||||||
|
|
||||||
|
export const useVideos = (providerId: string) => {
|
||||||
|
const videos = useAppSelector((state) => state.video.videoMap[providerId])
|
||||||
|
const videosRef = useRef(videos)
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
videosRef.current = videos
|
||||||
|
}, [videos])
|
||||||
|
|
||||||
|
const addVideo = useCallback(
|
||||||
|
(video: Video) => {
|
||||||
|
if (videos && videos.every((v) => v.id !== video.id)) {
|
||||||
|
dispatch(addVideoAction({ providerId, video }))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch, providerId, videos]
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateVideo = useCallback(
|
||||||
|
(update: Partial<Omit<Video, 'status'>> & { id: string }) => {
|
||||||
|
dispatch(updateVideoAction({ providerId, update }))
|
||||||
|
},
|
||||||
|
[dispatch, providerId]
|
||||||
|
)
|
||||||
|
|
||||||
|
const setVideo = useCallback(
|
||||||
|
(video: Video) => {
|
||||||
|
dispatch(setVideoAction({ providerId, video }))
|
||||||
|
},
|
||||||
|
[dispatch, providerId]
|
||||||
|
)
|
||||||
|
|
||||||
|
const setVideos = useCallback(
|
||||||
|
(newVideos: Video[]) => {
|
||||||
|
dispatch(setVideosAction({ providerId, videos: newVideos }))
|
||||||
|
},
|
||||||
|
[dispatch, providerId]
|
||||||
|
)
|
||||||
|
|
||||||
|
const removeVideo = useCallback(
|
||||||
|
(videoId: string) => {
|
||||||
|
dispatch(removeVideoAction({ providerId, videoId }))
|
||||||
|
},
|
||||||
|
[dispatch, providerId]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!videos) {
|
||||||
|
setVideos([])
|
||||||
|
}
|
||||||
|
}, [setVideos, videos])
|
||||||
|
|
||||||
|
// update videos from api
|
||||||
|
// NOTE: This provider should support openai videos endpoint. No runtime check here.
|
||||||
|
const provider = getProviderById(providerId)
|
||||||
|
const fetcher = async () => {
|
||||||
|
if (!videos || !provider) return []
|
||||||
|
const openaiVideos = videos.filter((v) => v.type === 'openai')
|
||||||
|
const jobs = openaiVideos.map((v) => retrieveVideo({ type: 'openai', videoId: v.id, provider }))
|
||||||
|
const result = await Promise.allSettled(jobs)
|
||||||
|
return result.filter((p) => p.status === 'fulfilled').map((p) => p.value)
|
||||||
|
}
|
||||||
|
const { data, error } = useSWR('video/openai/videos', fetcher, { refreshInterval: 3000 })
|
||||||
|
useEffect(() => {
|
||||||
|
if (error) {
|
||||||
|
logger.error('Failed to fetch video status updates', error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!provider) {
|
||||||
|
logger.warn(`Provider ${providerId} not found.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const videos = videosRef.current
|
||||||
|
|
||||||
|
if (!data || !videos) return
|
||||||
|
data.forEach((v) => {
|
||||||
|
const retrievedVideo = v.video
|
||||||
|
const storeVideo = videos.find((v) => v.id === retrievedVideo.id)
|
||||||
|
if (!storeVideo) {
|
||||||
|
logger.warn(`Try to update video ${retrievedVideo.id}, but it's not in the store.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch (retrievedVideo.status) {
|
||||||
|
case 'in_progress':
|
||||||
|
if (storeVideo.status === 'queued' || storeVideo.status === 'in_progress') {
|
||||||
|
setVideo({
|
||||||
|
...storeVideo,
|
||||||
|
status: 'in_progress',
|
||||||
|
progress: retrievedVideo.progress,
|
||||||
|
metadata: retrievedVideo
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'completed':
|
||||||
|
// Only update it when in_progress/queued -> completed
|
||||||
|
if (storeVideo.status === 'in_progress' || storeVideo.status === 'queued') {
|
||||||
|
setVideo({ ...storeVideo, status: 'completed', thumbnail: null, metadata: retrievedVideo })
|
||||||
|
// try to request thumbnail here.
|
||||||
|
retrieveVideoContent({
|
||||||
|
type: 'openai',
|
||||||
|
provider,
|
||||||
|
videoId: retrievedVideo.id,
|
||||||
|
query: { variant: 'thumbnail' }
|
||||||
|
})
|
||||||
|
.then((v) => {
|
||||||
|
// TODO: this is a iamge/webp type response. save it somewhere.
|
||||||
|
logger.debug('thumbnail resposne', v.response)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
logger.error(`Failed to get thumbnail for video ${retrievedVideo.id}`, e as Error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'failed':
|
||||||
|
setVideo({
|
||||||
|
...storeVideo,
|
||||||
|
status: 'failed',
|
||||||
|
error: retrievedVideo.error,
|
||||||
|
metadata: retrievedVideo
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [data, error, provider, providerId, setVideo])
|
||||||
|
|
||||||
|
return {
|
||||||
|
videos: videos ?? [],
|
||||||
|
addVideo,
|
||||||
|
updateVideo,
|
||||||
|
setVideos,
|
||||||
|
setVideo,
|
||||||
|
removeVideo
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -146,7 +146,8 @@ const titleKeyMap = {
|
|||||||
notes: 'title.notes',
|
notes: 'title.notes',
|
||||||
paintings: 'title.paintings',
|
paintings: 'title.paintings',
|
||||||
settings: 'title.settings',
|
settings: 'title.settings',
|
||||||
translate: 'title.translate'
|
translate: 'title.translate',
|
||||||
|
video: 'title.video'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const getTitleLabel = (key: string): string => {
|
export const getTitleLabel = (key: string): string => {
|
||||||
@@ -172,7 +173,8 @@ const sidebarIconKeyMap = {
|
|||||||
knowledge: 'knowledge.title',
|
knowledge: 'knowledge.title',
|
||||||
files: 'files.title',
|
files: 'files.title',
|
||||||
code_tools: 'code.title',
|
code_tools: 'code.title',
|
||||||
notes: 'notes.title'
|
notes: 'notes.title',
|
||||||
|
video: 'video.title'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const getSidebarIconLabel = (key: string): string => {
|
export const getSidebarIconLabel = (key: string): string => {
|
||||||
|
|||||||
@@ -1014,10 +1014,12 @@
|
|||||||
"prompt": "Prompt",
|
"prompt": "Prompt",
|
||||||
"provider": "Provider",
|
"provider": "Provider",
|
||||||
"reasoning_content": "Deep reasoning",
|
"reasoning_content": "Deep reasoning",
|
||||||
|
"redownload": "Redownload",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"regenerate": "Regenerate",
|
"regenerate": "Regenerate",
|
||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
|
"retry": "Retry",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"saved": "Saved",
|
"saved": "Saved",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
@@ -1025,6 +1027,7 @@
|
|||||||
"selected": "Selected",
|
"selected": "Selected",
|
||||||
"selectedItems": "Selected {{count}} items",
|
"selectedItems": "Selected {{count}} items",
|
||||||
"selectedMessages": "Selected {{count}} messages",
|
"selectedMessages": "Selected {{count}} messages",
|
||||||
|
"send": "Send",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"sort": {
|
"sort": {
|
||||||
"pinyin": {
|
"pinyin": {
|
||||||
@@ -1191,7 +1194,8 @@
|
|||||||
"size": "Size",
|
"size": "Size",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"title": "Files",
|
"title": "Files",
|
||||||
"type": "Type"
|
"type": "Type",
|
||||||
|
"video": "Video"
|
||||||
},
|
},
|
||||||
"gpustack": {
|
"gpustack": {
|
||||||
"keep_alive_time": {
|
"keep_alive_time": {
|
||||||
@@ -4486,7 +4490,8 @@
|
|||||||
"paintings": "Paintings",
|
"paintings": "Paintings",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"store": "Assistant Library",
|
"store": "Assistant Library",
|
||||||
"translate": "Translate"
|
"translate": "Translate",
|
||||||
|
"video": "Video"
|
||||||
},
|
},
|
||||||
"trace": {
|
"trace": {
|
||||||
"backList": "Back To List",
|
"backList": "Back To List",
|
||||||
@@ -4644,6 +4649,44 @@
|
|||||||
"saveDataError": "Failed to save data, please try again.",
|
"saveDataError": "Failed to save data, please try again.",
|
||||||
"title": "Update"
|
"title": "Update"
|
||||||
},
|
},
|
||||||
|
"video": {
|
||||||
|
"error": {
|
||||||
|
"create": "Failed to create video",
|
||||||
|
"download": "Failed to download video.",
|
||||||
|
"invalid": "Invalid video",
|
||||||
|
"load": {
|
||||||
|
"message": "Failed to load the video",
|
||||||
|
"reason": "The file may be corrupted or has been deleted externally."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expired": "Expired",
|
||||||
|
"input_reference": {
|
||||||
|
"add": {
|
||||||
|
"error": {
|
||||||
|
"format": "Not a image",
|
||||||
|
"size": "This image is too large. It should be under 5MB."
|
||||||
|
},
|
||||||
|
"tooltip": "Add image reference"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"placeholder": "describes the video to generate"
|
||||||
|
},
|
||||||
|
"seconds": "Seconds",
|
||||||
|
"size": "Size",
|
||||||
|
"status": {
|
||||||
|
"completed": "Generation Completed",
|
||||||
|
"downloading": "Downloading",
|
||||||
|
"failed": "Generation Failed",
|
||||||
|
"in_progress": "Generating",
|
||||||
|
"queued": "Queued"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"placeholder": "No thumbnail"
|
||||||
|
},
|
||||||
|
"title": "Video",
|
||||||
|
"undefined": "No available video"
|
||||||
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
"missing_provider": "The supplier does not exist; reverted to the default supplier {{provider}}. This may cause issues."
|
"missing_provider": "The supplier does not exist; reverted to the default supplier {{provider}}. This may cause issues."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1014,10 +1014,12 @@
|
|||||||
"prompt": "提示词",
|
"prompt": "提示词",
|
||||||
"provider": "提供商",
|
"provider": "提供商",
|
||||||
"reasoning_content": "已深度思考",
|
"reasoning_content": "已深度思考",
|
||||||
|
"redownload": "重新下载",
|
||||||
"refresh": "刷新",
|
"refresh": "刷新",
|
||||||
"regenerate": "重新生成",
|
"regenerate": "重新生成",
|
||||||
"rename": "重命名",
|
"rename": "重命名",
|
||||||
"reset": "重置",
|
"reset": "重置",
|
||||||
|
"retry": "重试",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"saved": "已保存",
|
"saved": "已保存",
|
||||||
"search": "搜索",
|
"search": "搜索",
|
||||||
@@ -1025,6 +1027,7 @@
|
|||||||
"selected": "已选择",
|
"selected": "已选择",
|
||||||
"selectedItems": "已选择 {{count}} 项",
|
"selectedItems": "已选择 {{count}} 项",
|
||||||
"selectedMessages": "选中 {{count}} 条消息",
|
"selectedMessages": "选中 {{count}} 条消息",
|
||||||
|
"send": "发送",
|
||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
"sort": {
|
"sort": {
|
||||||
"pinyin": {
|
"pinyin": {
|
||||||
@@ -4486,7 +4489,8 @@
|
|||||||
"paintings": "绘画",
|
"paintings": "绘画",
|
||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
"store": "助手库",
|
"store": "助手库",
|
||||||
"translate": "翻译"
|
"translate": "翻译",
|
||||||
|
"video": "视频"
|
||||||
},
|
},
|
||||||
"trace": {
|
"trace": {
|
||||||
"backList": "返回列表",
|
"backList": "返回列表",
|
||||||
@@ -4644,6 +4648,42 @@
|
|||||||
"saveDataError": "保存数据失败,请重试",
|
"saveDataError": "保存数据失败,请重试",
|
||||||
"title": "更新提示"
|
"title": "更新提示"
|
||||||
},
|
},
|
||||||
|
"video": {
|
||||||
|
"error": {
|
||||||
|
"create": "创建视频失败",
|
||||||
|
"invalid": "无效的视频",
|
||||||
|
"load": {
|
||||||
|
"message": "加载视频失败",
|
||||||
|
"reason": "文件可能已损坏或已被外部删除。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"input_reference": {
|
||||||
|
"add": {
|
||||||
|
"error": {
|
||||||
|
"format": "需要上传图片格式的文件",
|
||||||
|
"size": "图片过大,应小于 5MB"
|
||||||
|
},
|
||||||
|
"tooltip": "添加图像参考"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"placeholder": "描述要生成的视频"
|
||||||
|
},
|
||||||
|
"seconds": "秒数",
|
||||||
|
"size": "尺寸",
|
||||||
|
"status": {
|
||||||
|
"completed": "生成完成",
|
||||||
|
"downloading": "下载中",
|
||||||
|
"failed": "生成失败",
|
||||||
|
"in_progress": "生成中",
|
||||||
|
"queued": "排队中"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"placeholder": "无缩略图"
|
||||||
|
},
|
||||||
|
"title": "视频",
|
||||||
|
"undefined": "无可用视频"
|
||||||
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
"missing_provider": "供应商不存在,已回退到默认供应商 {{provider}}。这可能导致问题。"
|
"missing_provider": "供应商不存在,已回退到默认供应商 {{provider}}。这可能导致问题。"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1014,10 +1014,12 @@
|
|||||||
"prompt": "提示詞",
|
"prompt": "提示詞",
|
||||||
"provider": "供應商",
|
"provider": "供應商",
|
||||||
"reasoning_content": "已深度思考",
|
"reasoning_content": "已深度思考",
|
||||||
|
"redownload": "[to be translated]:Redownload",
|
||||||
"refresh": "重新整理",
|
"refresh": "重新整理",
|
||||||
"regenerate": "重新生成",
|
"regenerate": "重新生成",
|
||||||
"rename": "重新命名",
|
"rename": "重新命名",
|
||||||
"reset": "重設",
|
"reset": "重設",
|
||||||
|
"retry": "[to be translated]:Retry",
|
||||||
"save": "儲存",
|
"save": "儲存",
|
||||||
"saved": "已儲存",
|
"saved": "已儲存",
|
||||||
"search": "搜尋",
|
"search": "搜尋",
|
||||||
@@ -1025,6 +1027,7 @@
|
|||||||
"selected": "已選擇",
|
"selected": "已選擇",
|
||||||
"selectedItems": "已選擇 {{count}} 項",
|
"selectedItems": "已選擇 {{count}} 項",
|
||||||
"selectedMessages": "選中 {{count}} 條訊息",
|
"selectedMessages": "選中 {{count}} 條訊息",
|
||||||
|
"send": "[to be translated]:Send",
|
||||||
"settings": "設定",
|
"settings": "設定",
|
||||||
"sort": {
|
"sort": {
|
||||||
"pinyin": {
|
"pinyin": {
|
||||||
@@ -4486,7 +4489,8 @@
|
|||||||
"paintings": "繪畫",
|
"paintings": "繪畫",
|
||||||
"settings": "設定",
|
"settings": "設定",
|
||||||
"store": "助手庫",
|
"store": "助手庫",
|
||||||
"translate": "翻譯"
|
"translate": "翻譯",
|
||||||
|
"video": "[to be translated]:Video"
|
||||||
},
|
},
|
||||||
"trace": {
|
"trace": {
|
||||||
"backList": "返回清單",
|
"backList": "返回清單",
|
||||||
@@ -4644,6 +4648,42 @@
|
|||||||
"saveDataError": "保存數據失敗,請重試",
|
"saveDataError": "保存數據失敗,請重試",
|
||||||
"title": "更新提示"
|
"title": "更新提示"
|
||||||
},
|
},
|
||||||
|
"video": {
|
||||||
|
"error": {
|
||||||
|
"create": "[to be translated]:Failed to create video",
|
||||||
|
"invalid": "[to be translated]:Invalid video",
|
||||||
|
"load": {
|
||||||
|
"message": "[to be translated]:Failed to load the video",
|
||||||
|
"reason": "[to be translated]:The file may be corrupted or has been deleted externally."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"input_reference": {
|
||||||
|
"add": {
|
||||||
|
"error": {
|
||||||
|
"format": "[to be translated]:Not a image",
|
||||||
|
"size": "[to be translated]:This image is too large. It should be under 5MB."
|
||||||
|
},
|
||||||
|
"tooltip": "[to be translated]:Add image reference"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"placeholder": "[to be translated]:describes the video to generate"
|
||||||
|
},
|
||||||
|
"seconds": "[to be translated]:Seconds",
|
||||||
|
"size": "[to be translated]:Size",
|
||||||
|
"status": {
|
||||||
|
"completed": "[to be translated]:Generation Completed",
|
||||||
|
"downloading": "[to be translated]:Downloading",
|
||||||
|
"failed": "[to be translated]:Failed to generate video",
|
||||||
|
"in_progress": "[to be translated]:Generating",
|
||||||
|
"queued": "[to be translated]:Queued"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"placeholder": "[to be translated]:No thumbnail"
|
||||||
|
},
|
||||||
|
"title": "[to be translated]:Video",
|
||||||
|
"undefined": "[to be translated]:No available video"
|
||||||
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
"missing_provider": "供應商不存在,已回退到預設供應商 {{provider}}。這可能導致問題。"
|
"missing_provider": "供應商不存在,已回退到預設供應商 {{provider}}。這可能導致問題。"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1014,10 +1014,12 @@
|
|||||||
"prompt": "Ενδεικτικός ρήματος",
|
"prompt": "Ενδεικτικός ρήματος",
|
||||||
"provider": "Παρέχων",
|
"provider": "Παρέχων",
|
||||||
"reasoning_content": "Έχει σκεφτεί πολύ καλά",
|
"reasoning_content": "Έχει σκεφτεί πολύ καλά",
|
||||||
|
"redownload": "[to be translated]:Redownload",
|
||||||
"refresh": "Ανανέωση",
|
"refresh": "Ανανέωση",
|
||||||
"regenerate": "Ξαναπαραγωγή",
|
"regenerate": "Ξαναπαραγωγή",
|
||||||
"rename": "Μετονομασία",
|
"rename": "Μετονομασία",
|
||||||
"reset": "Επαναφορά",
|
"reset": "Επαναφορά",
|
||||||
|
"retry": "[to be translated]:Retry",
|
||||||
"save": "Αποθήκευση",
|
"save": "Αποθήκευση",
|
||||||
"saved": "Αποθηκεύτηκε",
|
"saved": "Αποθηκεύτηκε",
|
||||||
"search": "Αναζήτηση",
|
"search": "Αναζήτηση",
|
||||||
@@ -1025,6 +1027,7 @@
|
|||||||
"selected": "Επιλεγμένο",
|
"selected": "Επιλεγμένο",
|
||||||
"selectedItems": "Επιλέχθηκαν {{count}} αντικείμενα",
|
"selectedItems": "Επιλέχθηκαν {{count}} αντικείμενα",
|
||||||
"selectedMessages": "Επιλέχθηκαν {{count}} μηνύματα",
|
"selectedMessages": "Επιλέχθηκαν {{count}} μηνύματα",
|
||||||
|
"send": "[to be translated]:Send",
|
||||||
"settings": "Ρυθμίσεις",
|
"settings": "Ρυθμίσεις",
|
||||||
"sort": {
|
"sort": {
|
||||||
"pinyin": {
|
"pinyin": {
|
||||||
@@ -4486,7 +4489,8 @@
|
|||||||
"paintings": "Ζωγραφική",
|
"paintings": "Ζωγραφική",
|
||||||
"settings": "Ρυθμίσεις",
|
"settings": "Ρυθμίσεις",
|
||||||
"store": "Βιβλιοθήκη βοηθών",
|
"store": "Βιβλιοθήκη βοηθών",
|
||||||
"translate": "Μετάφραση"
|
"translate": "Μετάφραση",
|
||||||
|
"video": "[to be translated]:Video"
|
||||||
},
|
},
|
||||||
"trace": {
|
"trace": {
|
||||||
"backList": "Επιστροφή στη λίστα",
|
"backList": "Επιστροφή στη λίστα",
|
||||||
@@ -4644,6 +4648,42 @@
|
|||||||
"saveDataError": "Η αποθήκευση των δεδομένων απέτυχε, δοκιμάστε ξανά",
|
"saveDataError": "Η αποθήκευση των δεδομένων απέτυχε, δοκιμάστε ξανά",
|
||||||
"title": "Ενημέρωση"
|
"title": "Ενημέρωση"
|
||||||
},
|
},
|
||||||
|
"video": {
|
||||||
|
"error": {
|
||||||
|
"create": "[to be translated]:Failed to create video",
|
||||||
|
"invalid": "[to be translated]:Invalid video",
|
||||||
|
"load": {
|
||||||
|
"message": "[to be translated]:Failed to load the video",
|
||||||
|
"reason": "[to be translated]:The file may be corrupted or has been deleted externally."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"input_reference": {
|
||||||
|
"add": {
|
||||||
|
"error": {
|
||||||
|
"format": "[to be translated]:Not a image",
|
||||||
|
"size": "[to be translated]:This image is too large. It should be under 5MB."
|
||||||
|
},
|
||||||
|
"tooltip": "[to be translated]:Add image reference"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"placeholder": "[to be translated]:describes the video to generate"
|
||||||
|
},
|
||||||
|
"seconds": "[to be translated]:Seconds",
|
||||||
|
"size": "[to be translated]:Size",
|
||||||
|
"status": {
|
||||||
|
"completed": "[to be translated]:Generation Completed",
|
||||||
|
"downloading": "[to be translated]:Downloading",
|
||||||
|
"failed": "[to be translated]:Failed to generate video",
|
||||||
|
"in_progress": "[to be translated]:Generating",
|
||||||
|
"queued": "[to be translated]:Queued"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"placeholder": "[to be translated]:No thumbnail"
|
||||||
|
},
|
||||||
|
"title": "[to be translated]:Video",
|
||||||
|
"undefined": "[to be translated]:No available video"
|
||||||
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
"missing_provider": "Ο προμηθευτής δεν υπάρχει, έγινε επαναφορά στον προεπιλεγμένο προμηθευτή {{provider}}. Αυτό μπορεί να προκαλέσει προβλήματα."
|
"missing_provider": "Ο προμηθευτής δεν υπάρχει, έγινε επαναφορά στον προεπιλεγμένο προμηθευτή {{provider}}. Αυτό μπορεί να προκαλέσει προβλήματα."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1014,10 +1014,12 @@
|
|||||||
"prompt": "Prompt",
|
"prompt": "Prompt",
|
||||||
"provider": "Proveedor",
|
"provider": "Proveedor",
|
||||||
"reasoning_content": "Pensamiento profundo",
|
"reasoning_content": "Pensamiento profundo",
|
||||||
|
"redownload": "[to be translated]:Redownload",
|
||||||
"refresh": "Actualizar",
|
"refresh": "Actualizar",
|
||||||
"regenerate": "Regenerar",
|
"regenerate": "Regenerar",
|
||||||
"rename": "Renombrar",
|
"rename": "Renombrar",
|
||||||
"reset": "Restablecer",
|
"reset": "Restablecer",
|
||||||
|
"retry": "[to be translated]:Retry",
|
||||||
"save": "Guardar",
|
"save": "Guardar",
|
||||||
"saved": "Guardado",
|
"saved": "Guardado",
|
||||||
"search": "Buscar",
|
"search": "Buscar",
|
||||||
@@ -1025,6 +1027,7 @@
|
|||||||
"selected": "Seleccionado",
|
"selected": "Seleccionado",
|
||||||
"selectedItems": "{{count}} elementos seleccionados",
|
"selectedItems": "{{count}} elementos seleccionados",
|
||||||
"selectedMessages": "{{count}} mensajes seleccionados",
|
"selectedMessages": "{{count}} mensajes seleccionados",
|
||||||
|
"send": "[to be translated]:Send",
|
||||||
"settings": "Configuración",
|
"settings": "Configuración",
|
||||||
"sort": {
|
"sort": {
|
||||||
"pinyin": {
|
"pinyin": {
|
||||||
@@ -4486,7 +4489,8 @@
|
|||||||
"paintings": "Pinturas",
|
"paintings": "Pinturas",
|
||||||
"settings": "Configuración",
|
"settings": "Configuración",
|
||||||
"store": "Biblioteca de asistentes",
|
"store": "Biblioteca de asistentes",
|
||||||
"translate": "Traducir"
|
"translate": "Traducir",
|
||||||
|
"video": "[to be translated]:Video"
|
||||||
},
|
},
|
||||||
"trace": {
|
"trace": {
|
||||||
"backList": "Volver a la lista",
|
"backList": "Volver a la lista",
|
||||||
@@ -4644,6 +4648,42 @@
|
|||||||
"saveDataError": "Error al guardar los datos, inténtalo de nuevo",
|
"saveDataError": "Error al guardar los datos, inténtalo de nuevo",
|
||||||
"title": "Actualización"
|
"title": "Actualización"
|
||||||
},
|
},
|
||||||
|
"video": {
|
||||||
|
"error": {
|
||||||
|
"create": "[to be translated]:Failed to create video",
|
||||||
|
"invalid": "[to be translated]:Invalid video",
|
||||||
|
"load": {
|
||||||
|
"message": "[to be translated]:Failed to load the video",
|
||||||
|
"reason": "[to be translated]:The file may be corrupted or has been deleted externally."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"input_reference": {
|
||||||
|
"add": {
|
||||||
|
"error": {
|
||||||
|
"format": "[to be translated]:Not a image",
|
||||||
|
"size": "[to be translated]:This image is too large. It should be under 5MB."
|
||||||
|
},
|
||||||
|
"tooltip": "[to be translated]:Add image reference"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"placeholder": "[to be translated]:describes the video to generate"
|
||||||
|
},
|
||||||
|
"seconds": "[to be translated]:Seconds",
|
||||||
|
"size": "[to be translated]:Size",
|
||||||
|
"status": {
|
||||||
|
"completed": "[to be translated]:Generation Completed",
|
||||||
|
"downloading": "[to be translated]:Downloading",
|
||||||
|
"failed": "[to be translated]:Failed to generate video",
|
||||||
|
"in_progress": "[to be translated]:Generating",
|
||||||
|
"queued": "[to be translated]:Queued"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"placeholder": "[to be translated]:No thumbnail"
|
||||||
|
},
|
||||||
|
"title": "[to be translated]:Video",
|
||||||
|
"undefined": "[to be translated]:No available video"
|
||||||
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
"missing_provider": "El proveedor no existe, se ha revertido al proveedor predeterminado {{provider}}. Esto podría causar problemas."
|
"missing_provider": "El proveedor no existe, se ha revertido al proveedor predeterminado {{provider}}. Esto podría causar problemas."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1014,10 +1014,12 @@
|
|||||||
"prompt": "Prompt",
|
"prompt": "Prompt",
|
||||||
"provider": "Fournisseur",
|
"provider": "Fournisseur",
|
||||||
"reasoning_content": "Réflexion approfondie",
|
"reasoning_content": "Réflexion approfondie",
|
||||||
|
"redownload": "[to be translated]:Redownload",
|
||||||
"refresh": "Actualiser",
|
"refresh": "Actualiser",
|
||||||
"regenerate": "Regénérer",
|
"regenerate": "Regénérer",
|
||||||
"rename": "Renommer",
|
"rename": "Renommer",
|
||||||
"reset": "Réinitialiser",
|
"reset": "Réinitialiser",
|
||||||
|
"retry": "[to be translated]:Retry",
|
||||||
"save": "Enregistrer",
|
"save": "Enregistrer",
|
||||||
"saved": "enregistré",
|
"saved": "enregistré",
|
||||||
"search": "Rechercher",
|
"search": "Rechercher",
|
||||||
@@ -1025,6 +1027,7 @@
|
|||||||
"selected": "Sélectionné",
|
"selected": "Sélectionné",
|
||||||
"selectedItems": "{{count}} éléments sélectionnés",
|
"selectedItems": "{{count}} éléments sélectionnés",
|
||||||
"selectedMessages": "{{count}} messages sélectionnés",
|
"selectedMessages": "{{count}} messages sélectionnés",
|
||||||
|
"send": "[to be translated]:Send",
|
||||||
"settings": "Paramètres",
|
"settings": "Paramètres",
|
||||||
"sort": {
|
"sort": {
|
||||||
"pinyin": {
|
"pinyin": {
|
||||||
@@ -4486,7 +4489,8 @@
|
|||||||
"paintings": "Peintures",
|
"paintings": "Peintures",
|
||||||
"settings": "Paramètres",
|
"settings": "Paramètres",
|
||||||
"store": "Bibliothèque d'assistants",
|
"store": "Bibliothèque d'assistants",
|
||||||
"translate": "Traduire"
|
"translate": "Traduire",
|
||||||
|
"video": "[to be translated]:Video"
|
||||||
},
|
},
|
||||||
"trace": {
|
"trace": {
|
||||||
"backList": "Retour à la liste",
|
"backList": "Retour à la liste",
|
||||||
@@ -4644,6 +4648,42 @@
|
|||||||
"saveDataError": "Échec de la sauvegarde des données, veuillez réessayer",
|
"saveDataError": "Échec de la sauvegarde des données, veuillez réessayer",
|
||||||
"title": "Mise à jour"
|
"title": "Mise à jour"
|
||||||
},
|
},
|
||||||
|
"video": {
|
||||||
|
"error": {
|
||||||
|
"create": "[to be translated]:Failed to create video",
|
||||||
|
"invalid": "[to be translated]:Invalid video",
|
||||||
|
"load": {
|
||||||
|
"message": "[to be translated]:Failed to load the video",
|
||||||
|
"reason": "[to be translated]:The file may be corrupted or has been deleted externally."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"input_reference": {
|
||||||
|
"add": {
|
||||||
|
"error": {
|
||||||
|
"format": "[to be translated]:Not a image",
|
||||||
|
"size": "[to be translated]:This image is too large. It should be under 5MB."
|
||||||
|
},
|
||||||
|
"tooltip": "[to be translated]:Add image reference"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"placeholder": "[to be translated]:describes the video to generate"
|
||||||
|
},
|
||||||
|
"seconds": "[to be translated]:Seconds",
|
||||||
|
"size": "[to be translated]:Size",
|
||||||
|
"status": {
|
||||||
|
"completed": "[to be translated]:Generation Completed",
|
||||||
|
"downloading": "[to be translated]:Downloading",
|
||||||
|
"failed": "[to be translated]:Failed to generate video",
|
||||||
|
"in_progress": "[to be translated]:Generating",
|
||||||
|
"queued": "[to be translated]:Queued"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"placeholder": "[to be translated]:No thumbnail"
|
||||||
|
},
|
||||||
|
"title": "[to be translated]:Video",
|
||||||
|
"undefined": "[to be translated]:No available video"
|
||||||
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
"missing_provider": "Le fournisseur n’existe pas, retour au fournisseur par défaut {{provider}}. Cela peut entraîner des problèmes."
|
"missing_provider": "Le fournisseur n’existe pas, retour au fournisseur par défaut {{provider}}. Cela peut entraîner des problèmes."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1014,10 +1014,12 @@
|
|||||||
"prompt": "プロンプト",
|
"prompt": "プロンプト",
|
||||||
"provider": "プロバイダー",
|
"provider": "プロバイダー",
|
||||||
"reasoning_content": "深く考察済み",
|
"reasoning_content": "深く考察済み",
|
||||||
|
"redownload": "[to be translated]:Redownload",
|
||||||
"refresh": "更新",
|
"refresh": "更新",
|
||||||
"regenerate": "再生成",
|
"regenerate": "再生成",
|
||||||
"rename": "名前を変更",
|
"rename": "名前を変更",
|
||||||
"reset": "リセット",
|
"reset": "リセット",
|
||||||
|
"retry": "[to be translated]:Retry",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"saved": "保存されました",
|
"saved": "保存されました",
|
||||||
"search": "検索",
|
"search": "検索",
|
||||||
@@ -1025,6 +1027,7 @@
|
|||||||
"selected": "選択済み",
|
"selected": "選択済み",
|
||||||
"selectedItems": "{{count}}件の項目を選択しました",
|
"selectedItems": "{{count}}件の項目を選択しました",
|
||||||
"selectedMessages": "{{count}}件のメッセージを選択しました",
|
"selectedMessages": "{{count}}件のメッセージを選択しました",
|
||||||
|
"send": "[to be translated]:Send",
|
||||||
"settings": "設定",
|
"settings": "設定",
|
||||||
"sort": {
|
"sort": {
|
||||||
"pinyin": {
|
"pinyin": {
|
||||||
@@ -4486,7 +4489,8 @@
|
|||||||
"paintings": "ペインティング",
|
"paintings": "ペインティング",
|
||||||
"settings": "設定",
|
"settings": "設定",
|
||||||
"store": "アシスタントライブラリ",
|
"store": "アシスタントライブラリ",
|
||||||
"translate": "翻訳"
|
"translate": "翻訳",
|
||||||
|
"video": "[to be translated]:Video"
|
||||||
},
|
},
|
||||||
"trace": {
|
"trace": {
|
||||||
"backList": "リストに戻る",
|
"backList": "リストに戻る",
|
||||||
@@ -4644,6 +4648,42 @@
|
|||||||
"saveDataError": "データの保存に失敗しました。もう一度お試しください。",
|
"saveDataError": "データの保存に失敗しました。もう一度お試しください。",
|
||||||
"title": "更新"
|
"title": "更新"
|
||||||
},
|
},
|
||||||
|
"video": {
|
||||||
|
"error": {
|
||||||
|
"create": "[to be translated]:Failed to create video",
|
||||||
|
"invalid": "[to be translated]:Invalid video",
|
||||||
|
"load": {
|
||||||
|
"message": "[to be translated]:Failed to load the video",
|
||||||
|
"reason": "[to be translated]:The file may be corrupted or has been deleted externally."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"input_reference": {
|
||||||
|
"add": {
|
||||||
|
"error": {
|
||||||
|
"format": "[to be translated]:Not a image",
|
||||||
|
"size": "[to be translated]:This image is too large. It should be under 5MB."
|
||||||
|
},
|
||||||
|
"tooltip": "[to be translated]:Add image reference"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"placeholder": "[to be translated]:describes the video to generate"
|
||||||
|
},
|
||||||
|
"seconds": "[to be translated]:Seconds",
|
||||||
|
"size": "[to be translated]:Size",
|
||||||
|
"status": {
|
||||||
|
"completed": "[to be translated]:Generation Completed",
|
||||||
|
"downloading": "[to be translated]:Downloading",
|
||||||
|
"failed": "[to be translated]:Failed to generate video",
|
||||||
|
"in_progress": "[to be translated]:Generating",
|
||||||
|
"queued": "[to be translated]:Queued"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"placeholder": "[to be translated]:No thumbnail"
|
||||||
|
},
|
||||||
|
"title": "[to be translated]:Video",
|
||||||
|
"undefined": "[to be translated]:No available video"
|
||||||
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
"missing_provider": "サプライヤーが存在しないため、デフォルトのサプライヤー {{provider}} にロールバックされました。これにより問題が発生する可能性があります。"
|
"missing_provider": "サプライヤーが存在しないため、デフォルトのサプライヤー {{provider}} にロールバックされました。これにより問題が発生する可能性があります。"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1014,10 +1014,12 @@
|
|||||||
"prompt": "Prompt",
|
"prompt": "Prompt",
|
||||||
"provider": "Fornecedor",
|
"provider": "Fornecedor",
|
||||||
"reasoning_content": "Pensamento profundo concluído",
|
"reasoning_content": "Pensamento profundo concluído",
|
||||||
|
"redownload": "[to be translated]:Redownload",
|
||||||
"refresh": "Atualizar",
|
"refresh": "Atualizar",
|
||||||
"regenerate": "Regenerar",
|
"regenerate": "Regenerar",
|
||||||
"rename": "Renomear",
|
"rename": "Renomear",
|
||||||
"reset": "Redefinir",
|
"reset": "Redefinir",
|
||||||
|
"retry": "[to be translated]:Retry",
|
||||||
"save": "Salvar",
|
"save": "Salvar",
|
||||||
"saved": "Guardado",
|
"saved": "Guardado",
|
||||||
"search": "Pesquisar",
|
"search": "Pesquisar",
|
||||||
@@ -1025,6 +1027,7 @@
|
|||||||
"selected": "Selecionado",
|
"selected": "Selecionado",
|
||||||
"selectedItems": "{{count}} itens selecionados",
|
"selectedItems": "{{count}} itens selecionados",
|
||||||
"selectedMessages": "{{count}} mensagens selecionadas",
|
"selectedMessages": "{{count}} mensagens selecionadas",
|
||||||
|
"send": "[to be translated]:Send",
|
||||||
"settings": "Configurações",
|
"settings": "Configurações",
|
||||||
"sort": {
|
"sort": {
|
||||||
"pinyin": {
|
"pinyin": {
|
||||||
@@ -4486,7 +4489,8 @@
|
|||||||
"paintings": "Pinturas",
|
"paintings": "Pinturas",
|
||||||
"settings": "Configurações",
|
"settings": "Configurações",
|
||||||
"store": "Biblioteca de assistentes",
|
"store": "Biblioteca de assistentes",
|
||||||
"translate": "Traduzir"
|
"translate": "Traduzir",
|
||||||
|
"video": "[to be translated]:Video"
|
||||||
},
|
},
|
||||||
"trace": {
|
"trace": {
|
||||||
"backList": "Voltar à lista",
|
"backList": "Voltar à lista",
|
||||||
@@ -4644,6 +4648,42 @@
|
|||||||
"saveDataError": "Falha ao salvar os dados, tente novamente",
|
"saveDataError": "Falha ao salvar os dados, tente novamente",
|
||||||
"title": "Atualização"
|
"title": "Atualização"
|
||||||
},
|
},
|
||||||
|
"video": {
|
||||||
|
"error": {
|
||||||
|
"create": "[to be translated]:Failed to create video",
|
||||||
|
"invalid": "[to be translated]:Invalid video",
|
||||||
|
"load": {
|
||||||
|
"message": "[to be translated]:Failed to load the video",
|
||||||
|
"reason": "[to be translated]:The file may be corrupted or has been deleted externally."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"input_reference": {
|
||||||
|
"add": {
|
||||||
|
"error": {
|
||||||
|
"format": "[to be translated]:Not a image",
|
||||||
|
"size": "[to be translated]:This image is too large. It should be under 5MB."
|
||||||
|
},
|
||||||
|
"tooltip": "[to be translated]:Add image reference"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"placeholder": "[to be translated]:describes the video to generate"
|
||||||
|
},
|
||||||
|
"seconds": "[to be translated]:Seconds",
|
||||||
|
"size": "[to be translated]:Size",
|
||||||
|
"status": {
|
||||||
|
"completed": "[to be translated]:Generation Completed",
|
||||||
|
"downloading": "[to be translated]:Downloading",
|
||||||
|
"failed": "[to be translated]:Failed to generate video",
|
||||||
|
"in_progress": "[to be translated]:Generating",
|
||||||
|
"queued": "[to be translated]:Queued"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"placeholder": "[to be translated]:No thumbnail"
|
||||||
|
},
|
||||||
|
"title": "[to be translated]:Video",
|
||||||
|
"undefined": "[to be translated]:No available video"
|
||||||
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
"missing_provider": "O fornecedor não existe; foi revertido para o fornecedor predefinido {{provider}}. Isto pode causar problemas."
|
"missing_provider": "O fornecedor não existe; foi revertido para o fornecedor predefinido {{provider}}. Isto pode causar problemas."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1014,10 +1014,12 @@
|
|||||||
"prompt": "Промпт",
|
"prompt": "Промпт",
|
||||||
"provider": "Провайдер",
|
"provider": "Провайдер",
|
||||||
"reasoning_content": "Глубокий анализ",
|
"reasoning_content": "Глубокий анализ",
|
||||||
|
"redownload": "[to be translated]:Redownload",
|
||||||
"refresh": "Обновить",
|
"refresh": "Обновить",
|
||||||
"regenerate": "Пересоздать",
|
"regenerate": "Пересоздать",
|
||||||
"rename": "Переименовать",
|
"rename": "Переименовать",
|
||||||
"reset": "Сбросить",
|
"reset": "Сбросить",
|
||||||
|
"retry": "[to be translated]:Retry",
|
||||||
"save": "Сохранить",
|
"save": "Сохранить",
|
||||||
"saved": "Сохранено",
|
"saved": "Сохранено",
|
||||||
"search": "Поиск",
|
"search": "Поиск",
|
||||||
@@ -1025,6 +1027,7 @@
|
|||||||
"selected": "Выбрано",
|
"selected": "Выбрано",
|
||||||
"selectedItems": "Выбрано {{count}} элементов",
|
"selectedItems": "Выбрано {{count}} элементов",
|
||||||
"selectedMessages": "Выбрано {{count}} сообщений",
|
"selectedMessages": "Выбрано {{count}} сообщений",
|
||||||
|
"send": "[to be translated]:Send",
|
||||||
"settings": "Настройки",
|
"settings": "Настройки",
|
||||||
"sort": {
|
"sort": {
|
||||||
"pinyin": {
|
"pinyin": {
|
||||||
@@ -4486,7 +4489,8 @@
|
|||||||
"paintings": "Рисунки",
|
"paintings": "Рисунки",
|
||||||
"settings": "Настройки",
|
"settings": "Настройки",
|
||||||
"store": "Библиотека помощников",
|
"store": "Библиотека помощников",
|
||||||
"translate": "Перевод"
|
"translate": "Перевод",
|
||||||
|
"video": "[to be translated]:Video"
|
||||||
},
|
},
|
||||||
"trace": {
|
"trace": {
|
||||||
"backList": "Вернуться к списку",
|
"backList": "Вернуться к списку",
|
||||||
@@ -4644,6 +4648,42 @@
|
|||||||
"saveDataError": "Ошибка сохранения данных, повторите попытку",
|
"saveDataError": "Ошибка сохранения данных, повторите попытку",
|
||||||
"title": "Обновление"
|
"title": "Обновление"
|
||||||
},
|
},
|
||||||
|
"video": {
|
||||||
|
"error": {
|
||||||
|
"create": "[to be translated]:Failed to create video",
|
||||||
|
"invalid": "[to be translated]:Invalid video",
|
||||||
|
"load": {
|
||||||
|
"message": "[to be translated]:Failed to load the video",
|
||||||
|
"reason": "[to be translated]:The file may be corrupted or has been deleted externally."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"input_reference": {
|
||||||
|
"add": {
|
||||||
|
"error": {
|
||||||
|
"format": "[to be translated]:Not a image",
|
||||||
|
"size": "[to be translated]:This image is too large. It should be under 5MB."
|
||||||
|
},
|
||||||
|
"tooltip": "[to be translated]:Add image reference"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"placeholder": "[to be translated]:describes the video to generate"
|
||||||
|
},
|
||||||
|
"seconds": "[to be translated]:Seconds",
|
||||||
|
"size": "[to be translated]:Size",
|
||||||
|
"status": {
|
||||||
|
"completed": "[to be translated]:Generation Completed",
|
||||||
|
"downloading": "[to be translated]:Downloading",
|
||||||
|
"failed": "[to be translated]:Failed to generate video",
|
||||||
|
"in_progress": "[to be translated]:Generating",
|
||||||
|
"queued": "[to be translated]:Queued"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"placeholder": "[to be translated]:No thumbnail"
|
||||||
|
},
|
||||||
|
"title": "[to be translated]:Video",
|
||||||
|
"undefined": "[to be translated]:No available video"
|
||||||
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
"missing_provider": "Поставщик не существует, возвращение к поставщику по умолчанию {{provider}}. Это может привести к проблемам."
|
"missing_provider": "Поставщик не существует, возвращение к поставщику по умолчанию {{provider}}. Это может привести к проблемам."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import {
|
|||||||
File as FileIcon,
|
File as FileIcon,
|
||||||
FileImage,
|
FileImage,
|
||||||
FileText,
|
FileText,
|
||||||
FileType as FileTypeIcon
|
FileType as FileTypeIcon,
|
||||||
|
FileVideo
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -138,6 +139,7 @@ const FilesPage: FC = () => {
|
|||||||
{ key: FileTypes.DOCUMENT, label: t('files.document'), icon: <FileIcon size={16} /> },
|
{ key: FileTypes.DOCUMENT, label: t('files.document'), icon: <FileIcon size={16} /> },
|
||||||
{ key: FileTypes.IMAGE, label: t('files.image'), icon: <FileImage size={16} /> },
|
{ key: FileTypes.IMAGE, label: t('files.image'), icon: <FileImage size={16} /> },
|
||||||
{ key: FileTypes.TEXT, label: t('files.text'), icon: <FileTypeIcon size={16} /> },
|
{ key: FileTypes.TEXT, label: t('files.text'), icon: <FileTypeIcon size={16} /> },
|
||||||
|
{ key: FileTypes.VIDEO, label: t('files.video'), icon: <FileVideo size={16} /> },
|
||||||
{ key: 'all', label: t('files.all'), icon: <FileText size={16} /> }
|
{ key: 'all', label: t('files.all'), icon: <FileText size={16} /> }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import App from '@renderer/components/MinApp/MinApp'
|
|||||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { Code, FileSearch, Folder, Languages, LayoutGrid, NotepadText, Palette, Sparkle } from 'lucide-react'
|
import { Code, FileSearch, Folder, Languages, LayoutGrid, NotepadText, Palette, Sparkle, Video } from 'lucide-react'
|
||||||
import { FC, useMemo } from 'react'
|
import { FC, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
@@ -63,6 +63,12 @@ const LaunchpadPage: FC = () => {
|
|||||||
text: t('title.notes'),
|
text: t('title.notes'),
|
||||||
path: '/notes',
|
path: '/notes',
|
||||||
bgColor: 'linear-gradient(135deg, #F97316, #FB923C)' // 笔记:橙色,代表活力和清晰思路
|
bgColor: 'linear-gradient(135deg, #F97316, #FB923C)' // 笔记:橙色,代表活力和清晰思路
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Video size={32} className="icon" />,
|
||||||
|
text: t('title.video'),
|
||||||
|
path: '/video',
|
||||||
|
bgColor: 'linear-gradient(135deg, #7C3AED, #A78BFA)' // Video Generation: deep purple, representing creativity and dynamic media
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ import {
|
|||||||
MessageSquareQuote,
|
MessageSquareQuote,
|
||||||
NotepadText,
|
NotepadText,
|
||||||
Palette,
|
Palette,
|
||||||
Sparkle
|
Sparkle,
|
||||||
|
Video
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { FC, useCallback, useMemo } from 'react'
|
import { FC, useCallback, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -127,7 +128,8 @@ const SidebarIconsManager: FC<SidebarIconsManagerProps> = ({
|
|||||||
knowledge: <FileSearch size={16} />,
|
knowledge: <FileSearch size={16} />,
|
||||||
files: <Folder size={16} />,
|
files: <Folder size={16} />,
|
||||||
notes: <NotepadText size={16} />,
|
notes: <NotepadText size={16} />,
|
||||||
code_tools: <Code size={16} />
|
code_tools: <Code size={16} />,
|
||||||
|
video: <Video size={16} />
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|||||||
133
src/renderer/src/pages/video/VideoList.tsx
Normal file
133
src/renderer/src/pages/video/VideoList.tsx
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { cn, Progress, Spinner } from '@heroui/react'
|
||||||
|
import { Video } from '@renderer/types'
|
||||||
|
import { CheckCircleIcon, CircleXIcon, ClockIcon, DownloadIcon, PlusIcon } from 'lucide-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
export type VideoListProps = {
|
||||||
|
videos: Video[]
|
||||||
|
activeVideoId?: string
|
||||||
|
setActiveVideoId: (id: string | undefined) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VideoList = ({ videos, activeVideoId, setActiveVideoId }: VideoListProps) => {
|
||||||
|
return (
|
||||||
|
<div className="w-40 space-y-3 overflow-auto p-2">
|
||||||
|
<div
|
||||||
|
className="group relative flex aspect-square cursor-pointer items-center justify-center overflow-hidden rounded-xl border-2 transition-all hover:scale-105 hover:shadow-lg"
|
||||||
|
onClick={() => setActiveVideoId(undefined)}>
|
||||||
|
<PlusIcon size={24} />
|
||||||
|
</div>
|
||||||
|
{/* {mockVideos.map((video) => ( */}
|
||||||
|
{videos.map((video) => (
|
||||||
|
<VideoListItem
|
||||||
|
key={video.id}
|
||||||
|
video={video}
|
||||||
|
isActive={activeVideoId === video.id}
|
||||||
|
onClick={() => setActiveVideoId(video.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const VideoListItem = ({ video, isActive, onClick }: { video: Video; isActive: boolean; onClick: () => void }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const getStatusIcon = () => {
|
||||||
|
switch (video.status) {
|
||||||
|
case 'queued':
|
||||||
|
return <ClockIcon size={20} className="text-default-500" />
|
||||||
|
case 'in_progress':
|
||||||
|
return <Spinner size="sm" color="primary" />
|
||||||
|
case 'completed':
|
||||||
|
return <CheckCircleIcon size={20} className="text-success" />
|
||||||
|
case 'downloading':
|
||||||
|
return <DownloadIcon size={20} className="text-primary" />
|
||||||
|
case 'downloaded':
|
||||||
|
return null // No indicator for downloaded state
|
||||||
|
case 'failed':
|
||||||
|
return <CircleXIcon size={20} className="text-danger" />
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusColor = () => {
|
||||||
|
switch (video.status) {
|
||||||
|
case 'queued':
|
||||||
|
return 'bg-default-100'
|
||||||
|
case 'in_progress':
|
||||||
|
return 'bg-primary-50'
|
||||||
|
case 'completed':
|
||||||
|
return 'bg-success-50'
|
||||||
|
case 'downloading':
|
||||||
|
return 'bg-primary-50'
|
||||||
|
case 'downloaded':
|
||||||
|
return 'bg-success-50'
|
||||||
|
case 'failed':
|
||||||
|
return 'bg-danger-50'
|
||||||
|
default:
|
||||||
|
return 'bg-default-50'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showProgress = video.status === 'in_progress' || video.status === 'downloading'
|
||||||
|
const showThumbnail = video.status === 'completed' || video.status === 'downloading' || video.status === 'downloaded'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
`group relative aspect-square cursor-pointer overflow-hidden rounded-xl border-2 transition-all hover:scale-105 hover:shadow-lg ${getStatusColor()}`,
|
||||||
|
isActive ? 'border-primary' : undefined
|
||||||
|
)}
|
||||||
|
onClick={onClick}>
|
||||||
|
{/* Thumbnail placeholder */}
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-default-100 to-default-200">
|
||||||
|
{showThumbnail ? (
|
||||||
|
<img src={video.thumbnail ?? ''} alt="Video thumbnail" className="h-full w-full object-cover" />
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center gap-2 text-default-400">
|
||||||
|
<div className="text-2xl">🎬</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status overlay */}
|
||||||
|
<div className="absolute inset-0 bg-black/20 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||||
|
|
||||||
|
{/* Status indicator */}
|
||||||
|
{getStatusIcon() && (
|
||||||
|
<div className="absolute top-2 right-2 flex items-center gap-1 rounded-full bg-white/90 px-2 py-1 backdrop-blur-sm">
|
||||||
|
{getStatusIcon()}
|
||||||
|
<span className="font-medium text-xs">{t(`video.status.${video.status}`)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Progress bar for in_progress and downloading states */}
|
||||||
|
{showProgress && (
|
||||||
|
<div className="absolute right-0 bottom-0 left-0 p-2">
|
||||||
|
<Progress
|
||||||
|
size="sm"
|
||||||
|
value={video.progress}
|
||||||
|
color={video.status === 'downloading' ? 'primary' : 'primary'}
|
||||||
|
className="w-full"
|
||||||
|
showValueLabel={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Video info overlay */}
|
||||||
|
<div className="absolute right-0 bottom-0 left-0 bg-gradient-to-t from-black/60 to-transparent p-3 pt-6 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
|
<div className="text-white">
|
||||||
|
<p className="truncate font-medium text-sm">{video.metadata.id}</p>
|
||||||
|
{video.prompt && <p className="mt-1 line-clamp-2 text-xs opacity-80">{video.prompt}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Failed state overlay */}
|
||||||
|
{video.status === 'failed' && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-danger/10"></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
80
src/renderer/src/pages/video/VideoPage.tsx
Normal file
80
src/renderer/src/pages/video/VideoPage.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// interface VideoPageProps {}
|
||||||
|
|
||||||
|
import { Divider } from '@heroui/react'
|
||||||
|
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||||
|
import { useProvider } from '@renderer/hooks/useProvider'
|
||||||
|
import { useVideos } from '@renderer/hooks/video/useVideos'
|
||||||
|
import { SystemProviderIds } from '@renderer/types'
|
||||||
|
import { CreateVideoParams } from '@renderer/types/video'
|
||||||
|
import { deepUpdate } from '@renderer/utils/deepUpdate'
|
||||||
|
import { isVideoModel } from '@renderer/utils/model/video'
|
||||||
|
import { DeepPartial } from 'ai'
|
||||||
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { ModelSetting } from './settings/ModelSetting'
|
||||||
|
import { OpenAIParamSettings } from './settings/OpenAIParamSettings'
|
||||||
|
import { ProviderSetting } from './settings/ProviderSetting'
|
||||||
|
import { SettingsGroup } from './settings/shared'
|
||||||
|
import { VideoList } from './VideoList'
|
||||||
|
import { VideoPanel } from './VideoPanel'
|
||||||
|
|
||||||
|
export const VideoPage = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [providerId, setProviderId] = useState<string>(SystemProviderIds.openai)
|
||||||
|
const { provider } = useProvider(providerId)
|
||||||
|
const [params, setParams] = useState<CreateVideoParams>({
|
||||||
|
type: 'openai',
|
||||||
|
provider,
|
||||||
|
params: {
|
||||||
|
model: 'sora-2',
|
||||||
|
prompt: ''
|
||||||
|
},
|
||||||
|
options: {}
|
||||||
|
})
|
||||||
|
const [activeVideoId, setActiveVideoId] = useState<string>()
|
||||||
|
|
||||||
|
const updateParams = useCallback((update: DeepPartial<Omit<CreateVideoParams, 'type'>>) => {
|
||||||
|
setParams((prev) => deepUpdate<CreateVideoParams>(prev, update))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const updateModelId = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
if (isVideoModel(id)) {
|
||||||
|
updateParams({ params: { model: id } })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[updateParams]
|
||||||
|
)
|
||||||
|
|
||||||
|
const { videos } = useVideos(providerId)
|
||||||
|
// const activeVideo = useMemo(() => mockVideos.find((v) => v.id === activeVideoId), [activeVideoId])
|
||||||
|
const activeVideo = useMemo(() => videos.find((v) => v.id === activeVideoId), [activeVideoId, videos])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 flex-col">
|
||||||
|
<Navbar>
|
||||||
|
<NavbarCenter style={{ borderRight: 'none' }}>{t('video.title')}</NavbarCenter>
|
||||||
|
</Navbar>
|
||||||
|
<div id="content-container" className="flex max-h-full flex-1">
|
||||||
|
{/* Settings */}
|
||||||
|
<div className="flex w-70 flex-col p-2">
|
||||||
|
<SettingsGroup>
|
||||||
|
<ProviderSetting providerId={providerId} setProviderId={setProviderId} />
|
||||||
|
<ModelSetting
|
||||||
|
providerId={providerId}
|
||||||
|
modelId={params.params.model ?? 'sora-2'}
|
||||||
|
setModelId={updateModelId}
|
||||||
|
/>
|
||||||
|
</SettingsGroup>
|
||||||
|
{provider.type === 'openai-response' && <OpenAIParamSettings params={params} updateParams={updateParams} />}
|
||||||
|
</div>
|
||||||
|
<Divider orientation="vertical" />
|
||||||
|
<VideoPanel provider={provider} params={params} updateParams={updateParams} video={activeVideo} />
|
||||||
|
<Divider orientation="vertical" />
|
||||||
|
{/* Video list */}
|
||||||
|
<VideoList videos={videos} activeVideoId={activeVideoId} setActiveVideoId={setActiveVideoId} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
287
src/renderer/src/pages/video/VideoPanel.tsx
Normal file
287
src/renderer/src/pages/video/VideoPanel.tsx
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
import { Button, cn, Image, Skeleton, Textarea, Tooltip } from '@heroui/react'
|
||||||
|
import { loggerService } from '@logger'
|
||||||
|
import { useAddOpenAIVideo } from '@renderer/hooks/video/useAddOpenAIVideo'
|
||||||
|
import { useVideos } from '@renderer/hooks/video/useVideos'
|
||||||
|
import { createVideo, retrieveVideoContent } from '@renderer/services/ApiService'
|
||||||
|
import FileManager from '@renderer/services/FileManager'
|
||||||
|
import { FileTypes, Provider, VideoFileMetadata } from '@renderer/types'
|
||||||
|
import { CreateVideoParams, Video } from '@renderer/types/video'
|
||||||
|
import { getErrorMessage } from '@renderer/utils'
|
||||||
|
import { MB } from '@shared/config/constant'
|
||||||
|
import { DeepPartial } from 'ai'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
import { ArrowUp, CircleXIcon, ImageIcon } from 'lucide-react'
|
||||||
|
import mime from 'mime-types'
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { VideoViewer } from './VideoViewer'
|
||||||
|
|
||||||
|
export type VideoPanelProps = {
|
||||||
|
provider: Provider
|
||||||
|
video?: Video
|
||||||
|
params: CreateVideoParams
|
||||||
|
updateParams: (upadte: DeepPartial<Omit<CreateVideoParams, 'type'>>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('VideoPanel')
|
||||||
|
|
||||||
|
export const VideoPanel = ({ provider, video, params, updateParams }: VideoPanelProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const addOpenAIVideo = useAddOpenAIVideo(provider.id)
|
||||||
|
const { setVideo } = useVideos(provider.id)
|
||||||
|
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false)
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const inputReference = params.params.input_reference
|
||||||
|
|
||||||
|
const couldCreateVideo = useMemo(
|
||||||
|
() =>
|
||||||
|
!isProcessing &&
|
||||||
|
!isEmpty(params.params.prompt) &&
|
||||||
|
video?.status !== 'queued' &&
|
||||||
|
video?.status !== 'downloading' &&
|
||||||
|
video?.status !== 'in_progress',
|
||||||
|
[isProcessing, params.params.prompt, video?.status]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (video) {
|
||||||
|
updateParams({ params: { prompt: video.prompt } })
|
||||||
|
} else {
|
||||||
|
updateParams({ params: { prompt: '' } })
|
||||||
|
}
|
||||||
|
}, [updateParams, video])
|
||||||
|
|
||||||
|
const handleCreateVideo = useCallback(async () => {
|
||||||
|
if (!couldCreateVideo) return
|
||||||
|
setIsProcessing(true)
|
||||||
|
try {
|
||||||
|
if (video === undefined) {
|
||||||
|
const result = await createVideo(params)
|
||||||
|
const video = result.video
|
||||||
|
switch (result.type) {
|
||||||
|
case 'openai':
|
||||||
|
addOpenAIVideo(video, params.params.prompt)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
logger.error(`Invalid video type ${result.type}.`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: remix video
|
||||||
|
window.toast.info('Remix video is not implemented.')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
window.toast.error({ title: t('video.error.create'), description: getErrorMessage(e), timeout: 5000 })
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false)
|
||||||
|
}
|
||||||
|
}, [addOpenAIVideo, couldCreateVideo, params, t, video])
|
||||||
|
|
||||||
|
const handleRegenerateVideo = useCallback(() => {
|
||||||
|
window.toast.info('Not implemented')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleDownloadVideo = useCallback(async () => {
|
||||||
|
if (!video) return
|
||||||
|
if (video.status !== 'completed' && video.status !== 'downloaded') return
|
||||||
|
|
||||||
|
const baseVideo: Video = {
|
||||||
|
...video,
|
||||||
|
status: 'downloading',
|
||||||
|
progress: 0,
|
||||||
|
thumbnail: video.thumbnail
|
||||||
|
}
|
||||||
|
setVideo(baseVideo)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { response } = await retrieveVideoContent({ type: 'openai', videoId: video.id, provider })
|
||||||
|
if (!response.body) {
|
||||||
|
throw new Error('Video response body is empty')
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body.getReader()
|
||||||
|
const contentLengthHeader = response.headers.get('content-length')
|
||||||
|
const totalSize = contentLengthHeader ? Number(contentLengthHeader) : undefined
|
||||||
|
const chunks: Uint8Array[] = []
|
||||||
|
let receivedLength = 0
|
||||||
|
let progressValue = 0
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read()
|
||||||
|
if (done) break
|
||||||
|
if (!value) continue
|
||||||
|
|
||||||
|
chunks.push(value)
|
||||||
|
receivedLength += value.length
|
||||||
|
|
||||||
|
if (totalSize && Number.isFinite(totalSize) && totalSize > 0) {
|
||||||
|
progressValue = Math.floor((receivedLength / totalSize) * 100)
|
||||||
|
} else {
|
||||||
|
progressValue = Math.min(progressValue + 1, 99)
|
||||||
|
}
|
||||||
|
|
||||||
|
setVideo({
|
||||||
|
...baseVideo,
|
||||||
|
progress: Math.min(progressValue, 99)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileData = new Uint8Array(receivedLength)
|
||||||
|
let offset = 0
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
fileData.set(chunk, offset)
|
||||||
|
offset += chunk.length
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = response.headers.get('content-type') ?? 'video/mp4'
|
||||||
|
const normalizedContentType = contentType.split(';')[0]?.trim() || 'video/mp4'
|
||||||
|
const extension = (() => {
|
||||||
|
const ext = mime.extension(normalizedContentType)
|
||||||
|
return ext ? `.${ext}` : '.mp4'
|
||||||
|
})()
|
||||||
|
|
||||||
|
const fileName = `${video.id}${extension}`.toLowerCase()
|
||||||
|
|
||||||
|
const tempFilePath = await window.api.file.createTempFile(fileName)
|
||||||
|
await window.api.file.write(tempFilePath, fileData)
|
||||||
|
|
||||||
|
const tempFileMetadata = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
name: fileName,
|
||||||
|
origin_name: fileName,
|
||||||
|
path: tempFilePath,
|
||||||
|
size: receivedLength,
|
||||||
|
ext: extension,
|
||||||
|
type: FileTypes.VIDEO,
|
||||||
|
created_at: dayjs().toISOString(),
|
||||||
|
count: 1
|
||||||
|
} satisfies VideoFileMetadata
|
||||||
|
|
||||||
|
const uploadedFile = await FileManager.uploadFile(tempFileMetadata)
|
||||||
|
|
||||||
|
setVideo({
|
||||||
|
...video,
|
||||||
|
status: 'downloaded',
|
||||||
|
thumbnail: video.thumbnail,
|
||||||
|
fileId: uploadedFile.id,
|
||||||
|
name: uploadedFile.origin_name
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to download video ${video.id}.`, error as Error)
|
||||||
|
window.toast.error(t('video.error.download'))
|
||||||
|
setVideo(video)
|
||||||
|
}
|
||||||
|
}, [provider, setVideo, t, video])
|
||||||
|
|
||||||
|
const handleUploadFile = useCallback(() => {
|
||||||
|
fileInputRef.current?.click()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const setPrompt = useCallback((value: string) => updateParams({ params: { prompt: value } }), [updateParams])
|
||||||
|
|
||||||
|
const UploadImageReferenceButton = useCallback(() => {
|
||||||
|
const content = inputReference ? (
|
||||||
|
<div className="group">
|
||||||
|
<Image
|
||||||
|
className="aspect-square max-h-50 max-w-50 object-contain"
|
||||||
|
src={URL.createObjectURL(inputReference as File)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="danger"
|
||||||
|
className="absolute top-1 right-1 z-100 h-6 w-6 min-w-0 opacity-0 group-hover:opacity-100"
|
||||||
|
isIconOnly
|
||||||
|
startContent={<CircleXIcon size={16} className="text-danger" />}
|
||||||
|
onPress={() => updateParams({ params: { input_reference: undefined } })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
t('video.input_reference.add.tooltip')
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip content={content} closeDelay={0}>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
startContent={<ImageIcon size={16} className={cn(inputReference ? 'text-primary' : undefined)} />}
|
||||||
|
isIconOnly
|
||||||
|
className="h-6 w-6 min-w-0"
|
||||||
|
isDisabled={isProcessing}
|
||||||
|
onPress={handleUploadFile}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}, [handleUploadFile, isProcessing, inputReference, t, updateParams])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 flex-col p-2">
|
||||||
|
<div className="m-8 flex-1 overflow-hidden">
|
||||||
|
<Skeleton className="h-full w-full rounded-2xl" classNames={{ content: 'h-full w-full' }} isLoaded={true}>
|
||||||
|
{video && <VideoViewer video={video} onDownload={handleDownloadVideo} onRegenerate={handleRegenerateVideo} />}
|
||||||
|
{!video && <VideoViewer video={video} />}
|
||||||
|
</Skeleton>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<Textarea
|
||||||
|
label={t('common.prompt')}
|
||||||
|
placeholder={t('video.prompt.placeholder')}
|
||||||
|
value={params.params.prompt}
|
||||||
|
onValueChange={setPrompt}
|
||||||
|
isClearable
|
||||||
|
isDisabled={isProcessing}
|
||||||
|
classNames={{ inputWrapper: 'pb-8' }}
|
||||||
|
onKeyDown={(e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
handleCreateVideo()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-0 flex w-full items-end justify-between p-2">
|
||||||
|
<div className="flex">
|
||||||
|
<UploadImageReferenceButton />
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
hidden
|
||||||
|
onChange={(e) => {
|
||||||
|
const files = e.target.files
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
const file = files[0]
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
window.toast.error(t('video.input_reference.add.error.format'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const maxSize = 5 * MB
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
window.toast.error(t('video.input_reference.add.error.size'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateParams({ params: { input_reference: file } })
|
||||||
|
} else {
|
||||||
|
updateParams({ params: { input_reference: undefined } })
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tooltip content={t('common.send')} closeDelay={0}>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
radius="full"
|
||||||
|
isIconOnly
|
||||||
|
isDisabled={!couldCreateVideo}
|
||||||
|
isLoading={isProcessing}
|
||||||
|
className="h-6 w-6 min-w-0"
|
||||||
|
onPress={handleCreateVideo}>
|
||||||
|
<ArrowUp size={16} className="text-primary-foreground" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
213
src/renderer/src/pages/video/VideoViewer.tsx
Normal file
213
src/renderer/src/pages/video/VideoViewer.tsx
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
Progress,
|
||||||
|
Skeleton,
|
||||||
|
Spinner,
|
||||||
|
useDisclosure
|
||||||
|
} from '@heroui/react'
|
||||||
|
import FileManager from '@renderer/services/FileManager'
|
||||||
|
import { Video, VideoDownloaded, VideoFailed } from '@renderer/types/video'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { CheckCircleIcon, CircleXIcon, Clock9Icon } from 'lucide-react'
|
||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import useSWRImmutable from 'swr/immutable'
|
||||||
|
|
||||||
|
export type VideoViewerProps =
|
||||||
|
| {
|
||||||
|
video: undefined
|
||||||
|
onDownload?: never
|
||||||
|
onRegenerate?: never
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
video: Video
|
||||||
|
onDownload: () => void
|
||||||
|
onRegenerate: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VideoViewer = ({ video, onDownload, onRegenerate }: VideoViewerProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [loadSuccess, setLoadSuccess] = useState<boolean | undefined>(undefined)
|
||||||
|
useEffect(() => {
|
||||||
|
setLoadSuccess(undefined)
|
||||||
|
}, [video?.id])
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex h-full max-h-full w-full items-center justify-center rounded-2xl bg-foreground-200">
|
||||||
|
{video === undefined && t('video.undefined')}
|
||||||
|
{video && video.status === 'queued' && <QueuedVideo />}
|
||||||
|
{video && video.status === 'in_progress' && <InProgressVideo progress={video.progress} />}
|
||||||
|
{video && video.status === 'completed' && (
|
||||||
|
<CompletedVideo video={video} onDownload={onDownload} onRegenerate={onRegenerate} />
|
||||||
|
)}
|
||||||
|
{video && video.status === 'downloading' && <DownloadingVideo progress={video.progress} />}
|
||||||
|
{video && video.status === 'downloaded' && loadSuccess !== false && (
|
||||||
|
<VideoPlayer video={video} setLoadSuccess={setLoadSuccess} />
|
||||||
|
)}
|
||||||
|
{video && video.status === 'failed' && <FailedVideo error={video.error} />}
|
||||||
|
{video && video.status === 'downloaded' && loadSuccess === false && (
|
||||||
|
<LoadFailedVideo onRedownload={onDownload} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const QueuedVideo = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center rounded-2xl">
|
||||||
|
<Spinner variant="dots" />
|
||||||
|
<span>{t('video.status.queued')}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const InProgressVideo = ({ progress }: { progress: number }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center rounded-2xl">
|
||||||
|
<Progress
|
||||||
|
label={t('video.status.in_progress')}
|
||||||
|
aria-label={t('video.status.in_progress')}
|
||||||
|
className="max-w-md"
|
||||||
|
color="primary"
|
||||||
|
showValueLabel={true}
|
||||||
|
size="md"
|
||||||
|
value={progress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CompletedVideo = ({
|
||||||
|
video,
|
||||||
|
onDownload,
|
||||||
|
onRegenerate
|
||||||
|
}: {
|
||||||
|
video: Video
|
||||||
|
onDownload: () => void
|
||||||
|
onRegenerate: () => void
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const isExpired = video.metadata.expires_at !== null && video.metadata.expires_at < dayjs().unix()
|
||||||
|
if (isExpired) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center gap-2 rounded-2xl bg-warning-200">
|
||||||
|
<Clock9Icon size={64} className="text-warning" />
|
||||||
|
<span className="font-bold text-2xl">{t('video.expired')}</span>
|
||||||
|
<Button onPress={onRegenerate}>{t('common.regenerate')}</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center gap-2 rounded-2xl bg-success-200">
|
||||||
|
<CheckCircleIcon size={64} className="text-success" />
|
||||||
|
<span className="font-bold text-2xl">{t('video.status.completed')}</span>
|
||||||
|
<Button onPress={onDownload}>{t('common.download')}</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DownloadingVideo = ({ progress }: { progress?: number }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center rounded-2xl">
|
||||||
|
<Progress
|
||||||
|
label={t('video.status.downloading')}
|
||||||
|
aria-label={t('video.status.downloading')}
|
||||||
|
className="max-w-md"
|
||||||
|
color="primary"
|
||||||
|
showValueLabel={true}
|
||||||
|
size="md"
|
||||||
|
value={progress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FailedVideo = ({ error }: { error: VideoFailed['error'] }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
|
|
||||||
|
const alert = useMemo(() => {
|
||||||
|
if (error === null) {
|
||||||
|
return <Alert color="danger" title={t('error.unknown')} />
|
||||||
|
} else {
|
||||||
|
return <Alert color="danger" title={error.code} description={error.message} />
|
||||||
|
}
|
||||||
|
}, [error, t])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center rounded-2xl bg-danger-200">
|
||||||
|
<CircleXIcon size={64} className="fill-danger text-danger-200" />
|
||||||
|
<span className="font-bold text-2xl">{t('video.status.failed')}</span>
|
||||||
|
<div className="my-2 flex justify-between gap-2">
|
||||||
|
<Button onPress={onOpen}>{t('common.detail')}</Button>
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
|
<ModalBody>
|
||||||
|
<ModalContent>
|
||||||
|
<div className="p-4">{alert}</div>
|
||||||
|
</ModalContent>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter></ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
<Button onPress={() => window.toast.info('Not implemented')}>{t('common.retry')}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoadFailedVideo = ({ onRedownload }: { onRedownload: () => void }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center rounded-2xl bg-danger-200">
|
||||||
|
<CircleXIcon size={64} className="fill-danger text-danger-200" />
|
||||||
|
<span className="font-bold text-2xl">{t('video.error.load.message')}</span>
|
||||||
|
<span>{t('video.error.load.reason')}</span>
|
||||||
|
<div className="my-2 flex justify-between gap-2">
|
||||||
|
<Button onPress={onRedownload}>{t('common.redownload')}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const VideoPlayer = ({
|
||||||
|
video,
|
||||||
|
setLoadSuccess
|
||||||
|
}: {
|
||||||
|
video: VideoDownloaded
|
||||||
|
setLoadSuccess: (value: boolean) => void
|
||||||
|
}) => {
|
||||||
|
const fetcher = async () => {
|
||||||
|
const file = await FileManager.getFile(video.fileId)
|
||||||
|
if (!file) {
|
||||||
|
throw new Error(`Video file ${video.fileId} not exist.`)
|
||||||
|
}
|
||||||
|
return FileManager.getFilePath(file)
|
||||||
|
}
|
||||||
|
const { data: src, isLoading, error } = useSWRImmutable(`video/file/${video.id}`, fetcher)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
setLoadSuccess(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Skeleton />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<video
|
||||||
|
controls
|
||||||
|
className="h-full w-full rounded-2xl bg-black object-contain"
|
||||||
|
onLoadedData={() => setLoadSuccess(true)}
|
||||||
|
onError={() => setLoadSuccess(false)}>
|
||||||
|
<source src={`file://${src}`} type="video/mp4" />
|
||||||
|
</video>
|
||||||
|
)
|
||||||
|
}
|
||||||
44
src/renderer/src/pages/video/settings/ModelSetting.tsx
Normal file
44
src/renderer/src/pages/video/settings/ModelSetting.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Select, SelectItem } from '@heroui/react'
|
||||||
|
import { videoModelsMap } from '@renderer/config/models/video'
|
||||||
|
import { Model } from '@renderer/types'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { SettingItem } from './shared'
|
||||||
|
|
||||||
|
export interface ModelSettingProps {
|
||||||
|
providerId: string
|
||||||
|
modelId: string
|
||||||
|
setModelId: (id: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModelSelectItem extends Model {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModelSetting = ({ providerId, modelId, setModelId }: ModelSettingProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const items: ModelSelectItem[] = videoModelsMap[providerId]?.map((m: string) => ({ key: m, label: m })) ?? []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingItem>
|
||||||
|
<Select
|
||||||
|
label={t('common.model')}
|
||||||
|
labelPlacement="outside"
|
||||||
|
selectionMode="single"
|
||||||
|
items={items}
|
||||||
|
defaultSelectedKeys={[modelId]}
|
||||||
|
disallowEmptySelection
|
||||||
|
onSelectionChange={(keys) => {
|
||||||
|
if (keys.currentKey) setModelId(keys.currentKey)
|
||||||
|
}}>
|
||||||
|
{(model) => (
|
||||||
|
<SelectItem textValue={model.label}>
|
||||||
|
<span>{model.label}</span>
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</SettingItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { VideoSeconds, VideoSize } from '@cherrystudio/openai/resources'
|
||||||
|
import { Select, SelectItem } from '@heroui/react'
|
||||||
|
import { OpenAICreateVideoParams } from '@renderer/types/video'
|
||||||
|
import { DeepPartial } from 'ai'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { SettingItem, SettingsGroup } from './shared'
|
||||||
|
|
||||||
|
export type OpenAIParamSettingsProps = {
|
||||||
|
params: OpenAICreateVideoParams
|
||||||
|
updateParams: (update: DeepPartial<Omit<OpenAICreateVideoParams, 'type'>>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OpenAIParamSettings = ({ params, updateParams }: OpenAIParamSettingsProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const secondItems = [{ key: '4' }, { key: '8' }, { key: '12' }] as const satisfies { key: VideoSeconds }[]
|
||||||
|
const sizeItems = [
|
||||||
|
{ key: '720x1280' },
|
||||||
|
{ key: '1280x720' },
|
||||||
|
{ key: '1024x1792' },
|
||||||
|
{ key: '1792x1024' }
|
||||||
|
] as const satisfies { key: VideoSize }[]
|
||||||
|
|
||||||
|
const updateSeconds = useCallback(
|
||||||
|
(seconds: VideoSeconds) => {
|
||||||
|
updateParams({ params: { seconds } })
|
||||||
|
},
|
||||||
|
[updateParams]
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateSize = useCallback(
|
||||||
|
(size: VideoSize) => {
|
||||||
|
updateParams({ params: { size } })
|
||||||
|
},
|
||||||
|
[updateParams]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsGroup>
|
||||||
|
<SettingItem>
|
||||||
|
<Select
|
||||||
|
label={t('video.seconds')}
|
||||||
|
labelPlacement="outside"
|
||||||
|
selectedKeys={[params.params.seconds ?? '4']}
|
||||||
|
onSelectionChange={(keys) => {
|
||||||
|
if (keys.currentKey) updateSeconds(keys.currentKey as VideoSeconds)
|
||||||
|
}}
|
||||||
|
items={secondItems}
|
||||||
|
selectionMode="single"
|
||||||
|
disallowEmptySelection>
|
||||||
|
{(item) => (
|
||||||
|
<SelectItem key={item.key} textValue={item.key}>
|
||||||
|
<span>{item.key}</span>
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem>
|
||||||
|
<Select
|
||||||
|
label={t('video.size')}
|
||||||
|
labelPlacement="outside"
|
||||||
|
selectedKeys={[params.params.size ?? '720x1280']}
|
||||||
|
onSelectionChange={(keys) => {
|
||||||
|
if (keys.currentKey) updateSize(keys.currentKey as VideoSize)
|
||||||
|
}}
|
||||||
|
items={sizeItems}
|
||||||
|
selectionMode="single"
|
||||||
|
disallowEmptySelection>
|
||||||
|
{(item) => (
|
||||||
|
<SelectItem key={item.key} textValue={item.key}>
|
||||||
|
<span>{item.key}</span>
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</SettingItem>
|
||||||
|
</SettingsGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
60
src/renderer/src/pages/video/settings/ProviderSetting.tsx
Normal file
60
src/renderer/src/pages/video/settings/ProviderSetting.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { Select, SelectItem } from '@heroui/react'
|
||||||
|
import { ProviderAvatar } from '@renderer/components/ProviderAvatar'
|
||||||
|
import { useProviders } from '@renderer/hooks/useProvider'
|
||||||
|
import { Provider, SystemProviderId } from '@renderer/types'
|
||||||
|
import { getFancyProviderName } from '@renderer/utils'
|
||||||
|
import { Dispatch, SetStateAction } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { SettingItem } from './shared'
|
||||||
|
|
||||||
|
export interface ProviderSettingProps {
|
||||||
|
providerId: string
|
||||||
|
setProviderId: Dispatch<SetStateAction<string>>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProviderSelectItem extends Provider {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProviderSetting = ({ providerId, setProviderId }: ProviderSettingProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
// Support limited providers.
|
||||||
|
const supportedProviderIds = ['openai'] satisfies SystemProviderId[]
|
||||||
|
const { providers } = useProviders()
|
||||||
|
const items: ProviderSelectItem[] = providers
|
||||||
|
.filter((p) => supportedProviderIds.some((id) => id === p.id))
|
||||||
|
.map((p) => ({ ...p, key: p.id, label: getFancyProviderName(p) }))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingItem>
|
||||||
|
<Select
|
||||||
|
label={t('common.provider')}
|
||||||
|
labelPlacement="outside"
|
||||||
|
selectionMode="single"
|
||||||
|
items={items}
|
||||||
|
defaultSelectedKeys={[providerId]}
|
||||||
|
disallowEmptySelection
|
||||||
|
onSelectionChange={(keys) => {
|
||||||
|
if (keys.currentKey) setProviderId(keys.currentKey)
|
||||||
|
}}
|
||||||
|
renderValue={(items) => {
|
||||||
|
const provider = items[0].data
|
||||||
|
if (!provider) return null
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ProviderAvatar provider={provider} size={16} />
|
||||||
|
<span>{provider.label}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}>
|
||||||
|
{(provider) => (
|
||||||
|
<SelectItem textValue={provider.label} startContent={<ProviderAvatar provider={provider} size={16} />}>
|
||||||
|
<span>{provider.label}</span>
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</SettingItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
15
src/renderer/src/pages/video/settings/shared.tsx
Normal file
15
src/renderer/src/pages/video/settings/shared.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Divider } from '@heroui/react'
|
||||||
|
import { PropsWithChildren } from 'react'
|
||||||
|
|
||||||
|
export const SettingsGroup = ({ children }: PropsWithChildren) => {
|
||||||
|
return <div className="mb-4 flex flex-col rounded-2xl border border-foreground-200 p-3">{children}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SettingItem = ({ children, divider = false }: PropsWithChildren<{ divider?: boolean }>) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-2">{children}</div>
|
||||||
|
{divider && <Divider className="my-2" />}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -10,12 +10,19 @@ import { isDedicatedImageGenerationModel, isEmbeddingModel } from '@renderer/con
|
|||||||
import { getStoreSetting } from '@renderer/hooks/useSettings'
|
import { getStoreSetting } from '@renderer/hooks/useSettings'
|
||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import type { FetchChatCompletionParams } from '@renderer/types'
|
import type { FetchChatCompletionParams, RetrieveVideoContentParams } from '@renderer/types'
|
||||||
import { Assistant, MCPServer, MCPTool, Model, Provider } from '@renderer/types'
|
import { Assistant, MCPServer, MCPTool, Model, Provider } from '@renderer/types'
|
||||||
import type { StreamTextParams } from '@renderer/types/aiCoreTypes'
|
import type { StreamTextParams } from '@renderer/types/aiCoreTypes'
|
||||||
import { type Chunk, ChunkType } from '@renderer/types/chunk'
|
import { type Chunk, ChunkType } from '@renderer/types/chunk'
|
||||||
import { Message } from '@renderer/types/newMessage'
|
import { Message } from '@renderer/types/newMessage'
|
||||||
import { SdkModel } from '@renderer/types/sdk'
|
import { SdkModel } from '@renderer/types/sdk'
|
||||||
|
import {
|
||||||
|
CreateVideoParams,
|
||||||
|
CreateVideoResult,
|
||||||
|
RetrieveVideoContentResult,
|
||||||
|
RetrieveVideoParams,
|
||||||
|
RetrieveVideoResult
|
||||||
|
} from '@renderer/types/video'
|
||||||
import { removeSpecialCharactersForTopicName, uuid } from '@renderer/utils'
|
import { removeSpecialCharactersForTopicName, uuid } from '@renderer/utils'
|
||||||
import { abortCompletion, readyToAbort } from '@renderer/utils/abortController'
|
import { abortCompletion, readyToAbort } from '@renderer/utils/abortController'
|
||||||
import { isAbortError } from '@renderer/utils/error'
|
import { isAbortError } from '@renderer/utils/error'
|
||||||
@@ -397,6 +404,21 @@ export async function fetchGenerate({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createVideo(params: CreateVideoParams): Promise<CreateVideoResult> {
|
||||||
|
const ai = new AiProviderNew(params.provider)
|
||||||
|
return ai.createVideo(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function retrieveVideo(params: RetrieveVideoParams): Promise<RetrieveVideoResult> {
|
||||||
|
const ai = new AiProviderNew(params.provider)
|
||||||
|
return ai.retrieveVideo(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function retrieveVideoContent(params: RetrieveVideoContentParams): Promise<RetrieveVideoContentResult> {
|
||||||
|
const ai = new AiProviderNew(params.provider)
|
||||||
|
return ai.retrieveVideoContent(params)
|
||||||
|
}
|
||||||
|
|
||||||
export function hasApiKey(provider: Provider) {
|
export function hasApiKey(provider: Provider) {
|
||||||
if (!provider) return false
|
if (!provider) return false
|
||||||
if (['ollama', 'lmstudio', 'vertexai', 'cherryai'].includes(provider.id)) return true
|
if (['ollama', 'lmstudio', 'vertexai', 'cherryai'].includes(provider.id)) return true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { ChatCompletionContentPart, ChatCompletionMessageParam } from '@cherrystudio/openai/resources'
|
||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
import { findLast } from 'lodash'
|
import { findLast } from 'lodash'
|
||||||
import { ChatCompletionContentPart, ChatCompletionMessageParam } from 'openai/resources'
|
|
||||||
|
|
||||||
export function processReqMessages(
|
export function processReqMessages(
|
||||||
model: Model,
|
model: Model,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { MessageStream } from '@anthropic-ai/sdk/resources/messages/messages'
|
import { MessageStream } from '@anthropic-ai/sdk/resources/messages/messages'
|
||||||
|
import { Stream } from '@cherrystudio/openai/streaming'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
|
import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
|
||||||
import { cleanContext, endContext, getContext, startContext } from '@mcp-trace/trace-web'
|
import { cleanContext, endContext, getContext, startContext } from '@mcp-trace/trace-web'
|
||||||
@@ -16,7 +17,6 @@ import { Model, Topic } from '@renderer/types'
|
|||||||
import type { Message } from '@renderer/types/newMessage'
|
import type { Message } from '@renderer/types/newMessage'
|
||||||
import { MessageBlockType } from '@renderer/types/newMessage'
|
import { MessageBlockType } from '@renderer/types/newMessage'
|
||||||
import { SdkRawChunk } from '@renderer/types/sdk'
|
import { SdkRawChunk } from '@renderer/types/sdk'
|
||||||
import { Stream } from 'openai/streaming'
|
|
||||||
|
|
||||||
const logger = loggerService.withContext('SpanManagerService')
|
const logger = loggerService.withContext('SpanManagerService')
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
WebSearchResultBlock,
|
WebSearchResultBlock,
|
||||||
WebSearchToolResultError
|
WebSearchToolResultError
|
||||||
} from '@anthropic-ai/sdk/resources/messages'
|
} from '@anthropic-ai/sdk/resources/messages'
|
||||||
|
import OpenAI from '@cherrystudio/openai'
|
||||||
|
import { ChatCompletionChunk } from '@cherrystudio/openai/resources'
|
||||||
import { FinishReason, MediaModality } from '@google/genai'
|
import { FinishReason, MediaModality } from '@google/genai'
|
||||||
import { FunctionCall } from '@google/genai'
|
import { FunctionCall } from '@google/genai'
|
||||||
import AiProvider from '@renderer/aiCore'
|
import AiProvider from '@renderer/aiCore'
|
||||||
@@ -38,8 +40,6 @@ import {
|
|||||||
import { mcpToolCallResponseToGeminiMessage } from '@renderer/utils/mcp-tools'
|
import { mcpToolCallResponseToGeminiMessage } from '@renderer/utils/mcp-tools'
|
||||||
import * as McpToolsModule from '@renderer/utils/mcp-tools'
|
import * as McpToolsModule from '@renderer/utils/mcp-tools'
|
||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep } from 'lodash'
|
||||||
import OpenAI from 'openai'
|
|
||||||
import { ChatCompletionChunk } from 'openai/resources'
|
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
// Mock the ApiClientFactory
|
// Mock the ApiClientFactory
|
||||||
vi.mock('@renderer/aiCore/legacy/clients/ApiClientFactory', () => ({
|
vi.mock('@renderer/aiCore/legacy/clients/ApiClientFactory', () => ({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { ChatCompletionMessageParam } from '@cherrystudio/openai/resources'
|
||||||
import type { Model } from '@renderer/types'
|
import type { Model } from '@renderer/types'
|
||||||
import { ChatCompletionMessageParam } from 'openai/resources'
|
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
import { processReqMessages } from '../ModelMessageService'
|
import { processReqMessages } from '../ModelMessageService'
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import settings from './settings'
|
|||||||
import shortcuts from './shortcuts'
|
import shortcuts from './shortcuts'
|
||||||
import tabs from './tabs'
|
import tabs from './tabs'
|
||||||
import translate from './translate'
|
import translate from './translate'
|
||||||
|
import video from './video'
|
||||||
import websearch from './websearch'
|
import websearch from './websearch'
|
||||||
|
|
||||||
const logger = loggerService.withContext('Store')
|
const logger = loggerService.withContext('Store')
|
||||||
@@ -58,14 +59,15 @@ const rootReducer = combineReducers({
|
|||||||
inputTools: inputToolsReducer,
|
inputTools: inputToolsReducer,
|
||||||
translate,
|
translate,
|
||||||
ocr,
|
ocr,
|
||||||
note
|
note,
|
||||||
|
video
|
||||||
})
|
})
|
||||||
|
|
||||||
const persistedReducer = persistReducer(
|
const persistedReducer = persistReducer(
|
||||||
{
|
{
|
||||||
key: 'cherry-studio',
|
key: 'cherry-studio',
|
||||||
storage,
|
storage,
|
||||||
version: 162,
|
version: 163,
|
||||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
||||||
migrate
|
migrate
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { WebSearchResultBlock } from '@anthropic-ai/sdk/resources'
|
import { WebSearchResultBlock } from '@anthropic-ai/sdk/resources'
|
||||||
|
import type OpenAI from '@cherrystudio/openai'
|
||||||
import type { GroundingMetadata } from '@google/genai'
|
import type { GroundingMetadata } from '@google/genai'
|
||||||
import { createEntityAdapter, createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit'
|
import { createEntityAdapter, createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit'
|
||||||
import { AISDKWebSearchResult, Citation, WebSearchProviderResponse, WebSearchSource } from '@renderer/types'
|
import { AISDKWebSearchResult, Citation, WebSearchProviderResponse, WebSearchSource } from '@renderer/types'
|
||||||
import type { CitationMessageBlock, MessageBlock } from '@renderer/types/newMessage'
|
import type { CitationMessageBlock, MessageBlock } from '@renderer/types/newMessage'
|
||||||
import { MessageBlockType } from '@renderer/types/newMessage'
|
import { MessageBlockType } from '@renderer/types/newMessage'
|
||||||
import type OpenAI from 'openai'
|
|
||||||
|
|
||||||
import type { RootState } from './index' // 确认 RootState 从 store/index.ts 导出
|
import type { RootState } from './index' // 确认 RootState 从 store/index.ts 导出
|
||||||
|
|
||||||
|
|||||||
@@ -2667,6 +2667,20 @@ const migrateConfig = {
|
|||||||
logger.error('migrate 162 error', error as Error)
|
logger.error('migrate 162 error', error as Error)
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'163': (state: RootState) => {
|
||||||
|
try {
|
||||||
|
if (state.settings && state.settings.sidebarIcons) {
|
||||||
|
if (!state.settings.sidebarIcons.visible.includes('video')) {
|
||||||
|
state.settings.sidebarIcons.visible = [...state.settings.sidebarIcons.visible, 'video']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.video.videoMap = {}
|
||||||
|
return state
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('migrate 161 error', error as Error)
|
||||||
|
return state
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
78
src/renderer/src/store/video.ts
Normal file
78
src/renderer/src/store/video.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { loggerService } from '@logger'
|
||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
|
import { Video } from '@renderer/types/video'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('Store:video')
|
||||||
|
|
||||||
|
export interface VideoState {
|
||||||
|
/** Provider ID to videos */
|
||||||
|
videoMap: Record<string, Video[] | undefined>
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: VideoState = {
|
||||||
|
videoMap: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoSlice = createSlice({
|
||||||
|
name: 'video',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
addVideo: (state: VideoState, action: PayloadAction<{ providerId: string; video: Video }>) => {
|
||||||
|
const { providerId, video } = action.payload
|
||||||
|
if (state.videoMap[providerId]) {
|
||||||
|
state.videoMap[providerId].unshift(video)
|
||||||
|
} else {
|
||||||
|
state.videoMap[providerId] = [video]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeVideo: (state: VideoState, action: PayloadAction<{ providerId: string; videoId: string }>) => {
|
||||||
|
const { providerId, videoId } = action.payload
|
||||||
|
const videos = state.videoMap[providerId]
|
||||||
|
state.videoMap[providerId] = videos?.filter((c) => c.id !== videoId)
|
||||||
|
},
|
||||||
|
updateVideo: (
|
||||||
|
state: VideoState,
|
||||||
|
action: PayloadAction<{ providerId: string; update: Partial<Omit<Video, 'status'>> & { id: string } }>
|
||||||
|
) => {
|
||||||
|
const { providerId, update } = action.payload
|
||||||
|
const videos = state.videoMap[providerId]
|
||||||
|
if (videos) {
|
||||||
|
let video = videos.find((v) => v.id === update.id)
|
||||||
|
if (video) {
|
||||||
|
video = { ...video, ...update }
|
||||||
|
} else {
|
||||||
|
logger.error(`Video with id ${update.id} not found in ${providerId}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.error(`Videos with Provider ${providerId} is undefined.`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setVideo: (state: VideoState, action: PayloadAction<{ providerId: string; video: Video }>) => {
|
||||||
|
const { providerId, video } = action.payload
|
||||||
|
if (state.videoMap[providerId]) {
|
||||||
|
const index = state.videoMap[providerId].findIndex((v) => v.id === video.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
state.videoMap[providerId][index] = video
|
||||||
|
} else {
|
||||||
|
state.videoMap[providerId].push(video)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.videoMap[providerId] = [video]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setVideos: (state: VideoState, action: PayloadAction<{ providerId: string; videos: Video[] }>) => {
|
||||||
|
const { providerId, videos } = action.payload
|
||||||
|
state.videoMap[providerId] = videos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const {
|
||||||
|
addVideo: addVideoAction,
|
||||||
|
removeVideo: removeVideoAction,
|
||||||
|
updateVideo: updateVideoAction,
|
||||||
|
setVideo: setVideoAction,
|
||||||
|
setVideos: setVideosAction
|
||||||
|
} = videoSlice.actions
|
||||||
|
|
||||||
|
export default videoSlice.reducer
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { OpenAI } from '@cherrystudio/openai'
|
||||||
|
import { Stream } from '@cherrystudio/openai/streaming'
|
||||||
import { TokenUsage } from '@mcp-trace/trace-core'
|
import { TokenUsage } from '@mcp-trace/trace-core'
|
||||||
import { Span } from '@opentelemetry/api'
|
import { Span } from '@opentelemetry/api'
|
||||||
import { endSpan } from '@renderer/services/SpanManagerService'
|
import { endSpan } from '@renderer/services/SpanManagerService'
|
||||||
import { OpenAI } from 'openai'
|
|
||||||
import { Stream } from 'openai/streaming'
|
|
||||||
|
|
||||||
export class StreamHandler {
|
export class StreamHandler {
|
||||||
private topicId: string
|
private topicId: string
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import type OpenAI from '@cherrystudio/openai'
|
||||||
import type { File } from '@google/genai'
|
import type { File } from '@google/genai'
|
||||||
import type { FileSchema } from '@mistralai/mistralai/models/components'
|
import type { FileSchema } from '@mistralai/mistralai/models/components'
|
||||||
import type OpenAI from 'openai'
|
|
||||||
|
|
||||||
export type RemoteFile =
|
export type RemoteFile =
|
||||||
| {
|
| {
|
||||||
@@ -127,6 +127,10 @@ export type ImageFileMetadata = FileMetadata & {
|
|||||||
type: FileTypes.IMAGE
|
type: FileTypes.IMAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type VideoFileMetadata = FileMetadata & {
|
||||||
|
type: FileTypes.VIDEO
|
||||||
|
}
|
||||||
|
|
||||||
export type PdfFileMetadata = FileMetadata & {
|
export type PdfFileMetadata = FileMetadata & {
|
||||||
ext: '.pdf'
|
ext: '.pdf'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { LanguageModelV2Source } from '@ai-sdk/provider'
|
import type { LanguageModelV2Source } from '@ai-sdk/provider'
|
||||||
import type { WebSearchResultBlock } from '@anthropic-ai/sdk/resources'
|
import type { WebSearchResultBlock } from '@anthropic-ai/sdk/resources'
|
||||||
|
import type OpenAI from '@cherrystudio/openai'
|
||||||
import type { GenerateImagesConfig, GroundingMetadata, PersonGeneration } from '@google/genai'
|
import type { GenerateImagesConfig, GroundingMetadata, PersonGeneration } from '@google/genai'
|
||||||
import type OpenAI from 'openai'
|
|
||||||
import type { CSSProperties } from 'react'
|
import type { CSSProperties } from 'react'
|
||||||
|
|
||||||
export * from './file'
|
export * from './file'
|
||||||
@@ -22,6 +22,7 @@ export * from './mcp'
|
|||||||
export * from './notification'
|
export * from './notification'
|
||||||
export * from './ocr'
|
export * from './ocr'
|
||||||
export * from './provider'
|
export * from './provider'
|
||||||
|
export * from './video'
|
||||||
|
|
||||||
export type Assistant = {
|
export type Assistant = {
|
||||||
id: string
|
id: string
|
||||||
@@ -535,6 +536,7 @@ export type SidebarIcon =
|
|||||||
| 'files'
|
| 'files'
|
||||||
| 'code_tools'
|
| 'code_tools'
|
||||||
| 'notes'
|
| 'notes'
|
||||||
|
| 'video'
|
||||||
|
|
||||||
export type ExternalToolResult = {
|
export type ExternalToolResult = {
|
||||||
mcpTools?: MCPTool[]
|
mcpTools?: MCPTool[]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import type { CompletionUsage } from '@cherrystudio/openai/resources'
|
||||||
import type { ProviderMetadata } from 'ai'
|
import type { ProviderMetadata } from 'ai'
|
||||||
import type { CompletionUsage } from 'openai/resources'
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Assistant,
|
Assistant,
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import { MessageStream } from '@anthropic-ai/sdk/resources/messages/messages'
|
|||||||
import AnthropicVertex from '@anthropic-ai/vertex-sdk'
|
import AnthropicVertex from '@anthropic-ai/vertex-sdk'
|
||||||
import type { BedrockClient } from '@aws-sdk/client-bedrock'
|
import type { BedrockClient } from '@aws-sdk/client-bedrock'
|
||||||
import type { BedrockRuntimeClient } from '@aws-sdk/client-bedrock-runtime'
|
import type { BedrockRuntimeClient } from '@aws-sdk/client-bedrock-runtime'
|
||||||
|
import OpenAI, { AzureOpenAI } from '@cherrystudio/openai'
|
||||||
|
import { ChatCompletionContentPartImage } from '@cherrystudio/openai/resources'
|
||||||
|
import { Stream } from '@cherrystudio/openai/streaming'
|
||||||
import {
|
import {
|
||||||
Content,
|
Content,
|
||||||
CreateChatParameters,
|
CreateChatParameters,
|
||||||
@@ -21,9 +24,6 @@ import {
|
|||||||
SendMessageParameters,
|
SendMessageParameters,
|
||||||
Tool
|
Tool
|
||||||
} from '@google/genai'
|
} from '@google/genai'
|
||||||
import OpenAI, { AzureOpenAI } from 'openai'
|
|
||||||
import { ChatCompletionContentPartImage } from 'openai/resources'
|
|
||||||
import { Stream } from 'openai/streaming'
|
|
||||||
|
|
||||||
import { EndpointType } from './index'
|
import { EndpointType } from './index'
|
||||||
|
|
||||||
|
|||||||
162
src/renderer/src/types/video.ts
Normal file
162
src/renderer/src/types/video.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import OpenAI from '@cherrystudio/openai'
|
||||||
|
|
||||||
|
import { Provider } from './provider'
|
||||||
|
|
||||||
|
// Only OpenAI (Responses) is supported for now.
|
||||||
|
export type VideoEndpointType = 'openai'
|
||||||
|
export type VideoStatus = 'queued' | 'in_progress' | 'completed' | 'downloading' | 'downloaded' | 'failed'
|
||||||
|
|
||||||
|
interface VideoBase {
|
||||||
|
readonly id: string
|
||||||
|
readonly type: VideoEndpointType
|
||||||
|
name: string
|
||||||
|
readonly providerId: string
|
||||||
|
prompt: string
|
||||||
|
/**
|
||||||
|
* Represents the possible states of a video generation or download process.
|
||||||
|
*
|
||||||
|
* - `queued`: The video task has been submitted and is waiting to be processed.
|
||||||
|
* - `in_progress`: The video is currently being generated.
|
||||||
|
* - `completed`: The video has been successfully generated and is ready for download.
|
||||||
|
* - `downloading`: The video content is being downloaded to local storage.
|
||||||
|
* - `downloaded`: The video has been fully downloaded and is available locally.
|
||||||
|
* - `failed`: The video task encountered an error and could not be completed.
|
||||||
|
*/
|
||||||
|
readonly status: VideoStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpenAIVideoBase {
|
||||||
|
readonly type: 'openai'
|
||||||
|
metadata: OpenAI.Videos.Video
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VideoQueued extends VideoBase {
|
||||||
|
readonly status: 'queued'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VideoInProgress extends VideoBase {
|
||||||
|
readonly status: 'in_progress'
|
||||||
|
/** integer percent */
|
||||||
|
progress: number
|
||||||
|
}
|
||||||
|
export interface VideoCompleted extends VideoBase {
|
||||||
|
readonly status: 'completed'
|
||||||
|
/** When generation completed, firstly try to retrieve thumbnail. */
|
||||||
|
thumbnail: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VideoDownloading extends VideoBase {
|
||||||
|
readonly status: 'downloading'
|
||||||
|
thumbnail: string | null
|
||||||
|
/** integer percent */
|
||||||
|
progress: number
|
||||||
|
}
|
||||||
|
export interface VideoDownloaded extends VideoBase {
|
||||||
|
readonly status: 'downloaded'
|
||||||
|
thumbnail: string | null
|
||||||
|
/** Managed by fileManager */
|
||||||
|
fileId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VideoFailedBase extends VideoBase {
|
||||||
|
readonly status: 'failed'
|
||||||
|
error: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OpenAIVideoQueued extends VideoQueued, OpenAIVideoBase {}
|
||||||
|
export interface OpenAIVideoInProgress extends VideoInProgress, OpenAIVideoBase {}
|
||||||
|
export interface OpenAIVideoCompleted extends VideoCompleted, OpenAIVideoBase {}
|
||||||
|
export interface OpenAIVideoDownloading extends VideoDownloading, OpenAIVideoBase {}
|
||||||
|
export interface OpenAIVideoDownloaded extends VideoDownloaded, OpenAIVideoBase {}
|
||||||
|
export interface OpenAIVideoFailed extends VideoFailedBase, OpenAIVideoBase {
|
||||||
|
error: OpenAI.Videos.Video['error']
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VideoFailed = OpenAIVideoFailed
|
||||||
|
|
||||||
|
export type OpenAIVideo =
|
||||||
|
| OpenAIVideoQueued
|
||||||
|
| OpenAIVideoInProgress
|
||||||
|
| OpenAIVideoCompleted
|
||||||
|
| OpenAIVideoDownloading
|
||||||
|
| OpenAIVideoDownloaded
|
||||||
|
| OpenAIVideoFailed
|
||||||
|
|
||||||
|
export type Video = OpenAIVideo
|
||||||
|
|
||||||
|
// Create Video
|
||||||
|
interface CreateVideoBaseParams {
|
||||||
|
type: VideoEndpointType
|
||||||
|
provider: Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OpenAICreateVideoParams extends CreateVideoBaseParams {
|
||||||
|
type: 'openai'
|
||||||
|
params: OpenAI.VideoCreateParams
|
||||||
|
options?: OpenAI.RequestOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateVideoParams = OpenAICreateVideoParams
|
||||||
|
|
||||||
|
interface CreateVideoBaseResult {
|
||||||
|
type: VideoEndpointType
|
||||||
|
video: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OpenAICreateVideoResult extends CreateVideoBaseResult {
|
||||||
|
type: 'openai'
|
||||||
|
video: OpenAI.Videos.Video
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateVideoResult = OpenAICreateVideoResult
|
||||||
|
|
||||||
|
// Retrieve Video
|
||||||
|
interface RetrieveVideoBaseParams {
|
||||||
|
type: VideoEndpointType
|
||||||
|
provider: Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OpenAIRetrieveVideoParams extends RetrieveVideoBaseParams {
|
||||||
|
type: 'openai'
|
||||||
|
videoId: string
|
||||||
|
options?: OpenAI.RequestOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RetrieveVideoParams = OpenAIRetrieveVideoParams
|
||||||
|
|
||||||
|
interface RetrieveVideoBaseResult {
|
||||||
|
type: VideoEndpointType
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OpenAIRetrieveVideoResult extends RetrieveVideoBaseResult {
|
||||||
|
type: 'openai'
|
||||||
|
video: OpenAI.Videos.Video
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RetrieveVideoResult = OpenAIRetrieveVideoResult
|
||||||
|
|
||||||
|
// Retrieve Video Content
|
||||||
|
interface RetrieveVideoContentBaseParams {
|
||||||
|
type: VideoEndpointType
|
||||||
|
provider: Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OpenAIRetrieveVideoContentParams extends RetrieveVideoContentBaseParams {
|
||||||
|
type: 'openai'
|
||||||
|
videoId: string
|
||||||
|
query?: OpenAI.Videos.VideoDownloadContentParams
|
||||||
|
options?: OpenAI.RequestOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RetrieveVideoContentParams = OpenAIRetrieveVideoContentParams
|
||||||
|
|
||||||
|
interface RetrieveVideoContentBaseResult {
|
||||||
|
type: VideoEndpointType
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OpenAIRetrieveVideoContentResult extends RetrieveVideoContentBaseResult {
|
||||||
|
type: 'openai'
|
||||||
|
response: Response
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RetrieveVideoContentResult = OpenAIRetrieveVideoContentResult
|
||||||
35
src/renderer/src/utils/deepUpdate.ts
Normal file
35
src/renderer/src/utils/deepUpdate.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { DeepPartial } from 'ai'
|
||||||
|
import { cloneDeep } from 'lodash'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deeply updates an object, allowing undefined to overwrite existing properties, without using `any`
|
||||||
|
* @param target Original object
|
||||||
|
* @param update Update object (may contain undefined)
|
||||||
|
* @returns New object
|
||||||
|
*/
|
||||||
|
export function deepUpdate<T extends object>(target: T, update: DeepPartial<T>): T {
|
||||||
|
const result = cloneDeep(target)
|
||||||
|
for (const key in update) {
|
||||||
|
if (Object.hasOwn(update, key)) {
|
||||||
|
// @ts-ignore it's runtime safe
|
||||||
|
const prev = result[key]
|
||||||
|
const next = update[key]
|
||||||
|
|
||||||
|
if (
|
||||||
|
next &&
|
||||||
|
typeof next === 'object' &&
|
||||||
|
!Array.isArray(next) &&
|
||||||
|
prev &&
|
||||||
|
typeof prev === 'object' &&
|
||||||
|
!Array.isArray(prev)
|
||||||
|
) {
|
||||||
|
// @ts-ignore it's runtime safe
|
||||||
|
result[key] = deepUpdate(prev, next as any)
|
||||||
|
} else {
|
||||||
|
// @ts-ignore it's runtime safe
|
||||||
|
result[key] = next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -1,4 +1,11 @@
|
|||||||
import { ContentBlockParam, MessageParam, ToolUnion, ToolUseBlock } from '@anthropic-ai/sdk/resources'
|
import { ContentBlockParam, MessageParam, ToolUnion, ToolUseBlock } from '@anthropic-ai/sdk/resources'
|
||||||
|
import OpenAI from '@cherrystudio/openai'
|
||||||
|
import {
|
||||||
|
ChatCompletionContentPart,
|
||||||
|
ChatCompletionMessageParam,
|
||||||
|
ChatCompletionMessageToolCall,
|
||||||
|
ChatCompletionTool
|
||||||
|
} from '@cherrystudio/openai/resources'
|
||||||
import { Content, FunctionCall, Part, Tool, Type as GeminiSchemaType } from '@google/genai'
|
import { Content, FunctionCall, Part, Tool, Type as GeminiSchemaType } from '@google/genai'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { isFunctionCallingModel, isVisionModel } from '@renderer/config/models'
|
import { isFunctionCallingModel, isVisionModel } from '@renderer/config/models'
|
||||||
@@ -21,13 +28,6 @@ import { ChunkType } from '@renderer/types/chunk'
|
|||||||
import { AwsBedrockSdkMessageParam, AwsBedrockSdkTool, AwsBedrockSdkToolCall } from '@renderer/types/sdk'
|
import { AwsBedrockSdkMessageParam, AwsBedrockSdkTool, AwsBedrockSdkToolCall } from '@renderer/types/sdk'
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import OpenAI from 'openai'
|
|
||||||
import {
|
|
||||||
ChatCompletionContentPart,
|
|
||||||
ChatCompletionMessageParam,
|
|
||||||
ChatCompletionMessageToolCall,
|
|
||||||
ChatCompletionTool
|
|
||||||
} from 'openai/resources'
|
|
||||||
|
|
||||||
import { isToolUseModeFunction } from './assistant'
|
import { isToolUseModeFunction } from './assistant'
|
||||||
import { convertBase64ImageToAwsBedrockFormat } from './aws-bedrock-utils'
|
import { convertBase64ImageToAwsBedrockFormat } from './aws-bedrock-utils'
|
||||||
|
|||||||
7
src/renderer/src/utils/model/video.ts
Normal file
7
src/renderer/src/utils/model/video.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { VideoModel } from '@cherrystudio/openai/resources'
|
||||||
|
import { videoModelsMap } from '@renderer/config/models/video'
|
||||||
|
|
||||||
|
// Only for openai, use hard-encoded values
|
||||||
|
export const isVideoModel = (modelId: string): modelId is VideoModel => {
|
||||||
|
return videoModelsMap.openai.some((v) => v === modelId)
|
||||||
|
}
|
||||||
36
yarn.lock
36
yarn.lock
@@ -2665,6 +2665,23 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
"@cherrystudio/openai@npm:6.3.0-fork.1, openai@npm:@cherrystudio/openai@6.3.0-fork.1":
|
||||||
|
version: 6.3.0-fork.1
|
||||||
|
resolution: "@cherrystudio/openai@npm:6.3.0-fork.1"
|
||||||
|
peerDependencies:
|
||||||
|
ws: ^8.18.0
|
||||||
|
zod: ^3.25 || ^4.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
ws:
|
||||||
|
optional: true
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
bin:
|
||||||
|
openai: bin/cli
|
||||||
|
checksum: 10c0/dc8c5555aa6d12cd47586efc70175ec1bf7112c1f737b28d86d26e9666535d9e87d1c4811a069d11c324bab56f4dae242d7266efa88359d0926c7059240d24cc
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@chevrotain/cst-dts-gen@npm:11.0.3":
|
"@chevrotain/cst-dts-gen@npm:11.0.3":
|
||||||
version: 11.0.3
|
version: 11.0.3
|
||||||
resolution: "@chevrotain/cst-dts-gen@npm:11.0.3"
|
resolution: "@chevrotain/cst-dts-gen@npm:11.0.3"
|
||||||
@@ -14286,6 +14303,7 @@ __metadata:
|
|||||||
"@cherrystudio/embedjs-ollama": "npm:^0.1.31"
|
"@cherrystudio/embedjs-ollama": "npm:^0.1.31"
|
||||||
"@cherrystudio/embedjs-openai": "npm:^0.1.31"
|
"@cherrystudio/embedjs-openai": "npm:^0.1.31"
|
||||||
"@cherrystudio/extension-table-plus": "workspace:^"
|
"@cherrystudio/extension-table-plus": "workspace:^"
|
||||||
|
"@cherrystudio/openai": "npm:6.3.0-fork.1"
|
||||||
"@dnd-kit/core": "npm:^6.3.1"
|
"@dnd-kit/core": "npm:^6.3.1"
|
||||||
"@dnd-kit/modifiers": "npm:^9.0.0"
|
"@dnd-kit/modifiers": "npm:^9.0.0"
|
||||||
"@dnd-kit/sortable": "npm:^10.0.0"
|
"@dnd-kit/sortable": "npm:^10.0.0"
|
||||||
@@ -14466,7 +14484,6 @@ __metadata:
|
|||||||
notion-helper: "npm:^1.3.22"
|
notion-helper: "npm:^1.3.22"
|
||||||
npx-scope-finder: "npm:^1.2.0"
|
npx-scope-finder: "npm:^1.2.0"
|
||||||
officeparser: "npm:^4.2.0"
|
officeparser: "npm:^4.2.0"
|
||||||
openai: "patch:openai@npm%3A5.12.2#~/.yarn/patches/openai-npm-5.12.2-30b075401c.patch"
|
|
||||||
os-proxy-config: "npm:^1.1.2"
|
os-proxy-config: "npm:^1.1.2"
|
||||||
oxlint: "npm:^1.22.0"
|
oxlint: "npm:^1.22.0"
|
||||||
oxlint-tsgolint: "npm:^0.2.0"
|
oxlint-tsgolint: "npm:^0.2.0"
|
||||||
@@ -24491,23 +24508,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"openai@patch:openai@npm%3A5.12.2#~/.yarn/patches/openai-npm-5.12.2-30b075401c.patch":
|
|
||||||
version: 5.12.2
|
|
||||||
resolution: "openai@patch:openai@npm%3A5.12.2#~/.yarn/patches/openai-npm-5.12.2-30b075401c.patch::version=5.12.2&hash=ad5d10"
|
|
||||||
peerDependencies:
|
|
||||||
ws: ^8.18.0
|
|
||||||
zod: ^3.23.8
|
|
||||||
peerDependenciesMeta:
|
|
||||||
ws:
|
|
||||||
optional: true
|
|
||||||
zod:
|
|
||||||
optional: true
|
|
||||||
bin:
|
|
||||||
openai: bin/cli
|
|
||||||
checksum: 10c0/2964a1c88a98cf169c9b73e8cd6776c03c8f3103fee30961c6953e5d995ad57a697e2179615999356809349186df6496abae105928ff7ce0229e5016dec87cb3
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"openapi-types@npm:^12.1.3":
|
"openapi-types@npm:^12.1.3":
|
||||||
version: 12.1.3
|
version: 12.1.3
|
||||||
resolution: "openapi-types@npm:12.1.3"
|
resolution: "openapi-types@npm:12.1.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user