From 7c03293dc8e763469c4cf67e7c2e1917bcd84a50 Mon Sep 17 00:00:00 2001 From: jiajia Date: Wed, 27 May 2026 18:22:08 +0800 Subject: [PATCH] Preserve media generation contracts in node workflows Vidu rejects generic video reference payloads, while the canvas preview also needs to keep long prompt text inside the prompt node bounds. This keeps provider-specific video reference fields for Vidu and constrains long prompt previews without changing the Seedance media field mapping. Constraint: Vidu/NewApiWG expects reference_video_urls for video-reference generation. Rejected: Change all video providers to reference_* fields | Seedance has existing coverage and logs using generic images/videos/audios. Confidence: high Scope-risk: narrow Directive: Do not collapse Vidu video references back to videos without validating against the gateway schema. Tested: npm run test:run -- src/app/api/generate/providers/__tests__/newapiwg.test.ts src/components/__tests__/PromptNode.test.tsx Tested: git diff --check Tested: npm run build Not-tested: Real Vidu generation, to avoid consuming account points. --- .../providers/__tests__/newapiwg.test.ts | 25 +++++++++++++++++++ src/app/api/generate/providers/newapiwg.ts | 24 +++++++++++++++++- src/components/__tests__/PromptNode.test.tsx | 23 +++++++++++++++++ src/components/nodes/PromptNode.tsx | 8 ++++-- 4 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/app/api/generate/providers/__tests__/newapiwg.test.ts b/src/app/api/generate/providers/__tests__/newapiwg.test.ts index c50148de..f82eb650 100644 --- a/src/app/api/generate/providers/__tests__/newapiwg.test.ts +++ b/src/app/api/generate/providers/__tests__/newapiwg.test.ts @@ -509,6 +509,31 @@ describe("NewApiWG generation payloads", () => { expect(capturedBody!.image).toBeUndefined(); }); + it("keeps Vidu reference videos on the schema field expected by the gateway", async () => { + const result = await generateWithNewApiWG("test-key", "https://newapi.example/v1", makeInput({ + model: { + id: "viduq3-turbo", + name: "viduq3-turbo", + description: null, + provider: "newapiwg", + capabilities: ["text-to-video", "image-to-video"], + }, + dynamicInputs: { + reference_video_urls: ["data:video/mp4;base64,reference"], + video_urls: "data:video/mp4;base64,connected", + }, + })); + + expect(result.success).toBe(true); + expect(capturedBody).not.toBeNull(); + expect(capturedBody!.reference_video_urls).toEqual([ + "data:video/mp4;base64,reference", + "data:video/mp4;base64,connected", + ]); + expect(capturedBody!.videos).toBeUndefined(); + expect(capturedBody!.video_urls).toBeUndefined(); + }); + it("resolves temporary local media inputs before NewApiWG video generation", async () => { const imageId = storeImage("data:image/png;base64,dmlkZW8tcmVm"); try { diff --git a/src/app/api/generate/providers/newapiwg.ts b/src/app/api/generate/providers/newapiwg.ts index 5f48e5a6..cd09b655 100644 --- a/src/app/api/generate/providers/newapiwg.ts +++ b/src/app/api/generate/providers/newapiwg.ts @@ -839,6 +839,22 @@ function uniqueStrings(values: string[]): string[] { return [...new Set(values)]; } +function isViduVideoModel(modelId: string): boolean { + return /vidu/i.test(modelId); +} + +function appendDynamicInputArray( + inputs: Record, + key: string, + values: string[] +): void { + const merged = uniqueStrings([ + ...asStringArray(inputs[key]), + ...values, + ]); + if (merged.length > 0) inputs[key] = merged; +} + function normalizeNewApiWGVideoInputs( input: GenerationInput, dynamicInputs: Record @@ -850,12 +866,18 @@ function normalizeNewApiWGVideoInputs( const images = [...(input.images || [])]; const videos: string[] = []; const audios: string[] = []; + const shouldUseViduReferenceVideoField = isViduVideoModel(input.model.id); for (const [key, value] of Object.entries(dynamicInputs)) { if (NEWAPIWG_VIDEO_IMAGE_INPUTS.has(key)) { images.push(...asStringArray(value)); } else if (NEWAPIWG_VIDEO_INPUTS.has(key)) { - videos.push(...asStringArray(value)); + const videoValues = asStringArray(value); + if (shouldUseViduReferenceVideoField) { + appendDynamicInputArray(normalizedDynamicInputs, "reference_video_urls", videoValues); + } else { + videos.push(...videoValues); + } } else if (NEWAPIWG_VIDEO_AUDIO_INPUTS.has(key)) { audios.push(...asStringArray(value)); } else { diff --git a/src/components/__tests__/PromptNode.test.tsx b/src/components/__tests__/PromptNode.test.tsx index 6a5a1ea8..9cca4c2b 100644 --- a/src/components/__tests__/PromptNode.test.tsx +++ b/src/components/__tests__/PromptNode.test.tsx @@ -85,6 +85,29 @@ describe("PromptNode", () => { expect(screen.getByText("Initial prompt text")).toBeInTheDocument(); }); + it("should keep long prompt previews bounded inside the node", () => { + const longPrompt = "水电费".repeat(120); + const propsWithPrompt = { + ...defaultProps, + data: { prompt: longPrompt }, + }; + + render( + + + + ); + + const preview = screen.getByRole("button", { name: "Double-click to edit" }); + expect(preview).toHaveClass("overflow-hidden"); + expect(preview).toHaveClass("items-start"); + + const text = screen.getByText(longPrompt); + expect(text).toHaveClass("max-h-full"); + expect(text).toHaveClass("overflow-y-auto"); + expect(text).toHaveClass("break-words"); + }); + it("should call updateNodeData when typing in textarea and blurring", () => { render( diff --git a/src/components/nodes/PromptNode.tsx b/src/components/nodes/PromptNode.tsx index fa8e5a7b..55a9db43 100644 --- a/src/components/nodes/PromptNode.tsx +++ b/src/components/nodes/PromptNode.tsx @@ -235,10 +235,14 @@ export function PromptNode({ id, data, selected }: NodeProps) { setIsEditing(true); } }} - className="flex h-full w-full cursor-text items-center justify-center rounded-lg bg-neutral-800 p-8 text-center outline-none" + className={`flex h-full w-full cursor-text rounded-lg bg-neutral-800 p-8 text-center outline-none ${ + promptText + ? "items-start justify-start overflow-hidden" + : "items-center justify-center" + }`} > {promptText ? ( -
+
{localPrompt}
) : (