Browse Source

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.
TEST-s
jiajia 2 months ago
parent
commit
7c03293dc8
  1. 25
      src/app/api/generate/providers/__tests__/newapiwg.test.ts
  2. 24
      src/app/api/generate/providers/newapiwg.ts
  3. 23
      src/components/__tests__/PromptNode.test.tsx
  4. 8
      src/components/nodes/PromptNode.tsx

25
src/app/api/generate/providers/__tests__/newapiwg.test.ts

@ -509,6 +509,31 @@ describe("NewApiWG generation payloads", () => {
expect(capturedBody!.image).toBeUndefined(); 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 () => { it("resolves temporary local media inputs before NewApiWG video generation", async () => {
const imageId = storeImage("data:image/png;base64,dmlkZW8tcmVm"); const imageId = storeImage("data:image/png;base64,dmlkZW8tcmVm");
try { try {

24
src/app/api/generate/providers/newapiwg.ts

@ -839,6 +839,22 @@ function uniqueStrings(values: string[]): string[] {
return [...new Set(values)]; return [...new Set(values)];
} }
function isViduVideoModel(modelId: string): boolean {
return /vidu/i.test(modelId);
}
function appendDynamicInputArray(
inputs: Record<string, string | string[]>,
key: string,
values: string[]
): void {
const merged = uniqueStrings([
...asStringArray(inputs[key]),
...values,
]);
if (merged.length > 0) inputs[key] = merged;
}
function normalizeNewApiWGVideoInputs( function normalizeNewApiWGVideoInputs(
input: GenerationInput, input: GenerationInput,
dynamicInputs: Record<string, string | string[]> dynamicInputs: Record<string, string | string[]>
@ -850,12 +866,18 @@ function normalizeNewApiWGVideoInputs(
const images = [...(input.images || [])]; const images = [...(input.images || [])];
const videos: string[] = []; const videos: string[] = [];
const audios: string[] = []; const audios: string[] = [];
const shouldUseViduReferenceVideoField = isViduVideoModel(input.model.id);
for (const [key, value] of Object.entries(dynamicInputs)) { for (const [key, value] of Object.entries(dynamicInputs)) {
if (NEWAPIWG_VIDEO_IMAGE_INPUTS.has(key)) { if (NEWAPIWG_VIDEO_IMAGE_INPUTS.has(key)) {
images.push(...asStringArray(value)); images.push(...asStringArray(value));
} else if (NEWAPIWG_VIDEO_INPUTS.has(key)) { } 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)) { } else if (NEWAPIWG_VIDEO_AUDIO_INPUTS.has(key)) {
audios.push(...asStringArray(value)); audios.push(...asStringArray(value));
} else { } else {

23
src/components/__tests__/PromptNode.test.tsx

@ -85,6 +85,29 @@ describe("PromptNode", () => {
expect(screen.getByText("Initial prompt text")).toBeInTheDocument(); 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(
<TestWrapper>
<PromptNode {...propsWithPrompt} />
</TestWrapper>
);
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", () => { it("should call updateNodeData when typing in textarea and blurring", () => {
render( render(
<TestWrapper> <TestWrapper>

8
src/components/nodes/PromptNode.tsx

@ -235,10 +235,14 @@ export function PromptNode({ id, data, selected }: NodeProps<PromptNodeType>) {
setIsEditing(true); 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 ? ( {promptText ? (
<div className="w-full whitespace-pre-wrap text-left text-lg leading-relaxed text-neutral-100"> <div className="nowheel max-h-full w-full overflow-y-auto whitespace-pre-wrap break-words pr-1 text-left text-lg leading-relaxed text-neutral-100">
{localPrompt} {localPrompt}
</div> </div>
) : ( ) : (

Loading…
Cancel
Save