Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/client/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export function musicEndpoint(baseUrl: string): string {
return `${baseUrl}/v1/music_generation`;
}

export function musicCoverPreprocessEndpoint(baseUrl: string): string {
return `${baseUrl}/v1/music_cover_preprocess`;
}

export function searchEndpoint(baseUrl: string): string {
return `${baseUrl}/v1/coding_plan/search`;
}
Expand Down
42 changes: 33 additions & 9 deletions src/commands/music/cover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { defineCommand } from '../../command';
import { CLIError } from '../../errors/base';
import { ExitCode } from '../../errors/codes';
import { request, requestJson } from '../../client/http';
import { musicEndpoint } from '../../client/endpoints';
import { musicCoverPreprocessEndpoint, musicEndpoint } from '../../client/endpoints';
import { formatOutput, detectOutputFormat } from '../../output/formatter';
import { saveAudioOutput } from '../../output/audio';
import { MUSIC_FORMATS, formatList, validateAudioFormat } from '../../utils/audio-formats';
import { pipeAudioStream } from '../../utils/audio-stream';
import type { Config } from '../../config/schema';
import type { GlobalFlags } from '../../types/flags';
import type { MusicRequest, MusicResponse } from '../../types/api';
import type { CoverPreprocessRequest, CoverPreprocessResponse, MusicRequest, MusicResponse } from '../../types/api';
import { musicCoverModel } from './models';

export default defineCommand({
Expand All @@ -23,8 +23,8 @@ export default defineCommand({
{ flag: '--prompt <text>', description: 'Target cover style, e.g. "Indie folk, acoustic guitar, warm male vocal"' },
{ flag: '--audio <url>', description: 'URL of the reference audio (mp3, wav, flac, etc. — 6s to 6min, max 50MB)' },
{ flag: '--audio-file <path>', description: 'Local reference audio file (auto base64-encoded)' },
{ flag: '--lyrics <text>', description: 'Cover lyrics. If omitted, extracted from reference audio via ASR.' },
{ flag: '--lyrics-file <path>', description: 'Read lyrics from file (use - for stdin)' },
{ flag: '--lyrics <text>', description: 'Cover lyrics. If provided, CLI auto-runs cover preprocess first. If omitted, lyrics are extracted from reference audio via ASR.' },
{ flag: '--lyrics-file <path>', description: 'Read lyrics from file (use - for stdin). Triggers cover preprocess automatically.' },
{ flag: '--seed <number>', description: 'Random seed 0–1000000 for reproducible results', type: 'number' },
{ flag: '--format <fmt>', description: `Audio format: ${formatList(MUSIC_FORMATS)} (default: mp3)` },
{ flag: '--sample-rate <hz>', description: 'Sample rate: 16000, 24000, 32000, 44100 (default: 44100)', type: 'number' },
Expand Down Expand Up @@ -80,6 +80,10 @@ export default defineCommand({
'mmx music cover --model music-cover',
);
}
const audioSource: Pick<MusicRequest, 'audio_url' | 'audio_base64'> = audioUrl
? { audio_url: audioUrl }
: { audio_base64: readFileSync(audioFile!).toString('base64') };

const body: MusicRequest = {
model,
prompt,
Expand All @@ -93,21 +97,41 @@ export default defineCommand({
},
output_format: 'hex',
stream: flags.stream === true,
...audioSource,
};

if (audioUrl) {
body.audio_url = audioUrl;
} else {
body.audio_base64 = readFileSync(audioFile!).toString('base64');
const needsPreprocess = Boolean(lyrics?.trim());
const preprocessBody: CoverPreprocessRequest | undefined = needsPreprocess
? {
model: 'music-cover',
...audioSource,
}
: undefined;

if (needsPreprocess) {
delete body.audio_url;
delete body.audio_base64;
body.cover_feature_id = '__cover_feature_id_from_preprocess__';
}

if (config.dryRun) {
console.log(formatOutput({ request: body }, format));
console.log(formatOutput(needsPreprocess ? { preprocess_request: preprocessBody, request: body } : { request: body }, format));
return;
}

const url = musicEndpoint(config.baseUrl);

if (needsPreprocess) {
const preprocessUrl = musicCoverPreprocessEndpoint(config.baseUrl);
const preprocessResponse = await requestJson<CoverPreprocessResponse>(config, {
url: preprocessUrl,
method: 'POST',
body: preprocessBody,
});

body.cover_feature_id = preprocessResponse.cover_feature_id;
}

if (flags.stream) {
const res = await request(config, { url, method: 'POST', body, stream: true });
await pipeAudioStream(res);
Expand Down
16 changes: 16 additions & 0 deletions src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ export interface MusicRequest {
lyrics_optimizer?: boolean;
audio_url?: string;
audio_base64?: string;
cover_feature_id?: string;
seed?: number;
audio_setting?: {
format?: string;
Expand Down Expand Up @@ -239,6 +240,21 @@ export interface MusicResponse {
};
}

export interface CoverPreprocessRequest {
model: 'music-cover';
audio_url?: string;
audio_base64?: string;
}

export interface CoverPreprocessResponse {
cover_feature_id: string;
formatted_lyrics?: string;
structure_result?: string;
audio_duration?: number;
trace_id?: string;
base_resp: BaseResp;
}

// ---- Quota ----

export interface QuotaResponse {
Expand Down
4 changes: 4 additions & 0 deletions test/commands/music/cover.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,11 @@ describe('music cover command', () => {

console.log = origLog;
const parsed = JSON.parse(captured);
expect(parsed.preprocess_request.model).toBe('music-cover');
expect(parsed.preprocess_request.audio_url).toBe('https://example.com/ref.mp3');
expect(parsed.request.lyrics).toBe('New lyrics here');
expect(parsed.request.cover_feature_id).toBe('__cover_feature_id_from_preprocess__');
expect(parsed.request.audio_url).toBeUndefined();
});

it('accepts optional --seed', async () => {
Expand Down
Loading