@ai-sdk/assemblyai 3.0.3 → 3.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,8 +19,12 @@ export const assemblyaiTranscriptionModelOptionsSchema = z.object({
19
19
  */
20
20
  autoHighlights: z.boolean().nullish(),
21
21
  /**
22
- * Boost parameter for the transcription.
22
+ * Boost parameter for word boost (used with `wordBoost`).
23
23
  * Allowed values: 'low', 'default', 'high'.
24
+ *
25
+ * @deprecated Only applies to the deprecated `wordBoost` option. Use
26
+ * `keytermsPrompt` instead, which works with the recommended `universal-*`
27
+ * models.
24
28
  */
25
29
  boostParam: z.string().nullish(),
26
30
  /**
@@ -46,6 +50,11 @@ export const assemblyaiTranscriptionModelOptionsSchema = z.object({
46
50
  * Whether to include filler words (um, uh, etc.) in the transcription.
47
51
  */
48
52
  disfluencies: z.boolean().nullish(),
53
+ /**
54
+ * Enable a domain-specific model to improve accuracy for specialized
55
+ * terminology. Currently supports `'medical-v1'` (Medical Mode).
56
+ */
57
+ domain: z.string().nullish(),
49
58
  /**
50
59
  * Whether to enable entity detection.
51
60
  */
@@ -62,6 +71,13 @@ export const assemblyaiTranscriptionModelOptionsSchema = z.object({
62
71
  * Whether to enable IAB categories detection.
63
72
  */
64
73
  iabCategories: z.boolean().nullish(),
74
+ /**
75
+ * Domain-specific keyterms to boost recognition for (max 6 words per phrase).
76
+ * Replaces `wordBoost` for newer models: supported by `universal-3-pro` /
77
+ * `universal-3-5-pro` and `slam-1` (and `universal-2` when metaphone is
78
+ * enabled for the account).
79
+ */
80
+ keytermsPrompt: z.array(z.string()).nullish(),
65
81
  /**
66
82
  * Language code for the transcription.
67
83
  */
@@ -74,10 +90,30 @@ export const assemblyaiTranscriptionModelOptionsSchema = z.object({
74
90
  * Whether to enable language detection.
75
91
  */
76
92
  languageDetection: z.boolean().nullish(),
93
+ /**
94
+ * Options for automatic language detection.
95
+ */
96
+ languageDetectionOptions: z
97
+ .object({
98
+ /** List of languages expected in the audio file. */
99
+ expectedLanguages: z.array(z.string()).nullish(),
100
+ /** Fallback language if the detected language is not expected. */
101
+ fallbackLanguage: z.string().nullish(),
102
+ /** Whether code switching should be detected. */
103
+ codeSwitching: z.boolean().nullish(),
104
+ /** Confidence threshold for code switching detection (0-1). */
105
+ codeSwitchingConfidenceThreshold: z.number().min(0).max(1).nullish(),
106
+ })
107
+ .nullish(),
77
108
  /**
78
109
  * Whether to process audio as multichannel.
79
110
  */
80
111
  multichannel: z.boolean().nullish(),
112
+ /**
113
+ * Provide natural-language context (up to 1,500 words) to steer the model.
114
+ * Only supported by `universal-3-pro` / `universal-3-5-pro` and `slam-1`.
115
+ */
116
+ prompt: z.string().nullish(),
81
117
  /**
82
118
  * Whether to add punctuation to the transcription.
83
119
  */
@@ -90,6 +126,17 @@ export const assemblyaiTranscriptionModelOptionsSchema = z.object({
90
126
  * Whether to redact PII in the audio file.
91
127
  */
92
128
  redactPiiAudio: z.boolean().nullish(),
129
+ /**
130
+ * Options for PII-redacted audio files. Requires `redactPiiAudio`.
131
+ */
132
+ redactPiiAudioOptions: z
133
+ .object({
134
+ /** Return redacted audio even for files without detected speech. */
135
+ returnRedactedNoSpeechAudio: z.boolean().nullish(),
136
+ /** Redaction method; set to `'silence'` to replace PII with silence. */
137
+ overrideAudioRedactionMethod: z.enum(['silence']).nullish(),
138
+ })
139
+ .nullish(),
93
140
  /**
94
141
  * Audio format for PII redaction.
95
142
  */
@@ -98,10 +145,26 @@ export const assemblyaiTranscriptionModelOptionsSchema = z.object({
98
145
  * List of PII types to redact.
99
146
  */
100
147
  redactPiiPolicies: z.array(z.string()).nullish(),
148
+ /**
149
+ * Return the original unredacted transcript alongside the redacted one.
150
+ * Requires `redactPii`.
151
+ */
152
+ redactPiiReturnUnredacted: z.boolean().nullish(),
101
153
  /**
102
154
  * Substitution method for redacted PII.
103
155
  */
104
156
  redactPiiSub: z.string().nullish(),
157
+ /**
158
+ * Map of user-defined labels to exact terms to redact, e.g.
159
+ * `{ INTERNAL_TOOL: ['Bearclaw'] }`. Applied on top of standard PII redaction
160
+ * using `redactPiiSub`. Requires `redactPii`.
161
+ */
162
+ redactStaticEntities: z.record(z.string(), z.array(z.string())).nullish(),
163
+ /**
164
+ * Remove inline annotations from rich transcripts. `'all'` removes all inline
165
+ * annotations; `'speaker'` removes only speaker cues. Universal-3 Pro models.
166
+ */
167
+ removeAudioTags: z.enum(['all', 'speaker']).nullish(),
105
168
  /**
106
169
  * Whether to enable sentiment analysis.
107
170
  */
@@ -110,6 +173,17 @@ export const assemblyaiTranscriptionModelOptionsSchema = z.object({
110
173
  * Whether to identify different speakers in the audio.
111
174
  */
112
175
  speakerLabels: z.boolean().nullish(),
176
+ /**
177
+ * Options for speaker diarization, e.g. a range of possible speakers.
178
+ */
179
+ speakerOptions: z
180
+ .object({
181
+ /** Minimum number of speakers expected in the audio file. */
182
+ minSpeakersExpected: z.number().int().nullish(),
183
+ /** Maximum number of speakers expected in the audio file. */
184
+ maxSpeakersExpected: z.number().int().nullish(),
185
+ })
186
+ .nullish(),
113
187
  /**
114
188
  * Number of speakers expected in the audio.
115
189
  */
@@ -130,6 +204,10 @@ export const assemblyaiTranscriptionModelOptionsSchema = z.object({
130
204
  * Type of summary to generate.
131
205
  */
132
206
  summaryType: z.string().nullish(),
207
+ /**
208
+ * Sampling temperature (0-1) controlling randomness. Universal-3 Pro models.
209
+ */
210
+ temperature: z.number().min(0).max(1).nullish(),
133
211
  /**
134
212
  * Name of the authentication header for webhook requests.
135
213
  */
@@ -144,6 +222,10 @@ export const assemblyaiTranscriptionModelOptionsSchema = z.object({
144
222
  webhookUrl: z.string().nullish(),
145
223
  /**
146
224
  * List of words to boost recognition for.
225
+ *
226
+ * @deprecated `wordBoost` is rejected by `universal-3-pro` /
227
+ * `universal-3-5-pro` and `slam-1` (it only works on `universal-2`/`best`).
228
+ * Use `keytermsPrompt` instead.
147
229
  */
148
230
  wordBoost: z.array(z.string()).nullish(),
149
231
  });
@@ -1,4 +1,8 @@
1
- import type { TranscriptionModelV4, SharedV4Warning } from '@ai-sdk/provider';
1
+ import type {
2
+ TranscriptionModelV4,
3
+ SharedV4Warning,
4
+ SharedV4ProviderMetadata,
5
+ } from '@ai-sdk/provider';
2
6
  import {
3
7
  combineHeaders,
4
8
  createJsonResponseHandler,
@@ -66,9 +70,42 @@ export class AssemblyAITranscriptionModel implements TranscriptionModelV4 {
66
70
  schema: assemblyaiTranscriptionModelOptionsSchema,
67
71
  });
68
72
 
69
- const body: Omit<AssemblyAITranscriptionAPITypes, 'audio_url'> = {
70
- speech_model: this.modelId as 'best' | 'nano',
71
- };
73
+ const body: Omit<AssemblyAITranscriptionAPITypes, 'audio_url'> = {};
74
+
75
+ // The legacy `best` model is selected via the deprecated singular
76
+ // `speech_model` parameter. All other models (e.g. `universal-2`,
77
+ // `universal-3-pro`, `universal-3-5-pro`) are only accessible via the
78
+ // `speech_models` array and are rejected by `speech_model`.
79
+ // See https://www.assemblyai.com/docs/pre-recorded-audio/select-the-speech-model
80
+ if (this.modelId === 'best') {
81
+ body.speech_model = this.modelId as 'best';
82
+ warnings.push({
83
+ type: 'deprecated',
84
+ setting: `model '${this.modelId}'`,
85
+ message:
86
+ "The 'best' model is a legacy AssemblyAI model. Use 'universal-3-5-pro' instead. See documentation: https://www.assemblyai.com/docs/pre-recorded-audio/select-the-speech-model",
87
+ });
88
+ } else {
89
+ body.speech_models = [this.modelId];
90
+
91
+ // Forward-looking nudge: universal-3-5-pro is AssemblyAI's latest
92
+ // flagship and is set to replace universal-3-pro. Not a deprecation —
93
+ // both models still work — so this is an informational warning only.
94
+ if (
95
+ this.modelId === 'universal-3-pro' ||
96
+ this.modelId === 'universal-2'
97
+ ) {
98
+ const docsUrl =
99
+ 'https://www.assemblyai.com/docs/pre-recorded-audio/select-the-speech-model';
100
+ warnings.push({
101
+ type: 'other',
102
+ message:
103
+ this.modelId === 'universal-3-pro'
104
+ ? `'universal-3-5-pro' is AssemblyAI's latest flagship model and is set to replace 'universal-3-pro'. See ${docsUrl}`
105
+ : `'universal-3-5-pro' is AssemblyAI's latest flagship model. See ${docsUrl}`,
106
+ });
107
+ }
108
+ }
72
109
 
73
110
  // Add provider-specific options
74
111
  if (assemblyaiOptions) {
@@ -118,6 +155,103 @@ export class AssemblyAITranscriptionModel implements TranscriptionModelV4 {
118
155
  assemblyaiOptions.webhookAuthHeaderValue ?? undefined;
119
156
  body.webhook_url = assemblyaiOptions.webhookUrl ?? undefined;
120
157
  body.word_boost = assemblyaiOptions.wordBoost ?? undefined;
158
+ body.keyterms_prompt = assemblyaiOptions.keytermsPrompt ?? undefined;
159
+ body.prompt = assemblyaiOptions.prompt ?? undefined;
160
+ body.temperature = assemblyaiOptions.temperature ?? undefined;
161
+ body.remove_audio_tags = assemblyaiOptions.removeAudioTags ?? undefined;
162
+ body.domain = assemblyaiOptions.domain ?? undefined;
163
+ body.redact_pii_return_unredacted =
164
+ assemblyaiOptions.redactPiiReturnUnredacted ?? undefined;
165
+ body.redact_static_entities =
166
+ assemblyaiOptions.redactStaticEntities ?? undefined;
167
+
168
+ if (assemblyaiOptions.speakerOptions) {
169
+ body.speaker_options = {
170
+ min_speakers_expected:
171
+ assemblyaiOptions.speakerOptions.minSpeakersExpected ?? undefined,
172
+ max_speakers_expected:
173
+ assemblyaiOptions.speakerOptions.maxSpeakersExpected ?? undefined,
174
+ };
175
+ }
176
+
177
+ if (assemblyaiOptions.languageDetectionOptions) {
178
+ body.language_detection_options = {
179
+ expected_languages:
180
+ assemblyaiOptions.languageDetectionOptions.expectedLanguages ??
181
+ undefined,
182
+ fallback_language:
183
+ assemblyaiOptions.languageDetectionOptions.fallbackLanguage ??
184
+ undefined,
185
+ code_switching:
186
+ assemblyaiOptions.languageDetectionOptions.codeSwitching ??
187
+ undefined,
188
+ code_switching_confidence_threshold:
189
+ assemblyaiOptions.languageDetectionOptions
190
+ .codeSwitchingConfidenceThreshold ?? undefined,
191
+ };
192
+ }
193
+
194
+ if (assemblyaiOptions.redactPiiAudioOptions) {
195
+ body.redact_pii_audio_options = {
196
+ return_redacted_no_speech_audio:
197
+ assemblyaiOptions.redactPiiAudioOptions
198
+ .returnRedactedNoSpeechAudio ?? undefined,
199
+ override_audio_redaction_method:
200
+ assemblyaiOptions.redactPiiAudioOptions
201
+ .overrideAudioRedactionMethod ?? undefined,
202
+ };
203
+ }
204
+
205
+ const deprecatedBoostOptions: string[] = [];
206
+ if (assemblyaiOptions.wordBoost != null) {
207
+ deprecatedBoostOptions.push('wordBoost');
208
+ }
209
+ if (assemblyaiOptions.boostParam != null) {
210
+ deprecatedBoostOptions.push('boostParam');
211
+ }
212
+ if (deprecatedBoostOptions.length > 0) {
213
+ warnings.push({
214
+ type: 'deprecated',
215
+ setting: deprecatedBoostOptions.join(', '),
216
+ message:
217
+ "'wordBoost' and 'boostParam' are deprecated and are rejected by 'universal-3-pro' / 'universal-3-5-pro' and 'slam-1'. Use 'keytermsPrompt' instead.",
218
+ });
219
+ }
220
+
221
+ // The following options only take effect alongside a prerequisite
222
+ // option; without it AssemblyAI either rejects the request (400) or
223
+ // silently ignores the option. Warn rather than mutate user input.
224
+ if (
225
+ (assemblyaiOptions.redactPiiReturnUnredacted != null ||
226
+ assemblyaiOptions.redactStaticEntities != null) &&
227
+ !assemblyaiOptions.redactPii
228
+ ) {
229
+ warnings.push({
230
+ type: 'other',
231
+ message:
232
+ "'redactPiiReturnUnredacted' and 'redactStaticEntities' require 'redactPii' to be enabled; AssemblyAI rejects the request otherwise.",
233
+ });
234
+ }
235
+ if (
236
+ assemblyaiOptions.redactPiiAudioOptions != null &&
237
+ !assemblyaiOptions.redactPiiAudio
238
+ ) {
239
+ warnings.push({
240
+ type: 'other',
241
+ message:
242
+ "'redactPiiAudioOptions' only applies when 'redactPiiAudio' is enabled; it is otherwise ignored.",
243
+ });
244
+ }
245
+ if (
246
+ assemblyaiOptions.languageCode != null &&
247
+ assemblyaiOptions.languageDetection
248
+ ) {
249
+ warnings.push({
250
+ type: 'other',
251
+ message:
252
+ "'languageDetection' cannot be combined with an explicit 'languageCode'; AssemblyAI rejects requests that set both.",
253
+ });
254
+ }
121
255
  }
122
256
 
123
257
  return {
@@ -137,17 +271,22 @@ export class AssemblyAITranscriptionModel implements TranscriptionModelV4 {
137
271
  abortSignal?: AbortSignal,
138
272
  ): Promise<{
139
273
  transcript: z.infer<typeof assemblyaiTranscriptionResponseSchema>;
274
+ rawTranscript: unknown;
140
275
  responseHeaders: Record<string, string>;
141
276
  }> {
142
277
  const pollingInterval =
143
278
  this.config.pollingInterval ?? this.POLLING_INTERVAL_MS;
144
279
 
280
+ // Honor a caller-provided fetch (proxy, auth injection, tests) for the
281
+ // polling GETs, matching the upload/submit calls that use config.fetch.
282
+ const fetchImpl = this.config.fetch ?? globalThis.fetch;
283
+
145
284
  while (true) {
146
285
  if (abortSignal?.aborted) {
147
286
  throw new Error('Transcription request was aborted');
148
287
  }
149
288
 
150
- const response = await fetch(
289
+ const response = await fetchImpl(
151
290
  this.config.url({
152
291
  path: `/v2/transcript/${transcriptId}`,
153
292
  modelId: this.modelId,
@@ -173,13 +312,14 @@ export class AssemblyAITranscriptionModel implements TranscriptionModelV4 {
173
312
  });
174
313
  }
175
314
 
176
- const transcript = assemblyaiTranscriptionResponseSchema.parse(
177
- await response.json(),
178
- );
315
+ const rawTranscript = await response.json();
316
+ const transcript =
317
+ assemblyaiTranscriptionResponseSchema.parse(rawTranscript);
179
318
 
180
319
  if (transcript.status === 'completed') {
181
320
  return {
182
321
  transcript,
322
+ rawTranscript,
183
323
  responseHeaders: extractResponseHeaders(response),
184
324
  };
185
325
  }
@@ -240,29 +380,70 @@ export class AssemblyAITranscriptionModel implements TranscriptionModelV4 {
240
380
  fetch: this.config.fetch,
241
381
  });
242
382
 
243
- const { transcript, responseHeaders } = await this.waitForCompletion(
244
- submitResponse.id,
245
- options.headers,
246
- options.abortSignal,
247
- );
383
+ const { transcript, rawTranscript, responseHeaders } =
384
+ await this.waitForCompletion(
385
+ submitResponse.id,
386
+ options.headers,
387
+ options.abortSignal,
388
+ );
389
+
390
+ // Surface diarization and audio-intelligence results that the AI SDK's
391
+ // `segments` shape can't represent, keyed under `assemblyai`. Presence is
392
+ // gated on the parsed transcript, but values are taken from the raw
393
+ // response so no fields are stripped by the schema.
394
+ //
395
+ // NOTE: timings inside these objects (e.g. `utterances[].start`) are in
396
+ // milliseconds, matching the AssemblyAI API — unlike the top-level
397
+ // `segments`, whose `startSecond`/`endSecond` are in seconds.
398
+ const raw = (rawTranscript ?? {}) as Record<string, unknown>;
399
+ const assemblyaiMetadata: Record<string, unknown> = {};
400
+ if (transcript.utterances != null) {
401
+ assemblyaiMetadata.utterances = raw.utterances;
402
+ }
403
+ if (transcript.sentiment_analysis_results != null) {
404
+ assemblyaiMetadata.sentimentAnalysisResults =
405
+ raw.sentiment_analysis_results;
406
+ }
407
+ if (transcript.entities != null) {
408
+ assemblyaiMetadata.entities = raw.entities;
409
+ }
410
+ if (transcript.content_safety_labels != null) {
411
+ assemblyaiMetadata.contentSafetyLabels = raw.content_safety_labels;
412
+ }
413
+ if (transcript.iab_categories_result != null) {
414
+ assemblyaiMetadata.iabCategoriesResult = raw.iab_categories_result;
415
+ }
416
+ if (transcript.auto_highlights_result != null) {
417
+ assemblyaiMetadata.autoHighlightsResult = raw.auto_highlights_result;
418
+ }
419
+
420
+ const lastWordEndMs = transcript.words?.at(-1)?.end;
248
421
 
249
422
  return {
250
423
  text: transcript.text ?? '',
424
+ // AssemblyAI returns word timings in milliseconds; the AI SDK reports
425
+ // segment timings in seconds.
251
426
  segments:
252
427
  transcript.words?.map(word => ({
253
428
  text: word.text,
254
- startSecond: word.start,
255
- endSecond: word.end,
429
+ startSecond: word.start / 1000,
430
+ endSecond: word.end / 1000,
256
431
  })) ?? [],
257
432
  language: transcript.language_code ?? undefined,
258
433
  durationInSeconds:
259
- transcript.audio_duration ?? transcript.words?.at(-1)?.end ?? undefined,
434
+ transcript.audio_duration ??
435
+ (lastWordEndMs != null ? lastWordEndMs / 1000 : undefined),
260
436
  warnings,
437
+ ...(Object.keys(assemblyaiMetadata).length > 0 && {
438
+ providerMetadata: {
439
+ assemblyai: assemblyaiMetadata,
440
+ } as SharedV4ProviderMetadata,
441
+ }),
261
442
  response: {
262
443
  timestamp: currentDate,
263
444
  modelId: this.modelId,
264
445
  headers: responseHeaders, // Headers from final GET request
265
- body: transcript, // Raw response from final GET request
446
+ body: rawTranscript, // Full raw response from final GET request
266
447
  },
267
448
  };
268
449
  }
@@ -277,20 +458,65 @@ const assemblyaiSubmitResponseSchema = z.object({
277
458
  status: z.enum(['queued', 'processing', 'completed', 'error']),
278
459
  });
279
460
 
461
+ const assemblyaiWordSchema = z.object({
462
+ start: z.number(),
463
+ end: z.number(),
464
+ text: z.string(),
465
+ confidence: z.number().nullish(),
466
+ // Speaker label (e.g. 'A', 'B') when speaker diarization is enabled, else null.
467
+ speaker: z.string().nullish(),
468
+ channel: z.string().nullish(),
469
+ });
470
+
280
471
  const assemblyaiTranscriptionResponseSchema = z.object({
281
472
  id: z.string(),
282
473
  status: z.enum(['queued', 'processing', 'completed', 'error']),
283
474
  text: z.string().nullish(),
284
475
  language_code: z.string().nullish(),
285
- words: z
476
+ speech_model_used: z.string().nullish(),
477
+ words: z.array(assemblyaiWordSchema).nullish(),
478
+ // Speaker-diarized utterances (present when `speaker_labels` is enabled).
479
+ utterances: z
286
480
  .array(
287
481
  z.object({
288
482
  start: z.number(),
289
483
  end: z.number(),
290
484
  text: z.string(),
485
+ confidence: z.number().nullish(),
486
+ speaker: z.string().nullish(),
487
+ channel: z.string().nullish(),
488
+ words: z.array(assemblyaiWordSchema).nullish(),
489
+ }),
490
+ )
491
+ .nullish(),
492
+ // Audio-intelligence results, present only when the matching feature is
493
+ // enabled. Kept intentionally permissive (the full structures are also
494
+ // available on the raw `response.body`).
495
+ sentiment_analysis_results: z
496
+ .array(
497
+ z.object({
498
+ text: z.string(),
499
+ start: z.number().nullish(),
500
+ end: z.number().nullish(),
501
+ sentiment: z.string(),
502
+ confidence: z.number().nullish(),
503
+ speaker: z.string().nullish(),
504
+ }),
505
+ )
506
+ .nullish(),
507
+ entities: z
508
+ .array(
509
+ z.object({
510
+ entity_type: z.string(),
511
+ text: z.string(),
512
+ start: z.number().nullish(),
513
+ end: z.number().nullish(),
291
514
  }),
292
515
  )
293
516
  .nullish(),
517
+ content_safety_labels: z.record(z.string(), z.any()).nullish(),
518
+ iab_categories_result: z.record(z.string(), z.any()).nullish(),
519
+ auto_highlights_result: z.record(z.string(), z.any()).nullish(),
294
520
  audio_duration: z.number().nullish(),
295
521
  error: z.string().nullish(),
296
522
  });
@@ -1 +1,15 @@
1
- export type AssemblyAITranscriptionModelId = 'best' | 'nano' | (string & {});
1
+ /**
2
+ * Legacy AssemblyAI speech model, sent via the deprecated singular
3
+ * `speech_model` request parameter.
4
+ *
5
+ * @deprecated Use `universal-3-5-pro` instead.
6
+ * @see https://www.assemblyai.com/docs/pre-recorded-audio/select-the-speech-model
7
+ */
8
+ export type AssemblyAIDeprecatedTranscriptionModelId = 'best';
9
+
10
+ export type AssemblyAITranscriptionModelId =
11
+ | 'universal-2'
12
+ | 'universal-3-pro'
13
+ | 'universal-3-5-pro'
14
+ | AssemblyAIDeprecatedTranscriptionModelId
15
+ | (string & {});