From 7270aec556acda8c1f0f5239070d0f51f20e0bb0 Mon Sep 17 00:00:00 2001 From: Raylan LIN Date: Fri, 10 Apr 2026 16:41:08 +0800 Subject: [PATCH 1/2] feat(video): add SEF (--last-frame), S2V (--subject-image), model auto-switch - Add --last-frame flag for SEF (start-end frame) interpolation mode - Add --subject-image flag for subject reference (S2V-01 character consistency) - Auto-switch model: --last-frame -> Hailuo-02, --subject-image -> S2V-01 - Explicit --model flag always overrides auto-switch - Add apiDocs field - Update description with all available models (T2V/I2V/S2V) - Add last_frame_image and subject_reference to VideoRequest type - Add validation: --last-frame requires --first-frame - Add SEF and S2V examples --- src/command.ts | 3 ++ src/commands/video/generate.ts | 64 +++++++++++++++++++++++++++++----- src/types/api.ts | 5 +++ 3 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/command.ts b/src/command.ts index 7ccbbaf..8d89da8 100644 --- a/src/command.ts +++ b/src/command.ts @@ -14,6 +14,7 @@ export interface Command { usage?: string; options?: OptionDef[]; examples?: string[]; + apiDocs?: string; execute(config: Config, flags: GlobalFlags): Promise; } @@ -23,6 +24,7 @@ export interface CommandSpec { usage?: string; options?: OptionDef[]; examples?: string[]; + apiDocs?: string; run(config: Config, flags: GlobalFlags): Promise; } @@ -33,6 +35,7 @@ export function defineCommand(spec: CommandSpec): Command { usage: spec.usage, options: spec.options, examples: spec.examples, + apiDocs: spec.apiDocs, execute: spec.run, }; } diff --git a/src/commands/video/generate.ts b/src/commands/video/generate.ts index 364cdf5..140e375 100644 --- a/src/commands/video/generate.ts +++ b/src/commands/video/generate.ts @@ -21,12 +21,15 @@ import { promptText, failIfMissing } from '../../utils/prompt'; export default defineCommand({ name: 'video generate', - description: 'Generate a video (Hailuo-2.3 / 2.3-Fast)', + description: 'Generate a video (T2V: Hailuo-2.3 / 2.3-Fast / Hailuo-02 | I2V: + I2V-01 / I2V-01-Director / I2V-01-live | S2V: S2V-01)', + apiDocs: 'https://platform.minimax.io/docs/api-reference/video-generation', usage: 'mmx video generate --prompt [flags]', options: [ - { flag: '--model ', description: 'Model ID (default: MiniMax-Hailuo-2.3)' }, + { flag: '--model ', description: 'Model ID (default: MiniMax-Hailuo-2.3). Auto-switched to Hailuo-02 with --last-frame, or S2V-01 with --subject-image.' }, { flag: '--prompt ', description: 'Video description', required: true }, - { flag: '--first-frame ', description: 'First frame image' }, + { flag: '--first-frame ', description: 'First frame image (local path or URL). Auto base64-encoded for local files.' }, + { flag: '--last-frame ', description: 'Last frame image (local path or URL). Enables SEF (start-end frame) interpolation mode with Hailuo-02 model. Requires --first-frame.' }, + { flag: '--subject-image ', description: 'Subject reference image for character consistency (local path or URL). Switches to S2V-01 model.' }, { flag: '--callback-url ', description: 'Webhook URL for completion notification' }, { flag: '--download ', description: 'Save video to file on completion' }, { flag: '--no-wait', description: 'Return task ID immediately without waiting' }, @@ -38,6 +41,10 @@ export default defineCommand({ 'mmx video generate --prompt "Ocean waves at sunset." --download sunset.mp4', 'mmx video generate --prompt "A robot painting." --async --quiet', 'mmx video generate --prompt "A robot painting." --no-wait --quiet', + '# SEF: first + last frame interpolation (uses Hailuo-02 model)', + 'mmx video generate --prompt "Walk forward" --first-frame start.jpg --last-frame end.jpg', + '# Subject reference: character consistency (uses S2V-01 model)', + 'mmx video generate --prompt "A detective walking" --subject-image character.jpg', ], async run(config: Config, flags: GlobalFlags) { let prompt = flags.prompt as string | undefined; @@ -55,7 +62,14 @@ export default defineCommand({ } } - const model = (flags.model as string) || 'MiniMax-Hailuo-2.3'; + // Determine model: explicit --model overrides auto-switch + const explicitModel = flags.model as string | undefined; + let model = explicitModel || 'MiniMax-Hailuo-2.3'; + if (!explicitModel && flags.lastFrame) { + model = 'MiniMax-Hailuo-02'; + } else if (!explicitModel && flags.subjectImage) { + model = 'S2V-01'; + } const format = detectOutputFormat(config.output); const body: VideoRequest = { @@ -63,6 +77,7 @@ export default defineCommand({ prompt, }; + // First frame (I2V) if (flags.firstFrame) { const framePath = flags.firstFrame as string; if (framePath.startsWith('http')) { @@ -75,6 +90,41 @@ export default defineCommand({ } } + // Last frame (SEF mode) + if (flags.lastFrame) { + if (!flags.firstFrame) { + throw new CLIError( + '--last-frame requires --first-frame (SEF mode).', + ExitCode.USAGE, + 'mmx video generate --prompt --first-frame --last-frame ', + ); + } + const framePath = flags.lastFrame as string; + if (framePath.startsWith('http')) { + body.last_frame_image = framePath; + } else { + const imgData = readFileSync(framePath); + const ext = extname(framePath).toLowerCase(); + const mime = MIME_TYPES[ext] || 'image/jpeg'; + body.last_frame_image = `data:${mime};base64,${imgData.toString('base64')}`; + } + } + + // Subject reference (S2V mode) + if (flags.subjectImage) { + const imgPath = flags.subjectImage as string; + let imageData: string; + if (imgPath.startsWith('http')) { + imageData = imgPath; + } else { + const imgData = readFileSync(imgPath); + const ext = extname(imgPath).toLowerCase(); + const mime = MIME_TYPES[ext] || 'image/jpeg'; + imageData = `data:${mime};base64,${imgData.toString('base64')}`; + } + body.subject_reference = [{ type: 'character', image: [imageData] }]; + } + if (flags.callbackUrl) { body.callback_url = flags.callbackUrl as string; } @@ -93,15 +143,12 @@ export default defineCommand({ const taskId = response.task_id; - if (!config.quiet && !flags.noWait && !config.async) { - process.stderr.write(`[Model: ${model}]\n`); - } else if (!config.quiet) { + if (!config.quiet) { process.stderr.write(`[Model: ${model}]\n`); } // --no-wait or --async: return task ID immediately if (flags.noWait || config.async) { - // Always pure JSON — Agent/CI mode needs predictable stdout process.stdout.write(JSON.stringify({ taskId })); process.stdout.write('\n'); return; @@ -169,7 +216,6 @@ export default defineCommand({ await downloadFile(downloadUrl, destPath, { quiet: config.quiet }); - // Pure local path output (stdout stays clean for piping) process.stdout.write(destPath); process.stdout.write('\n'); }, diff --git a/src/types/api.ts b/src/types/api.ts index b62ebcf..f9eb23a 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -180,7 +180,12 @@ export interface VideoRequest { model: string; prompt: string; first_frame_image?: string; + last_frame_image?: string; callback_url?: string; + subject_reference?: Array<{ + type: string; + image: string[]; + }>; } export interface VideoResponse { From 881d822fc657eefe257116cefe3549472f73790a Mon Sep 17 00:00:00 2001 From: tars90percent Date: Fri, 10 Apr 2026 19:37:40 +0800 Subject: [PATCH 2/2] fix: apiDocs path, description typo, last-frame/subject-image mutual exclusion - Change apiDocs from full URL to path (fixes broken URL in --help) - Remove stray '+' in I2V description - Error when --last-frame and --subject-image are combined (different modes) --- src/commands/video/generate.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/commands/video/generate.ts b/src/commands/video/generate.ts index 140e375..26f984b 100644 --- a/src/commands/video/generate.ts +++ b/src/commands/video/generate.ts @@ -21,8 +21,8 @@ import { promptText, failIfMissing } from '../../utils/prompt'; export default defineCommand({ name: 'video generate', - description: 'Generate a video (T2V: Hailuo-2.3 / 2.3-Fast / Hailuo-02 | I2V: + I2V-01 / I2V-01-Director / I2V-01-live | S2V: S2V-01)', - apiDocs: 'https://platform.minimax.io/docs/api-reference/video-generation', + description: 'Generate a video (T2V: Hailuo-2.3 / 2.3-Fast / Hailuo-02 | I2V: I2V-01 / I2V-01-Director / I2V-01-live | S2V: S2V-01)', + apiDocs: '/docs/api-reference/video-generation', usage: 'mmx video generate --prompt [flags]', options: [ { flag: '--model ', description: 'Model ID (default: MiniMax-Hailuo-2.3). Auto-switched to Hailuo-02 with --last-frame, or S2V-01 with --subject-image.' }, @@ -62,6 +62,15 @@ export default defineCommand({ } } + // Validate mutually exclusive mode flags + if (flags.lastFrame && flags.subjectImage) { + throw new CLIError( + '--last-frame and --subject-image cannot be used together (SEF and S2V are different modes).', + ExitCode.USAGE, + 'mmx video generate --prompt --first-frame --last-frame ', + ); + } + // Determine model: explicit --model overrides auto-switch const explicitModel = flags.model as string | undefined; let model = explicitModel || 'MiniMax-Hailuo-2.3';