fix(canvas): keep uploaded resources in asset library (#388)
This commit is contained in:
@@ -14,7 +14,7 @@ import { CanvasCreateCard } from "@/components/canvas/canvas-project-card";
|
||||
import { CanvasFolderCard } from "@/components/canvas/canvas-folder-card";
|
||||
import { CanvasHistoryDrawer } from "@/components/canvas/canvas-history-drawer";
|
||||
import type { CanvasExportFile } from "@/types/canvas-export";
|
||||
import type { CanvasNodeData } from "@/types/canvas";
|
||||
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
|
||||
import { flushCanvasStorePersistence, useCanvasStore } from "@/stores/canvas/use-canvas-store";
|
||||
import { useCanvasUiStore } from "@/stores/canvas/use-canvas-ui-store";
|
||||
import { exportCanvasProjects } from "@/lib/canvas/canvas-export";
|
||||
@@ -25,6 +25,7 @@ import { loadCanvasProjectPage } from "@/lib/workspace-route-modules";
|
||||
import { resourceFileUrl, resourceStorageKey, uploadResourceFile } from "@/services/api/resources";
|
||||
import { primeResourceBlobCache } from "@/services/resource-blob-cache";
|
||||
import { useSyncProgressStore } from "@/stores/use-sync-progress-store";
|
||||
import { ensureCanvasNodeAsset } from "@/services/project-asset-sync";
|
||||
|
||||
export default function CanvasPage() {
|
||||
const { message } = App.useApp();
|
||||
@@ -206,8 +207,69 @@ export default function CanvasPage() {
|
||||
};
|
||||
};
|
||||
|
||||
const remappedNodes = (item.project.nodes || []).map(remapNodeMedia);
|
||||
updateProject(importedProjectId, { nodes: remappedNodes });
|
||||
let remappedNodes = (item.project.nodes || []).map(remapNodeMedia);
|
||||
let remappedTimeline = item.project.timeline
|
||||
? {
|
||||
...item.project.timeline,
|
||||
clips: item.project.timeline.clips.map((clip) => {
|
||||
const directMedia = clip.directMedia;
|
||||
if (!directMedia?.storageKey) return clip;
|
||||
const mapped = storageKeyMap.get(directMedia.storageKey);
|
||||
return mapped
|
||||
? {
|
||||
...clip,
|
||||
directMedia: { ...directMedia, storageKey: mapped.storageKey, url: mapped.url, dataUrl: directMedia.dataUrl ? mapped.url : directMedia.dataUrl, content: directMedia.content ? mapped.url : directMedia.content },
|
||||
}
|
||||
: clip;
|
||||
}),
|
||||
}
|
||||
: undefined;
|
||||
updateProject(importedProjectId, { nodes: remappedNodes, timeline: remappedTimeline });
|
||||
|
||||
const assetIdByStorageKey = new Map<string, string>();
|
||||
for (let index = 0; index < remappedNodes.length; index += 1) {
|
||||
const node = remappedNodes[index];
|
||||
const isMedia = node.type === CanvasNodeType.Image || node.type === CanvasNodeType.Video || node.type === CanvasNodeType.Audio;
|
||||
if (!isMedia || !node.metadata?.content) continue;
|
||||
const storageKey = node.metadata.storageKey || "";
|
||||
let assetId = storageKey ? assetIdByStorageKey.get(storageKey) : undefined;
|
||||
if (!assetId) {
|
||||
const result = await ensureCanvasNodeAsset({ canvasId: importedProjectId, domainProjectId: item.project.projectId, node, source: "canvas-upload" });
|
||||
assetId = result.assetId;
|
||||
if (storageKey) assetIdByStorageKey.set(storageKey, assetId);
|
||||
}
|
||||
remappedNodes[index] = { ...node, metadata: { ...node.metadata, assetId } };
|
||||
}
|
||||
if (remappedTimeline) {
|
||||
const clips: typeof remappedTimeline.clips = [];
|
||||
for (const clip of remappedTimeline.clips) {
|
||||
const media = clip.directMedia;
|
||||
const content = media?.url || media?.dataUrl || media?.content || "";
|
||||
if (!media || media.assetId || !media.storageKey || !content || media.kind === "text") {
|
||||
clips.push(clip);
|
||||
continue;
|
||||
}
|
||||
let assetId = assetIdByStorageKey.get(media.storageKey);
|
||||
if (!assetId) {
|
||||
const type = media.kind === "audio" ? CanvasNodeType.Audio : media.kind === "video" ? CanvasNodeType.Video : CanvasNodeType.Image;
|
||||
const node: CanvasNodeData = {
|
||||
id: media.id,
|
||||
type,
|
||||
title: media.title,
|
||||
position: { x: 0, y: 0 },
|
||||
width: media.width || 320,
|
||||
height: media.height || (type === CanvasNodeType.Audio ? 120 : 240),
|
||||
metadata: { content, storageKey: media.storageKey, naturalWidth: media.width, naturalHeight: media.height, durationMs: media.durationMs, bytes: media.bytes, mimeType: media.mimeType },
|
||||
};
|
||||
const result = await ensureCanvasNodeAsset({ canvasId: importedProjectId, domainProjectId: item.project.projectId, node, source: "canvas-upload" });
|
||||
assetId = result.assetId;
|
||||
assetIdByStorageKey.set(media.storageKey, assetId);
|
||||
}
|
||||
clips.push({ ...clip, directMedia: { ...media, assetId } });
|
||||
}
|
||||
remappedTimeline = { ...remappedTimeline, clips };
|
||||
}
|
||||
updateProject(importedProjectId, { nodes: remappedNodes, timeline: remappedTimeline });
|
||||
|
||||
await Promise.all(
|
||||
(item.drawingDocuments || []).map((document) => {
|
||||
|
||||
@@ -1346,11 +1346,13 @@ function InfiniteCanvasPage() {
|
||||
setNodes((current) => [...current, created]);
|
||||
setConnections((current) => [...current, connection]);
|
||||
setSelectedNodeIds(new Set([created.id]));
|
||||
const result = await ensureCanvasNodeAsset({ canvasId: projectId, domainProjectId: currentProject?.projectId, node: created, source: "canvas-manual" });
|
||||
setNodes((current) => current.map((item) => item.id === created.id ? { ...item, metadata: { ...item.metadata, assetId: result.assetId } } : item));
|
||||
message.success("候选图片已添加到画布并连接到排查节点");
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "候选图片添加失败");
|
||||
}
|
||||
}, [message, portraitClearanceNodeId, setConnections, setNodes, setSelectedNodeIds]);
|
||||
}, [currentProject?.projectId, message, portraitClearanceNodeId, projectId, setConnections, setNodes, setSelectedNodeIds]);
|
||||
const pendingConnectionSourceNode = pendingConnectionCreate?.connection.handleType === "source" ? nodeById.get(pendingConnectionCreate.connection.nodeId) : null;
|
||||
const canCreateDrawingFromConnection = !pendingConnectionCreate?.batchSourceNodeIds?.length && pendingConnectionSourceNode?.type === CanvasNodeType.Image && Boolean(pendingConnectionSourceNode.metadata?.content);
|
||||
|
||||
@@ -1462,6 +1464,7 @@ function InfiniteCanvasPage() {
|
||||
|
||||
const { applyDirectorOutput, createDirectorShot, openDirectorWorkbench, saveDirectorScene } = useCanvasDirector({
|
||||
projectId,
|
||||
domainProjectId: currentProject?.projectId,
|
||||
directorNodeId,
|
||||
directorScenes: currentProject?.directorScenes || [],
|
||||
nodesRef,
|
||||
@@ -1654,12 +1657,16 @@ function InfiniteCanvasPage() {
|
||||
message.loading({ key: feedbackKey, content: "正在保存并上传到方舟素材库...", duration: 0 });
|
||||
try {
|
||||
let resourceID = resourceIdFromStorageKey(node.metadata.storageKey);
|
||||
let persistedNode = node;
|
||||
if (!resourceID) {
|
||||
const uploaded = await uploadImage(node.metadata.content);
|
||||
resourceID = resourceIdFromStorageKey(uploaded.storageKey);
|
||||
if (!resourceID) throw new Error("图片未能保存到系统素材库,请检查对象存储配置后重试");
|
||||
handleConfigNodeChange(node.id, imageMetadata(uploaded));
|
||||
persistedNode = { ...node, metadata: { ...node.metadata, ...imageMetadata(uploaded) } };
|
||||
}
|
||||
const asset = await ensureCanvasNodeAsset({ canvasId: projectId, domainProjectId: currentProject?.projectId, node: persistedNode, source: "canvas-manual" });
|
||||
handleConfigNodeChange(node.id, { assetId: asset.assetId });
|
||||
await syncResourceToArkPrivateAsset(resourceID);
|
||||
message.success({ key: feedbackKey, content: "已同步到方舟素材库,Seedance 将自动复用该素材", duration: 4 });
|
||||
} catch (error) {
|
||||
@@ -1667,7 +1674,7 @@ function InfiniteCanvasPage() {
|
||||
} finally {
|
||||
setArkPrivateAssetUploadNodeId((current) => current === node.id ? null : current);
|
||||
}
|
||||
}, [arkPrivateAssetUploadNodeId, handleConfigNodeChange, message]);
|
||||
}, [arkPrivateAssetUploadNodeId, currentProject?.projectId, handleConfigNodeChange, message, projectId]);
|
||||
|
||||
const confirmUploadNodeImageToArkPrivateAsset = useCallback((node: CanvasNodeData) => {
|
||||
Modal.confirm({
|
||||
|
||||
@@ -9,12 +9,14 @@ import { createDirectorSceneFromTemplate, type DirectorTemplateId } from "@/lib/
|
||||
import { mergeDirectorOutputPreview, upsertDirectorSceneById } from "@/lib/canvas/director/director-session";
|
||||
import { uploadImage } from "@/services/image-storage";
|
||||
import { uploadMediaFile } from "@/services/file-storage";
|
||||
import { ensureCanvasNodeAsset } from "@/services/project-asset-sync";
|
||||
import { useCanvasStore } from "@/stores/canvas/use-canvas-store";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type CanvasNodeMetadata, type Position } from "@/types/canvas";
|
||||
import type { DirectorScene, DirectorSceneOutput } from "@/types/director";
|
||||
|
||||
type UseCanvasDirectorOptions = {
|
||||
projectId: string;
|
||||
domainProjectId?: string;
|
||||
directorNodeId: string | null;
|
||||
directorScenes: DirectorScene[];
|
||||
nodesRef: { current: CanvasNodeData[] };
|
||||
@@ -40,6 +42,7 @@ function currentDirectorScenes(projectId: string, fallback: DirectorScene[]) {
|
||||
}
|
||||
export function useCanvasDirector({
|
||||
projectId,
|
||||
domainProjectId,
|
||||
directorNodeId,
|
||||
directorScenes,
|
||||
nodesRef,
|
||||
@@ -196,13 +199,23 @@ export function useCanvasDirector({
|
||||
videoCameraMovePrompt: output.prompt,
|
||||
referenceAssetNodeIds,
|
||||
};
|
||||
const finalizedNodes = nextNodes.map((item) => item.id === sourceNode.id ? { ...item, metadata: { ...item.metadata, ...directorMetadata } } : item);
|
||||
const mediaNodes = nextNodes.filter((item) => item.id === previewId || Boolean(clayVideoId && item.id === clayVideoId));
|
||||
const assetIds = new Map<string, string>();
|
||||
for (const mediaNode of mediaNodes) {
|
||||
const result = await ensureCanvasNodeAsset({ canvasId: projectId, domainProjectId, node: mediaNode, source: "canvas-manual" });
|
||||
assetIds.set(mediaNode.id, result.assetId);
|
||||
}
|
||||
const finalizedNodes = nextNodes.map((item) => {
|
||||
const assetId = assetIds.get(item.id);
|
||||
if (assetId) return { ...item, metadata: { ...item.metadata, assetId } };
|
||||
return item.id === sourceNode.id ? { ...item, metadata: { ...item.metadata, ...directorMetadata } } : item;
|
||||
});
|
||||
nodesRef.current = finalizedNodes;
|
||||
connectionsRef.current = nextConnections;
|
||||
setNodes(finalizedNodes);
|
||||
setConnections(nextConnections);
|
||||
saveDirectorScene(mergedScene);
|
||||
}, [connectionsRef, directorNodeId, nodesRef, projectId, saveDirectorScene, setConnections, setNodes]);
|
||||
}, [connectionsRef, directorNodeId, domainProjectId, nodesRef, projectId, saveDirectorScene, setConnections, setNodes]);
|
||||
|
||||
return { applyDirectorOutput, createDirectorShot, openDirectorWorkbench, saveDirectorScene };
|
||||
}
|
||||
|
||||
@@ -126,6 +126,25 @@ export function useCanvasMediaTools({
|
||||
}
|
||||
}, [message, nodesRef]);
|
||||
|
||||
const persistMediaNodes = useCallback(async (mediaNodes: CanvasNodeData[]) => {
|
||||
const assetIds = new Map<string, string>();
|
||||
for (const mediaNode of mediaNodes) {
|
||||
try {
|
||||
const result = await ensureCanvasNodeAsset({ canvasId: projectId, domainProjectId, node: mediaNode, source: "canvas-manual" });
|
||||
assetIds.set(mediaNode.id, result.assetId);
|
||||
} catch (error) {
|
||||
message.warning(`媒体节点已创建,但素材库写入失败:${error instanceof Error ? error.message : "未知错误"}`);
|
||||
}
|
||||
}
|
||||
if (assetIds.size > 0) {
|
||||
setNodes((current) => current.map((item) => {
|
||||
const assetId = assetIds.get(item.id);
|
||||
return assetId ? { ...item, metadata: { ...item.metadata, assetId } } : item;
|
||||
}));
|
||||
}
|
||||
return assetIds;
|
||||
}, [domainProjectId, message, projectId, setNodes]);
|
||||
|
||||
const createImageReversePromptNodes = useCallback((node: CanvasNodeData) => {
|
||||
if (node.type !== CanvasNodeType.Image || !node.metadata?.content) {
|
||||
message.warning("图片节点为空,无法反推提示词");
|
||||
@@ -184,7 +203,8 @@ export function useCanvasMediaTools({
|
||||
setSelectedNodeIds(new Set([childId]));
|
||||
setDialogNodeId(childId);
|
||||
setCropNodeId(null);
|
||||
}, [setConnections, setDialogNodeId, setNodes, setSelectedNodeIds]);
|
||||
await persistMediaNodes([child]);
|
||||
}, [persistMediaNodes, setConnections, setDialogNodeId, setNodes, setSelectedNodeIds]);
|
||||
|
||||
const saveAnnotatedImageNode = useCallback(async (node: CanvasNodeData, dataUrl: string) => {
|
||||
const image = await uploadImage(dataUrl);
|
||||
@@ -197,8 +217,9 @@ export function useCanvasMediaTools({
|
||||
setSelectedConnectionId(null);
|
||||
setDialogNodeId(null);
|
||||
setAnnotationNodeId(null);
|
||||
await persistMediaNodes([child]);
|
||||
message.success("标注图片已保存为新节点");
|
||||
}, [message, setConnections, setDialogNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds]);
|
||||
}, [message, persistMediaNodes, setConnections, setDialogNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds]);
|
||||
|
||||
const openVideoFrameExtractor = useCallback((node: CanvasNodeData) => {
|
||||
if (!node.metadata?.content) {
|
||||
@@ -251,6 +272,7 @@ export function useCanvasMediaTools({
|
||||
setConnections(nextConnections);
|
||||
setSelectedNodeIds(selection);
|
||||
setSelectedConnectionId(null);
|
||||
await persistMediaNodes(frameNodes);
|
||||
const failedCount = captured.failures.length + uploadFailures.length;
|
||||
progress.done(failedCount ? `已提取 ${frameNodes.length} 帧,${failedCount} 帧失败` : `已提取 ${frameNodes.length} 帧并创建图片节点`);
|
||||
if (failedCount) message.warning(`${failedCount} 个时间点提取失败,其余画面已创建`);
|
||||
@@ -262,7 +284,7 @@ export function useCanvasMediaTools({
|
||||
extractingVideoFramesNodeIdRef.current = null;
|
||||
setExtractingVideoFramesNodeId(null);
|
||||
}
|
||||
}, [connectionsRef, message, nodesRef, selectedNodeIdsRef, setConnections, setNodes, setSelectedConnectionId, setSelectedNodeIds, startUploadStatus]);
|
||||
}, [connectionsRef, message, nodesRef, persistMediaNodes, selectedNodeIdsRef, setConnections, setNodes, setSelectedConnectionId, setSelectedNodeIds, startUploadStatus]);
|
||||
|
||||
const extractAudioFromVideo = useCallback((node: CanvasNodeData) => {
|
||||
if (!node.metadata?.content) {
|
||||
@@ -502,6 +524,7 @@ export function useCanvasMediaTools({
|
||||
setSelectedNodeIds(selection);
|
||||
setSelectedConnectionId(null);
|
||||
setDialogNodeId(null);
|
||||
await persistMediaNodes([mergedNode]);
|
||||
setMergeVideoProgress({ phase: "encoding", progress: 100 });
|
||||
message.success(`已合并 ${videos.length} 段视频,成片节点已添加`);
|
||||
} catch (error) {
|
||||
@@ -510,7 +533,7 @@ export function useCanvasMediaTools({
|
||||
mergeVideoRunningRef.current = false;
|
||||
window.setTimeout(() => setMergeVideoProgress(null), 700);
|
||||
}
|
||||
}, [connectionsRef, message, nodesRef, selectedNodeIdsRef, setConnections, setDialogNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds]);
|
||||
}, [connectionsRef, message, nodesRef, persistMediaNodes, selectedNodeIdsRef, setConnections, setDialogNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds]);
|
||||
|
||||
const mergeSelectedVideos = useCallback(() => mergeVideosByIds(Array.from(selectedNodeIdsRef.current)), [mergeVideosByIds, selectedNodeIdsRef]);
|
||||
|
||||
@@ -539,8 +562,9 @@ export function useCanvasMediaTools({
|
||||
setSelectedNodeIds(new Set(childNodes.map((child) => child.id)));
|
||||
setSelectedConnectionId(null);
|
||||
setDialogNodeId(null);
|
||||
await persistMediaNodes(childNodes);
|
||||
message.success(`已切分为 ${childNodes.length} 个子节点`);
|
||||
}, [message, setConnections, setDialogNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds]);
|
||||
}, [message, persistMediaNodes, setConnections, setDialogNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds]);
|
||||
|
||||
const maskEditImageNode = useCallback(async (node: CanvasNodeData, payload: CanvasImageMaskEditPayload) => {
|
||||
if (!node.metadata?.content) return;
|
||||
@@ -572,7 +596,11 @@ export function useCanvasMediaTools({
|
||||
if (!image?.dataUrl) throw new Error("后端任务没有返回图片");
|
||||
const uploaded = await uploadImage(image.dataUrl);
|
||||
const size = fitNodeSize(uploaded.width, uploaded.height, node.width, node.height);
|
||||
setNodes((current) => current.map((item) => item.id === childId ? { ...item, width: size.width, height: size.height, metadata: { ...item.metadata, ...imageMetadata(uploaded), prompt: effectivePrompt, ...generationMetadata } } : item));
|
||||
const currentNode = nodesRef.current.find((item) => item.id === childId);
|
||||
if (!currentNode) throw new Error("局部编辑节点已被删除");
|
||||
const finalizedNode = { ...currentNode, width: size.width, height: size.height, metadata: { ...currentNode.metadata, ...imageMetadata(uploaded), prompt: effectivePrompt, ...generationMetadata } };
|
||||
setNodes((current) => current.map((item) => item.id === childId ? finalizedNode : item));
|
||||
await persistMediaNodes([finalizedNode]);
|
||||
} catch (error) {
|
||||
if (isGenerationCanceled(error)) return;
|
||||
const details = generationErrorMessage(error);
|
||||
@@ -582,7 +610,7 @@ export function useCanvasMediaTools({
|
||||
finishGenerationRequest(childId, controller);
|
||||
setRunningNodeId(null);
|
||||
}
|
||||
}, [bindGenerationTask, effectiveConfig, finishGenerationRequest, isAiConfigReady, message, projectId, resolveImageEditStyle, setConnections, setDialogNodeId, setNodes, setRunningNodeId, setSelectedConnectionId, setSelectedNodeIds, startGenerationRequest]);
|
||||
}, [bindGenerationTask, effectiveConfig, finishGenerationRequest, isAiConfigReady, message, nodesRef, persistMediaNodes, projectId, resolveImageEditStyle, setConnections, setDialogNodeId, setNodes, setRunningNodeId, setSelectedConnectionId, setSelectedNodeIds, startGenerationRequest]);
|
||||
|
||||
const upscaleImageNode = useCallback(async (node: CanvasNodeData, params: CanvasImageUpscaleParams) => {
|
||||
if (!node.metadata?.content) return;
|
||||
@@ -596,7 +624,8 @@ export function useCanvasMediaTools({
|
||||
setConnections((current) => [...current, { id: nanoid(), fromNodeId: node.id, toNodeId: childId }]);
|
||||
setSelectedNodeIds(new Set([childId]));
|
||||
setDialogNodeId(childId);
|
||||
}, [setConnections, setDialogNodeId, setNodes, setSelectedNodeIds]);
|
||||
await persistMediaNodes([child]);
|
||||
}, [persistMediaNodes, setConnections, setDialogNodeId, setNodes, setSelectedNodeIds]);
|
||||
|
||||
const generateAngleNode = useCallback(async (node: CanvasNodeData, params: CanvasImageAngleParams) => {
|
||||
if (!node.metadata?.content) return;
|
||||
@@ -628,7 +657,11 @@ export function useCanvasMediaTools({
|
||||
if (!image?.dataUrl) throw new Error("后端任务没有返回图片");
|
||||
const uploaded = await uploadImage(image.dataUrl);
|
||||
const size = fitNodeSize(uploaded.width, uploaded.height, imageSpec.width, imageSpec.height);
|
||||
setNodes((current) => current.map((item) => item.id === childId ? { ...item, width: size.width, height: size.height, metadata: { ...item.metadata, ...imageMetadata(uploaded), prompt: effectivePrompt, ...generationMetadata } } : item));
|
||||
const currentNode = nodesRef.current.find((item) => item.id === childId);
|
||||
if (!currentNode) throw new Error("视角生成节点已被删除");
|
||||
const finalizedNode = { ...currentNode, width: size.width, height: size.height, metadata: { ...currentNode.metadata, ...imageMetadata(uploaded), prompt: effectivePrompt, ...generationMetadata } };
|
||||
setNodes((current) => current.map((item) => item.id === childId ? finalizedNode : item));
|
||||
await persistMediaNodes([finalizedNode]);
|
||||
} catch (error) {
|
||||
if (isGenerationCanceled(error)) return;
|
||||
const details = generationErrorMessage(error);
|
||||
@@ -637,7 +670,7 @@ export function useCanvasMediaTools({
|
||||
finishGenerationRequest(childId, controller);
|
||||
setRunningNodeId(null);
|
||||
}
|
||||
}, [bindGenerationTask, effectiveConfig, finishGenerationRequest, isAiConfigReady, projectId, resolveImageEditStyle, setConnections, setDialogNodeId, setNodes, setRunningNodeId, setSelectedNodeIds, startGenerationRequest]);
|
||||
}, [bindGenerationTask, effectiveConfig, finishGenerationRequest, isAiConfigReady, nodesRef, persistMediaNodes, projectId, resolveImageEditStyle, setConnections, setDialogNodeId, setNodes, setRunningNodeId, setSelectedNodeIds, startGenerationRequest]);
|
||||
|
||||
const generateEmotionNode = useCallback(async (node: CanvasNodeData, payload: CanvasImageEmotionPayload) => {
|
||||
if (!node.metadata?.content) return;
|
||||
@@ -689,14 +722,18 @@ export function useCanvasMediaTools({
|
||||
const composited = await compositeEmotionImage(node.metadata.content, image.dataUrl, payload.editRegion, payload.faceBox);
|
||||
const uploaded = await uploadImage(composited);
|
||||
const size = fitNodeSize(uploaded.width, uploaded.height, node.width, node.height);
|
||||
setNodes((current) => current.map((item) => item.id === childId ? { ...item, width: size.width, height: size.height, metadata: { ...item.metadata, ...imageMetadata(uploaded), prompt: providerPrompt, ...generationMetadata, emotionEdit } } : item));
|
||||
const currentNode = nodesRef.current.find((item) => item.id === childId);
|
||||
if (!currentNode) throw new Error("表情编辑节点已被删除");
|
||||
const finalizedNode = { ...currentNode, width: size.width, height: size.height, metadata: { ...currentNode.metadata, ...imageMetadata(uploaded), prompt: providerPrompt, ...generationMetadata, emotionEdit } };
|
||||
setNodes((current) => current.map((item) => item.id === childId ? finalizedNode : item));
|
||||
await persistMediaNodes([finalizedNode]);
|
||||
} catch (error) {
|
||||
if (isGenerationCanceled(error)) return;
|
||||
const details = generationErrorMessage(error);
|
||||
message.error(details);
|
||||
setNodes((current) => current.map((item) => item.id === childId ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_ERROR, errorDetails: details } } : item));
|
||||
} finally { finishGenerationRequest(childId, controller); setRunningNodeId(null); }
|
||||
}, [bindGenerationTask, effectiveConfig, finishGenerationRequest, isAiConfigReady, message, projectId, resolveImageEditStyle, setConnections, setDialogNodeId, setNodes, setRunningNodeId, setSelectedConnectionId, setSelectedNodeIds, startGenerationRequest]);
|
||||
}, [bindGenerationTask, effectiveConfig, finishGenerationRequest, isAiConfigReady, message, nodesRef, persistMediaNodes, projectId, resolveImageEditStyle, setConnections, setDialogNodeId, setNodes, setRunningNodeId, setSelectedConnectionId, setSelectedNodeIds, startGenerationRequest]);
|
||||
|
||||
return {
|
||||
angleNodeId,
|
||||
|
||||
@@ -116,6 +116,31 @@ export function useCanvasUpload({
|
||||
}
|
||||
}, [canvasId, domainProjectId, message, queryClient, setNodes]);
|
||||
|
||||
const persistTimelineMedia = useCallback(async (media: TimelineDirectMedia) => {
|
||||
const type = media.kind === "audio" ? CanvasNodeType.Audio : media.kind === "video" ? CanvasNodeType.Video : CanvasNodeType.Image;
|
||||
const defaults = NODE_DEFAULT_SIZE[type];
|
||||
const node: CanvasNodeData = {
|
||||
id: media.id,
|
||||
type,
|
||||
title: media.title,
|
||||
position: { x: 0, y: 0 },
|
||||
width: media.width || defaults.width,
|
||||
height: media.height || defaults.height,
|
||||
metadata: {
|
||||
content: media.url || media.dataUrl || media.content || "",
|
||||
storageKey: media.storageKey,
|
||||
naturalWidth: media.width,
|
||||
naturalHeight: media.height,
|
||||
durationMs: media.durationMs,
|
||||
bytes: media.bytes,
|
||||
mimeType: media.mimeType,
|
||||
},
|
||||
};
|
||||
const result = await ensureCanvasNodeAsset({ canvasId, domainProjectId, node, source: "canvas-upload" });
|
||||
if (domainProjectId) await queryClient.invalidateQueries({ queryKey: ["project", domainProjectId] });
|
||||
return result.assetId;
|
||||
}, [canvasId, domainProjectId, queryClient]);
|
||||
|
||||
const createImageFileNode = useCallback(async (file: File, position: Position) => {
|
||||
const progress = startUploadStatus("上传图片", "读取图片文件", domainProjectId ? 4 : 3);
|
||||
try {
|
||||
@@ -343,7 +368,7 @@ export function useCanvasUpload({
|
||||
try {
|
||||
if (isAudioFile(file)) {
|
||||
const audio = await uploadMediaFile(file, "audio");
|
||||
created.push({
|
||||
const media: TimelineDirectMedia = {
|
||||
id: `audio-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
kind: "audio",
|
||||
title: file.name,
|
||||
@@ -352,10 +377,12 @@ export function useCanvasUpload({
|
||||
durationMs: audio.durationMs,
|
||||
bytes: audio.bytes,
|
||||
mimeType: audio.mimeType,
|
||||
});
|
||||
};
|
||||
media.assetId = await persistTimelineMedia(media);
|
||||
created.push(media);
|
||||
} else {
|
||||
const video = await uploadMediaFile(file, "video");
|
||||
created.push({
|
||||
const media: TimelineDirectMedia = {
|
||||
id: `video-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
kind: "video",
|
||||
title: file.name,
|
||||
@@ -366,7 +393,9 @@ export function useCanvasUpload({
|
||||
durationMs: video.durationMs,
|
||||
bytes: video.bytes,
|
||||
mimeType: video.mimeType,
|
||||
});
|
||||
};
|
||||
media.assetId = await persistTimelineMedia(media);
|
||||
created.push(media);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? `素材上传失败:${error.message}` : "素材上传失败");
|
||||
@@ -374,7 +403,7 @@ export function useCanvasUpload({
|
||||
}
|
||||
if (created.length) message.success(`已上传 ${created.length} 个素材到时间线`);
|
||||
return created;
|
||||
}, [message]);
|
||||
}, [message, persistTimelineMedia]);
|
||||
|
||||
// 组装能力闭环:把时间线合成结果(MP4 Blob)上传并创建为新的视频节点放回画布,
|
||||
// 复用上传/持久化/选中逻辑,新节点可继续编辑字幕与样式。
|
||||
|
||||
Reference in New Issue
Block a user