diff --git a/README.md b/README.md index da568664..1b6eb540 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/README_CN.md b/README_CN.md index 73da5a98..eb041291 100644 --- a/README_CN.md +++ b/README_CN.md @@ -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` diff --git a/skill/h3-video/SKILL.md b/skill/h3-video/SKILL.md index 273a92e0..a84c9553 100644 --- a/skill/h3-video/SKILL.md +++ b/skill/h3-video/SKILL.md @@ -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. diff --git a/src/commands/video/generate.ts b/src/commands/video/generate.ts index 438d0560..f6465973 100644 --- a/src/commands/video/generate.ts +++ b/src/commands/video/generate.ts @@ -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 [flags]', options: [ @@ -51,6 +51,7 @@ export default defineCommand({ { flag: '--reference-video ', description: 'H3 reference video (repeatable; local MP4/MOV, URL, data URI, or mm_file:// ID).', type: 'array' }, { flag: '--reference-audio ', description: 'H3 reference audio (repeatable; requires a reference image or video).', type: 'array' }, { flag: '--duration ', description: 'Output duration. H3 supports integer values from 4 to 15 (default: 5).', type: 'number' }, + { flag: '--resolution ', description: 'H3 output resolution: 768P or 2K (default: 2K).' }, { flag: '--ratio ', description: 'H3 aspect ratio: adaptive, 21:9, 16:9, 4:3, 1:1, 3:4, or 9:16.' }, { flag: '--callback-url ', description: 'Webhook URL for completion notification' }, { flag: '--download ', description: 'Save video to file on completion' }, @@ -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', @@ -101,6 +103,7 @@ export default defineCommand({ flags.referenceImage || flags.referenceVideo || flags.referenceAudio || + flags.resolution !== undefined || flags.duration !== undefined || flags.ratio, ); @@ -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, @@ -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, ); } diff --git a/src/sdk/video/index.ts b/src/sdk/video/index.ts index 5eefe17e..15f5a4f8 100644 --- a/src/sdk/video/index.ts +++ b/src/sdk/video/index.ts @@ -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; diff --git a/src/types/api.ts b/src/types/api.ts index e1517c01..6aedd059 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -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; @@ -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; diff --git a/src/video/v2.ts b/src/video/v2.ts index ad69c2ba..e2fb1f7d 100644 --- a/src/video/v2.ts +++ b/src/video/v2.ts @@ -5,6 +5,7 @@ import type { VideoV2ImageRole, VideoV2Ratio, VideoV2Request, + VideoV2Resolution, VideoV2Task, } from '../types/api'; import { @@ -15,6 +16,8 @@ import { export const VIDEO_V2_MODEL = 'MiniMax-H3' as const; +const VIDEO_V2_RESOLUTIONS = new Set(['768P', '2K']); + const VIDEO_V2_RATIOS = new Set([ 'adaptive', '21:9', @@ -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' @@ -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.'); diff --git a/test/commands/video/generate.test.ts b/test/commands/video/generate.test.ts index 71cae547..0bc44b11 100644 --- a/test/commands/video/generate.test.ts +++ b/test/commands/video/generate.test.ts @@ -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'; @@ -67,7 +68,7 @@ describe('video generate command', () => { expect(optionFlags).toContain('--image '); expect(optionFlags).not.toContain('--first-frame '); expect(optionFlags).not.toContain('--image-role '); - expect(optionFlags).not.toContain('--resolution '); + expect(optionFlags).toContain('--resolution '); expect(optionFlags).not.toContain('--region '); expect(legacyFirstFrame?.hidden).toBe(true); }); @@ -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', diff --git a/test/sdk/video.test.ts b/test/sdk/video.test.ts index cab6a34c..27d01a67 100644 --- a/test/sdk/video.test.ts +++ b/test/sdk/video.test.ts @@ -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: { diff --git a/test/video/v2.test.ts b/test/video/v2.test.ts index 2d449d84..51d0d4a3 100644 --- a/test/video/v2.test.ts +++ b/test/video/v2.test.ts @@ -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 }))