|
|
@ -20,6 +20,7 @@ const KEYFRAMES_ACTION_LABEL = "\u9996\u5c3e\u5e27\u751f\u6210\u89c6\u9891"; |
|
|
const FIRST_FRAME_ACTION_LABEL = "\u9996\u5e27\u751f\u6210\u89c6\u9891"; |
|
|
const FIRST_FRAME_ACTION_LABEL = "\u9996\u5e27\u751f\u6210\u89c6\u9891"; |
|
|
const FIRST_FRAME_LABEL = "\u9996\u5e27"; |
|
|
const FIRST_FRAME_LABEL = "\u9996\u5e27"; |
|
|
const LAST_FRAME_LABEL = "\u5c3e\u5e27"; |
|
|
const LAST_FRAME_LABEL = "\u5c3e\u5e27"; |
|
|
|
|
|
const TARGET_VIDEO_LABEL = "\u89c6\u9891\u8282\u70b9"; |
|
|
|
|
|
|
|
|
export function VideoInputNode({ id, data, selected }: NodeProps<VideoInputNodeType>) { |
|
|
export function VideoInputNode({ id, data, selected }: NodeProps<VideoInputNodeType>) { |
|
|
const nodeData = data; |
|
|
const nodeData = data; |
|
|
@ -32,8 +33,8 @@ export function VideoInputNode({ id, data, selected }: NodeProps<VideoInputNodeT |
|
|
// Use blob URL for efficient playback of large base64 videos
|
|
|
// Use blob URL for efficient playback of large base64 videos
|
|
|
const playbackUrl = useVideoBlobUrl(nodeData.video ?? null); |
|
|
const playbackUrl = useVideoBlobUrl(nodeData.video ?? null); |
|
|
|
|
|
|
|
|
// 只负责搭建“首帧/尾帧 -> 生成视频”的节点链路,视频文件由用户后续在各节点上传。
|
|
|
// 只负责搭建“首帧/尾帧 -> 视频节点”的链路,视频文件由用户后续在各节点上传。
|
|
|
const createGenerateVideoChain = useCallback( |
|
|
const createVideoNodeChain = useCallback( |
|
|
(hasLastFrame: boolean) => { |
|
|
(hasLastFrame: boolean) => { |
|
|
const currentNode = nodes.find((node) => node.id === id); |
|
|
const currentNode = nodes.find((node) => node.id === id); |
|
|
const currentWidth = |
|
|
const currentWidth = |
|
|
@ -43,7 +44,7 @@ export function VideoInputNode({ id, data, selected }: NodeProps<VideoInputNodeT |
|
|
currentNode?.measured?.height ?? |
|
|
currentNode?.measured?.height ?? |
|
|
(typeof currentNode?.style?.height === "number" ? currentNode.style.height : defaultNodeDimensions.videoInput.height); |
|
|
(typeof currentNode?.style?.height === "number" ? currentNode.style.height : defaultNodeDimensions.videoInput.height); |
|
|
const currentPosition = currentNode?.position ?? { x: 0, y: 0 }; |
|
|
const currentPosition = currentNode?.position ?? { x: 0, y: 0 }; |
|
|
const generateVideoHeight = defaultNodeDimensions.generateVideo.height; |
|
|
const targetVideoHeight = defaultNodeDimensions.videoInput.height; |
|
|
const verticalGap = 52; |
|
|
const verticalGap = 52; |
|
|
|
|
|
|
|
|
// 复用当前空视频节点作为首帧节点,避免点击快捷入口后留下多余空节点。
|
|
|
// 复用当前空视频节点作为首帧节点,避免点击快捷入口后留下多余空节点。
|
|
|
@ -61,19 +62,23 @@ export function VideoInputNode({ id, data, selected }: NodeProps<VideoInputNodeT |
|
|
: null; |
|
|
: null; |
|
|
|
|
|
|
|
|
const targetY = hasLastFrame |
|
|
const targetY = hasLastFrame |
|
|
? currentPosition.y + currentHeight + verticalGap / 2 - generateVideoHeight / 2 |
|
|
? currentPosition.y + currentHeight + verticalGap / 2 - targetVideoHeight / 2 |
|
|
: currentPosition.y; |
|
|
: currentPosition.y; |
|
|
|
|
|
|
|
|
// 生成节点放在素材节点右侧;双帧模式下按两帧整体高度居中。
|
|
|
// 右侧目标同样创建为空视频节点,用户后续可以自行上传或继续接入下一步。
|
|
|
const generateVideoNodeId = addNode("generateVideo", { |
|
|
const targetVideoNodeId = addNode( |
|
|
x: currentPosition.x + currentWidth + 180, |
|
|
"videoInput", |
|
|
y: targetY, |
|
|
{ |
|
|
}); |
|
|
x: currentPosition.x + currentWidth + 180, |
|
|
|
|
|
y: targetY, |
|
|
|
|
|
}, |
|
|
|
|
|
{ label: TARGET_VIDEO_LABEL } |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
onConnect({ |
|
|
onConnect({ |
|
|
source: id, |
|
|
source: id, |
|
|
sourceHandle: "video", |
|
|
sourceHandle: "video", |
|
|
target: generateVideoNodeId, |
|
|
target: targetVideoNodeId, |
|
|
targetHandle: "video", |
|
|
targetHandle: "video", |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
@ -81,7 +86,7 @@ export function VideoInputNode({ id, data, selected }: NodeProps<VideoInputNodeT |
|
|
onConnect({ |
|
|
onConnect({ |
|
|
source: lastFrameNodeId, |
|
|
source: lastFrameNodeId, |
|
|
sourceHandle: "video", |
|
|
sourceHandle: "video", |
|
|
target: generateVideoNodeId, |
|
|
target: targetVideoNodeId, |
|
|
targetHandle: "video", |
|
|
targetHandle: "video", |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
@ -199,7 +204,7 @@ export function VideoInputNode({ id, data, selected }: NodeProps<VideoInputNodeT |
|
|
onClick={(event) => { |
|
|
onClick={(event) => { |
|
|
event.preventDefault(); |
|
|
event.preventDefault(); |
|
|
event.stopPropagation(); |
|
|
event.stopPropagation(); |
|
|
createGenerateVideoChain(true); |
|
|
createVideoNodeChain(true); |
|
|
}} |
|
|
}} |
|
|
> |
|
|
> |
|
|
<svg className="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}> |
|
|
<svg className="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}> |
|
|
@ -213,7 +218,7 @@ export function VideoInputNode({ id, data, selected }: NodeProps<VideoInputNodeT |
|
|
onClick={(event) => { |
|
|
onClick={(event) => { |
|
|
event.preventDefault(); |
|
|
event.preventDefault(); |
|
|
event.stopPropagation(); |
|
|
event.stopPropagation(); |
|
|
createGenerateVideoChain(false); |
|
|
createVideoNodeChain(false); |
|
|
}} |
|
|
}} |
|
|
> |
|
|
> |
|
|
<svg className="h-4 w-4 shrink-0" fill="currentColor" viewBox="0 0 24 24"> |
|
|
<svg className="h-4 w-4 shrink-0" fill="currentColor" viewBox="0 0 24 24"> |
|
|
|