@ai-sdk/cartesia 3.0.2 → 3.0.4
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/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/dist/index.d.ts +15 -4
- package/dist/index.js +327 -10
- package/dist/index.js.map +1 -1
- package/docs/95-cartesia.mdx +58 -1
- package/package.json +2 -2
- package/src/cartesia-config.ts +6 -1
- package/src/cartesia-provider.ts +8 -0
- package/src/cartesia-transcription-model-options.ts +10 -0
- package/src/cartesia-transcription-model.ts +387 -2
- package/src/cartesia-transcription-options.ts +4 -1
|
@@ -1,19 +1,36 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
InvalidArgumentError,
|
|
3
|
+
UnsupportedFunctionalityError,
|
|
4
|
+
type Experimental_TranscriptionModelV4StreamOptions as TranscriptionModelV4StreamOptions,
|
|
5
|
+
type Experimental_TranscriptionModelV4StreamPart as TranscriptionModelV4StreamPart,
|
|
6
|
+
type SharedV4Warning,
|
|
7
|
+
type TranscriptionModelV4,
|
|
8
|
+
} from '@ai-sdk/provider';
|
|
2
9
|
import {
|
|
3
10
|
combineHeaders,
|
|
11
|
+
connectToWebSocket,
|
|
4
12
|
convertBase64ToUint8Array,
|
|
5
13
|
createJsonResponseHandler,
|
|
6
14
|
mediaTypeToExtension,
|
|
7
15
|
parseProviderOptions,
|
|
8
16
|
postFormDataToApi,
|
|
17
|
+
postJsonToApi,
|
|
18
|
+
safeParseJSON,
|
|
9
19
|
serializeModelOptions,
|
|
20
|
+
toWebSocketUrl,
|
|
21
|
+
waitForWebSocketBufferDrain,
|
|
10
22
|
WORKFLOW_SERIALIZE,
|
|
11
23
|
WORKFLOW_DESERIALIZE,
|
|
24
|
+
type WebSocketConnection,
|
|
25
|
+
type WebSocketLike,
|
|
12
26
|
} from '@ai-sdk/provider-utils';
|
|
13
27
|
import { z } from 'zod/v4';
|
|
14
28
|
import type { CartesiaConfig } from './cartesia-config';
|
|
15
29
|
import { cartesiaFailedResponseHandler } from './cartesia-error';
|
|
16
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
cartesiaTranscriptionModelOptionsSchema,
|
|
32
|
+
type CartesiaTranscriptionModelOptions,
|
|
33
|
+
} from './cartesia-transcription-model-options';
|
|
17
34
|
import type { CartesiaTranscriptionModelId } from './cartesia-transcription-options';
|
|
18
35
|
|
|
19
36
|
interface CartesiaTranscriptionModelConfig extends CartesiaConfig {
|
|
@@ -22,6 +39,17 @@ interface CartesiaTranscriptionModelConfig extends CartesiaConfig {
|
|
|
22
39
|
};
|
|
23
40
|
}
|
|
24
41
|
|
|
42
|
+
type CartesiaStreamingTranscriptionEvent = {
|
|
43
|
+
type?: string;
|
|
44
|
+
request_id?: string;
|
|
45
|
+
transcript?: string;
|
|
46
|
+
text?: string;
|
|
47
|
+
is_final?: boolean;
|
|
48
|
+
duration?: number;
|
|
49
|
+
message?: string;
|
|
50
|
+
error_code?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
25
53
|
export class CartesiaTranscriptionModel implements TranscriptionModelV4 {
|
|
26
54
|
readonly specificationVersion = 'v4';
|
|
27
55
|
|
|
@@ -62,6 +90,15 @@ export class CartesiaTranscriptionModel implements TranscriptionModelV4 {
|
|
|
62
90
|
schema: cartesiaTranscriptionModelOptionsSchema,
|
|
63
91
|
});
|
|
64
92
|
|
|
93
|
+
if (cartesiaOptions?.streaming != null) {
|
|
94
|
+
warnings.push({
|
|
95
|
+
type: 'unsupported',
|
|
96
|
+
feature: 'providerOptions.cartesia.streaming',
|
|
97
|
+
details:
|
|
98
|
+
'Cartesia batch transcription does not support streaming options.',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
65
102
|
// Create form data with base fields
|
|
66
103
|
const formData = new FormData();
|
|
67
104
|
const blob =
|
|
@@ -98,6 +135,12 @@ export class CartesiaTranscriptionModel implements TranscriptionModelV4 {
|
|
|
98
135
|
async doGenerate(
|
|
99
136
|
options: Parameters<TranscriptionModelV4['doGenerate']>[0],
|
|
100
137
|
): Promise<Awaited<ReturnType<TranscriptionModelV4['doGenerate']>>> {
|
|
138
|
+
if (isStreamingTranscriptionModelId(this.modelId)) {
|
|
139
|
+
throw new UnsupportedFunctionalityError({
|
|
140
|
+
functionality: `non-streaming transcription with ${this.modelId}`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
101
144
|
const currentDate = this.config._internal?.currentDate?.() ?? new Date();
|
|
102
145
|
const { formData, warnings } = await this.getArgs(options);
|
|
103
146
|
|
|
@@ -139,6 +182,344 @@ export class CartesiaTranscriptionModel implements TranscriptionModelV4 {
|
|
|
139
182
|
},
|
|
140
183
|
};
|
|
141
184
|
}
|
|
185
|
+
|
|
186
|
+
async doStream(
|
|
187
|
+
options: TranscriptionModelV4StreamOptions,
|
|
188
|
+
): Promise<
|
|
189
|
+
Awaited<ReturnType<NonNullable<TranscriptionModelV4['doStream']>>>
|
|
190
|
+
> {
|
|
191
|
+
if (!isStreamingTranscriptionModelId(this.modelId)) {
|
|
192
|
+
throw new UnsupportedFunctionalityError({
|
|
193
|
+
functionality: `streaming transcription with ${this.modelId}`,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const currentDate = this.config._internal?.currentDate?.() ?? new Date();
|
|
198
|
+
const warnings: SharedV4Warning[] = [];
|
|
199
|
+
const cartesiaOptions = await parseProviderOptions({
|
|
200
|
+
provider: 'cartesia',
|
|
201
|
+
providerOptions: options.providerOptions,
|
|
202
|
+
schema: cartesiaTranscriptionModelOptionsSchema,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (
|
|
206
|
+
cartesiaOptions?.language != null &&
|
|
207
|
+
cartesiaOptions.language !== 'en'
|
|
208
|
+
) {
|
|
209
|
+
throw new InvalidArgumentError({
|
|
210
|
+
argument: 'providerOptions',
|
|
211
|
+
message: 'Cartesia Ink 2 currently supports English only.',
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (cartesiaOptions?.timestampGranularities != null) {
|
|
216
|
+
warnings.push({
|
|
217
|
+
type: 'unsupported',
|
|
218
|
+
feature: 'providerOptions.cartesia.timestampGranularities',
|
|
219
|
+
details:
|
|
220
|
+
'Cartesia streaming transcription does not support timestamp granularities.',
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const token = await this.createStreamingAccessToken(options);
|
|
225
|
+
const useTurnDetection =
|
|
226
|
+
cartesiaOptions?.streaming?.turnDetection !== false;
|
|
227
|
+
const url = buildCartesiaStreamingTranscriptionUrl({
|
|
228
|
+
baseURL: this.config.url({ path: '/', modelId: this.modelId }),
|
|
229
|
+
version: this.config.version ?? '2026-03-01',
|
|
230
|
+
modelId: this.modelId,
|
|
231
|
+
inputAudioFormat: options.inputAudioFormat,
|
|
232
|
+
providerOptions: cartesiaOptions,
|
|
233
|
+
token,
|
|
234
|
+
useTurnDetection,
|
|
235
|
+
});
|
|
236
|
+
const requestUrl = new URL(url);
|
|
237
|
+
requestUrl.searchParams.delete('access_token');
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
request: { body: requestUrl.toString() },
|
|
241
|
+
response: {
|
|
242
|
+
timestamp: currentDate,
|
|
243
|
+
modelId: this.modelId,
|
|
244
|
+
},
|
|
245
|
+
stream: createCartesiaStreamingTranscriptionStream({
|
|
246
|
+
webSocket: this.config.webSocket,
|
|
247
|
+
url,
|
|
248
|
+
warnings,
|
|
249
|
+
language: cartesiaOptions?.language ?? 'en',
|
|
250
|
+
useTurnDetection,
|
|
251
|
+
audio: options.audio,
|
|
252
|
+
abortSignal: options.abortSignal,
|
|
253
|
+
includeRawChunks: options.includeRawChunks,
|
|
254
|
+
}),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private async createStreamingAccessToken(
|
|
259
|
+
options: TranscriptionModelV4StreamOptions,
|
|
260
|
+
): Promise<string> {
|
|
261
|
+
const { value } = await postJsonToApi({
|
|
262
|
+
url: this.config.url({ path: '/access-token', modelId: this.modelId }),
|
|
263
|
+
headers: combineHeaders(this.config.headers?.(), options.headers),
|
|
264
|
+
body: { grants: { stt: true } },
|
|
265
|
+
failedResponseHandler: cartesiaFailedResponseHandler,
|
|
266
|
+
successfulResponseHandler: createJsonResponseHandler(
|
|
267
|
+
cartesiaAccessTokenResponseSchema,
|
|
268
|
+
),
|
|
269
|
+
abortSignal: options.abortSignal,
|
|
270
|
+
fetch: this.config.fetch,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return value.token;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function createCartesiaStreamingTranscriptionStream({
|
|
278
|
+
webSocket,
|
|
279
|
+
url,
|
|
280
|
+
warnings,
|
|
281
|
+
language,
|
|
282
|
+
useTurnDetection,
|
|
283
|
+
audio,
|
|
284
|
+
abortSignal,
|
|
285
|
+
includeRawChunks,
|
|
286
|
+
}: {
|
|
287
|
+
webSocket: CartesiaConfig['webSocket'];
|
|
288
|
+
url: URL;
|
|
289
|
+
warnings: SharedV4Warning[];
|
|
290
|
+
language: string;
|
|
291
|
+
useTurnDetection: boolean;
|
|
292
|
+
audio: ReadableStream<Uint8Array | string>;
|
|
293
|
+
abortSignal: AbortSignal | undefined;
|
|
294
|
+
includeRawChunks: boolean | undefined;
|
|
295
|
+
}): ReadableStream<TranscriptionModelV4StreamPart> {
|
|
296
|
+
let finished = false;
|
|
297
|
+
let cleanup: (closeCode?: number) => void = () => {};
|
|
298
|
+
|
|
299
|
+
return new ReadableStream({
|
|
300
|
+
start: controller => {
|
|
301
|
+
const finalTexts: string[] = [];
|
|
302
|
+
let durationInSeconds = 0;
|
|
303
|
+
let audioReader:
|
|
304
|
+
| ReadableStreamDefaultReader<Uint8Array | string>
|
|
305
|
+
| undefined;
|
|
306
|
+
let connection: WebSocketConnection | undefined;
|
|
307
|
+
|
|
308
|
+
cleanup = (closeCode?: number) => {
|
|
309
|
+
if (audioReader != null) {
|
|
310
|
+
void audioReader.cancel().catch(() => {});
|
|
311
|
+
} else {
|
|
312
|
+
// Pre-open failure or abort: cancel the caller's audio stream so an
|
|
313
|
+
// upstream producer piping into it does not hang.
|
|
314
|
+
void audio.cancel().catch(() => {});
|
|
315
|
+
}
|
|
316
|
+
connection?.close(closeCode);
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const finishWithError = (error: unknown) => {
|
|
320
|
+
if (finished) return;
|
|
321
|
+
finished = true;
|
|
322
|
+
cleanup();
|
|
323
|
+
controller.error(error);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const finish = () => {
|
|
327
|
+
if (finished) return;
|
|
328
|
+
finished = true;
|
|
329
|
+
controller.enqueue({
|
|
330
|
+
type: 'finish',
|
|
331
|
+
text: finalTexts.join(useTurnDetection ? ' ' : ''),
|
|
332
|
+
segments: [],
|
|
333
|
+
language,
|
|
334
|
+
...(durationInSeconds > 0 ? { durationInSeconds } : {}),
|
|
335
|
+
});
|
|
336
|
+
controller.close();
|
|
337
|
+
cleanup(1000);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const sendAudio = async (socket: WebSocketLike) => {
|
|
341
|
+
audioReader = audio.getReader();
|
|
342
|
+
try {
|
|
343
|
+
while (true) {
|
|
344
|
+
const { done, value } = await audioReader.read();
|
|
345
|
+
if (done || finished) break;
|
|
346
|
+
socket.send(
|
|
347
|
+
value instanceof Uint8Array
|
|
348
|
+
? value
|
|
349
|
+
: convertBase64ToUint8Array(value),
|
|
350
|
+
);
|
|
351
|
+
await waitForWebSocketBufferDrain(socket);
|
|
352
|
+
}
|
|
353
|
+
} finally {
|
|
354
|
+
audioReader.releaseLock();
|
|
355
|
+
audioReader = undefined;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (!finished) {
|
|
359
|
+
socket.send(
|
|
360
|
+
useTurnDetection ? JSON.stringify({ type: 'close' }) : 'finalize',
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
connection = connectToWebSocket({
|
|
366
|
+
url,
|
|
367
|
+
webSocket,
|
|
368
|
+
abortSignal,
|
|
369
|
+
onAbort: finishWithError,
|
|
370
|
+
onProcessingError: finishWithError,
|
|
371
|
+
onOpen: socket => {
|
|
372
|
+
controller.enqueue({ type: 'stream-start', warnings });
|
|
373
|
+
void sendAudio(socket).catch(finishWithError);
|
|
374
|
+
},
|
|
375
|
+
onMessageText: async text => {
|
|
376
|
+
const parsed = await safeParseJSON({ text });
|
|
377
|
+
if (!parsed.success) return;
|
|
378
|
+
const raw = parsed.value as CartesiaStreamingTranscriptionEvent;
|
|
379
|
+
|
|
380
|
+
if (includeRawChunks) {
|
|
381
|
+
controller.enqueue({ type: 'raw', rawValue: raw });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
switch (raw.type) {
|
|
385
|
+
case 'turn.update':
|
|
386
|
+
case 'turn.eager_end': {
|
|
387
|
+
controller.enqueue({
|
|
388
|
+
type: 'transcript-partial',
|
|
389
|
+
id: raw.request_id,
|
|
390
|
+
text: raw.transcript ?? '',
|
|
391
|
+
});
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
case 'turn.end': {
|
|
396
|
+
const text = raw.transcript ?? '';
|
|
397
|
+
finalTexts.push(text);
|
|
398
|
+
controller.enqueue({
|
|
399
|
+
type: 'transcript-final',
|
|
400
|
+
id: raw.request_id,
|
|
401
|
+
text,
|
|
402
|
+
});
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
case 'transcript': {
|
|
407
|
+
const transcript = raw.text ?? '';
|
|
408
|
+
if (raw.is_final === true) {
|
|
409
|
+
finalTexts.push(transcript);
|
|
410
|
+
durationInSeconds += raw.duration ?? 0;
|
|
411
|
+
controller.enqueue({
|
|
412
|
+
type: 'transcript-final',
|
|
413
|
+
id: raw.request_id,
|
|
414
|
+
text: transcript,
|
|
415
|
+
});
|
|
416
|
+
} else {
|
|
417
|
+
controller.enqueue({
|
|
418
|
+
type: 'transcript-partial',
|
|
419
|
+
id: raw.request_id,
|
|
420
|
+
text: transcript,
|
|
421
|
+
...(raw.duration != null
|
|
422
|
+
? { durationInSeconds: raw.duration }
|
|
423
|
+
: {}),
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
case 'flush_done': {
|
|
430
|
+
connection?.socket?.send('close');
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
case 'done': {
|
|
435
|
+
finish();
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
case 'error': {
|
|
440
|
+
finishWithError(
|
|
441
|
+
new Error(
|
|
442
|
+
raw.message ??
|
|
443
|
+
raw.error_code ??
|
|
444
|
+
'Cartesia streaming transcription error',
|
|
445
|
+
),
|
|
446
|
+
);
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
onSocketError: () => {
|
|
452
|
+
finishWithError(new Error('Cartesia streaming transcription error'));
|
|
453
|
+
},
|
|
454
|
+
onClose: () => {
|
|
455
|
+
if (!finished) finish();
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
cancel: () => {
|
|
461
|
+
if (finished) return;
|
|
462
|
+
finished = true;
|
|
463
|
+
cleanup();
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function buildCartesiaStreamingTranscriptionUrl({
|
|
469
|
+
baseURL,
|
|
470
|
+
version,
|
|
471
|
+
modelId,
|
|
472
|
+
inputAudioFormat,
|
|
473
|
+
providerOptions,
|
|
474
|
+
token,
|
|
475
|
+
useTurnDetection,
|
|
476
|
+
}: {
|
|
477
|
+
baseURL: string;
|
|
478
|
+
version: string;
|
|
479
|
+
modelId: string;
|
|
480
|
+
inputAudioFormat: TranscriptionModelV4StreamOptions['inputAudioFormat'];
|
|
481
|
+
providerOptions: CartesiaTranscriptionModelOptions | undefined;
|
|
482
|
+
token: string;
|
|
483
|
+
useTurnDetection: boolean;
|
|
484
|
+
}) {
|
|
485
|
+
const url = toWebSocketUrl(
|
|
486
|
+
new URL(
|
|
487
|
+
useTurnDetection ? '/stt/turns/websocket' : '/stt/websocket',
|
|
488
|
+
baseURL,
|
|
489
|
+
),
|
|
490
|
+
);
|
|
491
|
+
url.searchParams.set('model', modelId);
|
|
492
|
+
url.searchParams.set(
|
|
493
|
+
'encoding',
|
|
494
|
+
cartesiaEncodingFromInputAudioFormat(inputAudioFormat.type),
|
|
495
|
+
);
|
|
496
|
+
url.searchParams.set('sample_rate', String(inputAudioFormat.rate ?? 24000));
|
|
497
|
+
url.searchParams.set('cartesia_version', version);
|
|
498
|
+
url.searchParams.set('access_token', token);
|
|
499
|
+
if (!useTurnDetection && providerOptions?.language != null) {
|
|
500
|
+
url.searchParams.set('language', providerOptions.language);
|
|
501
|
+
}
|
|
502
|
+
return url;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function cartesiaEncodingFromInputAudioFormat(type: string): string {
|
|
506
|
+
switch (type) {
|
|
507
|
+
case 'audio/pcm':
|
|
508
|
+
return 'pcm_s16le';
|
|
509
|
+
case 'audio/pcmu':
|
|
510
|
+
return 'pcm_mulaw';
|
|
511
|
+
case 'audio/pcma':
|
|
512
|
+
return 'pcm_alaw';
|
|
513
|
+
default:
|
|
514
|
+
throw new InvalidArgumentError({
|
|
515
|
+
argument: 'inputAudioFormat',
|
|
516
|
+
message: `Unsupported Cartesia streaming audio format: ${type}`,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function isStreamingTranscriptionModelId(modelId: string): boolean {
|
|
522
|
+
return modelId === 'ink-2' || modelId.startsWith('ink-2-');
|
|
142
523
|
}
|
|
143
524
|
|
|
144
525
|
const cartesiaTranscriptionResponseSchema = z.object({
|
|
@@ -155,3 +536,7 @@ const cartesiaTranscriptionResponseSchema = z.object({
|
|
|
155
536
|
)
|
|
156
537
|
.nullish(),
|
|
157
538
|
});
|
|
539
|
+
|
|
540
|
+
const cartesiaAccessTokenResponseSchema = z.object({
|
|
541
|
+
token: z.string().min(1),
|
|
542
|
+
});
|