@@ -2,26 +2,59 @@ import { defineCommand } from '../../command';
22import { CLIError } from '../../errors/base' ;
33import { ExitCode } from '../../errors/codes' ;
44import { request , requestJson } from '../../client/http' ;
5- import { musicEndpoint } from '../../client/endpoints' ;
6- import { detectOutputFormat , dryRun } from '../../output/formatter' ;
5+ import { existsSync , mkdirSync , statSync , writeFileSync } from 'node:fs' ;
6+ import { basename , dirname , extname , join , resolve } from 'node:path' ;
7+ import { lyricsGenerationEndpoint , musicEndpoint } from '../../client/endpoints' ;
8+ import { detectOutputFormat , dryRun , formatOutput } from '../../output/formatter' ;
79import { saveAudioOutput } from '../../output/audio' ;
810import { readTextFromPathOrStdin } from '../../utils/fs' ;
911import { MUSIC_FORMATS , formatList , validateAudioFormat } from '../../utils/audio-formats' ;
1012import { pipeAudioStream } from '../../utils/audio-stream' ;
1113import type { Config } from '../../config/schema' ;
1214import type { GlobalFlags } from '../../types/flags' ;
13- import type { MusicRequest , MusicResponse } from '../../types/api' ;
15+ import type { LyricsGenerationRequest , LyricsGenerationResponse , MusicRequest , MusicResponse } from '../../types/api' ;
1416import { musicGenerateModel } from './models' ;
1517
18+ function defaultLyricsFilename ( audioOutPath : string ) : string {
19+ return `${ basename ( audioOutPath , extname ( audioOutPath ) ) } .lyrics.txt` ;
20+ }
21+
22+ function resolveLyricsOutputPath ( lyricsOut : string | undefined , audioOutPath : string ) : string {
23+ const filename = defaultLyricsFilename ( audioOutPath ) ;
24+
25+ if ( ! lyricsOut ) {
26+ return resolve ( filename ) ;
27+ }
28+
29+ const dest = resolve ( lyricsOut ) ;
30+ const treatAsDirectory =
31+ lyricsOut . endsWith ( '/' ) ||
32+ lyricsOut . endsWith ( '\\' ) ||
33+ ( existsSync ( dest ) && statSync ( dest ) . isDirectory ( ) ) ;
34+
35+ return treatAsDirectory ? join ( dest , filename ) : dest ;
36+ }
37+
38+ function saveLyricsOutput ( lyrics : string , lyricsOut : string | undefined , audioOutPath : string ) : string {
39+ const dest = resolveLyricsOutputPath ( lyricsOut , audioOutPath ) ;
40+ const dir = dirname ( dest ) ;
41+ if ( ! existsSync ( dir ) ) {
42+ mkdirSync ( dir , { recursive : true } ) ;
43+ }
44+ writeFileSync ( dest , lyrics , 'utf-8' ) ;
45+ return dest ;
46+ }
47+
1648export default defineCommand ( {
1749 name : 'music generate' ,
1850 description : 'Generate a song (music-2.6 / music-2.5+ / music-2.5)' ,
1951 apiDocs : '/docs/api-reference/music-generation' ,
20- usage : 'mmx music generate --prompt <text> (--lyrics <text> | --instrumental | --lyrics-optimizer) [--out <path>] [flags]' ,
52+ usage : 'mmx music generate --prompt <text> (--lyrics <text> | --instrumental | --lyrics-optimizer) [--out <path>] [--lyrics-out <path>] [ flags]' ,
2153 options : [
2254 { flag : '--prompt <text>' , description : 'Music style description (e.g. "cinematic orchestral, building tension"). Max 2000 chars when combined with structured flags.' } ,
2355 { flag : '--lyrics <text>' , description : 'Song lyrics with structure tags (newline separated). Supported: [Intro], [Verse], [Pre Chorus], [Chorus], [Interlude], [Bridge], [Outro], [Post Chorus], [Transition], [Break], [Hook], [Build Up], [Inst], [Solo]. Tags must be clean — no descriptions inside brackets (they will be sung). Max 3500 chars.' } ,
2456 { flag : '--lyrics-file <path>' , description : 'Read lyrics from file (use - for stdin). Same tag rules as --lyrics.' } ,
57+ { flag : '--lyrics-out <path>' , description : 'Save the resolved lyrics to a file or directory. Default: current directory as <audio-basename>.lyrics.txt.' } ,
2558 { flag : '--lyrics-optimizer' , description : 'Auto-generate lyrics from prompt (cannot be used with --lyrics or --instrumental)' } ,
2659 { flag : '--instrumental' , description : 'Generate instrumental music (no vocals). music-2.6/music-2.5+: native is_instrumental flag; music-2.5: lyrics workaround. Cannot be used with --lyrics.' } ,
2760 { flag : '--vocals <text>' , description : 'Vocal style, e.g. "warm male baritone", "bright female soprano", "duet with harmonies"' } ,
@@ -50,6 +83,7 @@ export default defineCommand({
5083 'mmx music generate --prompt "Indie folk, melancholic" --lyrics-file song.txt --out my_song.mp3' ,
5184 '# Auto-generate lyrics from prompt:' ,
5285 'mmx music generate --prompt "Upbeat pop about summer" --lyrics-optimizer --out summer.mp3' ,
86+ 'mmx music generate --prompt "Upbeat pop about summer" --lyrics-optimizer --out summer.mp3 --lyrics-out ./lyrics/' ,
5387 '# Instrumental:' ,
5488 'mmx music generate --prompt "Cinematic orchestral, building tension" --instrumental --out bgm.mp3' ,
5589 '# URL output (24h expiry — download promptly):' ,
@@ -171,10 +205,41 @@ export default defineCommand({
171205
172206 const format = detectOutputFormat ( config . output ) ;
173207 const url = musicEndpoint ( config . baseUrl ) ;
208+ let resolvedLyrics = lyrics ?. trim ( ) ? lyrics : undefined ;
209+
210+ if ( lyricsOptimizer ) {
211+ const lyricsResponse = await requestJson < LyricsGenerationResponse > ( config , {
212+ url : lyricsGenerationEndpoint ( config . baseUrl ) ,
213+ method : 'POST' ,
214+ body : {
215+ mode : 'write_full_song' ,
216+ prompt,
217+ } satisfies LyricsGenerationRequest ,
218+ } ) ;
219+
220+ if ( ! lyricsResponse . lyrics ?. trim ( ) ) {
221+ throw new CLIError (
222+ 'Lyrics optimizer did not return lyrics.' ,
223+ ExitCode . GENERAL ,
224+ ) ;
225+ }
226+
227+ resolvedLyrics = lyricsResponse . lyrics ;
228+ body . lyrics = resolvedLyrics ;
229+ body . lyrics_optimizer = undefined ;
230+ }
174231
175232 if ( flags . stream ) {
176233 const res = await request ( config , { url, method : 'POST' , body, stream : true } ) ;
177234 await pipeAudioStream ( res ) ;
235+ if ( resolvedLyrics ) {
236+ const lyricsPath = saveLyricsOutput ( resolvedLyrics , flags . lyricsOut as string | undefined , outPath ) ;
237+ if ( config . quiet ) {
238+ console . log ( lyricsPath ) ;
239+ } else {
240+ console . log ( formatOutput ( { lyrics : lyricsPath } , format ) ) ;
241+ }
242+ }
178243 return ;
179244 }
180245
@@ -194,8 +259,24 @@ export default defineCommand({
194259 ExitCode . GENERAL ,
195260 ) ;
196261 }
262+ if ( resolvedLyrics ) {
263+ const lyricsPath = saveLyricsOutput ( resolvedLyrics , flags . lyricsOut as string | undefined , outPath ) ;
264+ if ( config . quiet ) {
265+ console . log ( lyricsPath ) ;
266+ } else {
267+ console . log ( formatOutput ( { lyrics : lyricsPath } , format ) ) ;
268+ }
269+ }
197270 return ;
198271 }
199272 saveAudioOutput ( response , outPath , format , config . quiet ) ;
273+ if ( resolvedLyrics ) {
274+ const lyricsPath = saveLyricsOutput ( resolvedLyrics , flags . lyricsOut as string | undefined , outPath ) ;
275+ if ( config . quiet ) {
276+ console . log ( lyricsPath ) ;
277+ } else {
278+ console . log ( formatOutput ( { lyrics : lyricsPath } , format ) ) ;
279+ }
280+ }
200281 } ,
201282} ) ;
0 commit comments