diff --git a/extensions/google/video-generation-provider.test.ts b/extensions/google/video-generation-provider.test.ts index 72f4090bbf9..3a99ca8502a 100644 --- a/extensions/google/video-generation-provider.test.ts +++ b/extensions/google/video-generation-provider.test.ts @@ -116,4 +116,42 @@ describe("google video generation provider", () => { }), ).rejects.toThrow("Google video generation does not support image and video inputs together."); }); + + it("rounds unsupported durations to the nearest Veo value", async () => { + vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({ + apiKey: "google-key", + source: "env", + mode: "api-key", + }); + generateVideosMock.mockResolvedValue({ + done: true, + response: { + generatedVideos: [ + { + video: { + videoBytes: Buffer.from("mp4-bytes").toString("base64"), + mimeType: "video/mp4", + }, + }, + ], + }, + }); + + const provider = buildGoogleVideoGenerationProvider(); + await provider.generateVideo({ + provider: "google", + model: "veo-3.1-fast-generate-preview", + prompt: "A tiny robot watering a windowsill garden", + cfg: {}, + durationSeconds: 5, + }); + + expect(generateVideosMock).toHaveBeenCalledWith( + expect.objectContaining({ + config: expect.objectContaining({ + durationSeconds: 6, + }), + }), + ); + }); }); diff --git a/extensions/google/video-generation-provider.ts b/extensions/google/video-generation-provider.ts index 036fce173d9..66ade3dc23d 100644 --- a/extensions/google/video-generation-provider.ts +++ b/extensions/google/video-generation-provider.ts @@ -15,8 +15,10 @@ const DEFAULT_GOOGLE_VIDEO_MODEL = "veo-3.1-fast-generate-preview"; const DEFAULT_TIMEOUT_MS = 180_000; const POLL_INTERVAL_MS = 10_000; const MAX_POLL_ATTEMPTS = 90; -const GOOGLE_VIDEO_MIN_DURATION_SECONDS = 4; -const GOOGLE_VIDEO_MAX_DURATION_SECONDS = 8; +const GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS = [4, 6, 8] as const; +const GOOGLE_VIDEO_MIN_DURATION_SECONDS = GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[0]; +const GOOGLE_VIDEO_MAX_DURATION_SECONDS = + GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS.length - 1]; function resolveConfiguredGoogleVideoBaseUrl(req: VideoGenerationRequest): string | undefined { const configured = req.cfg?.models?.providers?.google?.baseUrl?.trim(); @@ -75,10 +77,21 @@ function resolveDurationSeconds(durationSeconds: number | undefined): number | u if (typeof durationSeconds !== "number" || !Number.isFinite(durationSeconds)) { return undefined; } - return Math.min( + const rounded = Math.min( GOOGLE_VIDEO_MAX_DURATION_SECONDS, Math.max(GOOGLE_VIDEO_MIN_DURATION_SECONDS, Math.round(durationSeconds)), ); + return GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS.reduce((best, current) => { + const currentDistance = Math.abs(current - rounded); + const bestDistance = Math.abs(best - rounded); + if (currentDistance < bestDistance) { + return current; + } + if (currentDistance === bestDistance && current > best) { + return current; + } + return best; + }); } function resolveInputImage(req: VideoGenerationRequest) { diff --git a/extensions/minimax/video-generation-provider.test.ts b/extensions/minimax/video-generation-provider.test.ts index abd58316cd4..27e04d10582 100644 --- a/extensions/minimax/video-generation-provider.test.ts +++ b/extensions/minimax/video-generation-provider.test.ts @@ -71,11 +71,15 @@ describe("minimax video generation provider", () => { model: "MiniMax-Hailuo-2.3", prompt: "A fox sprints across snowy hills", cfg: {}, + durationSeconds: 5, }); expect(postJsonRequestMock).toHaveBeenCalledWith( expect.objectContaining({ url: "https://api.minimax.io/v1/video_generation", + body: expect.objectContaining({ + duration: 6, + }), }), ); expect(result.videos).toHaveLength(1); diff --git a/extensions/minimax/video-generation-provider.ts b/extensions/minimax/video-generation-provider.ts index 2e6289d37a8..0dea3a597ff 100644 --- a/extensions/minimax/video-generation-provider.ts +++ b/extensions/minimax/video-generation-provider.ts @@ -17,6 +17,10 @@ const DEFAULT_MINIMAX_VIDEO_MODEL = "MiniMax-Hailuo-2.3"; const DEFAULT_TIMEOUT_MS = 120_000; const POLL_INTERVAL_MS = 10_000; const MAX_POLL_ATTEMPTS = 90; +const MINIMAX_MODEL_ALLOWED_DURATIONS: Readonly> = { + "MiniMax-Hailuo-2.3": [6, 10], + "MiniMax-Hailuo-02": [6, 10], +}; type MinimaxBaseResp = { status_code?: number; @@ -85,6 +89,23 @@ function resolveFirstFrameImage(req: VideoGenerationRequest): string | undefined return toDataUrl(input.buffer, input.mimeType?.trim() || "image/png"); } +function resolveDurationSeconds(params: { + model: string; + durationSeconds: number | undefined; +}): number | undefined { + if (typeof params.durationSeconds !== "number" || !Number.isFinite(params.durationSeconds)) { + return undefined; + } + const rounded = Math.max(1, Math.round(params.durationSeconds)); + const allowed = MINIMAX_MODEL_ALLOWED_DURATIONS[params.model]; + if (!allowed || allowed.length === 0) { + return rounded; + } + return allowed.reduce((best, current) => + Math.abs(current - rounded) < Math.abs(best - rounded) ? current : best, + ); +} + async function pollMinimaxVideo(params: { taskId: string; headers: Headers; @@ -242,8 +263,9 @@ export function buildMinimaxVideoGenerationProvider(): VideoGenerationProvider { capability: "video", transport: "http", }); + const model = req.model?.trim() || DEFAULT_MINIMAX_VIDEO_MODEL; const body: Record = { - model: req.model?.trim() || DEFAULT_MINIMAX_VIDEO_MODEL, + model, prompt: req.prompt, }; const firstFrameImage = resolveFirstFrameImage(req); @@ -253,8 +275,12 @@ export function buildMinimaxVideoGenerationProvider(): VideoGenerationProvider { if (req.resolution) { body.resolution = req.resolution; } - if (typeof req.durationSeconds === "number" && Number.isFinite(req.durationSeconds)) { - body.duration = Math.max(1, Math.round(req.durationSeconds)); + const durationSeconds = resolveDurationSeconds({ + model, + durationSeconds: req.durationSeconds, + }); + if (typeof durationSeconds === "number") { + body.duration = durationSeconds; } const { response, release } = await postJsonRequest({ url: `${baseUrl}/v1/video_generation`, @@ -303,7 +329,7 @@ export function buildMinimaxVideoGenerationProvider(): VideoGenerationProvider { })(); return { videos: [video], - model: req.model?.trim() || DEFAULT_MINIMAX_VIDEO_MODEL, + model, metadata: { taskId, status: completed.status,