diff --git a/src/components/__tests__/GenerationComposer.test.tsx b/src/components/__tests__/GenerationComposer.test.tsx index b28a1bf7..eb62e53f 100644 --- a/src/components/__tests__/GenerationComposer.test.tsx +++ b/src/components/__tests__/GenerationComposer.test.tsx @@ -814,6 +814,65 @@ describe("GenerationComposer", () => { }); }); + it("sums connected video durations and rounds up for Seedance estimates", async () => { + const fetchMock = vi.fn(async (input: RequestInfo | URL) => { + const url = String(input); + if (url.startsWith("/api/points/estimate")) { + return new Response(JSON.stringify({ + success: true, + data: { + estimated_points: 234, + }, + }), { status: 200 }); + } + if (url.startsWith("/api/models")) { + return new Response(JSON.stringify({ success: true, models: [] }), { status: 200 }); + } + throw new Error(`Unexpected fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + useWorkflowStore.setState({ + nodes: [ + videoNode("source-1", false, { + outputVideo: "https://static.popi.art/source-1.mp4", + durationSeconds: 2.2, + }), + videoNode("source-2", false, { + outputVideo: "https://static.popi.art/source-2.mp4", + outputDuration: 3.1, + }), + videoNode("vid-1", true, { + durationSeconds: 5, + resolution: "720p", + selectedModel: { + provider: "newapiwg", + modelId: "doubao-seedance-2-0-260128", + displayName: "doubao-seedance-2-0-260128", + capabilities: ["text-to-video", "image-to-video"], + }, + }), + ], + edges: [ + videoEdge("source-1", "vid-1", 0), + videoEdge("source-2", "vid-1", 1), + ], + }); + + render(); + + await waitFor(() => { + expect(fetchMock.mock.calls.some(([input]) => String(input).startsWith("/api/points/estimate"))).toBe(true); + }); + const estimateCall = fetchMock.mock.calls.find(([input]) => String(input).startsWith("/api/points/estimate")); + expect(estimateCall).toBeDefined(); + expect(JSON.parse(String((estimateCall?.[1] as RequestInit).body))).toMatchObject({ + seedanceVideo: { + input_video_duration_seconds: 6, + output_video_duration_seconds: 5, + }, + }); + }); + it("uses PopiServer task price estimates for selected image models", async () => { const fetchMock = vi.fn(async (input: RequestInfo | URL) => { const url = String(input); diff --git a/src/components/composer/GenerationComposer.tsx b/src/components/composer/GenerationComposer.tsx index 4194674b..239079c6 100644 --- a/src/components/composer/GenerationComposer.tsx +++ b/src/components/composer/GenerationComposer.tsx @@ -619,19 +619,6 @@ function isVideoHandle(handleId: string | null | undefined): boolean { return handleId === "video" || handleId.startsWith("video-") || handleId.includes("video"); } -function getConnectedVideoDurationSeconds( - targetNodeId: string, - nodes: WorkflowNode[], - edges: Array<{ source: string; target: string; sourceHandle?: string | null; targetHandle?: string | null }> -): number { - const videoEdge = edges.find((edge) => - edge.target === targetNodeId && - (isVideoHandle(edge.targetHandle) || isVideoHandle(edge.sourceHandle)) - ); - if (!videoEdge) return 0; - return getNodeVideoDurationSeconds(nodes.find((node) => node.id === videoEdge.source)); -} - function getConnectedVideoDurationList( targetNodeId: string, nodes: WorkflowNode[], @@ -1168,16 +1155,18 @@ export function GenerationComposer() { return getConnectedInputs(context.node.id); }, [context, getConnectedInputs, nodes, edges, dimmedNodeIds]); - const connectedVideoDurationSeconds = useMemo(() => { - if (context.mode !== "node-edit" || !context.node) return 0; - return getConnectedVideoDurationSeconds(context.node.id, nodes, edges); - }, [context, edges, nodes]); - const connectedVideoDurations = useMemo(() => { if (context.mode !== "node-edit" || !context.node) return []; return getConnectedVideoDurationList(context.node.id, nodes, edges); }, [context, edges, nodes]); + const connectedVideoDurationSeconds = useMemo(() => { + const totalDuration = connectedVideoDurations.reduce((sum, duration) => { + return Number.isFinite(duration) && duration > 0 ? sum + duration : sum; + }, 0); + return Math.ceil(totalDuration); + }, [connectedVideoDurations]); + const estimatedReferenceImageCount = context.mode === "node-edit" && (connectedInputs?.images.length ?? 0) > 0 ? connectedInputs?.images.length ?? 0 : draft.inputImages.length;