diff --git a/.yarn/patches/openai-npm-5.12.2-30b075401c.patch b/.yarn/patches/openai-npm-5.12.2-30b075401c.patch deleted file mode 100644 index 29b92dcc7b..0000000000 Binary files a/.yarn/patches/openai-npm-5.12.2-30b075401c.patch and /dev/null differ diff --git a/package.json b/package.json index dc8d1ccf31..51f0466041 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CherryStudio", - "version": "2.0.0-alpha", + "version": "2.0.0-alpha.sora.1", "private": true, "description": "A powerful AI assistant for producer.", "main": "./out/main/index.js", @@ -127,6 +127,7 @@ "@cherrystudio/embedjs-openai": "^0.1.31", "@cherrystudio/extension-table-plus": "workspace:^", "@cherrystudio/ui": "workspace:*", + "@cherrystudio/openai": "6.3.0-fork.1", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", @@ -297,7 +298,6 @@ "motion": "^12.10.5", "notion-helper": "^1.3.22", "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-tsgolint": "^0.2.0", "p-queue": "^8.1.0", @@ -378,8 +378,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", "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", - "openai@npm:^4.77.0": "patch:openai@npm%3A5.12.2#~/.yarn/patches/openai-npm-5.12.2-30b075401c.patch", - "openai@npm:^4.87.3": "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": "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", "pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch", "tar-fs": "^2.1.4", diff --git a/packages/ui/src/components/base/Toast/index.ts b/packages/ui/src/components/base/Toast/index.ts index 13b70088f6..e969381e32 100644 --- a/packages/ui/src/components/base/Toast/index.ts +++ b/packages/ui/src/components/base/Toast/index.ts @@ -1,5 +1,6 @@ import { addToast, closeAll, closeToast, getToastQueue, isToastClosing } from '@heroui/toast' import type { RequireSome } from '@types' +import { t } from 'i18next' type AddToastProps = Parameters[0] type ToastPropsColored = Omit @@ -54,7 +55,7 @@ const loading = (args: RequireSome) => { if (args.timeout === undefined) { args.timeout = 1 } - return addToast(args) + return addToast({ title: t('common.loading'), ...args }) } export type ToastUtilities = { diff --git a/scripts/auto-translate-i18n.ts b/scripts/auto-translate-i18n.ts index 6a90f5b23f..681e410795 100644 --- a/scripts/auto-translate-i18n.ts +++ b/scripts/auto-translate-i18n.ts @@ -2,9 +2,9 @@ * 该脚本用于少量自动翻译所有baseLocale以外的文本。待翻译文案必须以[to be translated]开头 * */ +import OpenAI from '@cherrystudio/openai' import cliProgress from 'cli-progress' import * as fs from 'fs' -import OpenAI from 'openai' import * as path from 'path' const localesDir = path.join(__dirname, '../src/renderer/src/i18n/locales') diff --git a/scripts/update-i18n.ts b/scripts/update-i18n.ts index 72fcca8ab9..103623bf1f 100644 --- a/scripts/update-i18n.ts +++ b/scripts/update-i18n.ts @@ -4,9 +4,9 @@ * 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 fs from 'fs' -import OpenAI from 'openai' type I18NValue = string | { [key: string]: I18NValue } type I18N = { [key: string]: I18NValue } diff --git a/src/main/apiServer/routes/chat.ts b/src/main/apiServer/routes/chat.ts index 0338fd26b7..3dd58b9654 100644 --- a/src/main/apiServer/routes/chat.ts +++ b/src/main/apiServer/routes/chat.ts @@ -1,6 +1,6 @@ +import type { ChatCompletionCreateParams } from '@cherrystudio/openai/resources' import type { Request, Response } from 'express' import express from 'express' -import type { ChatCompletionCreateParams } from 'openai/resources' import { loggerService } from '../../services/LoggerService' import { diff --git a/src/main/apiServer/services/chat-completion.ts b/src/main/apiServer/services/chat-completion.ts index 97e09d8bad..49627336b0 100644 --- a/src/main/apiServer/services/chat-completion.ts +++ b/src/main/apiServer/services/chat-completion.ts @@ -1,7 +1,7 @@ +import OpenAI from '@cherrystudio/openai' +import type { ChatCompletionCreateParams, ChatCompletionCreateParamsStreaming } from '@cherrystudio/openai/resources' import { loggerService } from '@logger' import type { Provider } from '@types' -import OpenAI from 'openai' -import type { ChatCompletionCreateParams, ChatCompletionCreateParamsStreaming } from 'openai/resources' import { type ModelValidationError, validateModelId } from '../utils' diff --git a/src/main/services/remotefile/OpenAIService.ts b/src/main/services/remotefile/OpenAIService.ts index 5211d99999..f92d6abebf 100644 --- a/src/main/services/remotefile/OpenAIService.ts +++ b/src/main/services/remotefile/OpenAIService.ts @@ -1,9 +1,9 @@ +import OpenAI from '@cherrystudio/openai' import { cacheService } from '@data/CacheService' import { loggerService } from '@logger' import { fileStorage } from '@main/services/FileStorage' import type { FileListResponse, FileMetadata, FileUploadResponse, Provider } from '@types' import * as fs from 'fs' -import OpenAI from 'openai' import { BaseFileService } from './BaseFileService' diff --git a/src/renderer/src/Router.tsx b/src/renderer/src/Router.tsx index fb555d8bc3..9e32bc53b4 100644 --- a/src/renderer/src/Router.tsx +++ b/src/renderer/src/Router.tsx @@ -21,6 +21,7 @@ import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage' import SettingsPage from './pages/settings/SettingsPage' import AssistantPresetsPage from './pages/store/assistants/presets/AssistantPresetsPage' import TranslatePage from './pages/translate/TranslatePage' +import { VideoPage } from './pages/video/VideoPage' const Router: FC = () => { const { navbarPosition } = useNavbarPosition() @@ -41,6 +42,7 @@ const Router: FC = () => { } /> } /> } /> + } /> ) diff --git a/src/renderer/src/aiCore/index_new.ts b/src/renderer/src/aiCore/index_new.ts index a210b956d0..207a2f7913 100644 --- a/src/renderer/src/aiCore/index_new.ts +++ b/src/renderer/src/aiCore/index_new.ts @@ -12,8 +12,23 @@ import { preferenceService } from '@data/PreferenceService' import { loggerService } from '@logger' import { addSpan, endSpan } from '@renderer/services/SpanManagerService' import type { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity' -import type { Assistant, GenerateImageParams, Model, Provider } from '@renderer/types' +import type { + Assistant, + DeleteVideoParams, + DeleteVideoResult, + GenerateImageParams, + Model, + Provider, + RetrieveVideoContentParams +} from '@renderer/types' import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes' +import type { + CreateVideoParams, + CreateVideoResult, + RetrieveVideoContentResult, + RetrieveVideoParams, + RetrieveVideoResult +} from '@renderer/types/video' import { buildClaudeCodeSystemModelMessage } from '@shared/anthropic' import { type ImageModel, type LanguageModel, type Provider as AiSdkProvider, wrapLanguageModel } from 'ai' @@ -501,6 +516,34 @@ export default class ModernAiProvider { return images } + /** + * We manually implement this method before aisdk supports it well + */ + public async createVideo(params: CreateVideoParams): Promise { + return this.legacyProvider.createVideo(params) + } + + /** + * We manually implement this method before aisdk supports it well + */ + public async retrieveVideo(params: RetrieveVideoParams): Promise { + return this.legacyProvider.retrieveVideo(params) + } + + /** + * We manually implement this method before aisdk supports it well + */ + public async retrieveVideoContent(params: RetrieveVideoContentParams): Promise { + return this.legacyProvider.retrieveVideoContent(params) + } + + /** + * We manually implement this method before aisdk supports it well + */ + public async deleteVideo(params: DeleteVideoParams): Promise { + return this.legacyProvider.deleteVideo(params) + } + public getBaseURL(): string { return this.legacyProvider.getBaseURL() } diff --git a/src/renderer/src/aiCore/legacy/clients/cherryai/CherryAiAPIClient.ts b/src/renderer/src/aiCore/legacy/clients/cherryai/CherryAiAPIClient.ts index bb95bdaac7..b72e0a8829 100644 --- a/src/renderer/src/aiCore/legacy/clients/cherryai/CherryAiAPIClient.ts +++ b/src/renderer/src/aiCore/legacy/clients/cherryai/CherryAiAPIClient.ts @@ -1,6 +1,6 @@ +import type OpenAI from '@cherrystudio/openai' import type { Provider } from '@renderer/types' import type { OpenAISdkParams, OpenAISdkRawOutput } from '@renderer/types/sdk' -import type OpenAI from 'openai' import { OpenAIAPIClient } from '../openai/OpenAIApiClient' diff --git a/src/renderer/src/aiCore/legacy/clients/openai/OpenAIApiClient.ts b/src/renderer/src/aiCore/legacy/clients/openai/OpenAIApiClient.ts index 13c48c4a4f..3194266f64 100644 --- a/src/renderer/src/aiCore/legacy/clients/openai/OpenAIApiClient.ts +++ b/src/renderer/src/aiCore/legacy/clients/openai/OpenAIApiClient.ts @@ -1,3 +1,10 @@ +import type { AzureOpenAI } from '@cherrystudio/openai' +import type OpenAI from '@cherrystudio/openai' +import type { + ChatCompletionContentPart, + ChatCompletionContentPartRefusal, + ChatCompletionTool +} from '@cherrystudio/openai/resources' import { loggerService } from '@logger' import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant' import { @@ -81,9 +88,6 @@ import { } from '@renderer/utils/mcp-tools' import { findFileBlocks, findImageBlocks } from '@renderer/utils/messageUtils/find' import { t } from 'i18next' -import type { AzureOpenAI } from 'openai' -import type OpenAI from 'openai' -import type { ChatCompletionContentPart, ChatCompletionContentPartRefusal, ChatCompletionTool } from 'openai/resources' import type { GenericChunk } from '../../middleware/schemas' import type { RequestTransformer, ResponseChunkTransformer, ResponseChunkTransformerContext } from '../types' diff --git a/src/renderer/src/aiCore/legacy/clients/openai/OpenAIBaseClient.ts b/src/renderer/src/aiCore/legacy/clients/openai/OpenAIBaseClient.ts index 6a1de3215e..e519fe45bb 100644 --- a/src/renderer/src/aiCore/legacy/clients/openai/OpenAIBaseClient.ts +++ b/src/renderer/src/aiCore/legacy/clients/openai/OpenAIBaseClient.ts @@ -1,3 +1,4 @@ +import OpenAI, { AzureOpenAI } from '@cherrystudio/openai' import { loggerService } from '@logger' import { isClaudeReasoningModel, @@ -24,7 +25,6 @@ import type { ReasoningEffortOptionalParams } from '@renderer/types/sdk' import { formatApiHost } from '@renderer/utils/api' -import OpenAI, { AzureOpenAI } from 'openai' import { BaseApiClient } from '../BaseApiClient' diff --git a/src/renderer/src/aiCore/legacy/clients/openai/OpenAIResponseAPIClient.ts b/src/renderer/src/aiCore/legacy/clients/openai/OpenAIResponseAPIClient.ts index d7982487cc..9bb33a8311 100644 --- a/src/renderer/src/aiCore/legacy/clients/openai/OpenAIResponseAPIClient.ts +++ b/src/renderer/src/aiCore/legacy/clients/openai/OpenAIResponseAPIClient.ts @@ -1,3 +1,5 @@ +import OpenAI, { AzureOpenAI } from '@cherrystudio/openai' +import type { ResponseInput } from '@cherrystudio/openai/resources/responses/responses' import { loggerService } from '@logger' import type { GenericChunk } from '@renderer/aiCore/legacy/middleware/schemas' import type { CompletionsContext } from '@renderer/aiCore/legacy/middleware/types' @@ -33,6 +35,12 @@ import type { OpenAIResponseSdkTool, OpenAIResponseSdkToolCall } from '@renderer/types/sdk' +import type { + CreateVideoParams, + DeleteVideoParams, + RetrieveVideoContentParams, + RetrieveVideoParams +} from '@renderer/types/video' import { addImageFileToContents } from '@renderer/utils/formats' import { isSupportedToolUse, @@ -44,8 +52,6 @@ import { findFileBlocks, findImageBlocks } from '@renderer/utils/messageUtils/fi import { MB } from '@shared/config/constant' import { t } from 'i18next' import { isEmpty } from 'lodash' -import OpenAI, { AzureOpenAI } from 'openai' -import type { ResponseInput } from 'openai/resources/responses/responses' import type { RequestTransformer, ResponseChunkTransformer } from '../types' import { OpenAIAPIClient } from './OpenAIApiClient' @@ -151,6 +157,26 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient< return await sdk.responses.create(payload, options) } + public async createVideo(params: CreateVideoParams): Promise { + const sdk = await this.getSdkInstance() + return sdk.videos.create(params.params, params.options) + } + + public async retrieveVideo(params: RetrieveVideoParams): Promise { + const sdk = await this.getSdkInstance() + return sdk.videos.retrieve(params.videoId, params.options) + } + + public async retrieveVideoContent(params: RetrieveVideoContentParams): Promise { + const sdk = await this.getSdkInstance() + return sdk.videos.downloadContent(params.videoId, params.query, params.options) + } + + public async deleteVideo(params: DeleteVideoParams): Promise { + const sdk = await this.getSdkInstance() + return sdk.videos.delete(params.videoId, params.options) + } + private async handlePdfFile(file: FileMetadata): Promise { if (file.size > 32 * MB) return undefined try { @@ -342,7 +368,14 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient< } switch (message.type) { 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 case 'function_call': sum += estimateTextTokens(message.arguments) diff --git a/src/renderer/src/aiCore/legacy/clients/ovms/OVMSClient.ts b/src/renderer/src/aiCore/legacy/clients/ovms/OVMSClient.ts index 1e4e87521b..1a93baa2de 100644 --- a/src/renderer/src/aiCore/legacy/clients/ovms/OVMSClient.ts +++ b/src/renderer/src/aiCore/legacy/clients/ovms/OVMSClient.ts @@ -1,7 +1,7 @@ +import type OpenAI from '@cherrystudio/openai' import { loggerService } from '@logger' import { isSupportedModel } from '@renderer/config/models' import { objectKeys, type Provider } from '@renderer/types' -import type OpenAI from 'openai' import { OpenAIAPIClient } from '../openai/OpenAIApiClient' diff --git a/src/renderer/src/aiCore/legacy/clients/ppio/PPIOAPIClient.ts b/src/renderer/src/aiCore/legacy/clients/ppio/PPIOAPIClient.ts index f5524b8962..345496e156 100644 --- a/src/renderer/src/aiCore/legacy/clients/ppio/PPIOAPIClient.ts +++ b/src/renderer/src/aiCore/legacy/clients/ppio/PPIOAPIClient.ts @@ -1,7 +1,7 @@ +import type OpenAI from '@cherrystudio/openai' import { loggerService } from '@logger' import { isSupportedModel } from '@renderer/config/models' import type { Model, Provider } from '@renderer/types' -import type OpenAI from 'openai' import { OpenAIAPIClient } from '../openai/OpenAIApiClient' diff --git a/src/renderer/src/aiCore/legacy/clients/types.ts b/src/renderer/src/aiCore/legacy/clients/types.ts index a129a241f8..2964ede526 100644 --- a/src/renderer/src/aiCore/legacy/clients/types.ts +++ b/src/renderer/src/aiCore/legacy/clients/types.ts @@ -1,6 +1,6 @@ import type Anthropic from '@anthropic-ai/sdk' -import type { Assistant, MCPTool, MCPToolResponse, Model, ToolCallResponse } from '@renderer/types' -import type { Provider } from '@renderer/types' +import type OpenAI from '@cherrystudio/openai' +import type { Assistant, MCPTool, MCPToolResponse, Model, Provider, ToolCallResponse } from '@renderer/types' import type { AnthropicSdkRawChunk, OpenAIResponseSdkRawChunk, @@ -13,7 +13,6 @@ import type { SdkTool, SdkToolCall } from '@renderer/types/sdk' -import type OpenAI from 'openai' import type { CompletionsParams, GenericChunk } from '../middleware/schemas' import type { CompletionsContext } from '../middleware/types' diff --git a/src/renderer/src/aiCore/legacy/clients/zhipu/ZhipuAPIClient.ts b/src/renderer/src/aiCore/legacy/clients/zhipu/ZhipuAPIClient.ts index bc5857dfbb..ea6c141e31 100644 --- a/src/renderer/src/aiCore/legacy/clients/zhipu/ZhipuAPIClient.ts +++ b/src/renderer/src/aiCore/legacy/clients/zhipu/ZhipuAPIClient.ts @@ -1,7 +1,7 @@ +import type OpenAI from '@cherrystudio/openai' import { loggerService } from '@logger' import type { Provider } from '@renderer/types' import type { GenerateImageParams } from '@renderer/types' -import type OpenAI from 'openai' import { OpenAIAPIClient } from '../openai/OpenAIApiClient' diff --git a/src/renderer/src/aiCore/legacy/index.ts b/src/renderer/src/aiCore/legacy/index.ts index da6cdb6726..e64ac23bd4 100644 --- a/src/renderer/src/aiCore/legacy/index.ts +++ b/src/renderer/src/aiCore/legacy/index.ts @@ -5,8 +5,22 @@ import { isDedicatedImageGenerationModel, isFunctionCallingModel } from '@render import { getProviderByModel } from '@renderer/services/AssistantService' import { withSpanResult } from '@renderer/services/SpanManagerService' import type { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity' -import type { GenerateImageParams, Model, Provider } from '@renderer/types' +import type { + DeleteVideoParams, + DeleteVideoResult, + GenerateImageParams, + Model, + Provider, + RetrieveVideoContentParams +} from '@renderer/types' import type { RequestOptions, SdkModel } from '@renderer/types/sdk' +import type { + CreateVideoParams, + CreateVideoResult, + RetrieveVideoContentResult, + RetrieveVideoParams, + RetrieveVideoResult +} from '@renderer/types/video' import { isSupportedToolUse } from '@renderer/utils/mcp-tools' import { AihubmixAPIClient } from './clients/aihubmix/AihubmixAPIClient' @@ -179,6 +193,54 @@ export default class AiProvider { return this.apiClient.generateImage(params) } + public async createVideo(params: CreateVideoParams): Promise { + 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 { + 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 { + 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 async deleteVideo(params: DeleteVideoParams): Promise { + if (this.apiClient instanceof OpenAIResponseAPIClient && params.type === 'openai') { + const result = await this.apiClient.deleteVideo(params) + return { + type: 'openai', + result + } + } else { + throw new Error('Video deletion is not supported by this provider') + } + } + public getBaseURL(): string { return this.apiClient.getBaseURL() } diff --git a/src/renderer/src/aiCore/legacy/middleware/feat/ImageGenerationMiddleware.ts b/src/renderer/src/aiCore/legacy/middleware/feat/ImageGenerationMiddleware.ts index 1879da5a4b..95b128bd8f 100644 --- a/src/renderer/src/aiCore/legacy/middleware/feat/ImageGenerationMiddleware.ts +++ b/src/renderer/src/aiCore/legacy/middleware/feat/ImageGenerationMiddleware.ts @@ -1,10 +1,10 @@ +import type OpenAI from '@cherrystudio/openai' +import { toFile } from '@cherrystudio/openai/uploads' import { isDedicatedImageGenerationModel } from '@renderer/config/models' import FileManager from '@renderer/services/FileManager' import { ChunkType } from '@renderer/types/chunk' import { findImageBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find' import { defaultTimeout } from '@shared/config/constant' -import type OpenAI from 'openai' -import { toFile } from 'openai/uploads' import type { BaseApiClient } from '../../clients/BaseApiClient' import type { CompletionsParams, CompletionsResult, GenericChunk } from '../schemas' diff --git a/src/renderer/src/aiCore/prepareParams/fileProcessor.ts b/src/renderer/src/aiCore/prepareParams/fileProcessor.ts index 004c19a06c..6ac45bf98b 100644 --- a/src/renderer/src/aiCore/prepareParams/fileProcessor.ts +++ b/src/renderer/src/aiCore/prepareParams/fileProcessor.ts @@ -3,6 +3,7 @@ * 处理文件内容提取、文件格式转换、文件上传等逻辑 */ +import type OpenAI from '@cherrystudio/openai' import { loggerService } from '@logger' import { getProviderByModel } from '@renderer/services/AssistantService' import type { FileMetadata, Message, Model } from '@renderer/types' @@ -10,7 +11,6 @@ import { FileTypes } from '@renderer/types' import type { FileMessageBlock } from '@renderer/types/newMessage' import { findFileBlocks } from '@renderer/utils/messageUtils/find' import type { FilePart, TextPart } from 'ai' -import type OpenAI from 'openai' import { getAiSdkProviderId } from '../provider/factory' import { getFileSizeLimit, supportsImageInput, supportsLargeFileUpload, supportsPdfInput } from './modelCapabilities' diff --git a/src/renderer/src/components/Tab/TabContainer.tsx b/src/renderer/src/components/Tab/TabContainer.tsx index 08f16bb9cc..9166354967 100644 --- a/src/renderer/src/components/Tab/TabContainer.tsx +++ b/src/renderer/src/components/Tab/TabContainer.tsx @@ -33,6 +33,7 @@ import { Sparkle, Sun, Terminal, + Video, X } from 'lucide-react' import { useCallback, useEffect, useMemo } from 'react' @@ -107,6 +108,8 @@ const getTabIcon = ( return case 'code': return + case 'video': + return