mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 01:31:08 +00:00
fix: normalize video provider durations
This commit is contained in:
@@ -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,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Record<string, readonly number[]>> = {
|
||||
"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<string, unknown> = {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user