From f45bb2e678a8d5beddf91d6959edc54ab80d593b Mon Sep 17 00:00:00 2001 From: RainbowBird Date: Fri, 14 Aug 2026 19:21:48 +0800 Subject: [PATCH] fix(stage-ui): make streaming transcription requests half-duplex (#2283) --- .../stream-transcription/index.test.ts | 32 +++++++++++++++++++ .../providers/stream-transcription/index.ts | 6 +++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/stage-ui/src/libs/providers/stream-transcription/index.test.ts b/packages/stage-ui/src/libs/providers/stream-transcription/index.test.ts index b0c48f231..71859767f 100644 --- a/packages/stage-ui/src/libs/providers/stream-transcription/index.test.ts +++ b/packages/stage-ui/src/libs/providers/stream-transcription/index.test.ts @@ -3,6 +3,38 @@ import { describe, expect, it } from 'vitest' import { streamTranscription } from './index' describe('streamTranscription', () => { + it('sets half-duplex transport for the browser audio upload', async () => { + // ROOT CAUSE: + // + // Browser fetch requires `duplex: 'half'` when the request body is a + // ReadableStream. The official provider set this in its fetch wrapper, + // but that wrapper does not own this adapter's stream transport. + // + // Report: T-3, Official provider transcription does not work reliably. + let requestInit: RequestInit | undefined + const audioStream = new ReadableStream({ + start(controller) { + controller.close() + }, + }) + + const result = streamTranscription({ + baseURL: 'https://example.invalid/transcription', + fetch: async (_input: RequestInfo | URL, init?: RequestInit) => { + requestInit = init + return new Response(new ReadableStream({ + start(controller) { + controller.close() + }, + })) + }, + inputAudioStream: audioStream, + }) + + await expect(result.text).resolves.toBe('') + expect((requestInit as RequestInit & { duplex?: string }).duplex).toBe('half') + }) + it('parses split SSE chunks and joins transcription deltas', async () => { const encoder = new TextEncoder() const responseBody = new ReadableStream({ diff --git a/packages/stage-ui/src/libs/providers/stream-transcription/index.ts b/packages/stage-ui/src/libs/providers/stream-transcription/index.ts index ae6fbc323..e2cadc47d 100644 --- a/packages/stage-ui/src/libs/providers/stream-transcription/index.ts +++ b/packages/stage-ui/src/libs/providers/stream-transcription/index.ts @@ -123,10 +123,14 @@ export function streamTranscription(options: StreamTranscriptionOptions): AIRISt : new URL(typeof options.baseURL === 'string' ? options.baseURL : 'http://localhost') const response = await fetcher(requestTarget, { body: audioStream, + // Browser fetch requires half-duplex mode for a ReadableStream body. + // Keep this at the transport boundary so every SSE transcription + // provider receives the required request option. + duplex: 'half', headers: options.headers, method: 'POST', signal: options.abortSignal, - }) + } as RequestInit & { duplex: 'half' }) if (!response.ok) throw new Error(`Streaming transcription request failed with status ${response.status}`)