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, 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 // Add image for image-to-video models
if (images && images.length > 0 && modelId.includes("image-to-video")) { if (images && images.length > 0 && modelId.includes("image-to-video")) {
const imageInput = images[0]; const imageInput = images[0];
@ -272,22 +278,30 @@ export async function generateWithGeminiVideo(
// Start video generation (async operation) // Start video generation (async operation)
const startTime = Date.now(); 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) let operation;
const POLL_INTERVAL = 10_000; try {
const TIMEOUT = 5 * 60 * 1000; operation = await ai.models.generateVideos(requestArgs as unknown as Parameters<typeof ai.models.generateVideos>[0]);
while (!operation.done) { // Poll for completion (10s intervals, 5min timeout)
const elapsed = Date.now() - startTime; const POLL_INTERVAL = 10_000;
if (elapsed > TIMEOUT) { const TIMEOUT = 5 * 60 * 1000;
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)`); while (!operation.done) {
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL)); const elapsed = Date.now() - startTime;
operation = await ai.operations.getVideosOperation({ operation }); 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; const duration = Date.now() - startTime;
@ -310,23 +324,32 @@ export async function generateWithGeminiVideo(
const videoUrl = `${videoUri}&key=${apiKey}`; const videoUrl = `${videoUri}&key=${apiKey}`;
console.log(`[API:${requestId}] Fetching video from URI...`); console.log(`[API:${requestId}] Fetching video from URI...`);
const videoResponse = await fetch(videoUrl); const controller = new AbortController();
if (!videoResponse.ok) { const fetchTimeout = setTimeout(() => controller.abort(), 60_000);
console.error(`[API:${requestId}] Failed to fetch video: ${videoResponse.status}`); try {
return { success: false, error: `Failed to download generated video: ${videoResponse.status}` }; 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 videoBuffer = await videoResponse.arrayBuffer();
const videoSizeMB = (videoBuffer.byteLength / (1024 * 1024)).toFixed(2); const videoSizeMB = (videoBuffer.byteLength / (1024 * 1024)).toFixed(2);
console.log(`[API:${requestId}] Video downloaded: ${videoSizeMB}MB`); console.log(`[API:${requestId}] Video downloaded: ${videoSizeMB}MB`);
const base64Video = Buffer.from(videoBuffer).toString("base64"); const base64Video = Buffer.from(videoBuffer).toString("base64");
const dataUrl = `data:video/mp4;base64,${base64Video}`; 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 { return {
success: true, success: true,
outputs: [{ type: "video", data: dataUrl }], 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} checked={sw.enabled}
onChange={() => handleToggle(sw.id)} 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="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 peer-checked:translate-x-4" /> <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> </div>
</label> </label>

Loading…
Cancel
Save