assemblyai 4.4.0 → 4.4.2-beta.0

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.
package/dist/bun.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import WebSocket from '#ws';
1
+ import ws from 'ws';
2
2
 
3
3
  /**
4
4
  * Base class for services that communicate with the API.
@@ -19,6 +19,7 @@ class BaseService {
19
19
  "Content-Type": "application/json",
20
20
  ...init.headers,
21
21
  };
22
+ init.cache = "no-store";
22
23
  if (!input.startsWith("http"))
23
24
  input = this.params.baseUrl + input;
24
25
  const response = await fetch(input, init);
@@ -88,6 +89,8 @@ const { WritableStream } = typeof window !== "undefined"
88
89
  ? global
89
90
  : globalThis;
90
91
 
92
+ const factory = (url, params) => new ws(url, params);
93
+
91
94
  var RealtimeErrorType;
92
95
  (function (RealtimeErrorType) {
93
96
  RealtimeErrorType[RealtimeErrorType["BadSampleRate"] = 4000] = "BadSampleRate";
@@ -140,7 +143,6 @@ class RealtimeTranscriber {
140
143
  this.wordBoost = params.wordBoost;
141
144
  this.encoding = params.encoding;
142
145
  this.endUtteranceSilenceThreshold = params.endUtteranceSilenceThreshold;
143
- this.enableExtraSessionInformation = params.enableExtraSessionInformation;
144
146
  this.disablePartialTranscripts = params.disablePartialTranscripts;
145
147
  if ("token" in params && params.token)
146
148
  this.token = params.token;
@@ -166,9 +168,7 @@ class RealtimeTranscriber {
166
168
  if (this.encoding) {
167
169
  searchParams.set("encoding", this.encoding);
168
170
  }
169
- if (this.enableExtraSessionInformation) {
170
- searchParams.set("enable_extra_session_information", this.enableExtraSessionInformation.toString());
171
- }
171
+ searchParams.set("enable_extra_session_information", "true");
172
172
  if (this.disablePartialTranscripts) {
173
173
  searchParams.set("disable_partial_transcripts", this.disablePartialTranscripts.toString());
174
174
  }
@@ -186,10 +186,10 @@ class RealtimeTranscriber {
186
186
  }
187
187
  const url = this.connectionUrl();
188
188
  if (this.token) {
189
- this.socket = new WebSocket(url.toString());
189
+ this.socket = factory(url.toString());
190
190
  }
191
191
  else {
192
- this.socket = new WebSocket(url.toString(), {
192
+ this.socket = factory(url.toString(), {
193
193
  headers: { Authorization: this.apiKey },
194
194
  });
195
195
  }
@@ -282,14 +282,14 @@ class RealtimeTranscriber {
282
282
  this.send(`{"end_utterance_silence_threshold":${threshold}}`);
283
283
  }
284
284
  send(data) {
285
- if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
285
+ if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
286
286
  throw new Error("Socket is not open for communication");
287
287
  }
288
288
  this.socket.send(data);
289
289
  }
290
290
  async close(waitForSessionTermination = true) {
291
291
  if (this.socket) {
292
- if (this.socket.readyState === WebSocket.OPEN) {
292
+ if (this.socket.readyState === this.socket.OPEN) {
293
293
  if (waitForSessionTermination) {
294
294
  const sessionTerminatedPromise = new Promise((resolve) => {
295
295
  this.sessionTerminatedResolve = resolve;
@@ -301,7 +301,7 @@ class RealtimeTranscriber {
301
301
  this.socket.send(terminateSessionMessage);
302
302
  }
303
303
  }
304
- if ("removeAllListeners" in this.socket)
304
+ if (this.socket?.removeAllListeners)
305
305
  this.socket.removeAllListeners();
306
306
  this.socket.close();
307
307
  }
@@ -352,6 +352,8 @@ function getPath(path) {
352
352
  return null;
353
353
  if (path.startsWith("https"))
354
354
  return null;
355
+ if (path.startsWith("data:"))
356
+ return null;
355
357
  if (path.startsWith("file://"))
356
358
  return path.substring(7);
357
359
  if (path.startsWith("file:"))
@@ -393,8 +395,13 @@ class TranscriptService extends BaseService {
393
395
  audioUrl = await this.files.upload(path);
394
396
  }
395
397
  else {
396
- // audio is not a local path, assume it's a URL
397
- audioUrl = audio;
398
+ if (audio.startsWith("data:")) {
399
+ audioUrl = await this.files.upload(audio);
400
+ }
401
+ else {
402
+ // audio is not a local path, and not a data-URI, assume it's a normal URL
403
+ audioUrl = audio;
404
+ }
398
405
  }
399
406
  }
400
407
  else {
@@ -571,8 +578,14 @@ class FileService extends BaseService {
571
578
  */
572
579
  async upload(input) {
573
580
  let fileData;
574
- if (typeof input === "string")
575
- fileData = await readFile(input);
581
+ if (typeof input === "string") {
582
+ if (input.startsWith("data:")) {
583
+ fileData = dataUrlToBlob(input);
584
+ }
585
+ else {
586
+ fileData = await readFile(input);
587
+ }
588
+ }
576
589
  else
577
590
  fileData = input;
578
591
  const data = await this.fetchJson("/v2/upload", {
@@ -586,6 +599,17 @@ class FileService extends BaseService {
586
599
  return data.upload_url;
587
600
  }
588
601
  }
602
+ function dataUrlToBlob(dataUrl) {
603
+ const arr = dataUrl.split(",");
604
+ const mime = arr[0].match(/:(.*?);/)[1];
605
+ const bstr = atob(arr[1]);
606
+ let n = bstr.length;
607
+ const u8arr = new Uint8Array(n);
608
+ while (n--) {
609
+ u8arr[n] = bstr.charCodeAt(n);
610
+ }
611
+ return new Blob([u8arr], { type: mime });
612
+ }
589
613
 
590
614
  const defaultBaseUrl = "https://api.assemblyai.com";
591
615
  class AssemblyAI {
package/dist/deno.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import WebSocket from '#ws';
1
+ import ws from 'ws';
2
2
 
3
3
  /**
4
4
  * Base class for services that communicate with the API.
@@ -19,6 +19,7 @@ class BaseService {
19
19
  "Content-Type": "application/json",
20
20
  ...init.headers,
21
21
  };
22
+ init.cache = "no-store";
22
23
  if (!input.startsWith("http"))
23
24
  input = this.params.baseUrl + input;
24
25
  const response = await fetch(input, init);
@@ -88,6 +89,8 @@ const { WritableStream } = typeof window !== "undefined"
88
89
  ? global
89
90
  : globalThis;
90
91
 
92
+ const factory = (url, params) => new ws(url, params);
93
+
91
94
  var RealtimeErrorType;
92
95
  (function (RealtimeErrorType) {
93
96
  RealtimeErrorType[RealtimeErrorType["BadSampleRate"] = 4000] = "BadSampleRate";
@@ -140,7 +143,6 @@ class RealtimeTranscriber {
140
143
  this.wordBoost = params.wordBoost;
141
144
  this.encoding = params.encoding;
142
145
  this.endUtteranceSilenceThreshold = params.endUtteranceSilenceThreshold;
143
- this.enableExtraSessionInformation = params.enableExtraSessionInformation;
144
146
  this.disablePartialTranscripts = params.disablePartialTranscripts;
145
147
  if ("token" in params && params.token)
146
148
  this.token = params.token;
@@ -166,9 +168,7 @@ class RealtimeTranscriber {
166
168
  if (this.encoding) {
167
169
  searchParams.set("encoding", this.encoding);
168
170
  }
169
- if (this.enableExtraSessionInformation) {
170
- searchParams.set("enable_extra_session_information", this.enableExtraSessionInformation.toString());
171
- }
171
+ searchParams.set("enable_extra_session_information", "true");
172
172
  if (this.disablePartialTranscripts) {
173
173
  searchParams.set("disable_partial_transcripts", this.disablePartialTranscripts.toString());
174
174
  }
@@ -186,10 +186,10 @@ class RealtimeTranscriber {
186
186
  }
187
187
  const url = this.connectionUrl();
188
188
  if (this.token) {
189
- this.socket = new WebSocket(url.toString());
189
+ this.socket = factory(url.toString());
190
190
  }
191
191
  else {
192
- this.socket = new WebSocket(url.toString(), {
192
+ this.socket = factory(url.toString(), {
193
193
  headers: { Authorization: this.apiKey },
194
194
  });
195
195
  }
@@ -282,14 +282,14 @@ class RealtimeTranscriber {
282
282
  this.send(`{"end_utterance_silence_threshold":${threshold}}`);
283
283
  }
284
284
  send(data) {
285
- if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
285
+ if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
286
286
  throw new Error("Socket is not open for communication");
287
287
  }
288
288
  this.socket.send(data);
289
289
  }
290
290
  async close(waitForSessionTermination = true) {
291
291
  if (this.socket) {
292
- if (this.socket.readyState === WebSocket.OPEN) {
292
+ if (this.socket.readyState === this.socket.OPEN) {
293
293
  if (waitForSessionTermination) {
294
294
  const sessionTerminatedPromise = new Promise((resolve) => {
295
295
  this.sessionTerminatedResolve = resolve;
@@ -301,7 +301,7 @@ class RealtimeTranscriber {
301
301
  this.socket.send(terminateSessionMessage);
302
302
  }
303
303
  }
304
- if ("removeAllListeners" in this.socket)
304
+ if (this.socket?.removeAllListeners)
305
305
  this.socket.removeAllListeners();
306
306
  this.socket.close();
307
307
  }
@@ -352,6 +352,8 @@ function getPath(path) {
352
352
  return null;
353
353
  if (path.startsWith("https"))
354
354
  return null;
355
+ if (path.startsWith("data:"))
356
+ return null;
355
357
  if (path.startsWith("file://"))
356
358
  return path.substring(7);
357
359
  if (path.startsWith("file:"))
@@ -393,8 +395,13 @@ class TranscriptService extends BaseService {
393
395
  audioUrl = await this.files.upload(path);
394
396
  }
395
397
  else {
396
- // audio is not a local path, assume it's a URL
397
- audioUrl = audio;
398
+ if (audio.startsWith("data:")) {
399
+ audioUrl = await this.files.upload(audio);
400
+ }
401
+ else {
402
+ // audio is not a local path, and not a data-URI, assume it's a normal URL
403
+ audioUrl = audio;
404
+ }
398
405
  }
399
406
  }
400
407
  else {
@@ -571,8 +578,14 @@ class FileService extends BaseService {
571
578
  */
572
579
  async upload(input) {
573
580
  let fileData;
574
- if (typeof input === "string")
575
- fileData = await readFile(input);
581
+ if (typeof input === "string") {
582
+ if (input.startsWith("data:")) {
583
+ fileData = dataUrlToBlob(input);
584
+ }
585
+ else {
586
+ fileData = await readFile(input);
587
+ }
588
+ }
576
589
  else
577
590
  fileData = input;
578
591
  const data = await this.fetchJson("/v2/upload", {
@@ -586,6 +599,17 @@ class FileService extends BaseService {
586
599
  return data.upload_url;
587
600
  }
588
601
  }
602
+ function dataUrlToBlob(dataUrl) {
603
+ const arr = dataUrl.split(",");
604
+ const mime = arr[0].match(/:(.*?);/)[1];
605
+ const bstr = atob(arr[1]);
606
+ let n = bstr.length;
607
+ const u8arr = new Uint8Array(n);
608
+ while (n--) {
609
+ u8arr[n] = bstr.charCodeAt(n);
610
+ }
611
+ return new Blob([u8arr], { type: mime });
612
+ }
589
613
 
590
614
  const defaultBaseUrl = "https://api.assemblyai.com";
591
615
  class AssemblyAI {
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var WebSocket = require('#ws');
3
+ var ws = require('ws');
4
4
 
5
5
  /******************************************************************************
6
6
  Copyright (c) Microsoft Corporation.
@@ -63,6 +63,7 @@ class BaseService {
63
63
  init = init !== null && init !== void 0 ? init : {};
64
64
  init.headers = (_a = init.headers) !== null && _a !== void 0 ? _a : {};
65
65
  init.headers = Object.assign({ Authorization: this.params.apiKey, "Content-Type": "application/json" }, init.headers);
66
+ init.cache = "no-store";
66
67
  if (!input.startsWith("http"))
67
68
  input = this.params.baseUrl + input;
68
69
  const response = yield fetch(input, init);
@@ -135,6 +136,8 @@ const { WritableStream } = typeof window !== "undefined"
135
136
  ? global
136
137
  : globalThis;
137
138
 
139
+ const factory = (url, params) => new ws(url, params);
140
+
138
141
  var RealtimeErrorType;
139
142
  (function (RealtimeErrorType) {
140
143
  RealtimeErrorType[RealtimeErrorType["BadSampleRate"] = 4000] = "BadSampleRate";
@@ -188,7 +191,6 @@ class RealtimeTranscriber {
188
191
  this.wordBoost = params.wordBoost;
189
192
  this.encoding = params.encoding;
190
193
  this.endUtteranceSilenceThreshold = params.endUtteranceSilenceThreshold;
191
- this.enableExtraSessionInformation = params.enableExtraSessionInformation;
192
194
  this.disablePartialTranscripts = params.disablePartialTranscripts;
193
195
  if ("token" in params && params.token)
194
196
  this.token = params.token;
@@ -214,9 +216,7 @@ class RealtimeTranscriber {
214
216
  if (this.encoding) {
215
217
  searchParams.set("encoding", this.encoding);
216
218
  }
217
- if (this.enableExtraSessionInformation) {
218
- searchParams.set("enable_extra_session_information", this.enableExtraSessionInformation.toString());
219
- }
219
+ searchParams.set("enable_extra_session_information", "true");
220
220
  if (this.disablePartialTranscripts) {
221
221
  searchParams.set("disable_partial_transcripts", this.disablePartialTranscripts.toString());
222
222
  }
@@ -234,10 +234,10 @@ class RealtimeTranscriber {
234
234
  }
235
235
  const url = this.connectionUrl();
236
236
  if (this.token) {
237
- this.socket = new WebSocket(url.toString());
237
+ this.socket = factory(url.toString());
238
238
  }
239
239
  else {
240
- this.socket = new WebSocket(url.toString(), {
240
+ this.socket = factory(url.toString(), {
241
241
  headers: { Authorization: this.apiKey },
242
242
  });
243
243
  }
@@ -333,15 +333,16 @@ class RealtimeTranscriber {
333
333
  this.send(`{"end_utterance_silence_threshold":${threshold}}`);
334
334
  }
335
335
  send(data) {
336
- if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
336
+ if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
337
337
  throw new Error("Socket is not open for communication");
338
338
  }
339
339
  this.socket.send(data);
340
340
  }
341
341
  close() {
342
342
  return __awaiter(this, arguments, void 0, function* (waitForSessionTermination = true) {
343
+ var _a;
343
344
  if (this.socket) {
344
- if (this.socket.readyState === WebSocket.OPEN) {
345
+ if (this.socket.readyState === this.socket.OPEN) {
345
346
  if (waitForSessionTermination) {
346
347
  const sessionTerminatedPromise = new Promise((resolve) => {
347
348
  this.sessionTerminatedResolve = resolve;
@@ -353,7 +354,7 @@ class RealtimeTranscriber {
353
354
  this.socket.send(terminateSessionMessage);
354
355
  }
355
356
  }
356
- if ("removeAllListeners" in this.socket)
357
+ if ((_a = this.socket) === null || _a === void 0 ? void 0 : _a.removeAllListeners)
357
358
  this.socket.removeAllListeners();
358
359
  this.socket.close();
359
360
  }
@@ -407,6 +408,8 @@ function getPath(path) {
407
408
  return null;
408
409
  if (path.startsWith("https"))
409
410
  return null;
411
+ if (path.startsWith("data:"))
412
+ return null;
410
413
  if (path.startsWith("file://"))
411
414
  return path.substring(7);
412
415
  if (path.startsWith("file:"))
@@ -451,8 +454,13 @@ class TranscriptService extends BaseService {
451
454
  audioUrl = yield this.files.upload(path);
452
455
  }
453
456
  else {
454
- // audio is not a local path, assume it's a URL
455
- audioUrl = audio;
457
+ if (audio.startsWith("data:")) {
458
+ audioUrl = yield this.files.upload(audio);
459
+ }
460
+ else {
461
+ // audio is not a local path, and not a data-URI, assume it's a normal URL
462
+ audioUrl = audio;
463
+ }
456
464
  }
457
465
  }
458
466
  else {
@@ -650,8 +658,14 @@ class FileService extends BaseService {
650
658
  upload(input) {
651
659
  return __awaiter(this, void 0, void 0, function* () {
652
660
  let fileData;
653
- if (typeof input === "string")
654
- fileData = yield readFile();
661
+ if (typeof input === "string") {
662
+ if (input.startsWith("data:")) {
663
+ fileData = dataUrlToBlob(input);
664
+ }
665
+ else {
666
+ fileData = yield readFile();
667
+ }
668
+ }
655
669
  else
656
670
  fileData = input;
657
671
  const data = yield this.fetchJson("/v2/upload", {
@@ -666,6 +680,17 @@ class FileService extends BaseService {
666
680
  });
667
681
  }
668
682
  }
683
+ function dataUrlToBlob(dataUrl) {
684
+ const arr = dataUrl.split(",");
685
+ const mime = arr[0].match(/:(.*?);/)[1];
686
+ const bstr = atob(arr[1]);
687
+ let n = bstr.length;
688
+ const u8arr = new Uint8Array(n);
689
+ while (n--) {
690
+ u8arr[n] = bstr.charCodeAt(n);
691
+ }
692
+ return new Blob([u8arr], { type: mime });
693
+ }
669
694
 
670
695
  const defaultBaseUrl = "https://api.assemblyai.com";
671
696
  class AssemblyAI {
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import WebSocket from '#ws';
1
+ import ws from 'ws';
2
2
 
3
3
  /******************************************************************************
4
4
  Copyright (c) Microsoft Corporation.
@@ -61,6 +61,7 @@ class BaseService {
61
61
  init = init !== null && init !== void 0 ? init : {};
62
62
  init.headers = (_a = init.headers) !== null && _a !== void 0 ? _a : {};
63
63
  init.headers = Object.assign({ Authorization: this.params.apiKey, "Content-Type": "application/json" }, init.headers);
64
+ init.cache = "no-store";
64
65
  if (!input.startsWith("http"))
65
66
  input = this.params.baseUrl + input;
66
67
  const response = yield fetch(input, init);
@@ -133,6 +134,8 @@ const { WritableStream } = typeof window !== "undefined"
133
134
  ? global
134
135
  : globalThis;
135
136
 
137
+ const factory = (url, params) => new ws(url, params);
138
+
136
139
  var RealtimeErrorType;
137
140
  (function (RealtimeErrorType) {
138
141
  RealtimeErrorType[RealtimeErrorType["BadSampleRate"] = 4000] = "BadSampleRate";
@@ -186,7 +189,6 @@ class RealtimeTranscriber {
186
189
  this.wordBoost = params.wordBoost;
187
190
  this.encoding = params.encoding;
188
191
  this.endUtteranceSilenceThreshold = params.endUtteranceSilenceThreshold;
189
- this.enableExtraSessionInformation = params.enableExtraSessionInformation;
190
192
  this.disablePartialTranscripts = params.disablePartialTranscripts;
191
193
  if ("token" in params && params.token)
192
194
  this.token = params.token;
@@ -212,9 +214,7 @@ class RealtimeTranscriber {
212
214
  if (this.encoding) {
213
215
  searchParams.set("encoding", this.encoding);
214
216
  }
215
- if (this.enableExtraSessionInformation) {
216
- searchParams.set("enable_extra_session_information", this.enableExtraSessionInformation.toString());
217
- }
217
+ searchParams.set("enable_extra_session_information", "true");
218
218
  if (this.disablePartialTranscripts) {
219
219
  searchParams.set("disable_partial_transcripts", this.disablePartialTranscripts.toString());
220
220
  }
@@ -232,10 +232,10 @@ class RealtimeTranscriber {
232
232
  }
233
233
  const url = this.connectionUrl();
234
234
  if (this.token) {
235
- this.socket = new WebSocket(url.toString());
235
+ this.socket = factory(url.toString());
236
236
  }
237
237
  else {
238
- this.socket = new WebSocket(url.toString(), {
238
+ this.socket = factory(url.toString(), {
239
239
  headers: { Authorization: this.apiKey },
240
240
  });
241
241
  }
@@ -331,15 +331,16 @@ class RealtimeTranscriber {
331
331
  this.send(`{"end_utterance_silence_threshold":${threshold}}`);
332
332
  }
333
333
  send(data) {
334
- if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
334
+ if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
335
335
  throw new Error("Socket is not open for communication");
336
336
  }
337
337
  this.socket.send(data);
338
338
  }
339
339
  close() {
340
340
  return __awaiter(this, arguments, void 0, function* (waitForSessionTermination = true) {
341
+ var _a;
341
342
  if (this.socket) {
342
- if (this.socket.readyState === WebSocket.OPEN) {
343
+ if (this.socket.readyState === this.socket.OPEN) {
343
344
  if (waitForSessionTermination) {
344
345
  const sessionTerminatedPromise = new Promise((resolve) => {
345
346
  this.sessionTerminatedResolve = resolve;
@@ -351,7 +352,7 @@ class RealtimeTranscriber {
351
352
  this.socket.send(terminateSessionMessage);
352
353
  }
353
354
  }
354
- if ("removeAllListeners" in this.socket)
355
+ if ((_a = this.socket) === null || _a === void 0 ? void 0 : _a.removeAllListeners)
355
356
  this.socket.removeAllListeners();
356
357
  this.socket.close();
357
358
  }
@@ -405,6 +406,8 @@ function getPath(path) {
405
406
  return null;
406
407
  if (path.startsWith("https"))
407
408
  return null;
409
+ if (path.startsWith("data:"))
410
+ return null;
408
411
  if (path.startsWith("file://"))
409
412
  return path.substring(7);
410
413
  if (path.startsWith("file:"))
@@ -449,8 +452,13 @@ class TranscriptService extends BaseService {
449
452
  audioUrl = yield this.files.upload(path);
450
453
  }
451
454
  else {
452
- // audio is not a local path, assume it's a URL
453
- audioUrl = audio;
455
+ if (audio.startsWith("data:")) {
456
+ audioUrl = yield this.files.upload(audio);
457
+ }
458
+ else {
459
+ // audio is not a local path, and not a data-URI, assume it's a normal URL
460
+ audioUrl = audio;
461
+ }
454
462
  }
455
463
  }
456
464
  else {
@@ -648,8 +656,14 @@ class FileService extends BaseService {
648
656
  upload(input) {
649
657
  return __awaiter(this, void 0, void 0, function* () {
650
658
  let fileData;
651
- if (typeof input === "string")
652
- fileData = yield readFile();
659
+ if (typeof input === "string") {
660
+ if (input.startsWith("data:")) {
661
+ fileData = dataUrlToBlob(input);
662
+ }
663
+ else {
664
+ fileData = yield readFile();
665
+ }
666
+ }
653
667
  else
654
668
  fileData = input;
655
669
  const data = yield this.fetchJson("/v2/upload", {
@@ -664,6 +678,17 @@ class FileService extends BaseService {
664
678
  });
665
679
  }
666
680
  }
681
+ function dataUrlToBlob(dataUrl) {
682
+ const arr = dataUrl.split(",");
683
+ const mime = arr[0].match(/:(.*?);/)[1];
684
+ const bstr = atob(arr[1]);
685
+ let n = bstr.length;
686
+ const u8arr = new Uint8Array(n);
687
+ while (n--) {
688
+ u8arr[n] = bstr.charCodeAt(n);
689
+ }
690
+ return new Blob([u8arr], { type: mime });
691
+ }
667
692
 
668
693
  const defaultBaseUrl = "https://api.assemblyai.com";
669
694
  class AssemblyAI {