Skip to content

Commit 09f4728

Browse files
authored
Merge pull request #130 from MiniMax-AI/fix/stream-audio-decode
fix: decode SSE audio stream for music and speech
2 parents 65aed9c + dd00b38 commit 09f4728

5 files changed

Lines changed: 37 additions & 27 deletions

File tree

src/client/http.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export async function request(config: Config, opts: RequestOpts): Promise<Respon
6262
? (opts.body as FormData)
6363
: JSON.stringify(opts.body)
6464
: undefined,
65-
signal: AbortSignal.timeout(timeoutMs),
65+
signal: opts.stream ? undefined : AbortSignal.timeout(timeoutMs),
6666
});
6767

6868
if (config.verbose) {

src/commands/music/cover.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { musicEndpoint } from '../../client/endpoints';
77
import { formatOutput, detectOutputFormat } from '../../output/formatter';
88
import { saveAudioOutput } from '../../output/audio';
99
import { MUSIC_FORMATS, formatList, validateAudioFormat } from '../../utils/audio-formats';
10+
import { pipeAudioStream } from '../../utils/audio-stream';
1011
import type { Config } from '../../config/schema';
1112
import type { GlobalFlags } from '../../types/flags';
1213
import type { MusicRequest, MusicResponse } from '../../types/api';
@@ -109,14 +110,7 @@ export default defineCommand({
109110

110111
if (flags.stream) {
111112
const res = await request(config, { url, method: 'POST', body, stream: true });
112-
const reader = res.body?.getReader();
113-
if (!reader) throw new CLIError('No response body', ExitCode.GENERAL);
114-
while (true) {
115-
const { done, value } = await reader.read();
116-
if (done) break;
117-
process.stdout.write(value);
118-
}
119-
reader.releaseLock();
113+
await pipeAudioStream(res);
120114
return;
121115
}
122116

src/commands/music/generate.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import { CLIError } from '../../errors/base';
33
import { ExitCode } from '../../errors/codes';
44
import { request, requestJson } from '../../client/http';
55
import { musicEndpoint } from '../../client/endpoints';
6-
import { formatOutput, detectOutputFormat, dryRun } from '../../output/formatter';
6+
import { detectOutputFormat, dryRun } from '../../output/formatter';
77
import { saveAudioOutput } from '../../output/audio';
88
import { readTextFromPathOrStdin } from '../../utils/fs';
99
import { MUSIC_FORMATS, formatList, validateAudioFormat } from '../../utils/audio-formats';
10+
import { pipeAudioStream } from '../../utils/audio-stream';
1011
import type { Config } from '../../config/schema';
1112
import type { GlobalFlags } from '../../types/flags';
1213
import type { MusicRequest, MusicResponse } from '../../types/api';
@@ -173,14 +174,7 @@ export default defineCommand({
173174

174175
if (flags.stream) {
175176
const res = await request(config, { url, method: 'POST', body, stream: true });
176-
const reader = res.body?.getReader();
177-
if (!reader) throw new CLIError('No response body', ExitCode.GENERAL);
178-
while (true) {
179-
const { done, value } = await reader.read();
180-
if (done) break;
181-
process.stdout.write(value);
182-
}
183-
reader.releaseLock();
177+
await pipeAudioStream(res);
184178
return;
185179
}
186180

src/commands/speech/synthesize.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import { CLIError } from '../../errors/base';
33
import { ExitCode } from '../../errors/codes';
44
import { request, requestJson } from '../../client/http';
55
import { speechEndpoint } from '../../client/endpoints';
6-
import { parseSSE } from '../../client/stream';
76
import { detectOutputFormat, formatOutput, dryRun } from '../../output/formatter';
87
import { saveAudioOutput } from '../../output/audio';
98
import { writeFileSync } from 'fs';
109
import { readTextFromPathOrStdin } from '../../utils/fs';
1110
import { T2A_FORMATS, formatList, validateAudioFormat, validateT2AStreaming, t2aDefaultSampleRate } from '../../utils/audio-formats';
11+
import { pipeAudioStream } from '../../utils/audio-stream';
1212
import type { Config } from '../../config/schema';
1313
import type { GlobalFlags } from '../../types/flags';
1414
import type { SpeechRequest, SpeechResponse } from '../../types/api';
@@ -105,14 +105,7 @@ export default defineCommand({
105105

106106
if (flags.stream) {
107107
const res = await request(config, { url, method: 'POST', body, stream: true });
108-
for await (const event of parseSSE(res)) {
109-
if (!event.data || event.data === '[DONE]') break;
110-
const parsed = JSON.parse(event.data);
111-
const audioHex = parsed?.data?.audio;
112-
if (audioHex) {
113-
process.stdout.write(Buffer.from(audioHex, 'hex'));
114-
}
115-
}
108+
await pipeAudioStream(res);
116109
return;
117110
}
118111

src/utils/audio-stream.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { parseSSE } from '../client/stream';
2+
3+
interface SseAudioPayload {
4+
data?: { audio?: string; status?: number };
5+
}
6+
7+
export async function pipeAudioStream(response: Response): Promise<void> {
8+
process.stdout.on('error', (err: NodeJS.ErrnoException) => {
9+
if (err.code === 'EPIPE') process.exit(0);
10+
throw err;
11+
});
12+
13+
for await (const event of parseSSE(response)) {
14+
if (!event.data || event.data === '[DONE]') break;
15+
16+
let parsed: SseAudioPayload;
17+
try { parsed = JSON.parse(event.data); } catch { continue; }
18+
19+
if (parsed.data?.status === 2) continue;
20+
21+
const hex = parsed.data?.audio;
22+
if (!hex) continue;
23+
24+
const chunk = Buffer.from(hex, 'hex');
25+
if (!process.stdout.write(chunk)) {
26+
await new Promise<void>(r => process.stdout.once('drain', r));
27+
}
28+
}
29+
}

0 commit comments

Comments
 (0)