Browse Source

fix: Veo video error handling, image validation, and SwitchNode toggle

- Add early validation for image-to-video models when no image is provided
- Wrap Veo generation/polling in try/catch with specific error messages
- Add 60s AbortController timeout to video download fetch
- Fix SwitchNode toggle knob not sliding (peer-checked on non-sibling)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
80443316a6
  1. 79
      src/app/api/generate/providers/gemini.ts
  2. 4
      src/components/nodes/SwitchNode.tsx

79
src/app/api/generate/providers/gemini.ts

@ -249,6 +249,12 @@ export async function generateWithGeminiVideo(
config,
};
// Validate image-to-video models have an image
if (modelId.includes("image-to-video") && (!images || images.length === 0)) {
console.error(`[API:${requestId}] Image required for image-to-video model: ${modelId}`);
return { success: false, error: "Image required for image-to-video model" };
}
// Add image for image-to-video models
if (images && images.length > 0 && modelId.includes("image-to-video")) {
const imageInput = images[0];
@ -272,22 +278,30 @@ export async function generateWithGeminiVideo(
// Start video generation (async operation)
const startTime = Date.now();
let operation = await ai.models.generateVideos(requestArgs as unknown as Parameters<typeof ai.models.generateVideos>[0]);
// Poll for completion (10s intervals, 5min timeout)
const POLL_INTERVAL = 10_000;
const TIMEOUT = 5 * 60 * 1000;
let operation;
try {
operation = await ai.models.generateVideos(requestArgs as unknown as Parameters<typeof ai.models.generateVideos>[0]);
while (!operation.done) {
const elapsed = Date.now() - startTime;
if (elapsed > TIMEOUT) {
console.error(`[API:${requestId}] Veo generation timed out after ${(elapsed / 1000).toFixed(0)}s`);
return { success: false, error: "Video generation timed out after 5 minutes" };
}
// Poll for completion (10s intervals, 5min timeout)
const POLL_INTERVAL = 10_000;
const TIMEOUT = 5 * 60 * 1000;
console.log(`[API:${requestId}] Veo polling... (${(elapsed / 1000).toFixed(0)}s elapsed)`);
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL));
operation = await ai.operations.getVideosOperation({ operation });
while (!operation.done) {
const elapsed = Date.now() - startTime;
if (elapsed > TIMEOUT) {
console.error(`[API:${requestId}] Veo generation timed out after ${(elapsed / 1000).toFixed(0)}s`);
return { success: false, error: "Video generation timed out after 5 minutes" };
}
console.log(`[API:${requestId}] Veo polling... (${(elapsed / 1000).toFixed(0)}s elapsed)`);
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL));
operation = await ai.operations.getVideosOperation({ operation });
}
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
console.error(`[API:${requestId}] Veo generation failed: ${msg}`);
return { success: false, error: `Video generation failed: ${msg}` };
}
const duration = Date.now() - startTime;
@ -310,23 +324,32 @@ export async function generateWithGeminiVideo(
const videoUrl = `${videoUri}&key=${apiKey}`;
console.log(`[API:${requestId}] Fetching video from URI...`);
const videoResponse = await fetch(videoUrl);
if (!videoResponse.ok) {
console.error(`[API:${requestId}] Failed to fetch video: ${videoResponse.status}`);
return { success: false, error: `Failed to download generated video: ${videoResponse.status}` };
}
const controller = new AbortController();
const fetchTimeout = setTimeout(() => controller.abort(), 60_000);
try {
const videoResponse = await fetch(videoUrl, { signal: controller.signal });
if (!videoResponse.ok) {
console.error(`[API:${requestId}] Failed to fetch video: ${videoResponse.status}`);
return { success: false, error: `Failed to download generated video: ${videoResponse.status}` };
}
const videoBuffer = await videoResponse.arrayBuffer();
const videoSizeMB = (videoBuffer.byteLength / (1024 * 1024)).toFixed(2);
console.log(`[API:${requestId}] Video downloaded: ${videoSizeMB}MB`);
const videoBuffer = await videoResponse.arrayBuffer();
const videoSizeMB = (videoBuffer.byteLength / (1024 * 1024)).toFixed(2);
console.log(`[API:${requestId}] Video downloaded: ${videoSizeMB}MB`);
const base64Video = Buffer.from(videoBuffer).toString("base64");
const dataUrl = `data:video/mp4;base64,${base64Video}`;
const base64Video = Buffer.from(videoBuffer).toString("base64");
const dataUrl = `data:video/mp4;base64,${base64Video}`;
console.log(`[API:${requestId}] SUCCESS - Returning ${videoSizeMB}MB video`);
console.log(`[API:${requestId}] SUCCESS - Returning ${videoSizeMB}MB video`);
return {
success: true,
outputs: [{ type: "video", data: dataUrl }],
};
return {
success: true,
outputs: [{ type: "video", data: dataUrl }],
};
} catch (error) {
console.error(`[API:${requestId}] Failed to download video: ${error}`);
return { success: false, error: "Failed to download generated video" };
} finally {
clearTimeout(fetchTimeout);
}
}

4
src/components/nodes/SwitchNode.tsx

@ -196,8 +196,8 @@ export const SwitchNode = memo(({ id, data, selected }: NodeProps<WorkflowNode>)
checked={sw.enabled}
onChange={() => handleToggle(sw.id)}
/>
<div className="w-8 h-4 bg-neutral-600 peer-checked:bg-violet-500 rounded-full peer transition-colors relative">
<div className="absolute top-0.5 left-0.5 bg-white h-3 w-3 rounded-full transition-transform peer-checked:translate-x-4" />
<div className="w-8 h-4 bg-neutral-600 peer-checked:bg-violet-500 rounded-full transition-colors relative">
<div className={`absolute top-0.5 left-0.5 bg-white h-3 w-3 rounded-full transition-transform ${sw.enabled ? "translate-x-4" : ""}`} />
</div>
</label>

Loading…
Cancel
Save