From 5e2ab5ab486f37c4bc889bf82cdc198ca1ce0188 Mon Sep 17 00:00:00 2001 From: David Lange Date: Wed, 23 Sep 2026 15:07:45 -0400 Subject: [PATCH 1/3] feat(streaming): add speakerLabelsRevisionIntervalMs param Add `speakerLabelsRevisionIntervalMs` to `StreamingTranscriberParams`, sent as the underscore-prefixed `_speaker_labels_revision_interval_ms` query param (the server's not-yet-GA wire name). It sets the cadence, in ms of audio time, at which the server emits mid-stream `speakerRevision` events when `speakerLabels` is enabled; unset or 0 sends only the end-of-stream revision, and values above the server default (300 000 ms) are clamped. `0` is sent explicitly rather than dropped. Bump version to 4.41.5 and add the changelog entry. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 4 ++++ CLAUDE.md | 5 +++- package.json | 2 +- src/services/streaming/service.ts | 8 +++++++ src/types/streaming/index.ts | 12 ++++++++++ tests/unit/streaming.test.ts | 38 +++++++++++++++++++++++++++++++ 6 files changed, 67 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de78dbd..92bedbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [4.41.5] + +- Add `speakerLabelsRevisionIntervalMs` streaming param (sent as `_speaker_labels_revision_interval_ms`) — the cadence, in ms of audio time, at which the server emits mid-stream `speakerRevision` events when `speakerLabels` is enabled. Unset or `0` sends only the end-of-stream revision; values above the server default (300 000 ms) are clamped to it. Not officially supported yet; name and semantics may change + ## [4.41.4] - Fix `RealtimeTranscriber.connect()` promise hanging indefinitely when a socket error or unexpected close occurs before `SessionBegins`. The promise now rejects, and the socket reference is discarded so that connections can be safely retried. diff --git a/CLAUDE.md b/CLAUDE.md index 153594b..ab03aed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -103,7 +103,10 @@ when `speakerLabels` is enabled). Sent once per offline-recluster resolve; each message batches every earlier Turn whose speaker label changed (unchanged turns are omitted). For each `revisions[i]`, match `turn_order` against the original Turn and replace its per-word `speaker` (and the turn-level `speaker_label`) -with the revision's values — text and word timestamps are unchanged: +with the revision's values — text and word timestamps are unchanged. +By default only the end-of-stream revision is sent; set +`speakerLabelsRevisionIntervalMs` (ms of audio time, e.g. `60_000`; sent as the +not-yet-GA `_speaker_labels_revision_interval_ms`) to also get them mid-stream: ```typescript transcriber.on("speakerRevision", (event) => { diff --git a/package.json b/package.json index cff4429..8266741 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "assemblyai", - "version": "4.41.4", + "version": "4.41.5", "description": "The AssemblyAI JavaScript SDK provides an easy-to-use interface for interacting with the AssemblyAI API, which supports async and real-time transcription.", "engines": { "node": ">=18" diff --git a/src/services/streaming/service.ts b/src/services/streaming/service.ts index ffa3d8a..518a2ac 100644 --- a/src/services/streaming/service.ts +++ b/src/services/streaming/service.ts @@ -406,6 +406,14 @@ export class StreamingTranscriber { searchParams.set("max_speakers", this.params.maxSpeakers.toString()); } + if (this.params.speakerLabelsRevisionIntervalMs !== undefined) { + // Underscore-prefixed wire name = "not officially supported / unstable". + searchParams.set( + "_speaker_labels_revision_interval_ms", + this.params.speakerLabelsRevisionIntervalMs.toString(), + ); + } + if (this.params.voiceFocus) { searchParams.set("voice_focus", this.params.voiceFocus); } diff --git a/src/types/streaming/index.ts b/src/types/streaming/index.ts index 2757179..a1908f2 100644 --- a/src/types/streaming/index.ts +++ b/src/types/streaming/index.ts @@ -139,6 +139,18 @@ export type StreamingTranscriberParams = { inactivityTimeout?: number; speakerLabels?: boolean; maxSpeakers?: number; + /** + * Cadence, in milliseconds of audio time, at which the server emits + * mid-stream `speakerRevision` events with corrected speaker labels for + * earlier turns (plus a final one at end of stream). Unset or `0` sends no + * mid-stream revisions — only the end-of-stream one. Values above the + * server's default cadence (300 000 ms) are clamped to it. Accepts 0 to + * 86 400 000 (24 h). Only used when `speakerLabels` is enabled. + * + * Not officially supported yet (sent as the underscore-prefixed + * `_speaker_labels_revision_interval_ms`); name and semantics may change. + */ + speakerLabelsRevisionIntervalMs?: number; voiceFocus?: VoiceFocusModel; voiceFocusThreshold?: number; continuousPartials?: boolean; diff --git a/tests/unit/streaming.test.ts b/tests/unit/streaming.test.ts index b642fdf..7089da8 100644 --- a/tests/unit/streaming.test.ts +++ b/tests/unit/streaming.test.ts @@ -127,6 +127,44 @@ describe("streaming", () => { await connect(rt, server); }); + it("should include _speaker_labels_revision_interval_ms in connection URL", async () => { + await cleanup(); + WS.clean(); + + const wsUrl = `${websocketBaseUrl}?token=123&sample_rate=16000&speech_model=universal-streaming-english&speaker_labels=true&_speaker_labels_revision_interval_ms=60000`; + server = new WS(wsUrl); + rt = new StreamingTranscriber({ + websocketBaseUrl, + token: "123", + sampleRate: 16_000, + speechModel: "universal-streaming-english", + speakerLabels: true, + speakerLabelsRevisionIntervalMs: 60_000, + }); + onOpen = jest.fn(); + rt.on("open", onOpen); + await connect(rt, server); + }); + + it("should send _speaker_labels_revision_interval_ms=0 when explicitly set to 0", async () => { + await cleanup(); + WS.clean(); + + const wsUrl = `${websocketBaseUrl}?token=123&sample_rate=16000&speech_model=universal-streaming-english&speaker_labels=true&_speaker_labels_revision_interval_ms=0`; + server = new WS(wsUrl); + rt = new StreamingTranscriber({ + websocketBaseUrl, + token: "123", + sampleRate: 16_000, + speechModel: "universal-streaming-english", + speakerLabels: true, + speakerLabelsRevisionIntervalMs: 0, + }); + onOpen = jest.fn(); + rt.on("open", onOpen); + await connect(rt, server); + }); + it("should normalize deprecated minEndOfTurnSilenceWhenConfident to min_turn_silence in connection URL", async () => { await cleanup(); WS.clean(); From 2e535948309e99f03fa5d49791fb2ae55d7440c0 Mon Sep 17 00:00:00 2001 From: David Lange Date: Wed, 23 Sep 2026 23:47:57 -0400 Subject: [PATCH 2/3] docs(streaming): note revision-interval warm-up floor and emit-on-change Verified live on prod: with speakerLabelsRevisionIntervalMs=60000 over 240 s of audio the first mid-stream speakerRevision arrived at 126 s (the server's 120 s min-content floor); the unset control got only the end-of-stream one. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 2 +- CLAUDE.md | 4 +++- src/types/streaming/index.ts | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92bedbe..e49768f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## [4.41.5] -- Add `speakerLabelsRevisionIntervalMs` streaming param (sent as `_speaker_labels_revision_interval_ms`) — the cadence, in ms of audio time, at which the server emits mid-stream `speakerRevision` events when `speakerLabels` is enabled. Unset or `0` sends only the end-of-stream revision; values above the server default (300 000 ms) are clamped to it. Not officially supported yet; name and semantics may change +- Add `speakerLabelsRevisionIntervalMs` streaming param (sent as `_speaker_labels_revision_interval_ms`) — the cadence, in ms of audio time, at which the server emits mid-stream `speakerRevision` events when `speakerLabels` is enabled. Unset or `0` sends only the end-of-stream revision; values above the server default (300 000 ms) are clamped to it. The first mid-stream revision needs ~120 s of streamed speech, and one is sent only when some earlier turn's label changed. Not officially supported yet; name and semantics may change ## [4.41.4] diff --git a/CLAUDE.md b/CLAUDE.md index ab03aed..21e9971 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -106,7 +106,9 @@ Turn and replace its per-word `speaker` (and the turn-level `speaker_label`) with the revision's values — text and word timestamps are unchanged. By default only the end-of-stream revision is sent; set `speakerLabelsRevisionIntervalMs` (ms of audio time, e.g. `60_000`; sent as the -not-yet-GA `_speaker_labels_revision_interval_ms`) to also get them mid-stream: +not-yet-GA `_speaker_labels_revision_interval_ms`) to also get them mid-stream — +the first can only arrive after ~120 s of speech, and one is sent only when a +label actually changed: ```typescript transcriber.on("speakerRevision", (event) => { diff --git a/src/types/streaming/index.ts b/src/types/streaming/index.ts index a1908f2..5405b16 100644 --- a/src/types/streaming/index.ts +++ b/src/types/streaming/index.ts @@ -147,6 +147,11 @@ export type StreamingTranscriberParams = { * server's default cadence (300 000 ms) are clamped to it. Accepts 0 to * 86 400 000 (24 h). Only used when `speakerLabels` is enabled. * + * The first mid-stream revision can only arrive after ~120 s of speech has + * been streamed (a server-side floor, not configurable), and a revision is + * sent only when some earlier turn's label actually changed — so expect + * "at most one per interval", not exactly one. + * * Not officially supported yet (sent as the underscore-prefixed * `_speaker_labels_revision_interval_ms`); name and semantics may change. */ From f3898b32ad0a29a7dbba10f8b34f24e811448b50 Mon Sep 17 00:00:00 2001 From: David Lange Date: Thu, 24 Sep 2026 11:09:36 -0400 Subject: [PATCH 3/3] fix(streaming): send GA wire name speaker_labels_revision_interval_ms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server made the un-prefixed name canonical in DeepLearning#20528 (the `_`-prefixed spelling is now a legacy alias), and non-zero values are clamped server-side to 120 000–300 000 ms. Send the GA name, drop the "not officially supported" wording, document the clamp, and use 120 000 in the tests. Verified live on prod: interval=120000 got a mid-stream speakerRevision at 122.7 s; the unset control got only the final one. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 2 +- CLAUDE.md | 7 +++---- src/services/streaming/service.ts | 3 +-- src/types/streaming/index.ts | 14 +++++--------- tests/unit/streaming.test.ts | 10 +++++----- 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e49768f..6106b84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## [4.41.5] -- Add `speakerLabelsRevisionIntervalMs` streaming param (sent as `_speaker_labels_revision_interval_ms`) — the cadence, in ms of audio time, at which the server emits mid-stream `speakerRevision` events when `speakerLabels` is enabled. Unset or `0` sends only the end-of-stream revision; values above the server default (300 000 ms) are clamped to it. The first mid-stream revision needs ~120 s of streamed speech, and one is sent only when some earlier turn's label changed. Not officially supported yet; name and semantics may change +- Add `speakerLabelsRevisionIntervalMs` streaming param (sent as `speaker_labels_revision_interval_ms`) — the cadence, in ms of audio time, at which the server emits mid-stream `speakerRevision` events when `speakerLabels` is enabled. Unset or `0` sends only the end-of-stream revision; non-zero values are clamped server-side to 120 000–300 000 ms. The first mid-stream revision needs ~120 s of streamed speech, and one is sent only when some earlier turn's label changed ## [4.41.4] diff --git a/CLAUDE.md b/CLAUDE.md index 21e9971..7946799 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -105,10 +105,9 @@ are omitted). For each `revisions[i]`, match `turn_order` against the original Turn and replace its per-word `speaker` (and the turn-level `speaker_label`) with the revision's values — text and word timestamps are unchanged. By default only the end-of-stream revision is sent; set -`speakerLabelsRevisionIntervalMs` (ms of audio time, e.g. `60_000`; sent as the -not-yet-GA `_speaker_labels_revision_interval_ms`) to also get them mid-stream — -the first can only arrive after ~120 s of speech, and one is sent only when a -label actually changed: +`speakerLabelsRevisionIntervalMs` (ms of audio time, clamped server-side to +120 000–300 000) to also get them mid-stream — the first can only arrive after +~120 s of speech, and one is sent only when a label actually changed: ```typescript transcriber.on("speakerRevision", (event) => { diff --git a/src/services/streaming/service.ts b/src/services/streaming/service.ts index 518a2ac..60ff025 100644 --- a/src/services/streaming/service.ts +++ b/src/services/streaming/service.ts @@ -407,9 +407,8 @@ export class StreamingTranscriber { } if (this.params.speakerLabelsRevisionIntervalMs !== undefined) { - // Underscore-prefixed wire name = "not officially supported / unstable". searchParams.set( - "_speaker_labels_revision_interval_ms", + "speaker_labels_revision_interval_ms", this.params.speakerLabelsRevisionIntervalMs.toString(), ); } diff --git a/src/types/streaming/index.ts b/src/types/streaming/index.ts index 5405b16..5f2e6c1 100644 --- a/src/types/streaming/index.ts +++ b/src/types/streaming/index.ts @@ -143,17 +143,13 @@ export type StreamingTranscriberParams = { * Cadence, in milliseconds of audio time, at which the server emits * mid-stream `speakerRevision` events with corrected speaker labels for * earlier turns (plus a final one at end of stream). Unset or `0` sends no - * mid-stream revisions — only the end-of-stream one. Values above the - * server's default cadence (300 000 ms) are clamped to it. Accepts 0 to - * 86 400 000 (24 h). Only used when `speakerLabels` is enabled. + * mid-stream revisions — only the end-of-stream one. Non-zero values are + * clamped server-side to 120 000–300 000 ms; the server rejects values + * outside 0–86 400 000 (24 h). Only used when `speakerLabels` is enabled. * * The first mid-stream revision can only arrive after ~120 s of speech has - * been streamed (a server-side floor, not configurable), and a revision is - * sent only when some earlier turn's label actually changed — so expect - * "at most one per interval", not exactly one. - * - * Not officially supported yet (sent as the underscore-prefixed - * `_speaker_labels_revision_interval_ms`); name and semantics may change. + * been streamed, and a revision is sent only when some earlier turn's label + * actually changed — so expect "at most one per interval", not exactly one. */ speakerLabelsRevisionIntervalMs?: number; voiceFocus?: VoiceFocusModel; diff --git a/tests/unit/streaming.test.ts b/tests/unit/streaming.test.ts index 7089da8..0786c91 100644 --- a/tests/unit/streaming.test.ts +++ b/tests/unit/streaming.test.ts @@ -127,11 +127,11 @@ describe("streaming", () => { await connect(rt, server); }); - it("should include _speaker_labels_revision_interval_ms in connection URL", async () => { + it("should include speaker_labels_revision_interval_ms in connection URL", async () => { await cleanup(); WS.clean(); - const wsUrl = `${websocketBaseUrl}?token=123&sample_rate=16000&speech_model=universal-streaming-english&speaker_labels=true&_speaker_labels_revision_interval_ms=60000`; + const wsUrl = `${websocketBaseUrl}?token=123&sample_rate=16000&speech_model=universal-streaming-english&speaker_labels=true&speaker_labels_revision_interval_ms=120000`; server = new WS(wsUrl); rt = new StreamingTranscriber({ websocketBaseUrl, @@ -139,18 +139,18 @@ describe("streaming", () => { sampleRate: 16_000, speechModel: "universal-streaming-english", speakerLabels: true, - speakerLabelsRevisionIntervalMs: 60_000, + speakerLabelsRevisionIntervalMs: 120_000, }); onOpen = jest.fn(); rt.on("open", onOpen); await connect(rt, server); }); - it("should send _speaker_labels_revision_interval_ms=0 when explicitly set to 0", async () => { + it("should send speaker_labels_revision_interval_ms=0 when explicitly set to 0", async () => { await cleanup(); WS.clean(); - const wsUrl = `${websocketBaseUrl}?token=123&sample_rate=16000&speech_model=universal-streaming-english&speaker_labels=true&_speaker_labels_revision_interval_ms=0`; + const wsUrl = `${websocketBaseUrl}?token=123&sample_rate=16000&speech_model=universal-streaming-english&speaker_labels=true&speaker_labels_revision_interval_ms=0`; server = new WS(wsUrl); rt = new StreamingTranscriber({ websocketBaseUrl,