Skip to content

Commit 7270aec

Browse files
author
Raylan LIN
committed
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
1 parent f07c4b3 commit 7270aec

3 files changed

Lines changed: 63 additions & 9 deletions

File tree

src/command.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface Command {
1414
usage?: string;
1515
options?: OptionDef[];
1616
examples?: string[];
17+
apiDocs?: string;
1718
execute(config: Config, flags: GlobalFlags): Promise<void>;
1819
}
1920

@@ -23,6 +24,7 @@ export interface CommandSpec {
2324
usage?: string;
2425
options?: OptionDef[];
2526
examples?: string[];
27+
apiDocs?: string;
2628
run(config: Config, flags: GlobalFlags): Promise<void>;
2729
}
2830

@@ -33,6 +35,7 @@ export function defineCommand(spec: CommandSpec): Command {
3335
usage: spec.usage,
3436
options: spec.options,
3537
examples: spec.examples,
38+
apiDocs: spec.apiDocs,
3639
execute: spec.run,
3740
};
3841
}

src/commands/video/generate.ts

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@ import { promptText, failIfMissing } from '../../utils/prompt';
2121

2222
export default defineCommand({
2323
name: 'video generate',
24-
description: 'Generate a video (Hailuo-2.3 / 2.3-Fast)',
24+
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)',
25+
apiDocs: 'https://platform.minimax.io/docs/api-reference/video-generation',
2526
usage: 'mmx video generate --prompt <text> [flags]',
2627
options: [
27-
{ flag: '--model <model>', description: 'Model ID (default: MiniMax-Hailuo-2.3)' },
28+
{ flag: '--model <model>', description: 'Model ID (default: MiniMax-Hailuo-2.3). Auto-switched to Hailuo-02 with --last-frame, or S2V-01 with --subject-image.' },
2829
{ flag: '--prompt <text>', description: 'Video description', required: true },
29-
{ flag: '--first-frame <path-or-url>', description: 'First frame image' },
30+
{ flag: '--first-frame <path-or-url>', description: 'First frame image (local path or URL). Auto base64-encoded for local files.' },
31+
{ flag: '--last-frame <path-or-url>', description: 'Last frame image (local path or URL). Enables SEF (start-end frame) interpolation mode with Hailuo-02 model. Requires --first-frame.' },
32+
{ flag: '--subject-image <path-or-url>', description: 'Subject reference image for character consistency (local path or URL). Switches to S2V-01 model.' },
3033
{ flag: '--callback-url <url>', description: 'Webhook URL for completion notification' },
3134
{ flag: '--download <path>', description: 'Save video to file on completion' },
3235
{ flag: '--no-wait', description: 'Return task ID immediately without waiting' },
@@ -38,6 +41,10 @@ export default defineCommand({
3841
'mmx video generate --prompt "Ocean waves at sunset." --download sunset.mp4',
3942
'mmx video generate --prompt "A robot painting." --async --quiet',
4043
'mmx video generate --prompt "A robot painting." --no-wait --quiet',
44+
'# SEF: first + last frame interpolation (uses Hailuo-02 model)',
45+
'mmx video generate --prompt "Walk forward" --first-frame start.jpg --last-frame end.jpg',
46+
'# Subject reference: character consistency (uses S2V-01 model)',
47+
'mmx video generate --prompt "A detective walking" --subject-image character.jpg',
4148
],
4249
async run(config: Config, flags: GlobalFlags) {
4350
let prompt = flags.prompt as string | undefined;
@@ -55,14 +62,22 @@ export default defineCommand({
5562
}
5663
}
5764

58-
const model = (flags.model as string) || 'MiniMax-Hailuo-2.3';
65+
// Determine model: explicit --model overrides auto-switch
66+
const explicitModel = flags.model as string | undefined;
67+
let model = explicitModel || 'MiniMax-Hailuo-2.3';
68+
if (!explicitModel && flags.lastFrame) {
69+
model = 'MiniMax-Hailuo-02';
70+
} else if (!explicitModel && flags.subjectImage) {
71+
model = 'S2V-01';
72+
}
5973
const format = detectOutputFormat(config.output);
6074

6175
const body: VideoRequest = {
6276
model,
6377
prompt,
6478
};
6579

80+
// First frame (I2V)
6681
if (flags.firstFrame) {
6782
const framePath = flags.firstFrame as string;
6883
if (framePath.startsWith('http')) {
@@ -75,6 +90,41 @@ export default defineCommand({
7590
}
7691
}
7792

93+
// Last frame (SEF mode)
94+
if (flags.lastFrame) {
95+
if (!flags.firstFrame) {
96+
throw new CLIError(
97+
'--last-frame requires --first-frame (SEF mode).',
98+
ExitCode.USAGE,
99+
'mmx video generate --prompt <text> --first-frame <path> --last-frame <path>',
100+
);
101+
}
102+
const framePath = flags.lastFrame as string;
103+
if (framePath.startsWith('http')) {
104+
body.last_frame_image = framePath;
105+
} else {
106+
const imgData = readFileSync(framePath);
107+
const ext = extname(framePath).toLowerCase();
108+
const mime = MIME_TYPES[ext] || 'image/jpeg';
109+
body.last_frame_image = `data:${mime};base64,${imgData.toString('base64')}`;
110+
}
111+
}
112+
113+
// Subject reference (S2V mode)
114+
if (flags.subjectImage) {
115+
const imgPath = flags.subjectImage as string;
116+
let imageData: string;
117+
if (imgPath.startsWith('http')) {
118+
imageData = imgPath;
119+
} else {
120+
const imgData = readFileSync(imgPath);
121+
const ext = extname(imgPath).toLowerCase();
122+
const mime = MIME_TYPES[ext] || 'image/jpeg';
123+
imageData = `data:${mime};base64,${imgData.toString('base64')}`;
124+
}
125+
body.subject_reference = [{ type: 'character', image: [imageData] }];
126+
}
127+
78128
if (flags.callbackUrl) {
79129
body.callback_url = flags.callbackUrl as string;
80130
}
@@ -93,15 +143,12 @@ export default defineCommand({
93143

94144
const taskId = response.task_id;
95145

96-
if (!config.quiet && !flags.noWait && !config.async) {
97-
process.stderr.write(`[Model: ${model}]\n`);
98-
} else if (!config.quiet) {
146+
if (!config.quiet) {
99147
process.stderr.write(`[Model: ${model}]\n`);
100148
}
101149

102150
// --no-wait or --async: return task ID immediately
103151
if (flags.noWait || config.async) {
104-
// Always pure JSON — Agent/CI mode needs predictable stdout
105152
process.stdout.write(JSON.stringify({ taskId }));
106153
process.stdout.write('\n');
107154
return;
@@ -169,7 +216,6 @@ export default defineCommand({
169216

170217
await downloadFile(downloadUrl, destPath, { quiet: config.quiet });
171218

172-
// Pure local path output (stdout stays clean for piping)
173219
process.stdout.write(destPath);
174220
process.stdout.write('\n');
175221
},

src/types/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,12 @@ export interface VideoRequest {
180180
model: string;
181181
prompt: string;
182182
first_frame_image?: string;
183+
last_frame_image?: string;
183184
callback_url?: string;
185+
subject_reference?: Array<{
186+
type: string;
187+
image: string[];
188+
}>;
184189
}
185190

186191
export interface VideoResponse {

0 commit comments

Comments
 (0)