feat(canvas): 添加视频节点支持功能

- 在后端添加 AI 视频相关接口处理函数
- 扩展前端画布组件支持视频节点类型
- 实现视频上传、生成和播放功能
- 更新画布数据结构文档以包含视频节点
- 添加视频节点的操作工具栏和配置面板
- 集成视频存储和检索服务
- 优化视频节点的渲染和交互体验
This commit is contained in:
HouYunFei
2026-05-23 16:53:21 +08:00
parent 0cc27b5a4c
commit 44c5825cb0
23 changed files with 306 additions and 57 deletions
+6 -2
View File
@@ -6,11 +6,13 @@ import { persist, type PersistStorage, type StorageValue } from "zustand/middlew
import { nanoid } from "nanoid";
import { localForageStorage } from "@/lib/localforage-storage";
import { cleanupUnusedImages, resolveImageUrl, uploadImage } from "@/services/image-storage";
import { cleanupUnusedMedia, resolveMediaUrl } from "@/services/file-storage";
export type AssetKind = "text" | "image";
export type AssetKind = "text" | "image" | "video";
export type TextAsset = AssetBase<"text"> & { data: { content: string } };
export type ImageAsset = AssetBase<"image"> & { data: { dataUrl: string; storageKey?: string; width: number; height: number; bytes: number; mimeType: string } };
export type Asset = TextAsset | ImageAsset;
export type VideoAsset = AssetBase<"video"> & { data: { url: string; storageKey?: string; width: number; height: number; bytes: number; mimeType: string } };
export type Asset = TextAsset | ImageAsset | VideoAsset;
type AssetBase<T extends AssetKind> = {
id: string;
@@ -42,6 +44,7 @@ const assetStorage: PersistStorage<AssetStore> = {
const parsed = JSON.parse(value) as StorageValue<AssetStore>;
parsed.state.assets = await Promise.all(
parsed.state.assets.map(async (asset) => {
if (asset.kind === "video" && asset.data.storageKey) return { ...asset, data: { ...asset.data, url: await resolveMediaUrl(asset.data.storageKey, asset.data.url) } };
if (asset.kind !== "image") return asset;
if (asset.data.storageKey)
return {
@@ -84,6 +87,7 @@ export const useAssetStore = create<AssetStore>()(
window.setTimeout(async () => {
const { useCanvasStore } = await import("@/app/(user)/canvas/stores/use-canvas-store");
await cleanupUnusedImages({ assets: get().assets, projects: useCanvasStore.getState().projects, extra });
await cleanupUnusedMedia({ assets: get().assets, projects: useCanvasStore.getState().projects, extra });
}, 0);
},
}),
+4 -1
View File
@@ -13,6 +13,7 @@ export type AiConfig = {
apiKey: string;
model: string;
imageModel: string;
videoModel: string;
textModel: string;
systemPrompt: string;
models: string[];
@@ -29,6 +30,7 @@ export const defaultConfig: AiConfig = {
apiKey: "",
model: "gpt-image-2",
imageModel: "gpt-image-2",
videoModel: "sora-2",
textModel: "gpt-5.5",
systemPrompt: "",
models: [],
@@ -61,6 +63,7 @@ function resolveEffectiveConfig(config: AiConfig, modelChannel: AdminPublicSetti
models,
model: models.includes(config.model) ? config.model : modelChannel.defaultModel,
imageModel: models.includes(config.imageModel) ? config.imageModel : modelChannel.defaultImageModel || modelChannel.defaultModel,
videoModel: models.includes(config.videoModel) ? config.videoModel : "sora-2",
textModel: models.includes(config.textModel) ? config.textModel : modelChannel.defaultTextModel || modelChannel.defaultModel,
systemPrompt: modelChannel.systemPrompt,
};
@@ -104,7 +107,7 @@ export const useConfigStore = create<ConfigStore>()(
partialize: (state) => ({ config: state.config }),
merge: (persisted, current) => {
const config = { ...defaultConfig, ...((persisted as Partial<ConfigStore>).config || {}) };
return { ...current, config: { ...config, channelMode: config.channelMode || "remote", imageModel: config.imageModel || config.model, textModel: config.textModel || config.model } };
return { ...current, config: { ...config, channelMode: config.channelMode || "remote", imageModel: config.imageModel || config.model, videoModel: config.videoModel || "sora-2", textModel: config.textModel || config.model } };
},
},
),