Skip to content
Merged
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 CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
23 changes: 17 additions & 6 deletions src/services/streaming/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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]));
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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++) {
Expand Down
7 changes: 6 additions & 1 deletion src/types/streaming/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand Down
49 changes: 49 additions & 0 deletions tests/unit/streaming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading