From 2af62739c7cd1a2a18c7207a8a82f5969e562d59 Mon Sep 17 00:00:00 2001 From: AssemblyAI Date: Tue, 7 Jul 2026 09:55:36 -0600 Subject: [PATCH] Project import generated by Copybara. GitOrigin-RevId: 2742649adc9f507c8b2563088b5a1b125440d875 --- CHANGELOG.md | 4 +++ package.json | 2 +- src/services/streaming/service.ts | 23 +++++++++++---- src/types/streaming/index.ts | 7 ++++- tests/unit/streaming.test.ts | 49 +++++++++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a2706a..efab3fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [4.35.4] + +- `sampleRate` is now optional when `encoding` is `opus` or `ogg_opus` (the Opus stream is self-describing and the server ignores the value). It remains required for PCM encodings and for dual-channel mode; omitting it there now throws at construction time + ## [4.35.3] - Allow `language_codes` in `updateConfiguration()` — re-steer the transcription language mid-stream without reconnecting; pass `[]` to clear steering and restore the model's default multilingual code-switching (Universal-3.5 Pro Streaming only) diff --git a/package.json b/package.json index 22429d1..084397e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "assemblyai", - "version": "4.35.3", + "version": "4.35.4", "description": "The AssemblyAI JavaScript SDK provides an easy-to-use interface for interacting with the AssemblyAI API, which supports async and real-time transcription, as well as the latest LeMUR models.", "engines": { "node": ">=18" diff --git a/src/services/streaming/service.ts b/src/services/streaming/service.ts index 87a1ddb..f894da4 100644 --- a/src/services/streaming/service.ts +++ b/src/services/streaming/service.ts @@ -164,6 +164,13 @@ export class StreamingTranscriber { throw new Error("API key or temporary token is required."); } + const isOpus = params.encoding === "opus" || params.encoding === "ogg_opus"; + if (params.sampleRate === undefined && (!isOpus || params.channels)) { + throw new Error( + '`sampleRate` is required; it may only be omitted when `encoding` is "opus" or "ogg_opus" (the Opus stream is self-describing) and dual-channel mode is not used.', + ); + } + if (params.channels) { if (params.channels.length !== 2) { throw new Error( @@ -194,15 +201,17 @@ export class StreamingTranscriber { ) { this.speakerHistory = new Map(); } - // 20 ms VAD frames at the transcriber's target sample rate. - this.vadFrameSamples = Math.max(1, Math.round(params.sampleRate * 0.02)); + // 20 ms VAD frames at the transcriber's target sample rate. The + // constructor check above guarantees sampleRate in dual-channel mode. + const sampleRate = params.sampleRate!; + this.vadFrameSamples = Math.max(1, Math.round(sampleRate * 0.02)); this.minChunkSamples = Math.max( 1, - Math.round(params.sampleRate * (MIN_CHUNK_MS / 1000)), + Math.round(sampleRate * (MIN_CHUNK_MS / 1000)), ); this.maxChunkSamples = Math.max( this.minChunkSamples, - Math.round(params.sampleRate * (MAX_CHUNK_MS / 1000)), + Math.round(sampleRate * (MAX_CHUNK_MS / 1000)), ); this.channelBuffers = new Map(names.map((n) => [n, [] as number[]])); this.channelSamplesReceived = new Map(names.map((n) => [n, 0])); @@ -230,7 +239,9 @@ export class StreamingTranscriber { searchParams.set("token", this.token); } - searchParams.set("sample_rate", this.params.sampleRate.toString()); + if (this.params.sampleRate !== undefined) { + searchParams.set("sample_rate", this.params.sampleRate.toString()); + } if (this.params.endOfTurnConfidenceThreshold) { searchParams.set( @@ -753,7 +764,7 @@ Learn more at https://github.com/AssemblyAI/assemblyai-node-sdk/blob/main/docs/c let vadIdx = this.channelVadBufferIdx!.get(name)!; let received = this.channelSamplesReceived!.get(name)!; const vad = this.channelVads!.get(name)!; - const sampleRate = this.params.sampleRate; + const sampleRate = this.params.sampleRate!; const frameSize = this.vadFrameSamples; for (let i = 0; i < samples.length; i++) { diff --git a/src/types/streaming/index.ts b/src/types/streaming/index.ts index 6a9a557..20f4d3c 100644 --- a/src/types/streaming/index.ts +++ b/src/types/streaming/index.ts @@ -92,7 +92,12 @@ export type StreamingTranscriberParams = { * Milliseconds to wait between connection attempts. Defaults to 500. */ connectionRetryDelay?: number; - sampleRate: number; + /** + * Required for PCM encodings (and for dual-channel mode). May be omitted + * for Opus encodings (`opus`, `ogg_opus`) — the stream is self-describing + * and the server ignores the value. + */ + sampleRate?: number; encoding?: AudioEncoding; endOfTurnConfidenceThreshold?: number; /** diff --git a/tests/unit/streaming.test.ts b/tests/unit/streaming.test.ts index af393d6..9b1f694 100644 --- a/tests/unit/streaming.test.ts +++ b/tests/unit/streaming.test.ts @@ -408,6 +408,55 @@ describe("streaming", () => { }, ); + it.each(["opus", "ogg_opus"] as const)( + "should allow omitting sampleRate for %s encoding", + async (encoding) => { + await cleanup(); + WS.clean(); + + const wsUrl = `${websocketBaseUrl}?token=123&encoding=${encoding}`; + server = new WS(wsUrl); + rt = new StreamingTranscriber({ + websocketBaseUrl, + token: "123", + encoding, + }); + onOpen = jest.fn(); + rt.on("open", onOpen); + await connect(rt, server); + }, + ); + + it("should throw when sampleRate is omitted for non-Opus encodings", () => { + expect( + () => + new StreamingTranscriber({ + websocketBaseUrl, + token: "123", + }), + ).toThrow("`sampleRate` is required"); + expect( + () => + new StreamingTranscriber({ + websocketBaseUrl, + token: "123", + encoding: "pcm_mulaw", + }), + ).toThrow("`sampleRate` is required"); + }); + + it("should throw when sampleRate is omitted in dual-channel mode even with Opus", () => { + expect( + () => + new StreamingTranscriber({ + websocketBaseUrl, + token: "123", + encoding: "ogg_opus", + channels: [{ name: "mic" }, { name: "system" }], + }), + ).toThrow("`sampleRate` is required"); + }); + it("should include whisper-rt speech model in connection URL", async () => { await cleanup(); WS.clean();