From cf225747ffd7bcb91ef2593a9a45311db77f1b8d Mon Sep 17 00:00:00 2001 From: shrimbly Date: Thu, 19 Feb 2026 23:18:41 +1300 Subject: [PATCH] fix: pre-merge fixes for develop->master PR - Add generateAudio and videoFrameGrab cases to executeNode dispatcher - Add selectedModel null guards in route.ts for all non-Gemini providers - Fix ConnectionDropMenu wrap-around test (5->6 items after generateAudio) - Fix audio duration edge cases (NaN guard in formatTime, truthiness check) Co-Authored-By: Claude Opus 4.6 --- src/app/api/generate/route.ts | 44 +++++++++++++++---- .../__tests__/ConnectionDropMenu.test.tsx | 5 ++- src/components/nodes/GenerateAudioNode.tsx | 2 +- src/hooks/useAudioPlayback.ts | 1 + src/store/execution/executeNode.ts | 9 +++- 5 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/app/api/generate/route.ts b/src/app/api/generate/route.ts index 3905b404..051c59bd 100644 --- a/src/app/api/generate/route.ts +++ b/src/app/api/generate/route.ts @@ -131,6 +131,13 @@ export async function POST(request: NextRequest) { // Route to appropriate provider if (provider === "replicate") { + if (!selectedModel?.modelId || !selectedModel?.displayName) { + return NextResponse.json( + { success: false, error: "selectedModel with modelId and displayName is required for Replicate" }, + { status: 400 } + ); + } + // User-provided key takes precedence over env variable const replicateApiKey = request.headers.get("X-Replicate-API-Key") || process.env.REPLICATE_API_KEY; if (!replicateApiKey) { @@ -167,8 +174,8 @@ export async function POST(request: NextRequest) { // Build generation input const genInput: GenerationInput = { model: { - id: selectedModel!.modelId, - name: selectedModel!.displayName, + id: selectedModel.modelId, + name: selectedModel.displayName, provider: "replicate", capabilities: capabilitiesForMediaType(mediaType), description: null, @@ -204,6 +211,13 @@ export async function POST(request: NextRequest) { } if (provider === "fal") { + if (!selectedModel?.modelId || !selectedModel?.displayName) { + return NextResponse.json( + { success: false, error: "selectedModel with modelId and displayName is required for fal.ai" }, + { status: 400 } + ); + } + // User-provided key takes precedence over env variable const falApiKey = request.headers.get("X-Fal-API-Key") || process.env.FAL_API_KEY || null; @@ -235,8 +249,8 @@ export async function POST(request: NextRequest) { // Build generation input const genInput: GenerationInput = { model: { - id: selectedModel!.modelId, - name: selectedModel!.displayName, + id: selectedModel.modelId, + name: selectedModel.displayName, provider: "fal", capabilities: capabilitiesForMediaType(mediaType), description: null, @@ -272,6 +286,13 @@ export async function POST(request: NextRequest) { } if (provider === "kie") { + if (!selectedModel?.modelId || !selectedModel?.displayName) { + return NextResponse.json( + { success: false, error: "selectedModel with modelId and displayName is required for Kie.ai" }, + { status: 400 } + ); + } + // User-provided key takes precedence over env variable const kieApiKey = request.headers.get("X-Kie-Key") || process.env.KIE_API_KEY; if (!kieApiKey) { @@ -307,8 +328,8 @@ export async function POST(request: NextRequest) { // Build generation input const genInput: GenerationInput = { model: { - id: selectedModel!.modelId, - name: selectedModel!.displayName, + id: selectedModel.modelId, + name: selectedModel.displayName, provider: "kie", capabilities: capabilitiesForMediaType(mediaType), description: null, @@ -344,6 +365,13 @@ export async function POST(request: NextRequest) { } if (provider === "wavespeed") { + if (!selectedModel?.modelId || !selectedModel?.displayName) { + return NextResponse.json( + { success: false, error: "selectedModel with modelId and displayName is required for WaveSpeed" }, + { status: 400 } + ); + } + // User-provided key takes precedence over env variable const wavespeedApiKey = request.headers.get("X-WaveSpeed-Key") || process.env.WAVESPEED_API_KEY; if (!wavespeedApiKey) { @@ -379,8 +407,8 @@ export async function POST(request: NextRequest) { // Build generation input const genInput: GenerationInput = { model: { - id: selectedModel!.modelId, - name: selectedModel!.displayName, + id: selectedModel.modelId, + name: selectedModel.displayName, provider: "wavespeed", capabilities: capabilitiesForMediaType(mediaType), description: null, diff --git a/src/components/__tests__/ConnectionDropMenu.test.tsx b/src/components/__tests__/ConnectionDropMenu.test.tsx index 9d47cc39..50ca9f0c 100644 --- a/src/components/__tests__/ConnectionDropMenu.test.tsx +++ b/src/components/__tests__/ConnectionDropMenu.test.tsx @@ -245,8 +245,9 @@ describe("ConnectionDropMenu", () => { it("should wrap around when navigating past last item", () => { render(); - // Text target options: Prompt, Prompt Constructor, nanoBanana, generateVideo, llmGenerate (5 items) - // Navigate down 5 times to wrap to first + // Text target options: Prompt, Prompt Constructor, nanoBanana, generateVideo, generateAudio, llmGenerate (6 items) + // Navigate down 6 times to wrap to first + fireEvent.keyDown(document, { key: "ArrowDown" }); fireEvent.keyDown(document, { key: "ArrowDown" }); fireEvent.keyDown(document, { key: "ArrowDown" }); fireEvent.keyDown(document, { key: "ArrowDown" }); diff --git a/src/components/nodes/GenerateAudioNode.tsx b/src/components/nodes/GenerateAudioNode.tsx index ca7ce33b..123cab37 100644 --- a/src/components/nodes/GenerateAudioNode.tsx +++ b/src/components/nodes/GenerateAudioNode.tsx @@ -302,7 +302,7 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps - {audioRef.current?.duration && isFinite(audioRef.current.duration) && ( + {!!audioRef.current?.duration && isFinite(audioRef.current.duration) && (
{ + if (!Number.isFinite(seconds)) return "0:00"; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, "0")}`; diff --git a/src/store/execution/executeNode.ts b/src/store/execution/executeNode.ts index 160fde94..826270a3 100644 --- a/src/store/execution/executeNode.ts +++ b/src/store/execution/executeNode.ts @@ -21,7 +21,8 @@ import { executeGenerateVideo } from "./generateVideoExecutor"; import { executeGenerate3D } from "./generate3dExecutor"; import { executeLlmGenerate } from "./llmGenerateExecutor"; import { executeSplitGrid } from "./splitGridExecutor"; -import { executeVideoStitch, executeEaseCurve, executeVideoTrim } from "./videoProcessingExecutors"; +import { executeVideoStitch, executeEaseCurve, executeVideoTrim, executeVideoFrameGrab } from "./videoProcessingExecutors"; +import { executeGenerateAudio } from "./generateAudioExecutor"; export interface ExecuteNodeOptions { /** When true, executors that support it will fall back to stored inputs. */ @@ -96,5 +97,11 @@ export async function executeNode( case "glbViewer": await executeGlbViewer(ctx); break; + case "generateAudio": + await executeGenerateAudio(ctx, regenOpts); + break; + case "videoFrameGrab": + await executeVideoFrameGrab(ctx); + break; } }