mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-04 04:53:39 +00:00
fix(ai): validate MiniMax video inputs and resolution tiers
This commit is contained in:
1 parent
0b4774c90c
commit
7445cd85f2
7 files changed
+90
-17
No files matched your search
@@ -1001,6 +1001,49 @@ describe('VideoGenerationDriver.generate puter_output_path', () => {
|
||||
// ── Size unification ───────────────────────────────────────────────
|
||||
|
||||
describe('VideoGenerationDriver.generate size unification', () => {
|
||||
it.each(['912x512', '512x912'])(
|
||||
'selects and bills Hailuo 02 at 512p for %s',
|
||||
async (size) => {
|
||||
const fetchSpy = vi.spyOn(globalThis, 'fetch');
|
||||
const incrementUsageSpy = vi.spyOn(
|
||||
server.services.metering,
|
||||
'incrementUsage',
|
||||
);
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ task_id: 'mm-task' })),
|
||||
);
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({
|
||||
status: 'Success',
|
||||
file_id: 'file-1',
|
||||
})),
|
||||
);
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({
|
||||
file: { download_url: 'https://minimax/out.mp4' },
|
||||
})),
|
||||
);
|
||||
|
||||
await withActor(() => driver.generate({
|
||||
prompt: 'a ball rolls',
|
||||
model: 'minimax-hailuo-02',
|
||||
size,
|
||||
input_reference: 'https://example.com/first.png',
|
||||
} as never));
|
||||
|
||||
const body = JSON.parse(
|
||||
fetchSpy.mock.calls[0]![1]!.body as string,
|
||||
);
|
||||
expect(body.resolution).toBe('512P');
|
||||
expect(incrementUsageSpy).toHaveBeenCalledWith(
|
||||
SYSTEM_ACTOR,
|
||||
'minimax-video-generation:minimax-hailuo-02:per-video:512p:6',
|
||||
1,
|
||||
10 * 1_000_000,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it('maps a WIDTHxHEIGHT size onto a tier plus aspect ratio for tier-based models', async () => {
|
||||
completeTogetherJob();
|
||||
|
||||
|
||||
@@ -66,14 +66,15 @@ const parsePixelSize = (
|
||||
};
|
||||
|
||||
/** Resolution tier of the shorter side, e.g. 1920x1080 and 1080x1920 → 1080p. */
|
||||
const tierForPixels = ({
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
}) => {
|
||||
const tierForPixels = (
|
||||
{ width, height }: { width: number; height: number },
|
||||
dimensions: readonly string[],
|
||||
) => {
|
||||
const shortSide = Math.min(width, height);
|
||||
const exactTier = dimensions.find(
|
||||
(tier) => tier.toLowerCase() === `${shortSide}p`,
|
||||
);
|
||||
if (exactTier) return exactTier;
|
||||
if (shortSide <= 480) return '480p';
|
||||
if (shortSide <= 720) return '720p';
|
||||
if (shortSide <= 1080) return '1080p';
|
||||
@@ -297,7 +298,7 @@ export class VideoGenerationDriver extends PuterDriver {
|
||||
const catalogIsTiers = model.dimensions.every(isResolutionTier);
|
||||
const wanted =
|
||||
catalogIsTiers && requestedPixels
|
||||
? tierForPixels(requestedPixels)
|
||||
? tierForPixels(requestedPixels, model.dimensions)
|
||||
: requestedResolution;
|
||||
|
||||
// Case-insensitive so '4K' matches a catalog entry spelled '4k';
|
||||
|
||||
@@ -384,19 +384,21 @@ describe('MiniMaxVideoProvider.generate Hailuo request shape', () => {
|
||||
model: 'minimax-hailuo-2.3-fast',
|
||||
seconds: 10,
|
||||
size: '1080p',
|
||||
input_reference: 'https://example.com/first.png',
|
||||
});
|
||||
expect(sentBody()).toMatchObject({
|
||||
model: 'MiniMax-Hailuo-2.3-Fast',
|
||||
duration: 6,
|
||||
resolution: '1080P',
|
||||
first_frame_image: 'https://example.com/first.png',
|
||||
});
|
||||
});
|
||||
|
||||
it('sends frames as first_frame_image and last_frame_image', async () => {
|
||||
it.each(['768p', '1080p'])('sends first and last frames at %s', async (size) => {
|
||||
mockHailuoFlow(hailuoTask('Success'));
|
||||
await generate({
|
||||
model: 'minimax-hailuo-02',
|
||||
size: '512p',
|
||||
size,
|
||||
input_reference: 'DDDD',
|
||||
last_frame: 'https://example.com/last.png',
|
||||
});
|
||||
@@ -404,7 +406,7 @@ describe('MiniMaxVideoProvider.generate Hailuo request shape', () => {
|
||||
model: 'MiniMax-Hailuo-02',
|
||||
prompt: 'a ball rolls',
|
||||
duration: 6,
|
||||
resolution: '512P',
|
||||
resolution: size.toUpperCase(),
|
||||
first_frame_image: 'data:image/png;base64,DDDD',
|
||||
last_frame_image: 'https://example.com/last.png',
|
||||
});
|
||||
@@ -476,6 +478,19 @@ describe('MiniMaxVideoProvider.generate rejects invalid input before calling Min
|
||||
'512p without a first frame on Hailuo 02',
|
||||
{ model: 'minimax-hailuo-02', size: '512p' },
|
||||
],
|
||||
[
|
||||
'a missing first frame on Hailuo 2.3 Fast',
|
||||
{ model: 'minimax-hailuo-2.3-fast' },
|
||||
],
|
||||
[
|
||||
'512p with first and last frames on Hailuo 02',
|
||||
{
|
||||
model: 'minimax-hailuo-02',
|
||||
size: '512p',
|
||||
input_reference: 'https://example.com/first.png',
|
||||
last_frame: 'https://example.com/last.png',
|
||||
},
|
||||
],
|
||||
[
|
||||
'a non-string image',
|
||||
{ model: 'minimax-h3', input_reference: 42 as never },
|
||||
@@ -485,11 +500,17 @@ describe('MiniMaxVideoProvider.generate rejects invalid input before calling Min
|
||||
{ model: 'minimax-h3', reference_images: 'x' as never },
|
||||
],
|
||||
])('rejects %s', async (_label, params) => {
|
||||
fetchSpy.mockRejectedValue(new Error('Unexpected MiniMax request'));
|
||||
const reserveCreditsSpy = vi.spyOn(
|
||||
server.services.metering,
|
||||
'reserveCredits',
|
||||
);
|
||||
await expect(generate(params)).rejects.toMatchObject({
|
||||
statusCode: 400,
|
||||
legacyCode: 'bad_request',
|
||||
});
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
expect(reserveCreditsSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('accepts a 7000-character prompt on H3', async () => {
|
||||
|
||||
@@ -529,6 +529,9 @@ export class MiniMaxVideoProvider extends VideoProvider {
|
||||
},
|
||||
) {
|
||||
const { firstFrame, endFrame, references } = inputs;
|
||||
if (spec.firstFrameRequired && !firstFrame) {
|
||||
throw badRequest(`${model.id} requires an input_reference image`);
|
||||
}
|
||||
if (endFrame && !spec.lastFrame) {
|
||||
throw badRequest(`${model.id} does not support last_frame`);
|
||||
}
|
||||
@@ -556,9 +559,12 @@ export class MiniMaxVideoProvider extends VideoProvider {
|
||||
`${model.id} takes at most ${spec.maxImages} image(s), frames and references together`,
|
||||
);
|
||||
}
|
||||
if (spec.imageOnlyResolutions?.includes(resolutionKey) && !firstFrame) {
|
||||
if (
|
||||
spec.imageOnlyResolutions?.includes(resolutionKey) &&
|
||||
(!firstFrame || endFrame)
|
||||
) {
|
||||
throw badRequest(
|
||||
`${model.id} renders ${resolutionKey} only from an input_reference image`,
|
||||
`${model.id} renders ${resolutionKey} only from an input_reference image without last_frame`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface MiniMaxVideoSpec {
|
||||
maxImages: number;
|
||||
/** Whether `reference_images` are taken (H3 reference-to-video). */
|
||||
references: boolean;
|
||||
firstFrameRequired?: boolean;
|
||||
lastFrame: boolean;
|
||||
/** Resolutions MiniMax renders only from a first frame. */
|
||||
imageOnlyResolutions?: string[];
|
||||
@@ -217,6 +218,7 @@ export const MINIMAX_VIDEO_SPECS: Record<string, MiniMaxVideoSpec> = {
|
||||
api: 'v1',
|
||||
maxImages: 1,
|
||||
references: false,
|
||||
firstFrameRequired: true,
|
||||
lastFrame: false,
|
||||
maxPromptChars: 2000,
|
||||
},
|
||||
|
||||
@@ -289,7 +289,7 @@ Available when using a MiniMax model (provider `'minimax-video-generation'`). Th
|
||||
| `minimax-hailuo-2.3-fast` | 6 or 10 | `768p`, `1080p` (6 seconds only) | No | No | No |
|
||||
| `minimax-hailuo-02` | 6 or 10 | `768p`, `1080p` (6 seconds only), `512p` (with `input_reference` only) | No | Yes | No |
|
||||
|
||||
The first resolution listed is the default. "Images in total" counts `input_reference`, `last_frame` and `reference_images` together.
|
||||
The first resolution listed is the default. "Images in total" counts `input_reference`, `last_frame` and `reference_images` together. Hailuo 2.3 Fast requires an `input_reference` image. Hailuo 02 supports `512p` with a first frame alone. First and last frames together require `768p` or `1080p`.
|
||||
|
||||
| Model | Price |
|
||||
|-------|-------|
|
||||
@@ -308,8 +308,8 @@ The first resolution listed is the default. "Images in total" counts `input_refe
|
||||
| `seconds` | `Number` | Clip length, from the table above. Any other value falls back to the default. On the Hailuo models, 10 seconds at `1080p` falls back to 6 |
|
||||
| `size` | `String` | Resolution tier from the table above (case-insensitive), or `'WIDTHxHEIGHT'`. `resolution` is an alias |
|
||||
| `width`, `height` | `Number` | Optional. Only their ratio is used, to pick the aspect ratio of an H3 clip made from text or `reference_images`: `21:9`, `16:9`, `4:3`, `1:1`, `3:4` or `9:16`. Any other ratio gives `16:9`. A clip from `input_reference` or `last_frame` takes the image's shape. The Hailuo models ignore them |
|
||||
| `input_reference` | `String \| Blob` | Image used as the first frame, in any [image input](#image-inputs) format. JPEG, PNG or WebP, at least 256 pixels per side on the H3 models and more than 300 pixels on the short side on the Hailuo models |
|
||||
| `last_frame` | `String \| Blob` | Image used as the last frame, same formats. Requires `input_reference` on Hailuo 02. The H3 models also take it alone |
|
||||
| `input_reference` | `String \| Blob` | Image used as the first frame, in any [image input](#image-inputs) format. Required on Hailuo 2.3 Fast. JPEG, PNG or WebP, at least 256 pixels per side on the H3 models and more than 300 pixels on the short side on the Hailuo models |
|
||||
| `last_frame` | `String \| Blob` | Image used as the last frame, same formats. Requires `input_reference` and `768p` or `1080p` on Hailuo 02. The H3 models also take it alone |
|
||||
| `reference_images` | `Array<String \| Blob>` | Images the model uses as subject, style or scene references, same formats, on the H3 models. Cannot be combined with `input_reference` or `last_frame` |
|
||||
|
||||
An option the model lacks, or a combination it doesn't support, is rejected with `bad_request` before any credits are spent. MiniMax returns a hosted URL that is temporary, so pass `puter_output_path` to keep a clip. A Hailuo link works for one hour.
|
||||
|
||||
@@ -139,7 +139,7 @@ The reservation is a conservative estimate based on verified generation usage, n
|
||||
|
||||
Kling video models take a prompt of up to 3,072 characters (Kling 3.0 models) or 2,500 (the others, and Kling 3.0 Turbo with `input_reference`). Kling 3.0 Omni and Kling O1 take a source video (`inputVideo`) as a URL or as a Puter file of up to 200 MiB, which fails with `413 input_too_large` above that. Kling fetches either one itself. A Puter file is passed as a link that works for 19 minutes. The source must be an MP4 or QuickTime video of 3 to 15.5 seconds (10 on Kling O1). Kling rejects a source outside its limits, or one it can't decode, with `upstream_bad_request`, and the request isn't charged. An edit holds the price of a 16-second clip (10 on Kling O1). Each call to Kling times out after 60 seconds, and a job is polled every 5 seconds.
|
||||
|
||||
MiniMax video models take a prompt of up to 7,000 characters (H3 models) or 2,000 (Hailuo models), and up to 5 images as frames and references together (2 on H3 Max and Hailuo 02, 1 on Hailuo 2.3 and 2.3 Fast). MiniMax fetches an image URL itself, and a Puter file is sent inline. Each call to MiniMax times out after 60 seconds, and a job is polled every 5 seconds.
|
||||
MiniMax video models take a prompt of up to 7,000 characters (H3 models) or 2,000 (Hailuo models), and up to 5 images as frames and references together (2 on H3 Max and Hailuo 02, 1 on Hailuo 2.3 and 2.3 Fast). Hailuo 2.3 Fast requires a first-frame image. Hailuo 02 supports first and last frames together at `768p` or `1080p`, and a first frame alone at `512p`. MiniMax fetches an image URL itself, and a Puter file is sent inline. Each call to MiniMax times out after 60 seconds, and a job is polled every 5 seconds.
|
||||
|
||||
FLUX 3 on Together AI takes a source video (`inputVideo`) of up to 32 MiB as a Puter file, or up to 1 MiB as a `data:` URI. A larger one fails with `413 input_too_large` before the job starts. A Puter file is passed to Together AI as a link that works for 19 minutes, and a URL is passed as is. Together AI fetches either one.
|
||||
|
||||
|
||||
Reference in new issue
Block a user