Skip to content

Commit 38cdd8d

Browse files
committed
fix: auto-download outputs for music/speech/image and fix README examples
- music/speech: always save to file (auto-generate filename if --out not given) - image: auto-download to current dir when --out-dir not specified - Fix broken README examples: music now requires --lyrics or --instrumental, replace invalid voice Boyan_new_hailuo with English_magnetic_voiced_man - Add test coverage for new music API contract (lyrics required) Made-with: Cursor
1 parent 8b48bd5 commit 38cdd8d

File tree

6 files changed

+51
-51
lines changed

6 files changed

+51
-51
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ mmx text chat --message "What is MiniMax?"
4747
mmx image "A cat in a spacesuit"
4848
mmx speech synthesize --text "Hello!" --out hello.mp3
4949
mmx video generate --prompt "Ocean waves at sunset"
50-
mmx music "Upbeat pop"
50+
mmx music generate --prompt "Upbeat pop" --lyrics "[verse] La da dee, sunny day"
5151
mmx search "MiniMax AI latest news"
5252
mmx vision photo.jpg
5353
mmx quota
@@ -87,16 +87,17 @@ mmx video download --file-id 176844028768320 --out video.mp4
8787
```bash
8888
mmx speech synthesize --text "Hello!" --out hello.mp3
8989
mmx speech synthesize --text "Stream me" --stream | mpv -
90-
mmx speech synthesize --text "Hi" --voice Boyan_new_hailuo --speed 1.2
90+
mmx speech synthesize --text "Hi" --voice English_magnetic_voiced_man --speed 1.2
9191
echo "Breaking news" | mmx speech synthesize --text-file - --out news.mp3
9292
mmx speech voices
9393
```
9494

9595
### `mmx music`
9696

9797
```bash
98-
mmx music "Upbeat pop"
98+
mmx music generate --prompt "Upbeat pop" --lyrics "[verse] La da dee, sunny day"
9999
mmx music generate --prompt "Jazz" --lyrics "La la la" --out song.mp3
100+
mmx music generate --prompt "Cinematic orchestral" --instrumental --out bgm.mp3
100101
```
101102

102103
### `mmx vision`

README_CN.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ mmx text chat --message "你好,MiniMax!"
4747
mmx image "一只穿宇航服的猫"
4848
mmx speech synthesize --text "你好!" --out hello.mp3
4949
mmx video generate --prompt "海浪拍打礁石"
50-
mmx music "欢快的流行乐"
50+
mmx music generate --prompt "欢快的流行乐" --lyrics "[主歌] 啦啦啦,阳光照"
5151
mmx search "MiniMax AI 最新动态"
5252
mmx vision photo.jpg
5353
mmx quota
@@ -87,16 +87,17 @@ mmx video download --file-id 176844028768320 --out video.mp4
8787
```bash
8888
mmx speech synthesize --text "你好!" --out hello.mp3
8989
mmx speech synthesize --text "流式输出" --stream | mpv -
90-
mmx speech synthesize --text "Hi" --voice Boyan_new_hailuo --speed 1.2
90+
mmx speech synthesize --text "Hi" --voice English_magnetic_voiced_man --speed 1.2
9191
echo "头条新闻" | mmx speech synthesize --text-file - --out news.mp3
9292
mmx speech voices
9393
```
9494

9595
### `mmx music`
9696

9797
```bash
98-
mmx music "欢快的流行乐"
98+
mmx music generate --prompt "欢快的流行乐" --lyrics "[主歌] 啦啦啦,阳光照"
9999
mmx music generate --prompt "爵士风" --lyrics "啦啦啦" --out song.mp3
100+
mmx music generate --prompt "史诗管弦乐" --instrumental --out bgm.mp3
100101
```
101102

102103
### `mmx vision`

src/commands/image/generate.ts

Lines changed: 18 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -107,44 +107,28 @@ export default defineCommand({
107107
process.stderr.write('[Model: image-01]\n');
108108
}
109109

110-
// Download if --out-dir specified
111-
if (flags.outDir) {
112-
const outDir = flags.outDir as string;
113-
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
114-
115-
const prefix = (flags.outPrefix as string) || 'image';
116-
const saved: string[] = [];
117-
118-
for (let i = 0; i < imageUrls.length; i++) {
119-
const filename = `${prefix}_${String(i + 1).padStart(3, '0')}.jpg`;
120-
const destPath = join(outDir, filename);
121-
await downloadFile(imageUrls[i]!, destPath, { quiet: config.quiet });
122-
saved.push(destPath);
123-
}
110+
const outDir = (flags.outDir as string | undefined) ?? '.';
111+
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
124112

125-
if (config.quiet) {
126-
console.log(saved.join('\n'));
127-
} else {
128-
console.log(formatOutput({
129-
id: response.data.task_id,
130-
saved,
131-
success_count: response.data.success_count,
132-
failed_count: response.data.failed_count,
133-
}, format));
134-
}
135-
return;
113+
const prefix = (flags.outPrefix as string) || 'image';
114+
const saved: string[] = [];
115+
116+
for (let i = 0; i < imageUrls.length; i++) {
117+
const filename = `${prefix}_${String(i + 1).padStart(3, '0')}.jpg`;
118+
const destPath = join(outDir, filename);
119+
await downloadFile(imageUrls[i]!, destPath, { quiet: config.quiet });
120+
saved.push(destPath);
136121
}
137122

138123
if (config.quiet) {
139-
console.log(imageUrls.join('\n'));
140-
return;
124+
console.log(saved.join('\n'));
125+
} else {
126+
console.log(formatOutput({
127+
id: response.data.task_id,
128+
saved,
129+
success_count: response.data.success_count,
130+
failed_count: response.data.failed_count,
131+
}, format));
141132
}
142-
143-
console.log(formatOutput({
144-
id: response.data.task_id,
145-
images: imageUrls,
146-
success_count: response.data.success_count,
147-
failed_count: response.data.failed_count,
148-
}, format));
149133
},
150134
});

src/commands/music/generate.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type { MusicRequest, MusicResponse } from '../../types/api';
1313
export default defineCommand({
1414
name: 'music generate',
1515
description: 'Generate a song (music-2.5)',
16-
usage: 'mmx music generate --prompt <text> [--lyrics <text>] [--out <path>] [flags]',
16+
usage: 'mmx music generate --prompt <text> (--lyrics <text> | --instrumental) [--out <path>] [flags]',
1717
options: [
1818
{ flag: '--prompt <text>', description: 'Music style description (can be detailed — see examples)' },
1919
{ flag: '--lyrics <text>', description: 'Song lyrics with structure tags. Use "无歌词" for instrumental music. Cannot be used with --instrumental.' },
@@ -100,21 +100,27 @@ export default defineCommand({
100100
throw new CLIError(
101101
'At least one of --prompt or --lyrics is required.',
102102
ExitCode.USAGE,
103-
'mmx music generate --prompt <text> [--lyrics <text>]',
103+
'mmx music generate --prompt <text> --lyrics <text>',
104104
);
105105
}
106106

107-
if (!lyrics) {
108-
process.stderr.write('Warning: No lyrics provided. Use --lyrics or --lyrics-file to include lyrics.\n');
107+
if (!lyrics?.trim()) {
108+
throw new CLIError(
109+
'The API requires lyrics. Add --lyrics or --lyrics-file, or use --instrumental (or --lyrics "无歌词") for instrumental output.',
110+
ExitCode.USAGE,
111+
'mmx music generate --prompt <text> --lyrics <text>',
112+
);
109113
}
110114

111115
if (structuredParts.length > 0) {
112116
const structured = structuredParts.join('. ');
113117
prompt = prompt ? `${prompt}. ${structured}` : structured;
114118
}
115119

116-
const outPath = flags.out as string | undefined;
117-
const outFormat = outPath ? 'hex' : 'url';
120+
const ts = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-');
121+
const ext = (flags.format as string) || 'mp3';
122+
const outPath = (flags.out as string | undefined) ?? `music_${ts}.${ext}`;
123+
const outFormat = 'hex';
118124
const format = detectOutputFormat(config.output);
119125

120126
const body: MusicRequest = {

src/commands/speech/synthesize.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@ export default defineCommand({
5555

5656
const model = (flags.model as string) || 'speech-2.8-hd';
5757
const voice = (flags.voice as string) || 'English_expressive_narrator';
58-
const outPath = flags.out as string | undefined;
59-
const outFormat = outPath ? 'hex' : 'url';
58+
const ts = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-');
59+
const outPath = (flags.out as string | undefined) ?? `speech_${ts}.mp3`;
60+
const outFormat = 'hex';
6061
const format = detectOutputFormat(config.output);
6162

6263
const body: SpeechRequest = {

test/commands/music/generate.test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ describe('music generate command', () => {
3838
).rejects.toThrow('At least one of --prompt or --lyrics is required');
3939
});
4040

41+
it('requires lyrics when only prompt is given (API contract)', async () => {
42+
await expect(
43+
generateCommand.execute(baseConfig, { ...baseFlags, prompt: 'Upbeat pop' }),
44+
).rejects.toThrow('The API requires lyrics');
45+
});
46+
4147
it('structured flags are appended to prompt (dry-run)', async () => {
4248
// Use dryRun=true so no real API call is made.
4349
let resolved = false;
@@ -48,6 +54,7 @@ describe('music generate command', () => {
4854
...baseFlags,
4955
dryRun: true,
5056
prompt: 'Indie folk',
57+
lyrics: '[verse] placeholder',
5158
vocals: 'warm male and bright female duet',
5259
genre: 'folk',
5360
mood: 'warm',
@@ -57,7 +64,7 @@ describe('music generate command', () => {
5764
},
5865
);
5966
resolved = true;
60-
} catch (_) {
67+
} catch {
6168
// dryRun may resolve or reject depending on output routing; either is fine
6269
resolved = true;
6370
}
@@ -116,7 +123,7 @@ describe('music generate command', () => {
116123
{ ...baseFlags, dryRun: true, prompt: 'Folk', lyrics: '无歌词' },
117124
);
118125
resolved = true;
119-
} catch (_) {
126+
} catch {
120127
resolved = true;
121128
}
122129
expect(resolved).toBe(true);
@@ -130,7 +137,7 @@ describe('music generate command', () => {
130137
{ ...baseFlags, dryRun: true, prompt: 'Folk', lyrics: 'no lyrics' },
131138
);
132139
resolved = true;
133-
} catch (_) {
140+
} catch {
134141
resolved = true;
135142
}
136143
expect(resolved).toBe(true);

0 commit comments

Comments
 (0)