From d8a1db6f8b6e645974af058c8a37eabb98221225 Mon Sep 17 00:00:00 2001 From: TianYun Date: Thu, 11 Jun 2026 15:01:07 +0800 Subject: [PATCH] =?UTF-8?q?bug=20=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.ts | 1 + .../__tests__/VideoStitchNode.test.tsx | 6 +- .../composer/GenerationComposer.tsx | 39 ++++--------- src/components/nodes/SmartAudioNode.tsx | 48 +++++++++++++-- src/components/nodes/SmartImageNode.tsx | 58 ++++++++++++++++++- src/components/nodes/SmartVideoNode.tsx | 52 +++++++++++++++-- src/components/nodes/VideoTrimNode.tsx | 9 +-- src/hooks/useVideoBlobUrl.ts | 5 +- src/utils/mediaResolver.ts | 2 +- src/utils/smartGenerateUpload.ts | 3 + 10 files changed, 168 insertions(+), 55 deletions(-) create mode 100644 src/utils/smartGenerateUpload.ts diff --git a/next.config.ts b/next.config.ts index 2160d996..d2233680 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,6 +3,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { reactStrictMode: true, output: "standalone", + allowedDevOrigins: ["192.168.77.32"], experimental: { proxyClientMaxBodySize: "32mb", serverActions: { diff --git a/src/components/__tests__/VideoStitchNode.test.tsx b/src/components/__tests__/VideoStitchNode.test.tsx index 9226cf24..34e7e361 100644 --- a/src/components/__tests__/VideoStitchNode.test.tsx +++ b/src/components/__tests__/VideoStitchNode.test.tsx @@ -123,12 +123,10 @@ describe("VideoStitchNode", () => { }); describe("Thumbnail Source Resolution", () => { - it("routes remote video thumbnails through the media proxy", () => { + it("keeps remote video thumbnails unchanged", () => { const remoteUrl = "https://cdn.example.com/video.mp4?token=a b"; - expect(getVideoStitchThumbnailSrc(remoteUrl)).toBe( - `/api/proxy-media?url=${encodeURIComponent(remoteUrl)}` - ); + expect(getVideoStitchThumbnailSrc(remoteUrl)).toBe(remoteUrl); }); it("keeps local video sources unchanged", () => { diff --git a/src/components/composer/GenerationComposer.tsx b/src/components/composer/GenerationComposer.tsx index fe3ab174..e7d536a2 100644 --- a/src/components/composer/GenerationComposer.tsx +++ b/src/components/composer/GenerationComposer.tsx @@ -656,34 +656,17 @@ function isVideoHandle(handleId: string | null | undefined): boolean { return handleId === "video" || handleId.startsWith("video-") || handleId.includes("video"); } -function isImageHandle(handleId: string | null | undefined): boolean { - if (!handleId) return false; - return handleId === "image" || handleId.startsWith("image-") || handleId.includes("frame"); -} - -function isTextHandle(handleId: string | null | undefined): boolean { - if (!handleId) return false; - return handleId === "text" || handleId.startsWith("text-") || handleId.includes("prompt"); -} - -function isAudioHandle(handleId: string | null | undefined): boolean { - if (!handleId) return false; - return handleId === "audio" || handleId.startsWith("audio-") || handleId.includes("audio"); -} - function getConnectedEdgeIdsByType( targetNodeId: string, + nodes: WorkflowNode[], edges: WorkflowEdge[], type: ComposerReferenceMaterialType | "text" ): string[] { return edges .filter((edge) => { if (edge.target !== targetNodeId) return false; - const handleIds = [edge.targetHandle, edge.sourceHandle]; - if (type === "image") return handleIds.some(isImageHandle); - if (type === "video") return handleIds.some(isVideoHandle); - if (type === "audio") return handleIds.some(isAudioHandle); - return handleIds.some(isTextHandle); + const sourceNode = nodes.find((node) => node.id === edge.source); + return sourceNode ? getNodeOutputMediaType(sourceNode) === type : false; }) .map((edge) => edge.id); } @@ -1822,12 +1805,12 @@ export function GenerationComposer() { const connectedTextItems = useMemo(() => getConnectedTextItems(connectedInputs), [connectedInputs]); const connectedTextCards = useMemo(() => { if (!context.node) return connectedTextItems.map((text) => ({ text })); - const textEdgeIds = getConnectedEdgeIdsByType(context.node.id, edges, "text"); + const textEdgeIds = getConnectedEdgeIdsByType(context.node.id, nodes, edges, "text"); return connectedTextItems.map((text, index) => ({ text, ...(textEdgeIds[index] ? { edgeId: textEdgeIds[index] } : {}), })); - }, [connectedTextItems, context.node, edges]); + }, [connectedTextItems, context.node, edges, nodes]); const promptValue = draft.prompt; const promptReadOnly = false; const activeCapability = context.mode === "empty-create" @@ -1892,34 +1875,34 @@ export function GenerationComposer() { const referenceImageInputs = useMemo(() => { const imageEdgeIds = context.mode === "node-edit" && context.node && connectedReferenceImages.length > 0 - ? getConnectedEdgeIdsByType(context.node.id, edges, "image") + ? getConnectedEdgeIdsByType(context.node.id, nodes, edges, "image") : []; return referenceImages.map((url, index) => ({ url, ...(imageEdgeIds[index] ? { edgeId: imageEdgeIds[index] } : {}), })); - }, [connectedReferenceImages.length, context.mode, context.node, edges, referenceImages]); + }, [connectedReferenceImages.length, context.mode, context.node, edges, nodes, referenceImages]); const referenceVideoInputs = useMemo(() => { const videoEdgeIds = context.mode === "node-edit" && context.node - ? getConnectedEdgeIdsByType(context.node.id, edges, "video") + ? getConnectedEdgeIdsByType(context.node.id, nodes, edges, "video") : []; return connectedReferenceVideos.map((url, index) => ({ url, ...(connectedVideoDurations[index] && connectedVideoDurations[index] > 0 ? { duration: connectedVideoDurations[index] } : {}), ...(videoEdgeIds[index] ? { edgeId: videoEdgeIds[index] } : {}), })); - }, [connectedReferenceVideos, connectedVideoDurations, context.mode, context.node, edges]); + }, [connectedReferenceVideos, connectedVideoDurations, context.mode, context.node, edges, nodes]); const referenceAudioInputs = useMemo(() => { const audioEdgeIds = context.mode === "node-edit" && context.node - ? getConnectedEdgeIdsByType(context.node.id, edges, "audio") + ? getConnectedEdgeIdsByType(context.node.id, nodes, edges, "audio") : []; return connectedReferenceAudio.map((url, index) => ({ url, ...(audioEdgeIds[index] ? { edgeId: audioEdgeIds[index] } : {}), })); - }, [connectedReferenceAudio, context.mode, context.node, edges]); + }, [connectedReferenceAudio, context.mode, context.node, edges, nodes]); const baseReferenceMaterials = useMemo(() => buildComposerReferenceMaterials( referenceImageInputs, referenceVideoInputs, diff --git a/src/components/nodes/SmartAudioNode.tsx b/src/components/nodes/SmartAudioNode.tsx index 1c2882d7..8f738cf0 100644 --- a/src/components/nodes/SmartAudioNode.tsx +++ b/src/components/nodes/SmartAudioNode.tsx @@ -8,7 +8,8 @@ import { useI18n } from "@/i18n"; import { useWorkflowStore } from "@/store/workflowStore"; import { SmartAudioNodeData } from "@/types"; import { uploadFileToGatewayMedia } from "@/utils/gatewayMediaUpload"; -import { deriveSmartAudioMode } from "@/utils/smartAudioMode"; +import { isSmartGenerateUploadEnabled } from "@/utils/smartGenerateUpload"; +import { deriveSmartAudioMode, isSmartAudioGenerateMode } from "@/utils/smartAudioMode"; import { AudioInputNodeView } from "./AudioInputNode"; import { GenerateAudioNodeView } from "./GenerateAudioNode"; @@ -19,16 +20,43 @@ function SmartAudioUploadToolbar({ id, selected, isUploading, + addToGenerationHistory = false, }: { id: string; selected?: boolean; isUploading: boolean; + addToGenerationHistory?: boolean; }) { const { t } = useI18n(); const updateNodeData = useWorkflowStore((state) => state.updateNodeData); const selectSingleNode = useWorkflowStore((state) => state.selectSingleNode); const fileInputRef = useRef(null); + const buildUploadedAudioPatch = useCallback( + (audioUrl: string): Partial => { + if (!addToGenerationHistory) return {}; + + const currentNode = useWorkflowStore.getState().getNodeById(id); + const currentData = (currentNode?.data ?? {}) as SmartAudioNodeData; + const timestamp = Date.now(); + return { + outputAudio: audioUrl, + audioHistory: [ + { + id: `${timestamp}`, + audio: audioUrl, + timestamp, + prompt: currentData.inputPrompt || "", + model: currentData.selectedModel?.modelId || "", + }, + ...(currentData.audioHistory || []), + ].slice(0, 50), + selectedAudioHistoryIndex: 0, + }; + }, + [addToGenerationHistory, id] + ); + const applyAudio = useCallback( (audioFile: string, filename: string, format: string, duration: number | null) => { updateNodeData(id, { @@ -37,9 +65,10 @@ function SmartAudioUploadToolbar({ filename, format, duration, + ...buildUploadedAudioPatch(audioFile), }); }, - [id, updateNodeData] + [buildUploadedAudioPatch, id, updateNodeData] ); const handleFile = useCallback( @@ -128,17 +157,24 @@ function SmartAudioEmptyView({ id, data, selected, + addToGenerationHistory = false, }: { id: string; data: SmartAudioNodeData; selected?: boolean; + addToGenerationHistory?: boolean; }) { const isUploading = data.uploadStatus === "loading"; return (
- + {isUploading && (
@@ -151,8 +187,12 @@ function SmartAudioEmptyView({ export function SmartAudioNode({ id, data, selected }: NodeProps) { const edges = useWorkflowStore((state) => state.edges); const mode = deriveSmartAudioMode(id, data, edges); + const smartGenerateUploadEnabled = isSmartGenerateUploadEnabled(); - if (mode === "generate") { + if (mode === "generate" || (smartGenerateUploadEnabled && isSmartAudioGenerateMode(id, edges))) { + if (smartGenerateUploadEnabled) { + return ; + } return ; } diff --git a/src/components/nodes/SmartImageNode.tsx b/src/components/nodes/SmartImageNode.tsx index bfcf0c02..12ada826 100644 --- a/src/components/nodes/SmartImageNode.tsx +++ b/src/components/nodes/SmartImageNode.tsx @@ -8,6 +8,7 @@ import { useI18n } from "@/i18n"; import { useWorkflowStore } from "@/store/workflowStore"; import { SmartImageNodeData } from "@/types"; import { uploadFileToGatewayMedia } from "@/utils/gatewayMediaUpload"; +import { isSmartGenerateUploadEnabled } from "@/utils/smartGenerateUpload"; import { deriveSmartImageMode } from "@/utils/smartImageMode"; import { getPopiImageAssetPreview, resolvePopiImageAsset } from "@/lib/popiAssetApi"; import { GenerateImageNodeView } from "./GenerateImageNode"; @@ -36,10 +37,12 @@ function SmartImageUploadToolbar({ id, selected, isUploading, + addToGenerationHistory = false, }: { id: string; selected?: boolean; isUploading: boolean; + addToGenerationHistory?: boolean; }) { const { t } = useI18n(); const updateNodeData = useWorkflowStore((state) => state.updateNodeData); @@ -47,6 +50,41 @@ function SmartImageUploadToolbar({ const fileInputRef = useRef(null); const [isAssetPickerOpen, setIsAssetPickerOpen] = useState(false); + const buildUploadedImagePatch = useCallback( + ( + imageUrl: string, + dimensions: { width: number; height: number } | null, + previewImg?: string + ): Partial => { + if (!addToGenerationHistory) return {}; + + const currentNode = useWorkflowStore.getState().getNodeById(id); + const currentData = (currentNode?.data ?? {}) as SmartImageNodeData; + const timestamp = Date.now(); + return { + outputImage: imageUrl, + previewImg, + imageHistory: [ + { + id: `${timestamp}`, + image: imageUrl, + previewImg, + timestamp, + prompt: currentData.inputPrompt || "", + aspectRatio: currentData.aspectRatio, + model: currentData.selectedModel?.modelId || currentData.model || "", + modelDisplayName: currentData.selectedModel?.displayName, + modelProvider: currentData.selectedModel?.provider, + }, + ...(currentData.imageHistory || []), + ].slice(0, 50), + selectedHistoryIndex: 0, + dimensions, + }; + }, + [addToGenerationHistory, id] + ); + const handleFileChange = useCallback( (event: React.ChangeEvent) => { const file = event.target.files?.[0]; @@ -73,6 +111,7 @@ function SmartImageUploadToolbar({ uploadError: null, filename: uploaded.filename, dimensions, + ...buildUploadedImagePatch(uploaded.url, dimensions), }); }) .catch((error) => { @@ -127,9 +166,11 @@ function SmartImageUploadToolbar({ const currentNode = useWorkflowStore.getState().getNodeById(id); const currentData = currentNode?.data as SmartImageNodeData | undefined; if (currentData?.assetId !== preview.assetId) return; + const imageUrl = resolved.imageUrl; + if (!imageUrl) return; updateNodeData(id, { - image: resolved.imageUrl, + image: imageUrl, imageRef: undefined, assetId: resolved.assetId, previewImage: resolved.previewImageUrl, @@ -137,6 +178,7 @@ function SmartImageUploadToolbar({ assetDetailError: null, filename: resolved.filename, dimensions, + ...buildUploadedImagePatch(imageUrl, dimensions, resolved.previewImageUrl), }); }; @@ -163,7 +205,7 @@ function SmartImageUploadToolbar({ assetDetailError: error instanceof Error ? error.message : "Failed to load asset detail", }); }); - }, [id, updateNodeData]); + }, [buildUploadedImagePatch, id, updateNodeData]); return ( <> @@ -225,17 +267,24 @@ function SmartImageNeutralView({ id, data, selected, + addToGenerationHistory = false, }: { id: string; data: SmartImageNodeData; selected?: boolean; + addToGenerationHistory?: boolean; }) { const isUploading = data.uploadStatus === "loading"; return (
- + {isUploading && (
@@ -250,6 +299,9 @@ export function SmartImageNode({ id, data, selected }: NodeProps; + } return ; } diff --git a/src/components/nodes/SmartVideoNode.tsx b/src/components/nodes/SmartVideoNode.tsx index 0c2dc549..60fb687c 100644 --- a/src/components/nodes/SmartVideoNode.tsx +++ b/src/components/nodes/SmartVideoNode.tsx @@ -8,6 +8,7 @@ import { useI18n } from "@/i18n"; import { useWorkflowStore } from "@/store/workflowStore"; import { SmartVideoNodeData } from "@/types"; import { uploadFileToGatewayMedia } from "@/utils/gatewayMediaUpload"; +import { isSmartGenerateUploadEnabled } from "@/utils/smartGenerateUpload"; import { deriveSmartVideoMode } from "@/utils/smartVideoMode"; import { getPopiVideoAssetPreview, resolvePopiVideoAsset } from "@/lib/popiAssetApi"; import { GenerateVideoNodeView } from "./GenerateVideoNode"; @@ -40,10 +41,12 @@ function SmartVideoUploadToolbar({ id, selected, isUploading, + addToGenerationHistory = false, }: { id: string; selected?: boolean; isUploading: boolean; + addToGenerationHistory?: boolean; }) { const { t } = useI18n(); const updateNodeData = useWorkflowStore((state) => state.updateNodeData); @@ -51,6 +54,33 @@ function SmartVideoUploadToolbar({ const fileInputRef = useRef(null); const [isAssetPickerOpen, setIsAssetPickerOpen] = useState(false); + const buildUploadedVideoPatch = useCallback( + (videoUrl: string): Partial => { + if (!addToGenerationHistory) return {}; + + const currentNode = useWorkflowStore.getState().getNodeById(id); + const currentData = (currentNode?.data ?? {}) as SmartVideoNodeData; + const timestamp = Date.now(); + return { + outputVideo: videoUrl, + videoHistory: [ + { + id: `${timestamp}`, + video: videoUrl, + timestamp, + prompt: currentData.inputPrompt || "", + model: currentData.selectedModel?.modelId || "", + modelDisplayName: currentData.selectedModel?.displayName, + modelProvider: currentData.selectedModel?.provider, + }, + ...(currentData.videoHistory || []), + ].slice(0, 50), + selectedVideoHistoryIndex: 0, + }; + }, + [addToGenerationHistory, id] + ); + const applyVideo = useCallback( (videoSrc: string, filename: string, format: string, duration: number | null, dimensions: { width: number; height: number } | null) => { updateNodeData(id, { @@ -64,9 +94,10 @@ function SmartVideoUploadToolbar({ format, duration, dimensions, + ...buildUploadedVideoPatch(videoSrc), }); }, - [id, updateNodeData] + [buildUploadedVideoPatch, id, updateNodeData] ); const handleFileChange = useCallback( @@ -153,9 +184,11 @@ function SmartVideoUploadToolbar({ const currentNode = useWorkflowStore.getState().getNodeById(id); const currentData = currentNode?.data as SmartVideoNodeData | undefined; if (currentData?.assetId !== preview.assetId) return; + const videoUrl = resolved.videoUrl; + if (!videoUrl) return; updateNodeData(id, { - video: resolved.videoUrl, + video: videoUrl, videoRef: undefined, assetId: resolved.assetId, previewVideoPoster: resolved.previewPosterUrl || undefined, @@ -165,6 +198,7 @@ function SmartVideoUploadToolbar({ format: "video/asset", duration: resolved.duration || metadata.duration, dimensions: resolved.dimensions || metadata.dimensions, + ...buildUploadedVideoPatch(videoUrl), }); }; @@ -202,7 +236,7 @@ function SmartVideoUploadToolbar({ assetDetailError: error instanceof Error ? error.message : "Failed to load asset detail", }); }); - }, [id, updateNodeData]); + }, [buildUploadedVideoPatch, id, updateNodeData]); const handleOpenFileDialog = useCallback(() => { if (isUploading) return; @@ -270,17 +304,24 @@ function SmartVideoNeutralView({ id, data, selected, + addToGenerationHistory = false, }: { id: string; data: SmartVideoNodeData; selected?: boolean; + addToGenerationHistory?: boolean; }) { const isUploading = data.uploadStatus === "loading"; return (
- + {isUploading && (
@@ -295,6 +336,9 @@ export function SmartVideoNode({ id, data, selected }: NodeProps; + } return ; } diff --git a/src/components/nodes/VideoTrimNode.tsx b/src/components/nodes/VideoTrimNode.tsx index bf01d29d..64429848 100644 --- a/src/components/nodes/VideoTrimNode.tsx +++ b/src/components/nodes/VideoTrimNode.tsx @@ -24,13 +24,6 @@ function formatTime(seconds: number): string { return `${mins}:${secs.toFixed(1).padStart(4, "0")}`; } -function getPlayableMetadataUrl(url: string): string { - if (url.startsWith("http://") || url.startsWith("https://")) { - return `/api/proxy-media?url=${encodeURIComponent(url)}`; - } - return url; -} - export function VideoTrimNode({ id, data, selected }: NodeProps) { const { t } = useI18n(); const nodeData = data; @@ -118,7 +111,7 @@ export function VideoTrimNode({ id, data, selected }: NodeProps { diff --git a/src/hooks/useVideoBlobUrl.ts b/src/hooks/useVideoBlobUrl.ts index 9cb3b554..428fa6ac 100644 --- a/src/hooks/useVideoBlobUrl.ts +++ b/src/hooks/useVideoBlobUrl.ts @@ -11,7 +11,7 @@ import { isHttpMediaUrl, resolveVideoPlaybackSrc } from "@/utils/mediaResolver"; * * - If input is null, returns null * - If input is already a blob URL, returns it as-is - * - If input is an HTTP URL, routes it through the same-origin media proxy + * - If input is an HTTP URL, passes it through unchanged * - If input is a data URL, immediately returns it as fallback, then swaps * to a blob URL once conversion completes (~50ms) * - Revokes previous blob URLs on input change and unmount @@ -39,8 +39,7 @@ export function useVideoBlobUrl(videoUrl: string | null): string | null { return; } - // Remote videos can fail in