From bb63ec2561dda4d61edb8e07f60441ed95914946 Mon Sep 17 00:00:00 2001 From: Matt Lawler Date: Tue, 11 Aug 2026 16:33:26 -0700 Subject: [PATCH 1/2] feat: add effort to SpeakerIdentificationRequest The speaker identification API accepts an `effort` field ("low" | "medium", default "low") to trade cost for quality on harder audio, but it was missing from the SDK types, so setting it failed to type check. Fixes #167 Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 4 + package.json | 2 +- src/types/openapi.generated.ts | 13 ++++ tests/unit/speaker-identification.test.ts | 89 +++++++++++++++++++++++ 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 tests/unit/speaker-identification.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 17af2f3..649182e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [4.36.6] + +- Add `effort` to `SpeakerIdentificationRequest` — `"low"` (default) or `"medium"`, matching the [speaker identification docs](https://www.assemblyai.com/docs/speech-understanding/speaker-identification#controlling-effort). The field was already accepted by the API but missing from the SDK types, so setting it failed to type check + ## [4.36.4] - Add `aac` to the streaming `encoding` options — accepts an AAC stream in ADTS framing. Like `opus`/`ogg_opus`, AAC is self-describing, so `sampleRate` is optional for it (it remains required for PCM encodings and for dual-channel mode) diff --git a/package.json b/package.json index c14271d..6ba404a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "assemblyai", - "version": "4.36.5", + "version": "4.36.6", "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/types/openapi.generated.ts b/src/types/openapi.generated.ts index b93d37d..fdd0cf4 100644 --- a/src/types/openapi.generated.ts +++ b/src/types/openapi.generated.ts @@ -1508,6 +1508,11 @@ export type SeverityScoreSummary = { */ export type SpeakerType = "role" | "name"; +/** + * Effort level for speaker identification + */ +export type SpeakerIdentificationEffort = "low" | "medium"; + /** * Speaker identification configuration for speech understanding */ @@ -1520,6 +1525,14 @@ export type SpeakerIdentificationRequest = { * Known speaker values (required when speaker_type is 'role') */ known_values?: string[]; + /** + * How much effort to spend identifying speakers. Use 'medium' for higher complexity + * audio, such as meetings with interruptions or transcripts where names aren't + * clearly stated, at a higher cost. + * + * @defaultValue "low" + */ + effort?: SpeakerIdentificationEffort; }; /** diff --git a/tests/unit/speaker-identification.test.ts b/tests/unit/speaker-identification.test.ts new file mode 100644 index 0000000..8396a44 --- /dev/null +++ b/tests/unit/speaker-identification.test.ts @@ -0,0 +1,89 @@ +import fetchMock from "jest-fetch-mock"; +import { + SpeakerIdentificationEffort, + SpeakerIdentificationRequest, +} from "../../src"; +import { createClient, requestMatches } from "./utils"; + +fetchMock.enableMocks(); + +const assembly = createClient(); +const transcriptId = "transcript_123"; +const remoteAudioURL = "https://assembly.ai/espn.m4a"; + +beforeEach(() => { + jest.clearAllMocks(); + fetchMock.resetMocks(); + fetchMock.doMock(); +}); + +describe("speaker identification", () => { + it("should create transcript with speaker_identification effort", async () => { + const speakerIdentification: SpeakerIdentificationRequest = { + speaker_type: "name", + effort: "medium", + }; + + fetchMock.doMockOnceIf( + requestMatches({ url: "/v2/transcript", method: "POST" }), + JSON.stringify({ id: transcriptId, status: "queued" }), + ); + + const transcript = await assembly.transcripts.submit({ + audio_url: remoteAudioURL, + speaker_labels: true, + speech_understanding: { + request: { speaker_identification: speakerIdentification }, + }, + }); + + expect(transcript.id).toBe(transcriptId); + expect(transcript.status).toBe("queued"); + + const requestBody = JSON.parse(fetchMock.mock.calls[0][1]?.body as string); + expect( + requestBody.speech_understanding.request.speaker_identification, + ).toEqual(speakerIdentification); + }); + + it("should create transcript with speaker_identification without effort", async () => { + fetchMock.doMockOnceIf( + requestMatches({ url: "/v2/transcript", method: "POST" }), + JSON.stringify({ id: transcriptId, status: "queued" }), + ); + + const transcript = await assembly.transcripts.submit({ + audio_url: remoteAudioURL, + speaker_labels: true, + speech_understanding: { + request: { + speaker_identification: { + speaker_type: "role", + known_values: ["Agent", "Customer"], + }, + }, + }, + }); + + expect(transcript.id).toBe(transcriptId); + + const requestBody = JSON.parse(fetchMock.mock.calls[0][1]?.body as string); + const speakerIdentification = + requestBody.speech_understanding.request.speaker_identification; + expect(speakerIdentification.speaker_type).toBe("role"); + expect(speakerIdentification.known_values).toEqual(["Agent", "Customer"]); + expect(speakerIdentification.effort).toBeUndefined(); + }); + + it("should accept every documented effort value", () => { + const efforts: SpeakerIdentificationEffort[] = ["low", "medium"]; + + for (const effort of efforts) { + const speakerIdentification: SpeakerIdentificationRequest = { + speaker_type: "name", + effort, + }; + expect(speakerIdentification.effort).toBe(effort); + } + }); +}); From 9e981666f4f631dfbf28a06ec5159b2c6ab1a96f Mon Sep 17 00:00:00 2001 From: Matt Lawler Date: Wed, 12 Aug 2026 16:01:55 -0700 Subject: [PATCH 2/2] feat: apply effort to all speech understanding tasks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review: effort isn't speaker-identification specific — every LLM speech understanding task accepts it. Rename the enum to SpeechUnderstandingEffort and add the field to TranslationRequest and CustomFormattingRequest alongside SpeakerIdentificationRequest. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 2 +- src/types/openapi.generated.ts | 23 ++- tests/unit/speaker-identification.test.ts | 89 ----------- .../unit/speech-understanding-effort.test.ts | 147 ++++++++++++++++++ 4 files changed, 165 insertions(+), 96 deletions(-) delete mode 100644 tests/unit/speaker-identification.test.ts create mode 100644 tests/unit/speech-understanding-effort.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 649182e..26cabd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## [4.36.6] -- Add `effort` to `SpeakerIdentificationRequest` — `"low"` (default) or `"medium"`, matching the [speaker identification docs](https://www.assemblyai.com/docs/speech-understanding/speaker-identification#controlling-effort). The field was already accepted by the API but missing from the SDK types, so setting it failed to type check +- Add `effort` to the speech understanding feature requests (`SpeakerIdentificationRequest`, `TranslationRequest`, `CustomFormattingRequest`) — `"low"` (default) or `"medium"`, set per task, typed as the new `SpeechUnderstandingEffort`. The field was already accepted by the API but missing from the SDK types, so setting it failed to type check ## [4.36.4] diff --git a/src/types/openapi.generated.ts b/src/types/openapi.generated.ts index fdd0cf4..5107e48 100644 --- a/src/types/openapi.generated.ts +++ b/src/types/openapi.generated.ts @@ -1509,9 +1509,10 @@ export type SeverityScoreSummary = { export type SpeakerType = "role" | "name"; /** - * Effort level for speaker identification + * How much processing power to spend on a speech understanding task. 'medium' produces + * higher quality results on harder audio at a higher cost. */ -export type SpeakerIdentificationEffort = "low" | "medium"; +export type SpeechUnderstandingEffort = "low" | "medium"; /** * Speaker identification configuration for speech understanding @@ -1526,13 +1527,11 @@ export type SpeakerIdentificationRequest = { */ known_values?: string[]; /** - * How much effort to spend identifying speakers. Use 'medium' for higher complexity - * audio, such as meetings with interruptions or transcripts where names aren't - * clearly stated, at a higher cost. + * How much effort to spend on this task * * @defaultValue "low" */ - effort?: SpeakerIdentificationEffort; + effort?: SpeechUnderstandingEffort; }; /** @@ -1551,6 +1550,12 @@ export type TranslationRequest = { * Whether to match the original utterance structure in translations (default: false) */ match_original_utterance?: boolean; + /** + * How much effort to spend on this task + * + * @defaultValue "low" + */ + effort?: SpeechUnderstandingEffort; }; /** @@ -1569,6 +1574,12 @@ export type CustomFormattingRequest = { * Custom email format pattern (e.g., 'username\@domain.com') */ email?: string; + /** + * How much effort to spend on this task + * + * @defaultValue "low" + */ + effort?: SpeechUnderstandingEffort; }; /** diff --git a/tests/unit/speaker-identification.test.ts b/tests/unit/speaker-identification.test.ts deleted file mode 100644 index 8396a44..0000000 --- a/tests/unit/speaker-identification.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import fetchMock from "jest-fetch-mock"; -import { - SpeakerIdentificationEffort, - SpeakerIdentificationRequest, -} from "../../src"; -import { createClient, requestMatches } from "./utils"; - -fetchMock.enableMocks(); - -const assembly = createClient(); -const transcriptId = "transcript_123"; -const remoteAudioURL = "https://assembly.ai/espn.m4a"; - -beforeEach(() => { - jest.clearAllMocks(); - fetchMock.resetMocks(); - fetchMock.doMock(); -}); - -describe("speaker identification", () => { - it("should create transcript with speaker_identification effort", async () => { - const speakerIdentification: SpeakerIdentificationRequest = { - speaker_type: "name", - effort: "medium", - }; - - fetchMock.doMockOnceIf( - requestMatches({ url: "/v2/transcript", method: "POST" }), - JSON.stringify({ id: transcriptId, status: "queued" }), - ); - - const transcript = await assembly.transcripts.submit({ - audio_url: remoteAudioURL, - speaker_labels: true, - speech_understanding: { - request: { speaker_identification: speakerIdentification }, - }, - }); - - expect(transcript.id).toBe(transcriptId); - expect(transcript.status).toBe("queued"); - - const requestBody = JSON.parse(fetchMock.mock.calls[0][1]?.body as string); - expect( - requestBody.speech_understanding.request.speaker_identification, - ).toEqual(speakerIdentification); - }); - - it("should create transcript with speaker_identification without effort", async () => { - fetchMock.doMockOnceIf( - requestMatches({ url: "/v2/transcript", method: "POST" }), - JSON.stringify({ id: transcriptId, status: "queued" }), - ); - - const transcript = await assembly.transcripts.submit({ - audio_url: remoteAudioURL, - speaker_labels: true, - speech_understanding: { - request: { - speaker_identification: { - speaker_type: "role", - known_values: ["Agent", "Customer"], - }, - }, - }, - }); - - expect(transcript.id).toBe(transcriptId); - - const requestBody = JSON.parse(fetchMock.mock.calls[0][1]?.body as string); - const speakerIdentification = - requestBody.speech_understanding.request.speaker_identification; - expect(speakerIdentification.speaker_type).toBe("role"); - expect(speakerIdentification.known_values).toEqual(["Agent", "Customer"]); - expect(speakerIdentification.effort).toBeUndefined(); - }); - - it("should accept every documented effort value", () => { - const efforts: SpeakerIdentificationEffort[] = ["low", "medium"]; - - for (const effort of efforts) { - const speakerIdentification: SpeakerIdentificationRequest = { - speaker_type: "name", - effort, - }; - expect(speakerIdentification.effort).toBe(effort); - } - }); -}); diff --git a/tests/unit/speech-understanding-effort.test.ts b/tests/unit/speech-understanding-effort.test.ts new file mode 100644 index 0000000..1a11806 --- /dev/null +++ b/tests/unit/speech-understanding-effort.test.ts @@ -0,0 +1,147 @@ +import fetchMock from "jest-fetch-mock"; +import { + CustomFormattingRequest, + SpeakerIdentificationRequest, + SpeechUnderstandingEffort, + TranslationRequest, +} from "../../src"; +import { createClient, requestMatches } from "./utils"; + +fetchMock.enableMocks(); + +const assembly = createClient(); +const transcriptId = "transcript_123"; +const remoteAudioURL = "https://assembly.ai/espn.m4a"; + +beforeEach(() => { + jest.clearAllMocks(); + fetchMock.resetMocks(); + fetchMock.doMock(); +}); + +const mockSubmit = () => + fetchMock.doMockOnceIf( + requestMatches({ url: "/v2/transcript", method: "POST" }), + JSON.stringify({ id: transcriptId, status: "queued" }), + ); + +const submittedRequest = () => + JSON.parse(fetchMock.mock.calls[0][1]?.body as string).speech_understanding + .request; + +describe("speech understanding effort", () => { + it("should create transcript with speaker_identification effort", async () => { + const speakerIdentification: SpeakerIdentificationRequest = { + speaker_type: "name", + effort: "medium", + }; + mockSubmit(); + + const transcript = await assembly.transcripts.submit({ + audio_url: remoteAudioURL, + speaker_labels: true, + speech_understanding: { + request: { speaker_identification: speakerIdentification }, + }, + }); + + expect(transcript.id).toBe(transcriptId); + expect(transcript.status).toBe("queued"); + expect(submittedRequest().speaker_identification).toEqual( + speakerIdentification, + ); + }); + + it("should create transcript with translation effort", async () => { + const translation: TranslationRequest = { + target_languages: ["es", "fr"], + effort: "medium", + }; + mockSubmit(); + + await assembly.transcripts.submit({ + audio_url: remoteAudioURL, + speech_understanding: { request: { translation } }, + }); + + expect(submittedRequest().translation).toEqual(translation); + }); + + it("should create transcript with custom_formatting effort", async () => { + const customFormatting: CustomFormattingRequest = { + date: "mm/dd/yyyy", + effort: "medium", + }; + mockSubmit(); + + await assembly.transcripts.submit({ + audio_url: remoteAudioURL, + speech_understanding: { + request: { custom_formatting: customFormatting }, + }, + }); + + expect(submittedRequest().custom_formatting).toEqual(customFormatting); + }); + + it("should set effort per task independently", async () => { + mockSubmit(); + + await assembly.transcripts.submit({ + audio_url: remoteAudioURL, + speaker_labels: true, + speech_understanding: { + request: { + speaker_identification: { speaker_type: "name", effort: "medium" }, + translation: { target_languages: ["es"], effort: "low" }, + }, + }, + }); + + const request = submittedRequest(); + expect(request.speaker_identification.effort).toBe("medium"); + expect(request.translation.effort).toBe("low"); + }); + + it("should omit effort when it isn't set", async () => { + mockSubmit(); + + await assembly.transcripts.submit({ + audio_url: remoteAudioURL, + speaker_labels: true, + speech_understanding: { + request: { + speaker_identification: { + speaker_type: "role", + known_values: ["Agent", "Customer"], + }, + }, + }, + }); + + const speakerIdentification = submittedRequest().speaker_identification; + expect(speakerIdentification.speaker_type).toBe("role"); + expect(speakerIdentification.known_values).toEqual(["Agent", "Customer"]); + expect(speakerIdentification.effort).toBeUndefined(); + }); + + it("should accept every documented effort value", () => { + const efforts: SpeechUnderstandingEffort[] = ["low", "medium"]; + + for (const effort of efforts) { + const speakerIdentification: SpeakerIdentificationRequest = { + speaker_type: "name", + effort, + }; + const translation: TranslationRequest = { + target_languages: ["es"], + effort, + }; + const customFormatting: CustomFormattingRequest = { effort }; + + expect(speakerIdentification.effort).toBe(effort); + expect(translation.effort).toBe(effort); + expect(customFormatting.effort).toBe(effort); + } + }); +});