Browse Source

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 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
cf225747ff
  1. 44
      src/app/api/generate/route.ts
  2. 5
      src/components/__tests__/ConnectionDropMenu.test.tsx
  3. 2
      src/components/nodes/GenerateAudioNode.tsx
  4. 1
      src/hooks/useAudioPlayback.ts
  5. 9
      src/store/execution/executeNode.ts

44
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<GenerateResponse>(
{ 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<GenerateResponse>(
{ 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<GenerateResponse>(
{ 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<GenerateResponse>(
{ 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,

5
src/components/__tests__/ConnectionDropMenu.test.tsx

@ -245,8 +245,9 @@ describe("ConnectionDropMenu", () => {
it("should wrap around when navigating past last item", () => {
render(<ConnectionDropMenu {...defaultProps} handleType="text" connectionType="source" />);
// 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" });

2
src/components/nodes/GenerateAudioNode.tsx

@ -302,7 +302,7 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps<GenerateAudi
{/* Progress bar */}
<div className="flex-1 h-1 bg-neutral-700 rounded-full overflow-hidden relative">
{audioRef.current?.duration && isFinite(audioRef.current.duration) && (
{!!audioRef.current?.duration && isFinite(audioRef.current.duration) && (
<div
className="h-full bg-violet-500 transition-all"
style={{ width: `${(currentTime / audioRef.current.duration) * 100}%` }}

1
src/hooks/useAudioPlayback.ts

@ -190,6 +190,7 @@ export function useAudioPlayback({ audioSrc, waveformData, isLoadingWaveform }:
}, []);
const formatTime = useCallback((seconds: number) => {
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")}`;

9
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;
}
}

Loading…
Cancel
Save