@ai-sdk/assemblyai 2.0.41 → 2.0.42

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.
@@ -1,4 +1,8 @@
1
- import type { TranscriptionModelV3, SharedV3Warning } from '@ai-sdk/provider';
1
+ import type {
2
+ TranscriptionModelV3,
3
+ SharedV3Warning,
4
+ SharedV3ProviderMetadata,
5
+ } from '@ai-sdk/provider';
2
6
  import {
3
7
  combineHeaders,
4
8
  createJsonResponseHandler,
@@ -32,8 +36,12 @@ const assemblyaiTranscriptionModelOptionsSchema = z.object({
32
36
  */
33
37
  autoHighlights: z.boolean().nullish(),
34
38
  /**
35
- * Boost parameter for the transcription.
39
+ * Boost parameter for word boost (used with `wordBoost`).
36
40
  * Allowed values: 'low', 'default', 'high'.
41
+ *
42
+ * @deprecated Only applies to the deprecated `wordBoost` option. Use
43
+ * `keytermsPrompt` instead, which works with the recommended `universal-*`
44
+ * models.
37
45
  */
38
46
  boostParam: z.string().nullish(),
39
47
  /**
@@ -59,6 +67,11 @@ const assemblyaiTranscriptionModelOptionsSchema = z.object({
59
67
  * Whether to include filler words (um, uh, etc.) in the transcription.
60
68
  */
61
69
  disfluencies: z.boolean().nullish(),
70
+ /**
71
+ * Enable a domain-specific model to improve accuracy for specialized
72
+ * terminology. Currently supports `'medical-v1'` (Medical Mode).
73
+ */
74
+ domain: z.string().nullish(),
62
75
  /**
63
76
  * Whether to enable entity detection.
64
77
  */
@@ -75,6 +88,13 @@ const assemblyaiTranscriptionModelOptionsSchema = z.object({
75
88
  * Whether to enable IAB categories detection.
76
89
  */
77
90
  iabCategories: z.boolean().nullish(),
91
+ /**
92
+ * Domain-specific keyterms to boost recognition for (max 6 words per phrase).
93
+ * Replaces `wordBoost` for newer models: supported by `universal-3-pro` /
94
+ * `universal-3-5-pro` and `slam-1` (and `universal-2` when metaphone is
95
+ * enabled for the account).
96
+ */
97
+ keytermsPrompt: z.array(z.string()).nullish(),
78
98
  /**
79
99
  * Language code for the transcription.
80
100
  */
@@ -87,10 +107,30 @@ const assemblyaiTranscriptionModelOptionsSchema = z.object({
87
107
  * Whether to enable language detection.
88
108
  */
89
109
  languageDetection: z.boolean().nullish(),
110
+ /**
111
+ * Options for automatic language detection.
112
+ */
113
+ languageDetectionOptions: z
114
+ .object({
115
+ /** List of languages expected in the audio file. */
116
+ expectedLanguages: z.array(z.string()).nullish(),
117
+ /** Fallback language if the detected language is not expected. */
118
+ fallbackLanguage: z.string().nullish(),
119
+ /** Whether code switching should be detected. */
120
+ codeSwitching: z.boolean().nullish(),
121
+ /** Confidence threshold for code switching detection (0-1). */
122
+ codeSwitchingConfidenceThreshold: z.number().min(0).max(1).nullish(),
123
+ })
124
+ .nullish(),
90
125
  /**
91
126
  * Whether to process audio as multichannel.
92
127
  */
93
128
  multichannel: z.boolean().nullish(),
129
+ /**
130
+ * Provide natural-language context (up to 1,500 words) to steer the model.
131
+ * Only supported by `universal-3-pro` / `universal-3-5-pro` and `slam-1`.
132
+ */
133
+ prompt: z.string().nullish(),
94
134
  /**
95
135
  * Whether to add punctuation to the transcription.
96
136
  */
@@ -103,6 +143,17 @@ const assemblyaiTranscriptionModelOptionsSchema = z.object({
103
143
  * Whether to redact PII in the audio file.
104
144
  */
105
145
  redactPiiAudio: z.boolean().nullish(),
146
+ /**
147
+ * Options for PII-redacted audio files. Requires `redactPiiAudio`.
148
+ */
149
+ redactPiiAudioOptions: z
150
+ .object({
151
+ /** Return redacted audio even for files without detected speech. */
152
+ returnRedactedNoSpeechAudio: z.boolean().nullish(),
153
+ /** Redaction method; set to `'silence'` to replace PII with silence. */
154
+ overrideAudioRedactionMethod: z.enum(['silence']).nullish(),
155
+ })
156
+ .nullish(),
106
157
  /**
107
158
  * Audio format for PII redaction.
108
159
  */
@@ -111,10 +162,26 @@ const assemblyaiTranscriptionModelOptionsSchema = z.object({
111
162
  * List of PII types to redact.
112
163
  */
113
164
  redactPiiPolicies: z.array(z.string()).nullish(),
165
+ /**
166
+ * Return the original unredacted transcript alongside the redacted one.
167
+ * Requires `redactPii`.
168
+ */
169
+ redactPiiReturnUnredacted: z.boolean().nullish(),
114
170
  /**
115
171
  * Substitution method for redacted PII.
116
172
  */
117
173
  redactPiiSub: z.string().nullish(),
174
+ /**
175
+ * Map of user-defined labels to exact terms to redact, e.g.
176
+ * `{ INTERNAL_TOOL: ['Bearclaw'] }`. Applied on top of standard PII redaction
177
+ * using `redactPiiSub`. Requires `redactPii`.
178
+ */
179
+ redactStaticEntities: z.record(z.string(), z.array(z.string())).nullish(),
180
+ /**
181
+ * Remove inline annotations from rich transcripts. `'all'` removes all inline
182
+ * annotations; `'speaker'` removes only speaker cues. Universal-3 Pro models.
183
+ */
184
+ removeAudioTags: z.enum(['all', 'speaker']).nullish(),
118
185
  /**
119
186
  * Whether to enable sentiment analysis.
120
187
  */
@@ -123,6 +190,17 @@ const assemblyaiTranscriptionModelOptionsSchema = z.object({
123
190
  * Whether to identify different speakers in the audio.
124
191
  */
125
192
  speakerLabels: z.boolean().nullish(),
193
+ /**
194
+ * Options for speaker diarization, e.g. a range of possible speakers.
195
+ */
196
+ speakerOptions: z
197
+ .object({
198
+ /** Minimum number of speakers expected in the audio file. */
199
+ minSpeakersExpected: z.number().int().nullish(),
200
+ /** Maximum number of speakers expected in the audio file. */
201
+ maxSpeakersExpected: z.number().int().nullish(),
202
+ })
203
+ .nullish(),
126
204
  /**
127
205
  * Number of speakers expected in the audio.
128
206
  */
@@ -143,6 +221,10 @@ const assemblyaiTranscriptionModelOptionsSchema = z.object({
143
221
  * Type of summary to generate.
144
222
  */
145
223
  summaryType: z.string().nullish(),
224
+ /**
225
+ * Sampling temperature (0-1) controlling randomness. Universal-3 Pro models.
226
+ */
227
+ temperature: z.number().min(0).max(1).nullish(),
146
228
  /**
147
229
  * Name of the authentication header for webhook requests.
148
230
  */
@@ -157,6 +239,10 @@ const assemblyaiTranscriptionModelOptionsSchema = z.object({
157
239
  webhookUrl: z.string().nullish(),
158
240
  /**
159
241
  * List of words to boost recognition for.
242
+ *
243
+ * @deprecated `wordBoost` is rejected by `universal-3-pro` /
244
+ * `universal-3-5-pro` and `slam-1` (it only works on `universal-2`/`best`).
245
+ * Use `keytermsPrompt` instead.
160
246
  */
161
247
  wordBoost: z.array(z.string()).nullish(),
162
248
  });
@@ -200,9 +286,41 @@ export class AssemblyAITranscriptionModel implements TranscriptionModelV3 {
200
286
  schema: assemblyaiTranscriptionModelOptionsSchema,
201
287
  });
202
288
 
203
- const body: Omit<AssemblyAITranscriptionAPITypes, 'audio_url'> = {
204
- speech_model: this.modelId,
205
- };
289
+ const body: Omit<AssemblyAITranscriptionAPITypes, 'audio_url'> = {};
290
+
291
+ // The legacy `best` model is selected via the deprecated singular
292
+ // `speech_model` parameter. All other models (e.g. `universal-2`,
293
+ // `universal-3-pro`, `universal-3-5-pro`) are only accessible via the
294
+ // `speech_models` array and are rejected by `speech_model`.
295
+ // See https://www.assemblyai.com/docs/pre-recorded-audio/select-the-speech-model
296
+ if (this.modelId === 'best') {
297
+ body.speech_model = this.modelId as 'best';
298
+ warnings.push({
299
+ type: 'other',
300
+ message:
301
+ "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",
302
+ });
303
+ } else {
304
+ body.speech_models = [this.modelId];
305
+
306
+ // Forward-looking nudge: universal-3-5-pro is AssemblyAI's latest
307
+ // flagship and is set to replace universal-3-pro. Not a deprecation —
308
+ // both models still work — so this is an informational warning only.
309
+ if (
310
+ this.modelId === 'universal-3-pro' ||
311
+ this.modelId === 'universal-2'
312
+ ) {
313
+ const docsUrl =
314
+ 'https://www.assemblyai.com/docs/pre-recorded-audio/select-the-speech-model';
315
+ warnings.push({
316
+ type: 'other',
317
+ message:
318
+ this.modelId === 'universal-3-pro'
319
+ ? `'universal-3-5-pro' is AssemblyAI's latest flagship model and is set to replace 'universal-3-pro'. See ${docsUrl}`
320
+ : `'universal-3-5-pro' is AssemblyAI's latest flagship model. See ${docsUrl}`,
321
+ });
322
+ }
323
+ }
206
324
 
207
325
  // Add provider-specific options
208
326
  if (assemblyaiOptions) {
@@ -252,6 +370,103 @@ export class AssemblyAITranscriptionModel implements TranscriptionModelV3 {
252
370
  assemblyaiOptions.webhookAuthHeaderValue ?? undefined;
253
371
  body.webhook_url = assemblyaiOptions.webhookUrl ?? undefined;
254
372
  body.word_boost = assemblyaiOptions.wordBoost ?? undefined;
373
+ body.keyterms_prompt = assemblyaiOptions.keytermsPrompt ?? undefined;
374
+ body.prompt = assemblyaiOptions.prompt ?? undefined;
375
+ body.temperature = assemblyaiOptions.temperature ?? undefined;
376
+ body.remove_audio_tags = assemblyaiOptions.removeAudioTags ?? undefined;
377
+ body.domain = assemblyaiOptions.domain ?? undefined;
378
+ body.redact_pii_return_unredacted =
379
+ assemblyaiOptions.redactPiiReturnUnredacted ?? undefined;
380
+ body.redact_static_entities =
381
+ assemblyaiOptions.redactStaticEntities ?? undefined;
382
+
383
+ if (assemblyaiOptions.speakerOptions) {
384
+ body.speaker_options = {
385
+ min_speakers_expected:
386
+ assemblyaiOptions.speakerOptions.minSpeakersExpected ?? undefined,
387
+ max_speakers_expected:
388
+ assemblyaiOptions.speakerOptions.maxSpeakersExpected ?? undefined,
389
+ };
390
+ }
391
+
392
+ if (assemblyaiOptions.languageDetectionOptions) {
393
+ body.language_detection_options = {
394
+ expected_languages:
395
+ assemblyaiOptions.languageDetectionOptions.expectedLanguages ??
396
+ undefined,
397
+ fallback_language:
398
+ assemblyaiOptions.languageDetectionOptions.fallbackLanguage ??
399
+ undefined,
400
+ code_switching:
401
+ assemblyaiOptions.languageDetectionOptions.codeSwitching ??
402
+ undefined,
403
+ code_switching_confidence_threshold:
404
+ assemblyaiOptions.languageDetectionOptions
405
+ .codeSwitchingConfidenceThreshold ?? undefined,
406
+ };
407
+ }
408
+
409
+ if (assemblyaiOptions.redactPiiAudioOptions) {
410
+ body.redact_pii_audio_options = {
411
+ return_redacted_no_speech_audio:
412
+ assemblyaiOptions.redactPiiAudioOptions
413
+ .returnRedactedNoSpeechAudio ?? undefined,
414
+ override_audio_redaction_method:
415
+ assemblyaiOptions.redactPiiAudioOptions
416
+ .overrideAudioRedactionMethod ?? undefined,
417
+ };
418
+ }
419
+
420
+ const deprecatedBoostOptions: string[] = [];
421
+ if (assemblyaiOptions.wordBoost != null) {
422
+ deprecatedBoostOptions.push('wordBoost');
423
+ }
424
+ if (assemblyaiOptions.boostParam != null) {
425
+ deprecatedBoostOptions.push('boostParam');
426
+ }
427
+ if (deprecatedBoostOptions.length > 0) {
428
+ warnings.push({
429
+ type: 'other',
430
+ message: `${deprecatedBoostOptions.join(', ')} ${
431
+ deprecatedBoostOptions.length > 1 ? 'are' : 'is'
432
+ } deprecated and rejected by 'universal-3-pro' / 'universal-3-5-pro' and 'slam-1'. Use 'keytermsPrompt' instead.`,
433
+ });
434
+ }
435
+
436
+ // The following options only take effect alongside a prerequisite
437
+ // option; without it AssemblyAI either rejects the request (400) or
438
+ // silently ignores the option. Warn rather than mutate user input.
439
+ if (
440
+ (assemblyaiOptions.redactPiiReturnUnredacted != null ||
441
+ assemblyaiOptions.redactStaticEntities != null) &&
442
+ !assemblyaiOptions.redactPii
443
+ ) {
444
+ warnings.push({
445
+ type: 'other',
446
+ message:
447
+ "'redactPiiReturnUnredacted' and 'redactStaticEntities' require 'redactPii' to be enabled; AssemblyAI rejects the request otherwise.",
448
+ });
449
+ }
450
+ if (
451
+ assemblyaiOptions.redactPiiAudioOptions != null &&
452
+ !assemblyaiOptions.redactPiiAudio
453
+ ) {
454
+ warnings.push({
455
+ type: 'other',
456
+ message:
457
+ "'redactPiiAudioOptions' only applies when 'redactPiiAudio' is enabled; it is otherwise ignored.",
458
+ });
459
+ }
460
+ if (
461
+ assemblyaiOptions.languageCode != null &&
462
+ assemblyaiOptions.languageDetection
463
+ ) {
464
+ warnings.push({
465
+ type: 'other',
466
+ message:
467
+ "'languageDetection' cannot be combined with an explicit 'languageCode'; AssemblyAI rejects requests that set both.",
468
+ });
469
+ }
255
470
  }
256
471
 
257
472
  return {
@@ -271,17 +486,22 @@ export class AssemblyAITranscriptionModel implements TranscriptionModelV3 {
271
486
  abortSignal?: AbortSignal,
272
487
  ): Promise<{
273
488
  transcript: z.infer<typeof assemblyaiTranscriptionResponseSchema>;
489
+ rawTranscript: unknown;
274
490
  responseHeaders: Record<string, string>;
275
491
  }> {
276
492
  const pollingInterval =
277
493
  this.config.pollingInterval ?? this.POLLING_INTERVAL_MS;
278
494
 
495
+ // Honor a caller-provided fetch (proxy, auth injection, tests) for the
496
+ // polling GETs, matching the upload/submit calls that use config.fetch.
497
+ const fetchImpl = this.config.fetch ?? globalThis.fetch;
498
+
279
499
  while (true) {
280
500
  if (abortSignal?.aborted) {
281
501
  throw new Error('Transcription request was aborted');
282
502
  }
283
503
 
284
- const response = await fetch(
504
+ const response = await fetchImpl(
285
505
  this.config.url({
286
506
  path: `/v2/transcript/${transcriptId}`,
287
507
  modelId: this.modelId,
@@ -307,13 +527,14 @@ export class AssemblyAITranscriptionModel implements TranscriptionModelV3 {
307
527
  });
308
528
  }
309
529
 
310
- const transcript = assemblyaiTranscriptionResponseSchema.parse(
311
- await response.json(),
312
- );
530
+ const rawTranscript = await response.json();
531
+ const transcript =
532
+ assemblyaiTranscriptionResponseSchema.parse(rawTranscript);
313
533
 
314
534
  if (transcript.status === 'completed') {
315
535
  return {
316
536
  transcript,
537
+ rawTranscript,
317
538
  responseHeaders: extractResponseHeaders(response),
318
539
  };
319
540
  }
@@ -374,29 +595,70 @@ export class AssemblyAITranscriptionModel implements TranscriptionModelV3 {
374
595
  fetch: this.config.fetch,
375
596
  });
376
597
 
377
- const { transcript, responseHeaders } = await this.waitForCompletion(
378
- submitResponse.id,
379
- options.headers,
380
- options.abortSignal,
381
- );
598
+ const { transcript, rawTranscript, responseHeaders } =
599
+ await this.waitForCompletion(
600
+ submitResponse.id,
601
+ options.headers,
602
+ options.abortSignal,
603
+ );
604
+
605
+ // Surface diarization and audio-intelligence results that the AI SDK's
606
+ // `segments` shape can't represent, keyed under `assemblyai`. Presence is
607
+ // gated on the parsed transcript, but values are taken from the raw
608
+ // response so no fields are stripped by the schema.
609
+ //
610
+ // NOTE: timings inside these objects (e.g. `utterances[].start`) are in
611
+ // milliseconds, matching the AssemblyAI API — unlike the top-level
612
+ // `segments`, whose `startSecond`/`endSecond` are in seconds.
613
+ const raw = (rawTranscript ?? {}) as Record<string, unknown>;
614
+ const assemblyaiMetadata: Record<string, unknown> = {};
615
+ if (transcript.utterances != null) {
616
+ assemblyaiMetadata.utterances = raw.utterances;
617
+ }
618
+ if (transcript.sentiment_analysis_results != null) {
619
+ assemblyaiMetadata.sentimentAnalysisResults =
620
+ raw.sentiment_analysis_results;
621
+ }
622
+ if (transcript.entities != null) {
623
+ assemblyaiMetadata.entities = raw.entities;
624
+ }
625
+ if (transcript.content_safety_labels != null) {
626
+ assemblyaiMetadata.contentSafetyLabels = raw.content_safety_labels;
627
+ }
628
+ if (transcript.iab_categories_result != null) {
629
+ assemblyaiMetadata.iabCategoriesResult = raw.iab_categories_result;
630
+ }
631
+ if (transcript.auto_highlights_result != null) {
632
+ assemblyaiMetadata.autoHighlightsResult = raw.auto_highlights_result;
633
+ }
634
+
635
+ const lastWordEndMs = transcript.words?.at(-1)?.end;
382
636
 
383
637
  return {
384
638
  text: transcript.text ?? '',
639
+ // AssemblyAI returns word timings in milliseconds; the AI SDK reports
640
+ // segment timings in seconds.
385
641
  segments:
386
642
  transcript.words?.map(word => ({
387
643
  text: word.text,
388
- startSecond: word.start,
389
- endSecond: word.end,
644
+ startSecond: word.start / 1000,
645
+ endSecond: word.end / 1000,
390
646
  })) ?? [],
391
647
  language: transcript.language_code ?? undefined,
392
648
  durationInSeconds:
393
- transcript.audio_duration ?? transcript.words?.at(-1)?.end ?? undefined,
649
+ transcript.audio_duration ??
650
+ (lastWordEndMs != null ? lastWordEndMs / 1000 : undefined),
394
651
  warnings,
652
+ ...(Object.keys(assemblyaiMetadata).length > 0 && {
653
+ providerMetadata: {
654
+ assemblyai: assemblyaiMetadata,
655
+ } as SharedV3ProviderMetadata,
656
+ }),
395
657
  response: {
396
658
  timestamp: currentDate,
397
659
  modelId: this.modelId,
398
660
  headers: responseHeaders, // Headers from final GET request
399
- body: transcript, // Raw response from final GET request
661
+ body: rawTranscript, // Full raw response from final GET request
400
662
  },
401
663
  };
402
664
  }
@@ -411,20 +673,65 @@ const assemblyaiSubmitResponseSchema = z.object({
411
673
  status: z.enum(['queued', 'processing', 'completed', 'error']),
412
674
  });
413
675
 
676
+ const assemblyaiWordSchema = z.object({
677
+ start: z.number(),
678
+ end: z.number(),
679
+ text: z.string(),
680
+ confidence: z.number().nullish(),
681
+ // Speaker label (e.g. 'A', 'B') when speaker diarization is enabled, else null.
682
+ speaker: z.string().nullish(),
683
+ channel: z.string().nullish(),
684
+ });
685
+
414
686
  const assemblyaiTranscriptionResponseSchema = z.object({
415
687
  id: z.string(),
416
688
  status: z.enum(['queued', 'processing', 'completed', 'error']),
417
689
  text: z.string().nullish(),
418
690
  language_code: z.string().nullish(),
419
- words: z
691
+ speech_model_used: z.string().nullish(),
692
+ words: z.array(assemblyaiWordSchema).nullish(),
693
+ // Speaker-diarized utterances (present when `speaker_labels` is enabled).
694
+ utterances: z
420
695
  .array(
421
696
  z.object({
422
697
  start: z.number(),
423
698
  end: z.number(),
424
699
  text: z.string(),
700
+ confidence: z.number().nullish(),
701
+ speaker: z.string().nullish(),
702
+ channel: z.string().nullish(),
703
+ words: z.array(assemblyaiWordSchema).nullish(),
704
+ }),
705
+ )
706
+ .nullish(),
707
+ // Audio-intelligence results, present only when the matching feature is
708
+ // enabled. Kept intentionally permissive (the full structures are also
709
+ // available on the raw `response.body`).
710
+ sentiment_analysis_results: z
711
+ .array(
712
+ z.object({
713
+ text: z.string(),
714
+ start: z.number().nullish(),
715
+ end: z.number().nullish(),
716
+ sentiment: z.string(),
717
+ confidence: z.number().nullish(),
718
+ speaker: z.string().nullish(),
719
+ }),
720
+ )
721
+ .nullish(),
722
+ entities: z
723
+ .array(
724
+ z.object({
725
+ entity_type: z.string(),
726
+ text: z.string(),
727
+ start: z.number().nullish(),
728
+ end: z.number().nullish(),
425
729
  }),
426
730
  )
427
731
  .nullish(),
732
+ content_safety_labels: z.record(z.string(), z.any()).nullish(),
733
+ iab_categories_result: z.record(z.string(), z.any()).nullish(),
734
+ auto_highlights_result: z.record(z.string(), z.any()).nullish(),
428
735
  audio_duration: z.number().nullish(),
429
736
  error: z.string().nullish(),
430
737
  });
@@ -1 +1,15 @@
1
- export type AssemblyAITranscriptionModelId = 'best' | 'nano';
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 & {});