Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ mmx video download --file-id 176844028768320 --out video.mp4

For MiniMax-H3, local files and Base64 data URIs are preflight-checked against the documented limits: image 30 MB, reference video 50 MB, reference audio 15 MB, and total JSON request body 64 MB. The API validates dimensions, aspect ratio, duration, frame rate, and codecs for all media.

`--region` is an existing global CLI option, not an H3 parameter. Normal video generation does not need it; the CLI uses the saved or automatically detected region. H3 defaults to 2K, 5 seconds, and 16:9 for text-to-video.
`--region` is an existing global CLI option, not an H3 parameter. Normal video generation does not need it; the CLI uses the saved or automatically detected region. H3 defaults to 2K, 5 seconds, and 16:9 for text-to-video. Use `--resolution 768P` for 768P output.

### `mmx speech`

Expand Down
2 changes: 1 addition & 1 deletion README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ mmx video download --file-id 176844028768320 --out video.mp4

MiniMax-H3 会在发送前检查本地文件和 Base64 数据:图片不超过 30 MB、参考视频不超过 50 MB、参考音频不超过 15 MB、JSON 请求体总计不超过 64 MB。所有素材的尺寸、宽高比、时长、帧率和编码格式仍由 API 服务端校验。

`--region` 是 CLI 原有的全局选项,不是 H3 参数。正常生成视频无需填写,CLI 会使用已保存或自动识别的区域。H3 默认使用 2K、5 秒,文生视频默认比例为 16:9。
`--region` 是 CLI 原有的全局选项,不是 H3 参数。正常生成视频无需填写,CLI 会使用已保存或自动识别的区域。H3 默认使用 2K、5 秒,文生视频默认比例为 16:9。如需 768P 输出,请使用 `--resolution 768P`。

### `mmx speech`

Expand Down
2 changes: 1 addition & 1 deletion skill/h3-video/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ Do not perform region fallback for validation errors, error `2013`, billing, rat

- Prompt: at most 7000 characters.
- Output duration: integer from 4 through 15 seconds.
- Resolution: 2K.
- Resolution (`--resolution`): `768P` or `2K` (default).
- Reference images: at most 9.
- Reference videos: at most 3.
- Reference audios: at most 3.
Expand Down
8 changes: 6 additions & 2 deletions src/commands/video/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {

export default defineCommand({
name: 'video generate',
description: 'Generate a video\n V2: MiniMax-H3 (text/image/video/audio content, 2K)\n T2V: Hailuo-2.3\n I2V: Hailuo-2.3 (default) / Hailuo-2.3-Fast (fast mode, requires --image)\n SEF: Hailuo-02 (requires --image and --last-frame)\n S2V: S2V-01 (requires --subject-image)',
description: 'Generate a video\n V2: MiniMax-H3 (text/image/video/audio content, 768P/2K)\n T2V: Hailuo-2.3\n I2V: Hailuo-2.3 (default) / Hailuo-2.3-Fast (fast mode, requires --image)\n SEF: Hailuo-02 (requires --image and --last-frame)\n S2V: S2V-01 (requires --subject-image)',
apiDocs: '/docs/api-reference/video-generation-v2-create',
usage: 'mmx video generate --prompt <text> [flags]',
options: [
Expand All @@ -51,6 +51,7 @@ export default defineCommand({
{ flag: '--reference-video <path-or-url>', description: 'H3 reference video (repeatable; local MP4/MOV, URL, data URI, or mm_file:// ID).', type: 'array' },
{ flag: '--reference-audio <path-or-url>', description: 'H3 reference audio (repeatable; requires a reference image or video).', type: 'array' },
{ flag: '--duration <seconds>', description: 'Output duration. H3 supports integer values from 4 to 15 (default: 5).', type: 'number' },
{ flag: '--resolution <resolution>', description: 'H3 output resolution: 768P or 2K (default: 2K).' },
{ flag: '--ratio <ratio>', description: 'H3 aspect ratio: adaptive, 21:9, 16:9, 4:3, 1:1, 3:4, or 9:16.' },
{ flag: '--callback-url <url>', description: 'Webhook URL for completion notification' },
{ flag: '--download <path>', description: 'Save video to file on completion' },
Expand All @@ -65,6 +66,7 @@ export default defineCommand({
'mmx video generate --prompt "A robot painting." --no-wait --quiet',
'# H3 text-to-video (Video Generation V2)',
'mmx video generate --model MiniMax-H3 --prompt "Ocean waves at sunset"',
'mmx video generate --model MiniMax-H3 --prompt "Ocean waves at sunset" --resolution 768P',
'# H3 image-to-video',
'mmx video generate --model MiniMax-H3 --prompt "The subject walks forward" --image start.jpg',
'# H3 reference-to-video',
Expand Down Expand Up @@ -101,6 +103,7 @@ export default defineCommand({
flags.referenceImage ||
flags.referenceVideo ||
flags.referenceAudio ||
flags.resolution !== undefined ||
flags.duration !== undefined ||
flags.ratio,
);
Expand Down Expand Up @@ -174,6 +177,7 @@ export default defineCommand({
.map(input => resolveMediaInput(input, 'video')),
referenceAudios: ((flags.referenceAudio as string[] | undefined) ?? [])
.map(input => resolveMediaInput(input, 'audio')),
resolution: flags.resolution as string | undefined,
duration: flags.duration as number | undefined,
ratio: flags.ratio as string | undefined,
callbackUrl: flags.callbackUrl as string | undefined,
Expand All @@ -191,7 +195,7 @@ export default defineCommand({
} else {
if (hasV2OnlyInput) {
throw new CLIError(
'--reference-image, --reference-video, --reference-audio, --duration, and --ratio require --model MiniMax-H3.',
'--reference-image, --reference-video, --reference-audio, --resolution, --duration, and --ratio require --model MiniMax-H3.',
ExitCode.USAGE,
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/sdk/video/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export class VideoSDK extends Client {
const body = {
...params,
model: VIDEO_V2_MODEL,
resolution: (params.resolution ?? '2K') as '2K',
resolution: (params.resolution ?? '2K') as VideoV2Request['resolution'],
duration: (params.duration ?? 5) as VideoV2Request['duration'],
ratio: params.ratio ?? (hasFrameInput || hasReferenceInput ? 'adaptive' : '16:9'),
} as VideoV2Request;
Expand Down
3 changes: 2 additions & 1 deletion src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ export interface VideoTaskResponse {
}

export type VideoV2ImageRole = 'first_frame' | 'last_frame' | 'reference_image';
export type VideoV2Resolution = '768P' | '2K';
export type VideoV2Ratio = 'adaptive' | '21:9' | '16:9' | '4:3' | '1:1' | '3:4' | '9:16';
export type VideoV2Duration = 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15;

Expand Down Expand Up @@ -293,7 +294,7 @@ export type VideoV2ContentItem =
export interface VideoV2Request {
model: 'MiniMax-H3';
content: VideoV2ContentItem[];
resolution: '2K';
resolution: VideoV2Resolution;
duration: VideoV2Duration;
ratio?: VideoV2Ratio;
callback_url?: string;
Expand Down
9 changes: 6 additions & 3 deletions src/video/v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
VideoV2ImageRole,
VideoV2Ratio,
VideoV2Request,
VideoV2Resolution,
VideoV2Task,
} from '../types/api';
import {
Expand All @@ -15,6 +16,8 @@ import {

export const VIDEO_V2_MODEL = 'MiniMax-H3' as const;

const VIDEO_V2_RESOLUTIONS = new Set<VideoV2Resolution>(['768P', '2K']);

const VIDEO_V2_RATIOS = new Set<VideoV2Ratio>([
'adaptive',
'21:9',
Expand Down Expand Up @@ -105,7 +108,7 @@ export function buildVideoV2Request(options: BuildVideoV2RequestOptions): VideoV
const request: VideoV2Request = {
model: VIDEO_V2_MODEL,
content,
resolution: (options.resolution ?? '2K') as '2K',
resolution: (options.resolution ?? '2K') as VideoV2Resolution,
duration: (options.duration ?? 5) as VideoV2Duration,
ratio: (hasFrameInput
? 'adaptive'
Expand All @@ -131,8 +134,8 @@ export function validateVideoV2Request(request: VideoV2Request): void {
throw new VideoV2InputError('MiniMax-H3 text content must not exceed 7000 characters.');
}

if (request.resolution !== '2K') {
throw new VideoV2InputError('MiniMax-H3 currently only supports 2K resolution.');
if (!VIDEO_V2_RESOLUTIONS.has(request.resolution)) {
throw new VideoV2InputError('MiniMax-H3 resolution must be one of: 768P, 2K.');
}
if (!Number.isInteger(request.duration) || request.duration < 4 || request.duration > 15) {
throw new VideoV2InputError('MiniMax-H3 duration must be an integer from 4 to 15 seconds.');
Expand Down
116 changes: 115 additions & 1 deletion test/commands/video/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { mkdtempSync, readFileSync, rmSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { default as generateCommand } from '../../../src/commands/video/generate';
import { ExitCode } from '../../../src/errors/codes';
import type { Config } from '../../../src/config/schema';
import type { GlobalFlags } from '../../../src/types/flags';
import { createMockServer } from '../../helpers/mock-server';
Expand Down Expand Up @@ -67,7 +68,7 @@ describe('video generate command', () => {
expect(optionFlags).toContain('--image <path-or-url>');
expect(optionFlags).not.toContain('--first-frame <path-or-url>');
expect(optionFlags).not.toContain('--image-role <role>');
expect(optionFlags).not.toContain('--resolution <resolution>');
expect(optionFlags).toContain('--resolution <resolution>');
expect(optionFlags).not.toContain('--region <region>');
expect(legacyFirstFrame?.hidden).toBe(true);
});
Expand Down Expand Up @@ -276,6 +277,119 @@ describe('video generate command', () => {
}
});

it('should send the requested H3 resolution when --resolution is set', async () => {
const config = {
apiKey: 'test-key',
region: 'global' as const,
baseUrl: 'https://api.mmx.io',
output: 'json' as const,
timeout: 10,
verbose: false,
quiet: false,
noColor: true,
yes: false,
dryRun: true,
nonInteractive: true,
async: false,
};

const originalLog = console.log;
let output = '';
console.log = (msg: string) => { output += msg; };

try {
await generateCommand.execute(config, {
prompt: 'Ocean waves at sunset',
model: 'MiniMax-H3',
resolution: '768P',
quiet: false,
verbose: false,
noColor: true,
yes: false,
dryRun: true,
help: false,
nonInteractive: true,
async: false,
});

expect(JSON.parse(output).request.resolution).toBe('768P');
} finally {
console.log = originalLog;
}
});

it('should reject unsupported H3 resolutions as usage errors', async () => {
const config = {
apiKey: 'test-key',
region: 'global' as const,
baseUrl: 'https://api.mmx.io',
output: 'json' as const,
timeout: 10,
verbose: false,
quiet: false,
noColor: true,
yes: false,
dryRun: true,
nonInteractive: true,
async: false,
};

await expect(
generateCommand.execute(config, {
prompt: 'Ocean waves at sunset',
model: 'MiniMax-H3',
resolution: '1080P',
quiet: false,
verbose: false,
noColor: true,
yes: false,
dryRun: true,
help: false,
nonInteractive: true,
async: false,
}),
).rejects.toMatchObject({
message: 'MiniMax-H3 resolution must be one of: 768P, 2K.',
exitCode: ExitCode.USAGE,
});
});

it('should reject --resolution for legacy models', async () => {
const config = {
apiKey: 'test-key',
region: 'global' as const,
baseUrl: 'https://api.mmx.io',
output: 'json' as const,
timeout: 10,
verbose: false,
quiet: false,
noColor: true,
yes: false,
dryRun: true,
nonInteractive: true,
async: false,
};

await expect(
generateCommand.execute(config, {
prompt: 'A cat',
model: 'MiniMax-Hailuo-2.3',
resolution: '768P',
quiet: false,
verbose: false,
noColor: true,
yes: false,
dryRun: true,
help: false,
nonInteractive: true,
async: false,
}),
).rejects.toMatchObject({
message: expect.stringContaining('require --model MiniMax-H3'),
exitCode: ExitCode.USAGE,
});
});

it('maps H3 reference inputs into typed content items', async () => {
const config = {
apiKey: 'test-key',
Expand Down
68 changes: 68 additions & 0 deletions test/sdk/video.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,74 @@ describe('MiniMaxSDK.video', () => {
});
});

it('should pass 768P resolution through both H3 request forms', async () => {
const requestBodies: Array<{ resolution?: string }> = [];
server = createMockServer({
routes: {
'/v2/video_generation': async (req) => {
requestBodies.push(await req.json() as { resolution?: string });
return jsonResponse({ task_id: 'h3-768' });
},
},
});

const sdk = new MiniMaxSDK({
apiKey: 'test-key',
baseUrl: server.url,
});

await sdk.video.generate({
model: 'MiniMax-H3',
content: [{ type: 'text', text: 'Ocean waves' }],
resolution: '768P',
async: true,
});
await sdk.video.generate({
model: 'MiniMax-H3',
prompt: 'Ocean waves',
resolution: '768P',
async: true,
});

expect(requestBodies.map(body => body.resolution)).toEqual(['768P', '768P']);
});

it('should reject unsupported H3 resolutions without sending a request', async () => {
let requestCount = 0;
server = createMockServer({
routes: {
'/v2/video_generation': () => {
requestCount += 1;
return jsonResponse({ task_id: 'h3-invalid' });
},
},
});

const sdk = new MiniMaxSDK({
apiKey: 'test-key',
baseUrl: server.url,
});

await expect(
sdk.video.generate({
model: 'MiniMax-H3',
prompt: 'Ocean waves',
resolution: '1080P',
async: true,
} as never),
).rejects.toThrow('resolution must be one of: 768P, 2K');
await expect(
sdk.video.generate({
model: 'MiniMax-H3',
content: [{ type: 'text', text: 'Ocean waves' }],
resolution: '1080P',
async: true,
} as never),
).rejects.toThrow('resolution must be one of: 768P, 2K');

expect(requestCount).toBe(0);
});

it('should get task status', async () => {
server = createMockServer({
routes: {
Expand Down
9 changes: 7 additions & 2 deletions test/video/v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,13 @@ describe('Video Generation V2 request builder', () => {
});

it('validates resolution, duration, and ratio boundaries', () => {
expect(() => buildVideoV2Request({ prompt, resolution: '1080P' }))
.toThrow('only supports 2K resolution');
for (const resolution of ['1080P', '480P', '768p', '2k', '']) {
expect(() => buildVideoV2Request({ prompt, resolution }))
.toThrow('resolution must be one of: 768P, 2K');
}
expect(buildVideoV2Request({ prompt, resolution: '768P' }).resolution).toBe('768P');
expect(buildVideoV2Request({ prompt, resolution: '2K' }).resolution).toBe('2K');
expect(buildVideoV2Request({ prompt }).resolution).toBe('2K');

for (const duration of [3, 16, 4.5]) {
expect(() => buildVideoV2Request({ prompt, duration }))
Expand Down