-
Notifications
You must be signed in to change notification settings - Fork 287
fix: increase generate API timeout to 15 minutes for video generation #100
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
8fb3066
ce7b5c0
0773ddd
88450fb
3b8ddc6
889f2f2
55908d7
b39b37d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
| import type { GenerateVideoNodeData } from "@/types"; | ||
| import { buildGenerateHeaders } from "@/store/utils/buildApiHeaders"; | ||
| import type { NodeExecutionContext } from "./types"; | ||
| import { compressImagesForUpload } from "@/utils/imageCompression"; | ||
|
|
||
| export interface GenerateVideoOptions { | ||
| /** When true, falls back to stored inputImages/inputPrompt if no connections provide them. */ | ||
|
|
@@ -87,12 +88,32 @@ export async function executeGenerateVideo( | |
| const provider = nodeData.selectedModel.provider; | ||
| const headers = buildGenerateHeaders(provider, providerSettings); | ||
|
|
||
| // Compress images to fit within Vercel's 4.5MB payload limit | ||
| const compressedImages = images.length > 0 ? await compressImagesForUpload(images) : []; | ||
|
|
||
| // Also compress any images in dynamicInputs | ||
| const compressedDynamicInputs: Record<string, string | string[]> = {}; | ||
| for (const [key, value] of Object.entries(dynamicInputs)) { | ||
| if (typeof value === "string" && value.startsWith("data:image")) { | ||
| compressedDynamicInputs[key] = await compressImagesForUpload([value]).then(arr => arr[0]); | ||
| } else if (Array.isArray(value)) { | ||
| const hasImages = value.some(v => typeof v === "string" && v.startsWith("data:image")); | ||
| if (hasImages) { | ||
| compressedDynamicInputs[key] = await compressImagesForUpload(value); | ||
| } else { | ||
| compressedDynamicInputs[key] = value; | ||
| } | ||
| } else { | ||
| compressedDynamicInputs[key] = value; | ||
| } | ||
| } | ||
|
Comment on lines
+91
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wrap compression in the existing error path. These awaits run after the node is set to Proposed fix- // Compress images to fit within Vercel's 4.5MB payload limit
- const compressedImages = images.length > 0 ? await compressImagesForUpload(images) : [];
-
- // Also compress any images in dynamicInputs
- const compressedDynamicInputs: Record<string, string | string[]> = {};
- for (const [key, value] of Object.entries(dynamicInputs)) {
- if (typeof value === "string" && value.startsWith("data:image")) {
- compressedDynamicInputs[key] = await compressImagesForUpload([value]).then(arr => arr[0]);
- } else if (Array.isArray(value)) {
- const hasImages = value.some(v => typeof v === "string" && v.startsWith("data:image"));
- if (hasImages) {
- compressedDynamicInputs[key] = await compressImagesForUpload(value);
- } else {
- compressedDynamicInputs[key] = value;
- }
- } else {
- compressedDynamicInputs[key] = value;
- }
- }
-
- const requestPayload = {
- images: compressedImages,
- prompt: text,
- selectedModel: nodeData.selectedModel,
- parameters: nodeData.parameters,
- dynamicInputs: compressedDynamicInputs,
- mediaType: "video" as const,
- };
-
try {
+ // Compress images to fit within Vercel's 4.5MB payload limit
+ const compressedImages = images.length > 0 ? await compressImagesForUpload(images) : [];
+
+ // Also compress any images in dynamicInputs
+ const compressedDynamicInputs: Record<string, string | string[]> = {};
+ for (const [key, value] of Object.entries(dynamicInputs)) {
+ if (typeof value === "string" && value.startsWith("data:image")) {
+ compressedDynamicInputs[key] = await compressImagesForUpload([value]).then(arr => arr[0]);
+ } else if (Array.isArray(value)) {
+ const hasImages = value.some(v => typeof v === "string" && v.startsWith("data:image"));
+ compressedDynamicInputs[key] = hasImages ? await compressImagesForUpload(value) : value;
+ } else {
+ compressedDynamicInputs[key] = value;
+ }
+ }
+
+ const requestPayload = {
+ images: compressedImages,
+ prompt: text,
+ selectedModel: nodeData.selectedModel,
+ parameters: nodeData.parameters,
+ dynamicInputs: compressedDynamicInputs,
+ mediaType: "video" as const,
+ };
+
const response = await fetch("/api/generate", {🤖 Prompt for AI Agents |
||
|
|
||
| const requestPayload = { | ||
| images, | ||
| images: compressedImages, | ||
| prompt: text, | ||
| selectedModel: nodeData.selectedModel, | ||
| parameters: nodeData.parameters, | ||
| dynamicInputs, | ||
| dynamicInputs: compressedDynamicInputs, | ||
| mediaType: "video" as const, | ||
| }; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,70 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||
| * Image compression utility to ensure images fit within API payload limits. | ||||||||||||||||||||||||||||||||||||||||||||||||
| * Vercel serverless functions have a 4.5MB body size limit. | ||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const MAX_PAYLOAD_SIZE = 4 * 1024 * 1024; // 4MB to leave room for other request data | ||||||||||||||||||||||||||||||||||||||||||||||||
| const MAX_DIMENSION = 2048; // Max width/height | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||
| * Compress a base64 image to fit within payload limits. | ||||||||||||||||||||||||||||||||||||||||||||||||
| * Returns the original if already small enough, otherwise compresses. | ||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||
| export async function compressImageForUpload(base64DataUrl: string): Promise<string> { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Not a data URL, return as-is | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (!base64DataUrl.startsWith("data:")) return base64DataUrl; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if already small enough | ||||||||||||||||||||||||||||||||||||||||||||||||
| const estimatedSize = Math.ceil((base64DataUrl.length - base64DataUrl.indexOf(",") - 1) * 3 / 4); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (estimatedSize < MAX_PAYLOAD_SIZE) return base64DataUrl; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Need to compress - use canvas | ||||||||||||||||||||||||||||||||||||||||||||||||
| return new Promise((resolve, reject) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const img = new Image(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| img.onload = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Calculate new dimensions | ||||||||||||||||||||||||||||||||||||||||||||||||
| let { width, height } = img; | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (width > MAX_DIMENSION || height > MAX_DIMENSION) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const scale = Math.min(MAX_DIMENSION / width, MAX_DIMENSION / height); | ||||||||||||||||||||||||||||||||||||||||||||||||
| width = Math.round(width * scale); | ||||||||||||||||||||||||||||||||||||||||||||||||
| height = Math.round(height * scale); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Draw to canvas | ||||||||||||||||||||||||||||||||||||||||||||||||
| const canvas = document.createElement("canvas"); | ||||||||||||||||||||||||||||||||||||||||||||||||
| canvas.width = width; | ||||||||||||||||||||||||||||||||||||||||||||||||
| canvas.height = height; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const ctx = canvas.getContext("2d"); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (!ctx) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| reject(new Error("Failed to get canvas context")); | ||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| ctx.drawImage(img, 0, 0, width, height); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Try progressively lower quality until under limit | ||||||||||||||||||||||||||||||||||||||||||||||||
| let quality = 0.9; | ||||||||||||||||||||||||||||||||||||||||||||||||
| let result = canvas.toDataURL("image/jpeg", quality); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| while (result.length > MAX_PAYLOAD_SIZE * 1.33 && quality > 0.1) { // 1.33 accounts for base64 overhead | ||||||||||||||||||||||||||||||||||||||||||||||||
| quality -= 0.1; | ||||||||||||||||||||||||||||||||||||||||||||||||
| result = canvas.toDataURL("image/jpeg", quality); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+42
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle transparent inputs explicitly before JPEG conversion. Oversized PNG/WebP inputs are always re-encoded as JPEG here. That silently drops alpha and changes the prompt image content, which can alter generation results. Proposed fix- ctx.drawImage(img, 0, 0, width, height);
+ const sourceMime = base64DataUrl.slice(5, base64DataUrl.indexOf(";"));
+ if (sourceMime === "image/png" || sourceMime === "image/webp") {
+ ctx.fillStyle = "#fff";
+ ctx.fillRect(0, 0, width, height);
+ }
+ ctx.drawImage(img, 0, 0, width, height);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`[ImageCompression] Compressed from ${(estimatedSize / 1024 / 1024).toFixed(2)}MB to ${(result.length / 1024 / 1024).toFixed(2)}MB (quality: ${quality.toFixed(1)})`); | ||||||||||||||||||||||||||||||||||||||||||||||||
| resolve(result); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| reject(error); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
| img.onerror = () => reject(new Error("Failed to load image for compression")); | ||||||||||||||||||||||||||||||||||||||||||||||||
| img.src = base64DataUrl; | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||
| * Compress multiple images | ||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||
| export async function compressImagesForUpload(images: string[]): Promise<string[]> { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return Promise.all(images.map(compressImageForUpload)); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "functions": { | ||
| "src/app/api/generate/route.ts": { | ||
| "maxDuration": 800 | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: shrimbly/node-banana
Length of output: 2523
Keep the timeout hotfix isolated; these SDK bumps appear unrelated.
This PR is scoped as a
/api/generatetimeout change, but these two dependency bumps widen the runtime surface area without clear justification. Since the bumps are not required for the timeout fix, split them into a separate PR. The lockfile does resolve cleanly, though it brings multiple versions of internal provider packages (@ai-sdk/[email protected]from Google,3.0.8from core AI SDK). This managed duplication is not a blocker, but the scope creep itself is worth avoiding in a hotfix.🤖 Prompt for AI Agents