Skip to content

Commit 42b8aac

Browse files
BLUE-coconutclaude
andcommitted
fix(speech): decode SSE stream and handle EPIPE gracefully
- Stream mode now parses SSE events, hex-decodes .data.audio, and writes raw binary MP3 to stdout (fixes --stream example piping to mpv) - Add EPIPE handler to main.ts so piping to a downstream process that exits early (e.g. mpv, head) no longer crashes with unhandled error Fixes #54 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 43fc9e2 commit 42b8aac

File tree

2 files changed

+14
-7
lines changed

2 files changed

+14
-7
lines changed

src/commands/speech/synthesize.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ 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';
67
import { detectOutputFormat, formatOutput } from '../../output/formatter';
78
import { saveAudioOutput } from '../../output/audio';
89
import { readTextFromPathOrStdin } from '../../utils/fs';
@@ -98,14 +99,14 @@ export default defineCommand({
9899

99100
if (flags.stream) {
100101
const res = await request(config, { url, method: 'POST', body, stream: true });
101-
const reader = res.body?.getReader();
102-
if (!reader) throw new CLIError('No response body', ExitCode.GENERAL);
103-
while (true) {
104-
const { done, value } = await reader.read();
105-
if (done) break;
106-
process.stdout.write(value);
102+
for await (const event of parseSSE(res)) {
103+
if (!event.data || event.data === '[DONE]') break;
104+
const parsed = JSON.parse(event.data);
105+
const audioHex = parsed?.data?.audio;
106+
if (audioHex) {
107+
process.stdout.write(Buffer.from(audioHex, 'hex'));
108+
}
107109
}
108-
reader.releaseLock();
109110
return;
110111
}
111112

src/main.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ process.on('SIGINT', () => {
1717
process.exit(130);
1818
});
1919

20+
// Handle stdout EPIPE gracefully (e.g., piped to `mpv` that exits early)
21+
process.stdout.on('error', (e) => {
22+
if (e.code === 'EPIPE') process.exit(0);
23+
else throw e;
24+
});
25+
2026
// Commands that manage their own auth or need no key
2127
const NO_AUTH_SETUP = [
2228
['auth', 'login'],

0 commit comments

Comments
 (0)