Skip to content

Commit 9946f25

Browse files
Merge pull request #74 from raylanlin/pr/video
feat(video): add SEF (--last-frame), S2V (--subject-image), model auto-switch
2 parents 2d83191 + 881d822 commit 9946f25

2 files changed

Lines changed: 69 additions & 9 deletions

File tree

src/commands/video/generate.ts

Lines changed: 64 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: '/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,31 @@ export default defineCommand({
5562
}
5663
}
5764

58-
const model = (flags.model as string) || 'MiniMax-Hailuo-2.3';
65+
// Validate mutually exclusive mode flags
66+
if (flags.lastFrame && flags.subjectImage) {
67+
throw new CLIError(
68+
'--last-frame and --subject-image cannot be used together (SEF and S2V are different modes).',
69+
ExitCode.USAGE,
70+
'mmx video generate --prompt <text> --first-frame <path> --last-frame <path>',
71+
);
72+
}
73+
74+
// Determine model: explicit --model overrides auto-switch
75+
const explicitModel = flags.model as string | undefined;
76+
let model = explicitModel || 'MiniMax-Hailuo-2.3';
77+
if (!explicitModel && flags.lastFrame) {
78+
model = 'MiniMax-Hailuo-02';
79+
} else if (!explicitModel && flags.subjectImage) {
80+
model = 'S2V-01';
81+
}
5982
const format = detectOutputFormat(config.output);
6083

6184
const body: VideoRequest = {
6285
model,
6386
prompt,
6487
};
6588

89+
// First frame (I2V)
6690
if (flags.firstFrame) {
6791
const framePath = flags.firstFrame as string;
6892
if (framePath.startsWith('http')) {
@@ -75,6 +99,41 @@ export default defineCommand({
7599
}
76100
}
77101

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

94153
const taskId = response.task_id;
95154

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

102159
// --no-wait or --async: return task ID immediately
103160
if (flags.noWait || config.async) {
104-
// Always pure JSON — Agent/CI mode needs predictable stdout
105161
process.stdout.write(JSON.stringify({ taskId }));
106162
process.stdout.write('\n');
107163
return;
@@ -169,7 +225,6 @@ export default defineCommand({
169225

170226
await downloadFile(downloadUrl, destPath, { quiet: config.quiet });
171227

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

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)