diff --git a/.env.example b/.env.example index 4aace4a4..d85fb2a7 100644 --- a/.env.example +++ b/.env.example @@ -97,6 +97,11 @@ SEEDANCE_BASE_URL=https://ark.cn-beijing.volces.com/api/v3 SEEDANCE_API_KEY= SEEDANCE_VIDEO_MODEL=doubao-seedance-2-0-260128 +# OFox multi-model gateway (LLM preset + video generation share one key) +LLM_OFOX_BASE_URL=https://api.ofox.ai/v1 +LLM_OFOX_API_KEY= +OFOX_VIDEO_MODEL=bytedance/seedance-2.0-fast + KLING_BASE_URL=https://api-singapore.klingai.com KLING_API_KEY= KLING_VIDEO_MODEL=kling-v3-omni diff --git a/README.md b/README.md index b45c2b4e..c95709a8 100644 --- a/README.md +++ b/README.md @@ -402,7 +402,7 @@ Clients must send `Authorization: Bearer `. The current bridge is designe |---|---| | Frontend | React 19, TypeScript 6, Vite 8 | | Editing core | Immutable timeline state, command layer, and proposal-based application | -| Agent | Vercel AI SDK 7 (Anthropic, OpenAI, Gemini, Kimi, Qwen, GLM, DeepSeek, MiniMax, Xiaomi MiMo, Mistral, xAI Grok by API key or SuperGrok/X Premium+ subscription sign-in, and compatible APIs), Agent Skills, MCP SDK | +| Agent | Vercel AI SDK 7 (Anthropic, OpenAI, Gemini, Kimi, Qwen, GLM, DeepSeek, MiniMax, Xiaomi MiMo, Mistral, xAI Grok by API key or SuperGrok/X Premium+ subscription sign-in, OFox, and compatible APIs), Agent Skills, MCP SDK | | Preview and visuals | Remotion Player, WebGL / GLSL | | Server | Dual-host Vite / Electron plugins and a server-side keystore | | Persistence | Shared local project store under `~/.openchatcut`, IndexedDB cache, configurable local media directory, optional Cloudflare R2 | diff --git a/README_ZH.md b/README_ZH.md index 1f37ba0c..88fc7b22 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -397,7 +397,7 @@ OPENCHATCUT_EDITOR_URL=https://your-editor.example.com |---|---| | 前端 | React 19、TypeScript 6、Vite 8 | | 编辑核心 | 不可变时间线状态、命令层、提案式应用 | -| Agent | Vercel AI SDK 7(Anthropic、OpenAI、Gemini、Kimi、Qwen、GLM、DeepSeek、MiniMax、小米 MiMo、Mistral、xAI Grok(API Key 或 SuperGrok/X Premium+ 订阅登录)与兼容接口)、Agent Skills、MCP SDK | +| Agent | Vercel AI SDK 7(Anthropic、OpenAI、Gemini、Kimi、Qwen、GLM、DeepSeek、MiniMax、小米 MiMo、Mistral、xAI Grok(API Key 或 SuperGrok/X Premium+ 订阅登录)、OFox 与兼容接口)、Agent Skills、MCP SDK | | 预览与视觉 | Remotion Player、WebGL / GLSL | | 服务端 | Vite / Electron 双宿主插件、服务端密钥仓 | | 持久化 | `~/.openchatcut` 下的本机共享工程库、IndexedDB 缓存、可配置本地素材目录、可选 Cloudflare R2 | diff --git a/assets/agent/openchatcut-tool-schemas.json b/assets/agent/openchatcut-tool-schemas.json index 81c639bd..781a6090 100644 --- a/assets/agent/openchatcut-tool-schemas.json +++ b/assets/agent/openchatcut-tool-schemas.json @@ -1922,9 +1922,10 @@ "kling", "hailuo", "byteplus", - "grok-imagine-video" + "grok-imagine-video", + "ofox" ], - "description": "hailuo is MiniMax: 6 or 10s; firstFrame optional; lastFrame allowed with firstFrame; no multi-ref or multi-shot. 1080p is 6s only. byteplus is BytePlus ModelArk Seedance — same request shape/limits as seedance2. grok-imagine-video is xAI Grok Imagine: text-to-video only, 1–15s, audio track included, no references/frames." + "description": "hailuo is MiniMax: 6 or 10s; firstFrame optional; lastFrame allowed with firstFrame; no multi-ref or multi-shot. 1080p is 6s only. byteplus is BytePlus ModelArk Seedance — same request shape/limits as seedance2. grok-imagine-video is xAI Grok Imagine: text-to-video only, 1–15s, audio track included, no references/frames. ofox is the OFox multi-model gateway (Seedance/Wan and more behind one key): 2–30s with per-model limits enforced by the API; supports firstFrame (and optional lastFrame), or up to 9 refImages (frames and refImages are mutually exclusive); no refVideos/refAudios yet." }, "prompt": { "type": "string", @@ -1942,11 +1943,11 @@ "type": "string" } ], - "description": "Integer seconds, 2–15 for Seedance, 3–15 for Kling, exactly 6 or 10 for Hailuo (Hailuo 1080p → 6 only), 1–15 for grok-imagine-video." + "description": "Integer seconds, 2–15 for Seedance, 3–15 for Kling, exactly 6 or 10 for Hailuo (Hailuo 1080p → 6 only), 1–15 for grok-imagine-video, 2–30 for ofox (per-model limits enforced by the API)." }, "ratio": { "type": "string", - "description": "Seedance: 16:9, 4:3, 1:1, 3:4, 9:16, 21:9, adaptive. Kling: 16:9, 9:16, 1:1. grok-imagine-video: 16:9, 9:16, 1:1, 4:3, 3:4, 3:2, 2:3. Do not send for hailuo." + "description": "Seedance: 16:9, 4:3, 1:1, 3:4, 9:16, 21:9, adaptive. Kling: 16:9, 9:16, 1:1. grok-imagine-video: 16:9, 9:16, 1:1, 4:3, 3:4, 3:2, 2:3. ofox: 16:9, 9:16, 1:1, 4:3, 3:4, 3:2, 2:3, 21:9, 9:21. Do not send for hailuo." }, "resolution": { "type": "string", @@ -1957,7 +1958,7 @@ "1080p", "4k" ], - "description": "Seedance: 480p/720p(default)/1080p/4k. Hailuo: 512p (Hailuo-02), 720p→API 768P, 1080p (6s only). Kling: pair with mode std/pro. grok-imagine-video: 480p(default)/720p/1080p." + "description": "Seedance: 480p/720p(default)/1080p/4k. Hailuo: 512p (Hailuo-02), 720p→API 768P, 1080p (6s only). Kling: pair with mode std/pro. grok-imagine-video: 480p(default)/720p/1080p. ofox: 480p/720p(default)/1080p, per-model support enforced by the API." }, "mode": { "type": "string", @@ -1973,7 +1974,7 @@ }, "lastFrame": { "type": "string", - "description": "Project image asset reference; requires firstFrame. Supported on seedance2, kling, and hailuo (not with multi-ref on seedance2)." + "description": "Project image asset reference; requires firstFrame. Supported on seedance2, kling, hailuo, and ofox (not with multi-ref on seedance2/ofox)." }, "refImages": { "type": "array", diff --git a/server/key-probes.ts b/server/key-probes.ts index ef848551..3e1eaf66 100644 --- a/server/key-probes.ts +++ b/server/key-probes.ts @@ -383,6 +383,13 @@ export const PROBES: Record = { 'video/hailuo': minimaxProbe, 'video/byteplus': byteplusProbe, 'video/xai': xaiMediaProbe, + 'video/ofox': { + needs: [['LLM_OFOX_API_KEY']], + run: (get) => fetch(`${base(get, 'LLM_OFOX_BASE_URL', 'https://api.ofox.ai/v1')}/models`, { + signal: t(), headers: bearer(get('LLM_OFOX_API_KEY')), + }), + models: parseModelCatalog, + }, 'music/mureka': { needs: [['MUREKA_API_KEY']], run: (get) => fetch(`${base(get, 'MUREKA_BASE_URL', 'https://api.mureka.ai')}/v1/account/billing`, { diff --git a/server/key-probes.verify.ts b/server/key-probes.verify.ts index 99bbfaf8..5c634735 100644 --- a/server/key-probes.verify.ts +++ b/server/key-probes.verify.ts @@ -28,7 +28,7 @@ const EXPECTED_PAGES = [ 'image/openai', 'image/gemini', 'image/minimax', 'image/wavespeed', 'image/byteplus', 'image/xai', 'voice/elevenlabs', 'voice/openai', 'voice/gemini', 'voice/mistral', 'voice/cartesia', 'voice/doubao', 'voice/minimax', 'voice/inworld', 'voice/fishaudio', 'voice/speechify', - 'video/seedance', 'video/kling', 'video/hailuo', 'video/byteplus', 'video/xai', + 'video/seedance', 'video/kling', 'video/hailuo', 'video/byteplus', 'video/xai', 'video/ofox', 'music/mureka', 'music/minimax', 'music/atlas', 'music/sonilo', 'stock/pexels', 'stock/pixabay', 'stock/unsplash', 'stock/freesound', 'transcription/assemblyai', 'transcription/openai', 'transcription/mistral', diff --git a/server/keystore.ts b/server/keystore.ts index 158dd563..146b7ddc 100644 --- a/server/keystore.ts +++ b/server/keystore.ts @@ -82,6 +82,10 @@ export const KEY_NAMES = [ "LLM_OPENROUTER_API_KEY", "LLM_OPENROUTER_BASE_URL", "LLM_OPENROUTER_MODEL", + "LLM_OFOX_API_KEY", + "LLM_OFOX_BASE_URL", + "LLM_OFOX_MODEL", + "OFOX_VIDEO_MODEL", "LLM_OLLAMA_API_KEY", "LLM_OLLAMA_BASE_URL", "LLM_OLLAMA_MODEL", @@ -237,6 +241,7 @@ export const NON_SECRET_NAMES: ReadonlySet = new Set([ "BYTEPLUS_VIDEO_MODEL", "XAI_IMAGE_MODEL", "XAI_VIDEO_MODEL", + "OFOX_VIDEO_MODEL", "INWORLD_TTS_MODEL", "FISHAUDIO_TTS_MODEL", "SPEECHIFY_TTS_MODEL", diff --git a/server/keystore.verify.ts b/server/keystore.verify.ts index b8fa40eb..bb91df8e 100644 --- a/server/keystore.verify.ts +++ b/server/keystore.verify.ts @@ -130,7 +130,7 @@ const MODEL_ROUTING_NAMES = [ 'MINIMAX_TTS_MODEL', 'MINIMAX_VIDEO_MODEL', 'MINIMAX_MUSIC_MODEL', 'MINIMAX_IMAGE_MODEL', 'ATLASCLOUD_API_BASE', 'ATLASCLOUD_MUSIC_MODEL', 'WAVESPEED_IMAGE_MODEL', 'BYTEPLUS_IMAGE_MODEL', 'BYTEPLUS_VIDEO_MODEL', - 'XAI_IMAGE_MODEL', 'XAI_VIDEO_MODEL', + 'XAI_IMAGE_MODEL', 'XAI_VIDEO_MODEL', 'OFOX_VIDEO_MODEL', 'INWORLD_TTS_MODEL', 'FISHAUDIO_TTS_MODEL', 'SPEECHIFY_TTS_MODEL', 'PREFERRED_IMAGE_VENDOR', 'PREFERRED_VOICE_VENDOR', 'PREFERRED_VIDEO_VENDOR', 'PREFERRED_MUSIC_VENDOR', 'PREFERRED_TRANSCRIPTION_PROVIDER', 'TRANSCRIPTION_LANGUAGE', 'TRANSCRIPTION_DIARIZATION', 'AUTO_TRANSCRIBE_INGEST', 'UI_SCALE', diff --git a/server/plugins/index.ts b/server/plugins/index.ts index dd3ba096..91be6a2f 100644 --- a/server/plugins/index.ts +++ b/server/plugins/index.ts @@ -310,6 +310,15 @@ export function serverPlugins(options: { projectStoreHttp?: boolean } = {}): Plu get xaiVideoModel() { return getKey("XAI_VIDEO_MODEL") || "grok-imagine-video-1.5"; }, + get ofoxBaseUrl() { + return getKey("LLM_OFOX_BASE_URL") || "https://api.ofox.ai/v1"; + }, + get ofoxApiKey() { + return getKey("LLM_OFOX_API_KEY"); + }, + get ofoxVideoModel() { + return getKey("OFOX_VIDEO_MODEL") || "bytedance/seedance-2.0-fast"; + }, get byteplusModel() { return getKey("BYTEPLUS_VIDEO_MODEL") || "seedance-1-5-pro-251215"; }, diff --git a/server/plugins/ofox-video-provider.ts b/server/plugins/ofox-video-provider.ts new file mode 100644 index 00000000..26271590 --- /dev/null +++ b/server/plugins/ofox-video-provider.ts @@ -0,0 +1,131 @@ +import { proxyDispatcher } from '../outbound-proxy.ts'; +import type { RegisterGenerationProviderTask } from './generation-jobs.ts'; +import { mediaDataUrl } from './video-media.ts'; +import type { ValidVideoRequest } from './video-validation.ts'; + +type FetchInit = Parameters[1] & { dispatcher?: unknown }; +const fetchWithProxy = (url: RequestInfo | URL, init?: FetchInit): Promise => + fetch(url, { ...init, dispatcher: proxyDispatcher() } as RequestInit); +const FAILURES = new Set(['failed', 'error', 'cancelled', 'canceled', 'expired']); +const SUCCESSES = new Set(['completed', 'succeeded']); +const wait = (milliseconds: number) => new Promise((resolve) => setTimeout(resolve, milliseconds)); + +interface OfoxVideoOptions { + ofoxBaseUrl: string; + ofoxApiKey: string; + ofoxVideoModel: string; +} + +async function providerError(response: Response): Promise { + const text = await response.text(); + try { + const data = JSON.parse(text) as { message?: string; error?: { message?: string } }; + return data.error?.message ?? data.message ?? `video provider failed (${response.status})`; + } catch { + return text.slice(0, 300) || `video provider failed (${response.status})`; + } +} + +interface OfoxMediaRef { + type: 'image_url'; + image_url: { url: string }; + frame_type?: 'first_frame' | 'last_frame'; +} + +/** Frame anchors and image references both ride as data URLs; the OFox + * gateway re-hosts data URIs on its own object storage before handing the + * request to the upstream model (verified live), so local project media + * needs no public hosting. frame_images and input_references are mutually + * exclusive at the API level; validation enforces that before submission. */ +async function ofoxMediaInputs(input: ValidVideoRequest): Promise<{ + frame_images?: OfoxMediaRef[]; + input_references?: OfoxMediaRef[]; +}> { + if (input.firstFramePath) { + const frames: OfoxMediaRef[] = [{ + type: 'image_url', + image_url: { url: await mediaDataUrl(input.firstFramePath) }, + frame_type: 'first_frame', + }]; + if (input.lastFramePath) { + frames.push({ + type: 'image_url', + image_url: { url: await mediaDataUrl(input.lastFramePath) }, + frame_type: 'last_frame', + }); + } + return { frame_images: frames }; + } + if (input.refImagePaths.length) { + return { + input_references: await Promise.all(input.refImagePaths.map(async (path) => ({ + type: 'image_url' as const, + image_url: { url: await mediaDataUrl(path) }, + }))), + }; + } + return {}; +} + +/** OFox multi-model video gateway: asynchronous task id plus polling. + * POST /videos -> { id } ; GET /videos/{id} -> { status, unsigned_urls }. + * Invalid duration/resolution/model values are rejected by the API with a + * clear 400 before any task is created, so no per-model limits are kept here. */ +export async function generateOfoxVideo( + input: ValidVideoRequest, + options: OfoxVideoOptions, + registerProviderTask: RegisterGenerationProviderTask, + existingTaskId?: string, +): Promise { + const baseUrl = options.ofoxBaseUrl.replace(/\/$/, ''); + const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${options.ofoxApiKey}` }; + let taskId = existingTaskId; + if (!taskId) { + const startedResponse = await fetchWithProxy(`${baseUrl}/videos`, { + method: 'POST', headers, + body: JSON.stringify({ + model: options.ofoxVideoModel, prompt: input.prompt, + duration: input.durationSeconds, aspect_ratio: input.ratio, + resolution: input.resolution ?? '720p', + ...(input.generateAudio !== undefined ? { generate_audio: input.generateAudio } : {}), + ...(input.seed !== undefined ? { seed: input.seed } : {}), + ...(await ofoxMediaInputs(input)), + }), + // Data-URL payloads carry whole reference images; give the submit + // request a proportionally larger timeout than the text-only case. + signal: AbortSignal.timeout(120_000), + }); + if (!startedResponse.ok) throw new Error(await providerError(startedResponse)); + const started = await startedResponse.json() as { id?: unknown }; + taskId = String(started.id ?? ''); + if (!taskId) throw new Error('ofox did not return a task id'); + await registerProviderTask('ofox', taskId); + } + const deadline = Date.now() + 15 * 60_000; + while (Date.now() < deadline) { + const poll = await fetchWithProxy(`${baseUrl}/videos/${encodeURIComponent(taskId)}`, { + headers, signal: AbortSignal.timeout(20_000), + }); + if (!poll.ok) throw new Error(await providerError(poll)); + const current = await poll.json() as { status?: unknown; unsigned_urls?: unknown; error?: { message?: unknown } }; + const status = String(current.status ?? ''); + if (SUCCESSES.has(status)) { + // Prefer mirror_urls (persistent signed CDN addresses, present only when + // the upstream has mirroring enabled) and fall back to unsigned_urls + // (temporary upstream links that may expire within 24 hours). + const body = current as { mirror_urls?: unknown; unsigned_urls?: unknown }; + for (const field of ['mirror_urls', 'unsigned_urls'] as const) { + const urls = Array.isArray(body[field]) ? body[field] as unknown[] : []; + const url = urls.find((item): item is string => typeof item === 'string' && /^https?:\/\//.test(item)); + if (url) return url; + } + throw new Error('ofox generation succeeded without a video URL'); + } + if (FAILURES.has(status)) { + const detail = typeof current.error?.message === 'string' ? `: ${current.error.message}` : ''; + throw new Error(`ofox generation ${status}${detail}`); + } + await wait(3_000); + } + throw new Error('ofox generation timed out'); +} diff --git a/server/plugins/video-validation.ts b/server/plugins/video-validation.ts index 233217ca..e797150d 100644 --- a/server/plugins/video-validation.ts +++ b/server/plugins/video-validation.ts @@ -4,7 +4,7 @@ export type KlingVideoReferType = 'feature' | 'base'; export interface VideoRequest { operationId?: string; - model?: 'seedance2' | 'kling' | 'hailuo' | 'byteplus' | 'grok-imagine-video'; + model?: 'seedance2' | 'kling' | 'hailuo' | 'byteplus' | 'grok-imagine-video' | 'ofox'; prompt?: string; name?: string; durationSeconds?: number | string; @@ -34,7 +34,7 @@ export interface VideoRequest { } export interface ValidVideoRequest extends Omit { - model: 'seedance2' | 'kling' | 'hailuo' | 'byteplus' | 'grok-imagine-video'; + model: 'seedance2' | 'kling' | 'hailuo' | 'byteplus' | 'grok-imagine-video' | 'ofox'; prompt: string; durationSeconds: number; durationSpecified: boolean; @@ -192,14 +192,52 @@ function validateGrok(input: ValidVideoRequest): ValidVideoRequest { return input; } +const OFOX_UNSUPPORTED_ARK_KEYS = ['cameraFixed', 'watermark', 'returnLastFrame', 'executionExpiresAfter', 'priority'] as const; + +/** OFox multi-model video gateway: the API validates duration, resolution and + * vendor per model with a clear 400 before any task is created, so only + * cross-provider limits and unsupported options are enforced here. Text, + * first/last-frame and image-reference modes are wired; frame anchors and + * references are mutually exclusive at the API level (400 references_conflict), + * enforced locally before any paid submission. */ +function validateOfox(input: ValidVideoRequest): ValidVideoRequest { + if (!input.prompt || input.prompt.length > 4000) throw new Error('ofox prompt is required and must be at most 4000 characters'); + if (input.durationSeconds < 2 || input.durationSeconds > 30) throw new Error('ofox durationSeconds must be between 2 and 30 (per-model limits are enforced by the API)'); + if (!['16:9', '9:16', '1:1', '4:3', '3:4', '3:2', '2:3', '21:9', '9:21'].includes(input.ratio)) { + throw new Error(`ofox does not support ratio ${input.ratio}`); + } + if (input.resolution && !['480p', '720p', '1080p'].includes(input.resolution)) throw new Error('ofox resolution must be 480p, 720p, or 1080p (per-model support is enforced by the API)'); + if (input.lastFramePath && !input.firstFramePath) throw new Error('lastFrame requires firstFrame'); + if ((input.firstFramePath || input.lastFramePath) && input.refImagePaths.length) { + throw new Error('ofox frame anchors (firstFrame/lastFrame) cannot be combined with refImages'); + } + if (input.refImagePaths.length > 9) throw new Error('ofox accepts at most 9 refImages'); + if (input.refVideoPaths.length || input.refAudioPaths.length) { + throw new Error('ofox refVideos/refAudios are not wired in this integration yet; use refImages or firstFrame/lastFrame'); + } + if (input.mode || input.shotType || input.multiPrompts?.length || input.refVideoMode) { + throw new Error('multi-shot and editing options are not supported by ofox'); + } + for (const key of OFOX_UNSUPPORTED_ARK_KEYS) { + if (input[key] !== undefined) throw new Error(`${key} is supported by seedance2/byteplus only`); + } + if (input.generateAudio !== undefined && typeof input.generateAudio !== 'boolean') throw new Error('generateAudio must be a boolean'); + if (input.seed !== undefined && !Number.isSafeInteger(input.seed)) throw new Error('seed must be a safe integer'); + if (input.promptOptimizer !== undefined || input.fastPretreatment !== undefined) { + throw new Error('promptOptimizer/fastPretreatment are supported by hailuo only'); + } + return input; +} + export function validateVideoRequest(input: VideoRequest): ValidVideoRequest { - if (input.model !== 'seedance2' && input.model !== 'kling' && input.model !== 'hailuo' && input.model !== 'byteplus' && input.model !== 'grok-imagine-video') { - throw new Error('model must be seedance2, kling, hailuo, byteplus, or grok-imagine-video'); + if (input.model !== 'seedance2' && input.model !== 'kling' && input.model !== 'hailuo' && input.model !== 'byteplus' && input.model !== 'grok-imagine-video' && input.model !== 'ofox') { + throw new Error('model must be seedance2, kling, hailuo, byteplus, grok-imagine-video, or ofox'); } if (input.model === 'hailuo' && input.ratio !== undefined) throw new Error('hailuo does not accept ratio; framing follows the first frame when present'); const normalized = common(input, input.model); if (normalized.model === 'hailuo') return validateHailuo(normalized); if (normalized.model === 'kling') return validateKling(normalized); if (normalized.model === 'grok-imagine-video') return validateGrok(normalized); + if (normalized.model === 'ofox') return validateOfox(normalized); return validateSeedance(normalized); } diff --git a/server/plugins/video.ts b/server/plugins/video.ts index 44997b30..bbcc3eb9 100644 --- a/server/plugins/video.ts +++ b/server/plugins/video.ts @@ -19,6 +19,7 @@ import { ServerReferencePreflightError, } from './video-media.ts'; import { generateGrokVideo } from './grok-video-provider.ts'; +import { generateOfoxVideo } from './ofox-video-provider.ts'; import { saveVideoResults } from './video-result-save.ts'; import { hailuoApiResolution, seedanceApiResolution, validateVideoRequest, videoSeconds, @@ -49,6 +50,9 @@ interface VideoOptions { xaiBaseUrl: string; xaiApiKey: string; xaiVideoModel: string; + ofoxBaseUrl: string; + ofoxApiKey: string; + ofoxVideoModel: string; } async function readJson(req: IncomingMessage): Promise { @@ -407,14 +411,16 @@ async function runVideoOperation( } else { const url = input.model === 'grok-imagine-video' ? await generateGrokVideo(input, options, registerProviderTask, providerTaskId) - : input.model === 'kling' - ? await generateKling(input, options, registerProviderTask, providerTaskId) - : await generateHailuo(input, options, registerProviderTask, providerTaskId); + : input.model === 'ofox' + ? await generateOfoxVideo(input, options, registerProviderTask, providerTaskId) + : input.model === 'kling' + ? await generateKling(input, options, registerProviderTask, providerTaskId) + : await generateHailuo(input, options, registerProviderTask, providerTaskId); urls = requireGenerationResultUrls([url], expectedResultCount); } } urls = requireGenerationResultUrls(urls, expectedResultCount); - const resultFetch = input.model === 'grok-imagine-video' ? fetchWithProxy : undefined; + const resultFetch = input.model === 'grok-imagine-video' || input.model === 'ofox' ? fetchWithProxy : undefined; const download = () => saveVideoResults( operationId, name, @@ -426,7 +432,7 @@ async function runVideoOperation( return download(); } export function videoGenerationPlugin(options: VideoOptions): Plugin { - for (const provider of ['seedance2', 'kling', 'hailuo', 'byteplus', 'grok-imagine-video'] as const) { + for (const provider of ['seedance2', 'kling', 'hailuo', 'byteplus', 'grok-imagine-video', 'ofox'] as const) { registerGenerationJobResumer('submit_video', provider, async ( snapshot: GenerationJobSnapshot, _update, diff --git a/server/plugins/video.verify.ts b/server/plugins/video.verify.ts index 64ad48a2..51e870c7 100644 --- a/server/plugins/video.verify.ts +++ b/server/plugins/video.verify.ts @@ -329,4 +329,54 @@ assert.throws( /supported by seedance2\/byteplus only/, ); +const ofox = validateVideoRequest({ model: 'ofox', prompt: 'a paper airplane gliding through a sunlit room', durationSeconds: 4, ratio: '9:16', resolution: '720p' }); +assert.equal(ofox.model, 'ofox'); +assert.equal(ofox.durationSeconds, 4); +assert.equal(ofox.ratio, '9:16'); +assert.equal(ofox.resolution, '720p'); +assert.equal(validateVideoRequest({ model: 'ofox', prompt: 'x', ratio: '21:9' }).ratio, '21:9'); +assert.throws( + () => validateVideoRequest({ model: 'ofox', prompt: 'x', durationSeconds: 40 }), + /durationSeconds must be between 2 and 30/, +); +assert.throws( + () => validateVideoRequest({ model: 'ofox', prompt: 'x', resolution: '4k' }), + /resolution must be 480p, 720p, or 1080p/, +); +const ofoxI2v = validateVideoRequest({ model: 'ofox', prompt: 'x', firstFramePath: '/media/uploads/a.jpg', lastFramePath: '/media/uploads/b.jpg', generateAudio: false, seed: 7 }); +assert.equal(ofoxI2v.firstFramePath, '/media/uploads/a.jpg'); +assert.equal(ofoxI2v.lastFramePath, '/media/uploads/b.jpg'); +assert.equal(ofoxI2v.generateAudio, false); +assert.equal(ofoxI2v.seed, 7); +const ofoxRefs = validateVideoRequest({ model: 'ofox', prompt: 'x', refImagePaths: Array.from({ length: 9 }, (_, i) => `/media/uploads/r${i}.jpg`) }); +assert.equal(ofoxRefs.refImagePaths.length, 9); +assert.throws( + () => validateVideoRequest({ model: 'ofox', prompt: 'x', lastFramePath: '/media/uploads/b.jpg' }), + /lastFrame requires firstFrame/, +); +assert.throws( + () => validateVideoRequest({ model: 'ofox', prompt: 'x', firstFramePath: '/media/uploads/a.jpg', refImagePaths: ['/media/uploads/r.jpg'] }), + /cannot be combined with refImages/, +); +assert.throws( + () => validateVideoRequest({ model: 'ofox', prompt: 'x', refImagePaths: Array.from({ length: 10 }, (_, i) => `/media/uploads/r${i}.jpg`) }), + /at most 9 refImages/, +); +assert.throws( + () => validateVideoRequest({ model: 'ofox', prompt: 'x', refVideoPaths: ['/media/uploads/v.mp4'] }), + /refVideos\/refAudios are not wired/, +); +assert.throws( + () => validateVideoRequest({ model: 'ofox', prompt: 'x', watermark: true }), + /supported by seedance2\/byteplus only/, +); +assert.throws( + () => validateVideoRequest({ model: 'ofox', prompt: 'x', promptOptimizer: true }), + /supported by hailuo only/, +); +assert.throws( + () => validateVideoRequest({ model: 'ofox', prompt: 'x', shotType: 'customize', multiPrompts: [{ prompt: 'a', duration: 2, index: 1 }, { prompt: 'b', duration: 2, index: 2 }] }), + /multi-shot and editing options are not supported by ofox/, +); + console.log('video.check: ok (seedance 480p + kling base/feature + hailuo)'); diff --git a/shared/llm-providers.ts b/shared/llm-providers.ts index ea6c9475..d184ed76 100644 --- a/shared/llm-providers.ts +++ b/shared/llm-providers.ts @@ -121,6 +121,13 @@ export const LLM_PROVIDER_PRESETS = [ baseUrl: 'https://openrouter.ai/api/v1', defaultModel: 'openrouter/auto', }, + { + id: 'ofox', + label: 'OFox', + protocol: 'openai-compatible', + baseUrl: 'https://api.ofox.ai/v1', + defaultModel: 'deepseek/deepseek-v3.2', + }, { id: 'ollama', label: 'Ollama (Local)', diff --git a/src/agent/ai-sdk.verify.ts b/src/agent/ai-sdk.verify.ts index d2189065..c9c4b3ea 100644 --- a/src/agent/ai-sdk.verify.ts +++ b/src/agent/ai-sdk.verify.ts @@ -46,6 +46,7 @@ assert.equal(normalizeLlmProvider('KIMI'), 'kimi'); assert.equal(normalizeLlmProvider('qwen'), 'qwen'); assert.equal(normalizeLlmProvider('glm'), 'glm'); assert.equal(normalizeLlmProvider('OpenRouter'), 'openrouter'); +assert.equal(normalizeLlmProvider('OFox'), 'ofox'); assert.equal(normalizeLlmProvider('unexpected'), 'anthropic'); assert.equal(defaultModelForProvider('anthropic'), 'claude-fable-5'); assert.equal(defaultModelForProvider('openai'), 'gpt-5'); @@ -53,12 +54,14 @@ assert.equal(defaultModelForProvider('kimi'), 'kimi-k3'); assert.equal(defaultModelForProvider('qwen'), 'qwen-plus'); assert.equal(defaultModelForProvider('glm'), 'glm-5.2'); assert.equal(defaultModelForProvider('openrouter'), 'openrouter/auto'); +assert.equal(defaultModelForProvider('ofox'), 'deepseek/deepseek-v3.2'); assert.equal(providerApiPath('anthropic'), '/messages'); assert.equal(providerApiPath('openai'), '/responses'); assert.equal(providerApiPath('openai', 'chat'), '/chat/completions'); assert.equal(providerApiPath('kimi'), '/chat/completions'); assert.equal(providerApiPath('gemini'), '/models'); assert.equal(providerApiPath('openrouter'), '/chat/completions'); +assert.equal(providerApiPath('ofox'), '/chat/completions'); const strippedVisualMessages = withoutModelImages([{ role: 'user', @@ -83,6 +86,7 @@ assert.equal((await getLanguageModel('openai', 'test-model', 'chat')).provider, assert.equal((await getLanguageModel('kimi', 'test-model')).provider, 'moonshotai.chat'); assert.equal((await getLanguageModel('gemini', 'test-model')).provider, 'google.generative-ai'); assert.equal((await getLanguageModel('openrouter', 'openrouter/auto')).provider, 'openrouter.chat'); +assert.equal((await getLanguageModel('ofox', 'deepseek/deepseek-v3.2')).provider, 'ofox.chat'); assert.deepEqual(getLanguageModelProviderOptions('openai'), { openai: { store: false } }); assert.equal(getLanguageModelProviderOptions('openai', 'chat'), undefined); assert.deepEqual(getLanguageModelProviderOptions('minimax'), { diff --git a/src/agent/capabilities.ts b/src/agent/capabilities.ts index c91b0fa7..f89c3fbb 100644 --- a/src/agent/capabilities.ts +++ b/src/agent/capabilities.ts @@ -63,6 +63,7 @@ const CAP_PROVIDERS: Partial> = { { label: 'MiniMax', arg: 'image-01', argKey: 'model', need: [['MINIMAX_API_KEY']] }, { label: 'WaveSpeed', arg: 'wavespeed', argKey: 'model', need: [['WAVESPEED_API_KEY']] }, { label: 'BytePlus', arg: 'byteplus', argKey: 'model', need: [['BYTEPLUS_API_KEY']] }, + { label: 'OFox', arg: 'ofox', argKey: 'model', need: [['LLM_OFOX_API_KEY']] }, { label: 'xAI Grok', arg: 'grok-imagine', argKey: 'model', need: [['LLM_XAI_OAUTH_API_KEY'], ['LLM_XAI_API_KEY']] }, ], voice: [ diff --git a/src/agent/skills/product-help/references/providers-and-keys.md b/src/agent/skills/product-help/references/providers-and-keys.md index 6e46a964..bcd44169 100644 --- a/src/agent/skills/product-help/references/providers-and-keys.md +++ b/src/agent/skills/product-help/references/providers-and-keys.md @@ -12,7 +12,7 @@ If a capability is off, say so and offer alternatives (upload, library, another | Capability | Tools (examples) | Keys (any configured vendor is enough) | | --- | --- | --- | | Image gen | `submit_image` | `IMAGE_API_KEY` / OpenAI, `GEMINI_API_KEY`, `MINIMAX_API_KEY` | -| Video gen | `submit_video` | `SEEDANCE_API_KEY`, `KLING_API_KEY`, `MINIMAX_API_KEY` (Hailuo) | +| Video gen | `submit_video` | `SEEDANCE_API_KEY`, `KLING_API_KEY`, `MINIMAX_API_KEY` (Hailuo), `LLM_OFOX_API_KEY` (OFox) | | TTS / voice | `submit_voice` | Provider-specific server key(s): Doubao, ElevenLabs, MiniMax, Inworld, Fish Audio, Speechify, OpenAI, Gemini, Mistral, or Cartesia | | Music | `submit_music` | `MUREKA_API_KEY`, `MINIMAX_API_KEY`, `ATLASCLOUD_API_KEY` | | Sound FX gen | `submit_sound` | `ELEVENLABS_API_KEY` | diff --git a/src/agent/skills/video-gen/SKILL.md b/src/agent/skills/video-gen/SKILL.md index 3b9516a2..17faaad1 100644 --- a/src/agent/skills/video-gen/SKILL.md +++ b/src/agent/skills/video-gen/SKILL.md @@ -21,6 +21,7 @@ Any time the user wants to generate a video clip — text-to-video, image-to-vid | `kling` | [references/kling.md](references/kling.md) | Technical camera/performance; Omni multi-shot; images ≤7 (≤4 with one feature `refVideos`); std/pro; 3–15s. | | `hailuo` | [references/hailuo.md](references/hailuo.md) | MiniMax 海螺. T2V / I2V / first+last; **6s or 10s**; 512P (Hailuo-02), 720p→768P, 1080P (6s); no multi-ref / multi-shot. | | `grok-imagine-video` | [references/grok-imagine-video.md](references/grok-imagine-video.md) | xAI Grok Imagine. Text-to-video only; 1–15s; 480p/720p/1080p; audio track included. | +| `ofox` | [references/ofox.md](references/ofox.md) | OFox multi-model gateway (Seedance/Wan and more behind one key). Text/image-to-video, first+last frame, up to 9 image refs; 2–30s with per-model API limits; 480p/720p/1080p per model. | **IMPORTANT:** Before generating, READ the chosen model's reference for capabilities, input channels, modes, prompt structure, and model-specific behavior. Never invent params the reference forbids. @@ -30,7 +31,7 @@ Respect **configured vendors** from the capabilities prompt (only call a model w 1. **User named a vendor** ("用海螺", "MiniMax", "Kling", "Seedance") → that `model`, if configured. 2. Else **default `seedance2`** when Seedance is configured. -3. Else if only Kling is on → `kling`. Else if only MiniMax is on → `hailuo`. Else if only xAI is on → `grok-imagine-video`. +3. Else if only Kling is on → `kling`. Else if only MiniMax is on → `hailuo`. Else if only xAI is on → `grok-imagine-video`. Else if only OFox is on → `ofox`. 4. Switch away from default when: - Need **multi-shot customize / intelligence** → `kling` (confirm if not user-named). - Need **rich multi-modal refs** (video/audio refs, edit/extend) → `seedance2`. @@ -44,9 +45,9 @@ Briefly tell the user what you will generate before submitting. | Param | Values | Default | | --- | --- | --- | -| `model` | `seedance2`, `kling`, `hailuo`, `grok-imagine-video` | seedance2 when available | -| `durationSeconds` | model-specific | seedance/kling ~5; **hailuo 6 or 10** (1080p → 6 only); **grok 1–15** | -| `ratio` | see model docs | 16:9 (seedance/kling/grok); **ignored on hailuo** | +| `model` | `seedance2`, `kling`, `hailuo`, `grok-imagine-video`, `ofox` | seedance2 when available | +| `durationSeconds` | model-specific | seedance/kling ~5; **hailuo 6 or 10** (1080p → 6 only); **grok 1–15**; **ofox 2–30 (per-model API limits)** | +| `ratio` | see model docs | 16:9 (seedance/kling/grok/ofox); **ignored on hailuo** | | `resolution` | `480p`, `512p`, `720p`, `1080p`, `4k` | provider-specific; hailuo adds 512p for Hailuo-02; grok: 480p/720p/1080p | | `refVideoMode` | `feature`, `base` | kling only, with `refVideos` | | `promptOptimizer` / `fastPretreatment` | boolean | hailuo only | diff --git a/src/agent/skills/video-gen/references/ofox.md b/src/agent/skills/video-gen/references/ofox.md new file mode 100644 index 00000000..418f84bd --- /dev/null +++ b/src/agent/skills/video-gen/references/ofox.md @@ -0,0 +1,49 @@ +# OFox Multi-Model Video (`ofox`) + +Text-to-video via the OFox multi-model gateway (one API key in front of the +Seedance series, Wan, and other text-to-video models). Auth: the +`LLM_OFOX_API_KEY` key (shared with the OFox LLM preset). Configured model id +comes from Settings (`OFOX_VIDEO_MODEL`), default +`bytedance/seedance-2.0-fast`; any text-to-video model from the OFox catalog +(`GET /v1/models`) works. + +## Wired capabilities + +- **Text-to-video, image-to-video, first-and-last-frame, and image + references.** `firstFrame` (optionally with `lastFrame`) anchors exact + frames; up to 9 `refImages` guide subject/style instead. The two modes are + **mutually exclusive** (the API rejects the combination). `refVideos` / + `refAudios` are supported by the OFox API but not wired here yet; + `refVideoMode` / `mode` / `shotType` / `multiPrompts` are rejected. +- **`generateAudio`** (default true for capable models) and **`seed`** pass + through; seed determinism is not guaranteed by every upstream vendor. +- **Duration** (`durationSeconds`): 2–30 seconds locally; the API enforces the + configured model's own range with a clear 400 before any task is created + (for example the default model accepts 4–15 seconds). +- **Aspect ratio** (`ratio`): `16:9` (default), `9:16`, `1:1`, `4:3`, `3:4`, + `3:2`, `2:3`, `21:9`, `9:21`. +- **Resolution** (`resolution`): `480p`, `720p` (default), `1080p` — per-model + support is enforced by the API (the default model accepts 480p/720p). +- Project media rides as base64 data URLs; the OFox gateway re-hosts them on + its own object storage before dispatching to the upstream model, so no + public hosting is needed. Reference images should be reasonably sized + (very small images can fail the upstream image validation). +- Results prefer `mirror_urls` (persistent signed CDN addresses, present when + the upstream has mirroring enabled) and fall back to the temporary + `unsigned_urls`. +- The job is asynchronous: `submit_video` returns a `jobId`; poll / wait with + `track_progress` exactly like the other video vendors. + +## Prompt guidance + +One coherent action per clip. Include subject, motion, environment, lighting, +and camera intent. Different catalog models have different strengths; keep +prompts model-agnostic unless the user pinned a specific model. + +## Errors + +- `duration N out of range [a, b]` / `resolution "…" not supported`: the + configured model's server-side limits — adjust the args or switch + `OFOX_VIDEO_MODEL`, do not retry unchanged. +- 401 `upstream_auth_failed`: the OFox account key is invalid or out of + balance — ask the user to check the key / top up. diff --git a/src/agent/tools/generate-schemas.ts b/src/agent/tools/generate-schemas.ts index f1a5f5d8..b1e7886e 100644 --- a/src/agent/tools/generate-schemas.ts +++ b/src/agent/tools/generate-schemas.ts @@ -150,15 +150,15 @@ export const GENERATE_TOOL_SCHEMAS: AgentToolSchema[] = [ input_schema: { type: 'object', properties: { - model: { type: 'string', enum: ['seedance2', 'kling', 'hailuo', 'byteplus', 'grok-imagine-video'], description: 'hailuo is MiniMax: 6 or 10s; firstFrame optional; lastFrame allowed with firstFrame; no multi-ref or multi-shot. 1080p is 6s only. byteplus is BytePlus ModelArk Seedance — same request shape/limits as seedance2. grok-imagine-video is xAI Grok Imagine: text-to-video only, 1–15s, audio track included, no references/frames.' }, // minimax: hailuo enum + model: { type: 'string', enum: ['seedance2', 'kling', 'hailuo', 'byteplus', 'grok-imagine-video', 'ofox'], description: 'hailuo is MiniMax: 6 or 10s; firstFrame optional; lastFrame allowed with firstFrame; no multi-ref or multi-shot. 1080p is 6s only. byteplus is BytePlus ModelArk Seedance — same request shape/limits as seedance2. grok-imagine-video is xAI Grok Imagine: text-to-video only, 1–15s, audio track included, no references/frames. ofox is the OFox multi-model gateway (Seedance/Wan and more behind one key): 2–30s with per-model limits enforced by the API; supports firstFrame (and optional lastFrame), or up to 9 refImages (frames and refImages are mutually exclusive); no refVideos/refAudios yet.' }, // minimax: hailuo enum prompt: { type: 'string', description: 'Required for normal generation and Kling intelligence; omit for Kling customize.' }, name: { type: 'string' }, - durationSeconds: { anyOf: [{ type: 'number' }, { type: 'string' }], description: 'Integer seconds, 2–15 for Seedance, 3–15 for Kling, exactly 6 or 10 for Hailuo (Hailuo 1080p → 6 only), 1–15 for grok-imagine-video.' }, // minimax: hailuo durations - ratio: { type: 'string', description: 'Seedance: 16:9, 4:3, 1:1, 3:4, 9:16, 21:9, adaptive. Kling: 16:9, 9:16, 1:1. grok-imagine-video: 16:9, 9:16, 1:1, 4:3, 3:4, 3:2, 2:3. Do not send for hailuo.' }, - resolution: { type: 'string', enum: ['480p', '512p', '720p', '1080p', '4k'], description: 'Seedance: 480p/720p(default)/1080p/4k. Hailuo: 512p (Hailuo-02), 720p→API 768P, 1080p (6s only). Kling: pair with mode std/pro. grok-imagine-video: 480p(default)/720p/1080p.' }, + durationSeconds: { anyOf: [{ type: 'number' }, { type: 'string' }], description: 'Integer seconds, 2–15 for Seedance, 3–15 for Kling, exactly 6 or 10 for Hailuo (Hailuo 1080p → 6 only), 1–15 for grok-imagine-video, 2–30 for ofox (per-model limits enforced by the API).' }, // minimax: hailuo durations + ratio: { type: 'string', description: 'Seedance: 16:9, 4:3, 1:1, 3:4, 9:16, 21:9, adaptive. Kling: 16:9, 9:16, 1:1. grok-imagine-video: 16:9, 9:16, 1:1, 4:3, 3:4, 3:2, 2:3. ofox: 16:9, 9:16, 1:1, 4:3, 3:4, 3:2, 2:3, 21:9, 9:21. Do not send for hailuo.' }, + resolution: { type: 'string', enum: ['480p', '512p', '720p', '1080p', '4k'], description: 'Seedance: 480p/720p(default)/1080p/4k. Hailuo: 512p (Hailuo-02), 720p→API 768P, 1080p (6s only). Kling: pair with mode std/pro. grok-imagine-video: 480p(default)/720p/1080p. ofox: 480p/720p(default)/1080p, per-model support enforced by the API.' }, mode: { type: 'string', enum: ['std', 'pro'], description: 'Kling only; std=720p, pro=1080p.' }, firstFrame: { type: 'string', description: 'Project image asset ID, asset:// ID, short unique ID prefix, or same-project asset path.' }, - lastFrame: { type: 'string', description: 'Project image asset reference; requires firstFrame. Supported on seedance2, kling, and hailuo (not with multi-ref on seedance2).' }, + lastFrame: { type: 'string', description: 'Project image asset reference; requires firstFrame. Supported on seedance2, kling, hailuo, and ofox (not with multi-ref on seedance2/ofox).' }, refImages: { type: 'array', items: { type: 'string' } }, refVideos: { type: 'array', items: { type: 'string' } }, refAudios: { type: 'array', items: { type: 'string' } }, diff --git a/src/components/settings/VisionModelPane.tsx b/src/components/settings/VisionModelPane.tsx index 11333201..15dec7a6 100644 --- a/src/components/settings/VisionModelPane.tsx +++ b/src/components/settings/VisionModelPane.tsx @@ -43,6 +43,7 @@ const PROVIDER_LABELS: Record = { xai: 'xAI', 'xai-oauth': 'xAI (订阅登录)', openrouter: 'OpenRouter', + ofox: 'OFox', ollama: 'Ollama', lmstudio: 'LM Studio', }; diff --git a/src/components/settings/settingsMediaProviders.ts b/src/components/settings/settingsMediaProviders.ts index 23e8cb7e..d81d5fd3 100644 --- a/src/components/settings/settingsMediaProviders.ts +++ b/src/components/settings/settingsMediaProviders.ts @@ -233,6 +233,7 @@ export const ROUTE_NEEDS: Record = { 'image-01': [['MINIMAX_API_KEY']], 'grok-imagine': [['LLM_XAI_OAUTH_API_KEY'], ['LLM_XAI_API_KEY']], 'grok-imagine-video': [['LLM_XAI_OAUTH_API_KEY'], ['LLM_XAI_API_KEY']], + ofox: [['LLM_OFOX_API_KEY']], elevenlabs: [['ELEVENLABS_API_KEY']], doubao: [['DOUBAO_TTS_APP_ID', 'DOUBAO_TTS_ACCESS_KEY']], minimax: [['MINIMAX_API_KEY']], diff --git a/src/components/settings/settingsSchema.ts b/src/components/settings/settingsSchema.ts index 73301aa1..147b85d5 100644 --- a/src/components/settings/settingsSchema.ts +++ b/src/components/settings/settingsSchema.ts @@ -126,6 +126,7 @@ export const SETTINGS_CATEGORIES: readonly SettingsCategory[] = [ { value: 'hailuo', label: 'MiniMax 海螺' }, { value: 'byteplus', label: 'BytePlus · Seedance' }, { value: 'grok-imagine-video', label: 'xAI Grok Imagine' }, + { value: 'ofox', label: 'OFox · 多模型' }, ]), vendors: [ { key: 'video/seedance', vendor: 'seedance', title: 'Seedance · 火山', fields: [ @@ -148,6 +149,12 @@ export const SETTINGS_CATEGORIES: readonly SettingsCategory[] = [ modelText('XAI_VIDEO_MODEL', '视频模型', 'grok-imagine-video-1.5', '测试连接后可直接选择接口返回的模型,也可以手动填写模型 ID。', true), ] }, + { key: 'video/ofox', vendor: 'ofox', title: 'OFox · 多模型网关', + note: '使用 LLM_OFOX_API_KEY(在 Agent 供应商里配置 OFox)生成视频。一个 Key 覆盖 Seedance、Wan 等视频模型;支持文生视频、首帧/首尾帧图生视频与图片参考(最多 9 张);时长/分辨率按模型由 API 校验,2–30 秒。', + fields: [ + modelText('OFOX_VIDEO_MODEL', '视频模型', 'bytedance/seedance-2.0-fast', + '测试连接后可直接选择接口返回的模型,也可以手动填写模型 ID。', true), + ] }, ] }, { key: 'music', title: '生音乐', hint: 'submit_music · 文字 / 成片生成配乐,任一厂商即可。', route: routeSelect('PREFERRED_MUSIC_VENDOR', [ diff --git a/src/components/settings/vendorIcons.tsx b/src/components/settings/vendorIcons.tsx index 9811c3b6..50362323 100644 --- a/src/components/settings/vendorIcons.tsx +++ b/src/components/settings/vendorIcons.tsx @@ -43,7 +43,7 @@ export type VendorId = | 'seedance' | 'kling' | 'mureka' | 'sonilo' | 'pexels' | 'pixabay' | 'unsplash' | 'freesound' | 'assemblyai' | 'deepgram' | 'groq' | 'cartesia' | 'e2b' | 'firecrawl' | 'r2' | 'localdisk' | 'localasr' | 'stepfun' | 'byteplus' | 'inworld' | 'fishaudio' | 'speechify' | 'wavespeed' - | 'vision' | 'proxy' | 'atlas' | 'xai' | 'xai-oauth'; + | 'vision' | 'proxy' | 'atlas' | 'xai' | 'xai-oauth' | 'ofox'; interface SvgIcon { readonly svg: string; @@ -101,6 +101,7 @@ const MONOGRAMS: Partial = { '使用 SuperGrok 或 X Premium+ 订阅登录:官方 Grok CLI 管理登录与凭据(终端运行 grok login),OpenChatCut 导入会话并自动续期,不会读取或显示 OAuth 凭据。': 'Accedi con un abbonamento SuperGrok o X Premium+: la CLI ufficiale di Grok gestisce accesso e credenziali (esegui grok login nel terminale). OpenChatCut importa e rinnova automaticamente la sessione senza leggere o mostrare le credenziali OAuth.', '使用 xAI 订阅会话(SuperGrok / X Premium+,优先)或 LLM_XAI_API_KEY 生成图片。文生图:最多 4 张,1K / 2K。': 'Genera immagini con la sessione di abbonamento xAI (SuperGrok / X Premium+, preferita) o con LLM_XAI_API_KEY. Da testo a immagine: fino a 4 immagini, 1K / 2K.', '使用 xAI 订阅会话(SuperGrok / X Premium+,优先)或 LLM_XAI_API_KEY 生成视频。文生视频:1–15 秒,自带音轨,480p / 720p / 1080p。': 'Genera video con la sessione di abbonamento xAI (SuperGrok / X Premium+, preferita) o con LLM_XAI_API_KEY. Da testo a video: 1-15 secondi, traccia audio inclusa, 480p / 720p / 1080p.', + 'OFox · 多模型': 'OFox · Multi-modello', + 'OFox · 多模型网关': 'OFox · Gateway multi-modello', + '使用 LLM_OFOX_API_KEY(在 Agent 供应商里配置 OFox)生成视频。一个 Key 覆盖 Seedance、Wan 等视频模型;支持文生视频、首帧/首尾帧图生视频与图片参考(最多 9 张);时长/分辨率按模型由 API 校验,2–30 秒。': + 'Genera video con LLM_OFOX_API_KEY (configura OFox tra i provider Agent). Una sola chiave copre Seedance, Wan e altri modelli video; supporta testo-video, immagine-video con primo fotogramma / primo e ultimo fotogramma e fino a 9 immagini di riferimento; durata e risoluzione sono convalidate per modello dalla API, 2–30 secondi.', '处理中…': 'Elaborazione…', '导入登录状态': 'Importa accesso', '新工程和生成素材的默认保存位置,以及可选的 R2 云备份。': 'Posizione predefinita per i nuovi progetti e i media generati, con backup cloud R2 facoltativo.', diff --git a/src/i18n/dict/ru/index.ts b/src/i18n/dict/ru/index.ts index 26d69b13..d27f5c43 100644 --- a/src/i18n/dict/ru/index.ts +++ b/src/i18n/dict/ru/index.ts @@ -1779,6 +1779,10 @@ export const RU: Record = { 'xAI · Grok (订阅登录)': 'xAI · Grok (вход по подписке)', '使用 xAI 订阅会话(SuperGrok / X Premium+,优先)或 LLM_XAI_API_KEY 生成图片。文生图:最多 4 张,1K / 2K。': 'Генерация изображений через сессию подписки xAI (SuperGrok / X Premium+, приоритет) или LLM_XAI_API_KEY. Текст → изображение: до 4 штук, 1K / 2K.', '使用 xAI 订阅会话(SuperGrok / X Premium+,优先)或 LLM_XAI_API_KEY 生成视频。文生视频:1–15 秒,自带音轨,480p / 720p / 1080p。': 'Генерация видео через сессию подписки xAI (SuperGrok / X Premium+, приоритет) или LLM_XAI_API_KEY. Текст → видео: 1–15 с, со звуковой дорожкой, 480p / 720p / 1080p.', + 'OFox · 多模型': 'OFox · Мультимодельный', + 'OFox · 多模型网关': 'OFox · Мультимодельный шлюз', + '使用 LLM_OFOX_API_KEY(在 Agent 供应商里配置 OFox)生成视频。一个 Key 覆盖 Seedance、Wan 等视频模型;支持文生视频、首帧/首尾帧图生视频与图片参考(最多 9 张);时长/分辨率按模型由 API 校验,2–30 秒。': + 'Генерация видео через LLM_OFOX_API_KEY (настройте OFox в провайдерах Agent). Один ключ покрывает Seedance, Wan и другие видеомодели; поддерживает текст-в-видео, изображение-в-видео (первый кадр / первый и последний кадры) и до 9 референсных изображений; длительность и разрешение проверяются API для каждой модели, 2–30 секунд.', 'xAI · Grok Imagine (视频)': 'xAI · Grok Imagine (видео)', '验证地址与密钥,并读取该接口可用的模型': 'Проверить адрес и ключ, прочитать доступные модели API', '选择模型': 'Выбрать модель',