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. 27
      src/app/api/generate/providers/gemini.ts
  2. 4
      src/components/nodes/SwitchNode.tsx

27
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,7 +278,10 @@ 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]);
let operation;
try {
operation = await ai.models.generateVideos(requestArgs as unknown as Parameters<typeof ai.models.generateVideos>[0]);
// Poll for completion (10s intervals, 5min timeout) // Poll for completion (10s intervals, 5min timeout)
const POLL_INTERVAL = 10_000; const POLL_INTERVAL = 10_000;
@ -289,6 +298,11 @@ export async function generateWithGeminiVideo(
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL)); await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL));
operation = await ai.operations.getVideosOperation({ operation }); 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;
console.log(`[API:${requestId}] Veo generation completed in ${(duration / 1000).toFixed(1)}s`); console.log(`[API:${requestId}] Veo generation completed in ${(duration / 1000).toFixed(1)}s`);
@ -310,7 +324,10 @@ 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();
const fetchTimeout = setTimeout(() => controller.abort(), 60_000);
try {
const videoResponse = await fetch(videoUrl, { signal: controller.signal });
if (!videoResponse.ok) { if (!videoResponse.ok) {
console.error(`[API:${requestId}] Failed to fetch video: ${videoResponse.status}`); console.error(`[API:${requestId}] Failed to fetch video: ${videoResponse.status}`);
return { success: false, error: `Failed to download generated video: ${videoResponse.status}` }; return { success: false, error: `Failed to download generated video: ${videoResponse.status}` };
@ -329,4 +346,10 @@ export async function generateWithGeminiVideo(
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