feat(canvas): 视频画面提取 - 支持批量取帧并将片段截取与生成解耦

This commit is contained in:
ddCat committed 2026-08-29 08:48:38 +08:00
1 parent 6202b6e762
commit 86019cccea
18 files changed
+586 -155

No files matched your search

+23 -8
View File
@@ -39,6 +39,7 @@ import { resolveProjectCanvasStyle } from "@/components/canvas/canvas-style-pick
import { createStyleProfileSnapshot, resolveStyleProfile, serializeStyleProfile } from "@/lib/canvas/style-profile";
import { CanvasNodeToolbar, CanvasNodeInfoModal } from "@/components/canvas/canvas-node-toolbar";
import { CanvasSubtitleDialog } from "@/components/canvas/canvas-subtitle-dialog";
import { CanvasVideoFrameDialog } from "@/components/canvas/canvas-video-frame-dialog";
import { CanvasVideoSegmentDialog } from "@/components/canvas/canvas-video-segment-dialog";
import { CanvasTimelineDialog } from "@/components/canvas/canvas-timeline-dialog";
import { syncNodeSubtitlesToTimeline } from "@/lib/timeline/timeline-build";
@@ -801,10 +802,12 @@ function InfiniteCanvasPage() {
openPortraitTextureEditor,
cropImageNode,
cropNodeId,
closeFrameDialog,
closeSegmentDialog,
extractAudioFromVideo,
extractVideoLastFrame,
extractingVideoFrameNodeId,
extractVideoFrames,
extractingVideoFramesNodeId,
frameDialogNodeId,
generateAngleNode,
generateEmotionNode,
handleSegmentConfirm,
@@ -817,6 +820,7 @@ function InfiniteCanvasPage() {
segmentDialogMode,
segmentDialogNodeId,
segmentRunningMode,
setFrameDialogNodeId,
setSegmentDialogNodeId,
setAngleNodeId,
setEmotionNodeId,
@@ -827,7 +831,8 @@ function InfiniteCanvasPage() {
setUpscaleNodeId,
splitImageNode,
splitNodeId,
trimVideoAndRegenerate,
openVideoFrameExtractor,
openVideoSegmentExtractor,
upscaleImageNode,
upscaleNodeId,
} = useCanvasMediaTools({
@@ -849,7 +854,6 @@ function InfiniteCanvasPage() {
startGenerationRequest,
finishGenerationRequest,
bindGenerationTask,
onGenerateVideoNode: (nodeId, mode, prompt) => generateNodeRef.current?.(nodeId, mode, prompt),
});
const handleNodesDeleted = useCallback(
@@ -863,6 +867,7 @@ function InfiniteCanvasPage() {
setDrawingNodeId(clearDeletedId);
setInfoNodeId(clearDeletedId);
setSubtitleNodeId(clearDeletedId);
setFrameDialogNodeId(clearDeletedId);
setSegmentDialogNodeId(clearDeletedId);
setCropNodeId(clearDeletedId);
setMaskEditNodeId(clearDeletedId);
@@ -886,7 +891,7 @@ function InfiniteCanvasPage() {
}
cleanupCanvasFiles({ projectId, nodes: nextNodes, chatSessions });
},
[chatSessions, cleanupCanvasFiles, message, projectId, setAngleNodeId, setAnnotationNodeId, setCropNodeId, setEmotionNodeId, setMaskEditNodeId, setSegmentDialogNodeId, setSplitNodeId, setUpscaleNodeId, setRunningNodeId],
[chatSessions, cleanupCanvasFiles, message, projectId, setAngleNodeId, setAnnotationNodeId, setCropNodeId, setEmotionNodeId, setFrameDialogNodeId, setMaskEditNodeId, setSegmentDialogNodeId, setSplitNodeId, setUpscaleNodeId, setRunningNodeId],
);
const {
@@ -1254,6 +1259,7 @@ function InfiniteCanvasPage() {
const dialogNode = dialogNodeId ? nodeById.get(dialogNodeId) || null : null;
const subtitleNode = subtitleNodeId ? nodeById.get(subtitleNodeId) || null : null;
const timelineNode = timelineNodeId ? nodeById.get(timelineNodeId) || null : null;
const frameNode = frameDialogNodeId ? nodeById.get(frameDialogNodeId) || null : null;
const segmentNode = segmentDialogNodeId ? nodeById.get(segmentDialogNodeId) || null : null;
const textEditorNode = textEditorNodeId ? nodeById.get(textEditorNodeId) || null : null;
const characterReferenceNode = characterReferenceNodeId ? nodeById.get(characterReferenceNodeId) || null : null;
@@ -2392,12 +2398,12 @@ function InfiniteCanvasPage() {
setAngleNodeId((current) => (current === node.id ? null : node.id));
}}
onViewImage={(node) => setPreviewNodeId(node.id)}
onExtractVideoLastFrame={(node) => void extractVideoLastFrame(node)}
onExtractVideoFrames={openVideoFrameExtractor}
onExtractAudioFromVideo={(node) => void extractAudioFromVideo(node)}
onTrimVideoRegenerate={(node) => void trimVideoAndRegenerate(node)}
onTrimVideoSegments={openVideoSegmentExtractor}
onSubtitles={(node) => setSubtitleNodeId(node.id)}
onTimeline={(node) => setTimelineNodeId(node.id)}
extractingVideoFrame={toolbarNode?.id === extractingVideoFrameNodeId}
extractingVideoFrames={toolbarNode?.id === extractingVideoFramesNodeId}
extractingAudio={segmentRunningMode === "audio"}
trimmingVideo={segmentRunningMode === "video"}
onReversePrompt={createImageReversePromptNodes}
@@ -2505,6 +2511,15 @@ function InfiniteCanvasPage() {
/>
) : null}
{frameNode ? (
<CanvasVideoFrameDialog
node={frameNode}
open={Boolean(frameNode)}
onClose={closeFrameDialog}
onConfirm={(params) => void extractVideoFrames(frameNode, params)}
/>
) : null}
{segmentNode && segmentDialogMode ? (
<CanvasVideoSegmentDialog
node={segmentNode}