@ai-sdk/google 4.0.53 → 4.0.55

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.
@@ -0,0 +1,714 @@
1
+ import {
2
+ InvalidArgumentError,
3
+ type Experimental_TranscriptionModelV4StreamOptions as TranscriptionModelV4StreamOptions,
4
+ type Experimental_TranscriptionModelV4StreamPart as TranscriptionModelV4StreamPart,
5
+ type JSONObject,
6
+ type SharedV4Warning,
7
+ type TranscriptionModelV4,
8
+ } from '@ai-sdk/provider';
9
+ import {
10
+ combineHeaders,
11
+ connectToWebSocket,
12
+ convertToBase64,
13
+ createJsonResponseHandler,
14
+ parseProviderOptions,
15
+ postJsonToApi,
16
+ resolve,
17
+ safeParseJSON,
18
+ serializeModelOptions,
19
+ waitForWebSocketBufferDrain,
20
+ WORKFLOW_DESERIALIZE,
21
+ WORKFLOW_SERIALIZE,
22
+ type FetchFunction,
23
+ type Resolvable,
24
+ type WebSocketConnection,
25
+ type WebSocketConstructor,
26
+ type WebSocketLike,
27
+ } from '@ai-sdk/provider-utils';
28
+ import { z } from 'zod/v4';
29
+ import { getModelPath } from '../get-model-path';
30
+ import { getRealtimeWebSocketURL } from '../get-realtime-base-url';
31
+ import { googleFailedResponseHandler } from '../google-error';
32
+ import {
33
+ googleTranscriptionModelOptions,
34
+ type GoogleTranscriptionModelId,
35
+ type GoogleTranscriptionModelOptions,
36
+ } from './google-transcription-model-options';
37
+
38
+ const liveWebSocketPath =
39
+ 'google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent';
40
+
41
+ /**
42
+ * After the input audio has ended, finish when no terminal signal
43
+ * (`turnComplete` / idle `interactionStatus`) arrives within this window.
44
+ * Trailing transcripts reset the timer.
45
+ */
46
+ const defaultFinishGraceMs = 3000;
47
+
48
+ function getLiveWebSocketURL(baseURL: string, apiKey: string): URL {
49
+ const url = getRealtimeWebSocketURL(baseURL, liveWebSocketPath);
50
+ url.searchParams.set('key', apiKey);
51
+ return url;
52
+ }
53
+
54
+ /** Live transcription is only supported by `*-live` model variants. */
55
+ function isLiveTranscriptionModelId(modelId: string): boolean {
56
+ return modelId.includes('-live');
57
+ }
58
+
59
+ type GoogleLiveWordInfo = {
60
+ text?: string;
61
+ word?: string;
62
+ startOffset?: string;
63
+ endOffset?: string;
64
+ };
65
+
66
+ type GoogleLiveTranscription = {
67
+ text?: string;
68
+ finished?: boolean;
69
+ languageCode?: string;
70
+ speakerLabel?: string;
71
+ words?: GoogleLiveWordInfo[];
72
+ };
73
+
74
+ type GoogleLiveServerMessage = {
75
+ setupComplete?: unknown;
76
+ serverContent?: {
77
+ inputTranscription?: GoogleLiveTranscription;
78
+ interimInputTranscription?: GoogleLiveTranscription;
79
+ turnComplete?: boolean;
80
+ generationComplete?: boolean;
81
+ interactionStatus?: string;
82
+ };
83
+ inputTranscription?: GoogleLiveTranscription;
84
+ usageMetadata?: JSONObject;
85
+ error?: { message?: string };
86
+ };
87
+
88
+ interface GoogleTranscriptionModelConfig {
89
+ provider: string;
90
+ baseURL: string;
91
+ headers?: Resolvable<Record<string, string | undefined>>;
92
+ fetch?: FetchFunction;
93
+ webSocket?: WebSocketConstructor;
94
+ _internal?: {
95
+ currentDate?: () => Date;
96
+ finishGraceMs?: number;
97
+ };
98
+ }
99
+
100
+ export class GoogleTranscriptionModel implements TranscriptionModelV4 {
101
+ readonly specificationVersion = 'v4';
102
+
103
+ static [WORKFLOW_SERIALIZE](model: GoogleTranscriptionModel) {
104
+ return serializeModelOptions({
105
+ modelId: model.modelId,
106
+ config: model.config,
107
+ });
108
+ }
109
+
110
+ static [WORKFLOW_DESERIALIZE](options: {
111
+ modelId: GoogleTranscriptionModelId;
112
+ config: GoogleTranscriptionModelConfig;
113
+ }) {
114
+ return new GoogleTranscriptionModel(options.modelId, options.config);
115
+ }
116
+
117
+ get provider(): string {
118
+ return this.config.provider;
119
+ }
120
+
121
+ constructor(
122
+ readonly modelId: GoogleTranscriptionModelId,
123
+ private readonly config: GoogleTranscriptionModelConfig,
124
+ ) {}
125
+
126
+ private async parseOptions(
127
+ providerOptions: Record<string, unknown> | undefined,
128
+ ): Promise<GoogleTranscriptionModelOptions | undefined> {
129
+ return parseProviderOptions({
130
+ provider: 'google',
131
+ providerOptions,
132
+ schema: googleTranscriptionModelOptions,
133
+ });
134
+ }
135
+
136
+ async doGenerate(
137
+ options: Parameters<TranscriptionModelV4['doGenerate']>[0],
138
+ ): Promise<Awaited<ReturnType<TranscriptionModelV4['doGenerate']>>> {
139
+ if (isLiveTranscriptionModelId(this.modelId)) {
140
+ throw new InvalidArgumentError({
141
+ argument: 'modelId',
142
+ message:
143
+ `Model '${this.modelId}' only supports streaming transcription. ` +
144
+ `Use experimental_streamTranscribe, or a unary model such as 'gemini-3.5-transcribe'.`,
145
+ });
146
+ }
147
+
148
+ const currentDate = this.config._internal?.currentDate?.() ?? new Date();
149
+ const warnings: SharedV4Warning[] = [];
150
+ const googleOptions = await this.parseOptions(options.providerOptions);
151
+ const transcriptionConfig = buildTranscriptionConfig(googleOptions);
152
+
153
+ // Unary transcription is served by the Interactions API
154
+ // (https://ai.google.dev/gemini-api/docs/transcribe).
155
+ const requestBody = {
156
+ model: this.modelId,
157
+ input: [
158
+ {
159
+ type: 'audio',
160
+ data: convertToBase64(options.audio),
161
+ mime_type: options.mediaType,
162
+ },
163
+ ],
164
+ ...(transcriptionConfig != null
165
+ ? { generation_config: { transcription_config: transcriptionConfig } }
166
+ : {}),
167
+ };
168
+
169
+ const {
170
+ value: response,
171
+ responseHeaders,
172
+ rawValue: rawResponse,
173
+ } = await postJsonToApi({
174
+ url: `${this.config.baseURL}/interactions`,
175
+ headers: combineHeaders(
176
+ this.config.headers ? await resolve(this.config.headers) : undefined,
177
+ options.headers,
178
+ ),
179
+ body: requestBody,
180
+ failedResponseHandler: googleFailedResponseHandler,
181
+ successfulResponseHandler: createJsonResponseHandler(
182
+ googleInteractionsTranscriptionResponseSchema,
183
+ ),
184
+ abortSignal: options.abortSignal,
185
+ fetch: this.config.fetch,
186
+ });
187
+
188
+ let text = '';
189
+ const segments: Array<{
190
+ text: string;
191
+ startSecond: number;
192
+ endSecond: number;
193
+ }> = [];
194
+ for (const step of response.steps ?? []) {
195
+ for (const content of step.content ?? []) {
196
+ if (content.type !== 'text' || content.text == null) continue;
197
+ text += content.text;
198
+ for (const annotation of content.annotations ?? []) {
199
+ if (annotation.type !== 'word_info') continue;
200
+ const startSecond = parseOffsetSeconds(annotation.start_offset);
201
+ const endSecond = parseOffsetSeconds(annotation.end_offset);
202
+ if (
203
+ annotation.text == null ||
204
+ startSecond == null ||
205
+ endSecond == null
206
+ ) {
207
+ continue;
208
+ }
209
+ segments.push({ text: annotation.text, startSecond, endSecond });
210
+ }
211
+ }
212
+ }
213
+
214
+ return {
215
+ text,
216
+ segments,
217
+ language: undefined,
218
+ durationInSeconds: undefined,
219
+ warnings,
220
+ response: {
221
+ timestamp: currentDate,
222
+ modelId: this.modelId,
223
+ headers: responseHeaders,
224
+ body: rawResponse,
225
+ },
226
+ ...(response.usage != null
227
+ ? {
228
+ providerMetadata: {
229
+ google: { usage: response.usage as JSONObject },
230
+ },
231
+ }
232
+ : {}),
233
+ };
234
+ }
235
+
236
+ async doStream(
237
+ options: TranscriptionModelV4StreamOptions,
238
+ ): Promise<
239
+ Awaited<ReturnType<NonNullable<TranscriptionModelV4['doStream']>>>
240
+ > {
241
+ if (!isLiveTranscriptionModelId(this.modelId)) {
242
+ throw new InvalidArgumentError({
243
+ argument: 'modelId',
244
+ message:
245
+ `Model '${this.modelId}' does not support streaming transcription. ` +
246
+ `Use a live model such as 'gemini-3.5-transcribe-live'.`,
247
+ });
248
+ }
249
+
250
+ const currentDate = this.config._internal?.currentDate?.() ?? new Date();
251
+ const warnings: SharedV4Warning[] = [];
252
+ const googleOptions = await this.parseOptions(options.providerOptions);
253
+
254
+ validateLiveInputAudioFormat(options.inputAudioFormat);
255
+
256
+ const headers = combineHeaders(
257
+ this.config.headers ? await resolve(this.config.headers) : undefined,
258
+ options.headers,
259
+ );
260
+ // last case-variant wins: combineHeaders keeps case-distinct keys and
261
+ // spreads per-call headers after configuration headers
262
+ let apiKey: string | undefined;
263
+ for (const [key, value] of Object.entries(headers)) {
264
+ if (key.toLowerCase() === 'x-goog-api-key' && value != null) {
265
+ apiKey = value;
266
+ }
267
+ }
268
+ if (apiKey == null) {
269
+ throw new Error(
270
+ 'Google Generative AI API key is required for streaming transcription.',
271
+ );
272
+ }
273
+ const webSocketHeaders = Object.fromEntries(
274
+ Object.entries(headers).filter(
275
+ ([key]) => key.toLowerCase() !== 'x-goog-api-key',
276
+ ),
277
+ );
278
+
279
+ // NOTE: Google's GA announcement shows the setup with
280
+ // `generationConfig: { responseModalities: ['TEXT'] }`, but sending it
281
+ // suppresses the final `inputTranscription` segments on the current
282
+ // endpoint (only interim partials arrive). Omit generationConfig — the
283
+ // empirically working shape — until the endpoint honors the documented
284
+ // form.
285
+ const setup = {
286
+ model: getModelPath(this.modelId),
287
+ inputAudioTranscription:
288
+ buildAudioTranscriptionConfig(googleOptions) ?? {},
289
+ };
290
+
291
+ return {
292
+ request: { body: setup },
293
+ response: {
294
+ timestamp: currentDate,
295
+ modelId: this.modelId,
296
+ },
297
+ stream: createGoogleLiveTranscriptionStream({
298
+ webSocket: this.config.webSocket,
299
+ url: getLiveWebSocketURL(this.config.baseURL, apiKey),
300
+ headers: webSocketHeaders,
301
+ setup,
302
+ inputAudioRate: options.inputAudioFormat.rate ?? 16000,
303
+ finishGraceMs:
304
+ this.config._internal?.finishGraceMs ?? defaultFinishGraceMs,
305
+ warnings,
306
+ audio: options.audio,
307
+ abortSignal: options.abortSignal,
308
+ includeRawChunks: options.includeRawChunks,
309
+ }),
310
+ };
311
+ }
312
+ }
313
+
314
+ function createGoogleLiveTranscriptionStream({
315
+ webSocket,
316
+ url,
317
+ headers,
318
+ setup,
319
+ inputAudioRate,
320
+ finishGraceMs,
321
+ warnings,
322
+ audio,
323
+ abortSignal,
324
+ includeRawChunks,
325
+ }: {
326
+ webSocket: WebSocketConstructor | undefined;
327
+ url: URL;
328
+ headers: Record<string, string | undefined>;
329
+ setup: unknown;
330
+ inputAudioRate: number;
331
+ finishGraceMs: number;
332
+ warnings: SharedV4Warning[];
333
+ audio: ReadableStream<Uint8Array | string>;
334
+ abortSignal: AbortSignal | undefined;
335
+ includeRawChunks: boolean | undefined;
336
+ }) {
337
+ let finished = false;
338
+ let cleanup: (closeCode?: number) => void = () => {};
339
+
340
+ return new ReadableStream<TranscriptionModelV4StreamPart>({
341
+ start: controller => {
342
+ let audioReader:
343
+ | ReadableStreamDefaultReader<Uint8Array | string>
344
+ | undefined;
345
+ let connection: WebSocketConnection | undefined;
346
+
347
+ // The Live API contract requires waiting for the `setupComplete`
348
+ // server message before sending realtime input: the audio send loop
349
+ // is gated on this promise.
350
+ let resolveSetupComplete!: () => void;
351
+ const setupComplete = new Promise<void>(resolve => {
352
+ resolveSetupComplete = resolve;
353
+ });
354
+
355
+ // Google Live messages carry no response/item IDs; a segment counter
356
+ // generates consistent synthetic IDs. Transcription fragments arrive
357
+ // incrementally and are accumulated per segment; a `finished: true`
358
+ // transcription or `turnComplete` finalizes the current segment.
359
+ let segmentCounter = 0;
360
+ let segmentBuffer = '';
361
+ let fullText = '';
362
+ // Latest revisable interim text: the fallback final when the server
363
+ // never delivers a finished `inputTranscription` segment.
364
+ let latestInterim = '';
365
+ let language: string | undefined;
366
+ let audioEnded = false;
367
+ let usageMetadata: JSONObject | undefined;
368
+ let finishTimer: ReturnType<typeof setTimeout> | undefined;
369
+
370
+ const segmentId = () => `google-segment-${segmentCounter}`;
371
+
372
+ const cancelPendingFinish = () => {
373
+ if (finishTimer != null) {
374
+ clearTimeout(finishTimer);
375
+ finishTimer = undefined;
376
+ }
377
+ };
378
+
379
+ // Trailing transcripts can arrive after audioStreamEnd; without a
380
+ // terminal signal, finish after a quiet grace window. Transcript
381
+ // activity reschedules the timer.
382
+ const schedulePendingFinish = () => {
383
+ if (finished || !audioEnded) return;
384
+ cancelPendingFinish();
385
+ finishTimer = setTimeout(() => {
386
+ finishTimer = undefined;
387
+ finish();
388
+ }, finishGraceMs);
389
+ };
390
+
391
+ cleanup = (closeCode?: number) => {
392
+ cancelPendingFinish();
393
+ if (audioReader != null) {
394
+ void audioReader.cancel().catch(() => {});
395
+ } else {
396
+ // pre-open failure or abort: cancel the caller's audio stream so an
397
+ // upstream producer piping into it does not hang:
398
+ void audio.cancel().catch(() => {});
399
+ }
400
+ connection?.close(closeCode);
401
+ };
402
+
403
+ const finishWithError = (error: unknown) => {
404
+ if (finished) return;
405
+ finished = true;
406
+ cleanup();
407
+ controller.error(error);
408
+ };
409
+
410
+ const completeSegment = () => {
411
+ // A finished segment supersedes any interim text it revises.
412
+ if (segmentBuffer === '') {
413
+ if (latestInterim === '') return;
414
+ segmentBuffer = latestInterim;
415
+ }
416
+ latestInterim = '';
417
+ controller.enqueue({
418
+ type: 'transcript-final',
419
+ id: segmentId(),
420
+ text: segmentBuffer,
421
+ });
422
+ fullText += fullText === '' ? segmentBuffer : ` ${segmentBuffer}`;
423
+ segmentBuffer = '';
424
+ segmentCounter++;
425
+ };
426
+
427
+ const finish = () => {
428
+ if (finished) return;
429
+ completeSegment();
430
+ finished = true;
431
+ controller.enqueue({
432
+ type: 'finish',
433
+ text: fullText,
434
+ segments: [],
435
+ language,
436
+ durationInSeconds: undefined,
437
+ ...(usageMetadata != null
438
+ ? { providerMetadata: { google: { usageMetadata } } }
439
+ : {}),
440
+ });
441
+ controller.close();
442
+ cleanup(1000);
443
+ };
444
+
445
+ const sendAudio = async (socket: WebSocketLike) => {
446
+ audioReader = audio.getReader();
447
+ try {
448
+ while (true) {
449
+ const { done, value } = await audioReader.read();
450
+ if (done || finished) break;
451
+ socket.send(
452
+ JSON.stringify({
453
+ realtimeInput: {
454
+ audio: {
455
+ data: convertToBase64(value),
456
+ mimeType: `audio/pcm;rate=${inputAudioRate}`,
457
+ },
458
+ },
459
+ }),
460
+ );
461
+ // backpressure: pause reads while the socket buffer is full
462
+ await waitForWebSocketBufferDrain(socket);
463
+ }
464
+ } finally {
465
+ audioReader.releaseLock();
466
+ // unlocked again: cleanup must cancel `audio`, not the reader
467
+ audioReader = undefined;
468
+ }
469
+ if (!finished) {
470
+ socket.send(
471
+ JSON.stringify({ realtimeInput: { audioStreamEnd: true } }),
472
+ );
473
+ audioEnded = true;
474
+ schedulePendingFinish();
475
+ }
476
+ };
477
+
478
+ connection = connectToWebSocket({
479
+ url,
480
+ headers,
481
+ webSocket,
482
+ abortSignal,
483
+ onAbort: finishWithError,
484
+ onProcessingError: finishWithError,
485
+ onOpen: socket => {
486
+ controller.enqueue({ type: 'stream-start', warnings });
487
+ socket.send(JSON.stringify({ setup }));
488
+ // audio may only be sent after the server acknowledged the setup:
489
+ void setupComplete
490
+ .then(() => (finished ? undefined : sendAudio(socket)))
491
+ .catch(finishWithError);
492
+ },
493
+ onMessageText: async text => {
494
+ if (finished) return;
495
+ const parsed = await safeParseJSON({ text });
496
+ if (!parsed.success) return;
497
+ const message = parsed.value as GoogleLiveServerMessage;
498
+
499
+ if (includeRawChunks) {
500
+ controller.enqueue({ type: 'raw', rawValue: message });
501
+ }
502
+
503
+ if (message.setupComplete != null) {
504
+ resolveSetupComplete();
505
+ }
506
+
507
+ if (message.usageMetadata != null) {
508
+ usageMetadata = message.usageMetadata;
509
+ }
510
+
511
+ if (message.error != null) {
512
+ finishWithError(
513
+ new Error(message.error.message ?? 'Google Live API error'),
514
+ );
515
+ return;
516
+ }
517
+
518
+ const serverContent = message.serverContent;
519
+
520
+ // Low-latency revisable transcription while the user is speaking.
521
+ const interim = serverContent?.interimInputTranscription;
522
+ if (interim?.text) {
523
+ schedulePendingFinish();
524
+ latestInterim = interim.text;
525
+ controller.enqueue({
526
+ type: 'transcript-partial',
527
+ id: segmentId(),
528
+ text: interim.text,
529
+ });
530
+ }
531
+
532
+ const transcription =
533
+ serverContent?.inputTranscription ?? message.inputTranscription;
534
+ if (transcription != null) {
535
+ if (transcription.languageCode != null) {
536
+ language = transcription.languageCode;
537
+ }
538
+ if (transcription.text) {
539
+ schedulePendingFinish();
540
+ // A real transcription delta supersedes interim fallback text.
541
+ latestInterim = '';
542
+ segmentBuffer += transcription.text;
543
+ controller.enqueue({
544
+ type: 'transcript-delta',
545
+ id: segmentId(),
546
+ delta: transcription.text,
547
+ });
548
+ }
549
+ if (transcription.finished === true) {
550
+ completeSegment();
551
+ }
552
+ }
553
+
554
+ if (serverContent?.turnComplete) {
555
+ completeSegment();
556
+ }
557
+
558
+ // `interactionStatus` idle (REQUIRES_ACTION in the EAP builds) is
559
+ // the definitive all-processing-complete signal: finish as soon as
560
+ // the input audio has ended.
561
+ const interactionStatus = serverContent?.interactionStatus;
562
+ if (
563
+ audioEnded &&
564
+ (interactionStatus === 'IDLE' ||
565
+ interactionStatus === 'REQUIRES_ACTION' ||
566
+ (serverContent?.turnComplete === true &&
567
+ interactionStatus == null))
568
+ ) {
569
+ finish();
570
+ }
571
+ },
572
+ onSocketError: () => {
573
+ finishWithError(new Error('Google Live transcription error'));
574
+ },
575
+ onClose: ({ code, reason }) => {
576
+ if (finished) return;
577
+ // a close after the input audio ended means the server delivered
578
+ // everything it will deliver: finish with the accumulated text
579
+ if (audioEnded) {
580
+ finish();
581
+ return;
582
+ }
583
+ finishWithError(
584
+ new Error(
585
+ `Google Live transcription WebSocket closed unexpectedly before finishing` +
586
+ ` (code ${code ?? 'unknown'}${reason ? `, reason: ${reason}` : ''}).`,
587
+ ),
588
+ );
589
+ },
590
+ });
591
+ },
592
+
593
+ cancel: () => {
594
+ if (finished) return;
595
+ finished = true;
596
+ cleanup();
597
+ },
598
+ });
599
+ }
600
+
601
+ /**
602
+ * Builds Google's `AudioTranscriptionConfig` from provider options; returns
603
+ * undefined when no options are set.
604
+ */
605
+ function buildAudioTranscriptionConfig(
606
+ options: GoogleTranscriptionModelOptions | undefined,
607
+ ): Record<string, unknown> | undefined {
608
+ if (options == null) return undefined;
609
+ const config: Record<string, unknown> = {};
610
+ if (options.languageCodes != null) {
611
+ config.languageCodes = options.languageCodes;
612
+ }
613
+ if (options.customVocabulary != null) {
614
+ config.customVocabulary = options.customVocabulary;
615
+ }
616
+ if (options.wordTimestamp != null) {
617
+ config.wordTimestamp = options.wordTimestamp;
618
+ }
619
+ if (options.diarization != null) {
620
+ config.diarization = options.diarization;
621
+ }
622
+ if (options.mode != null) {
623
+ config.mode = options.mode;
624
+ }
625
+ return Object.keys(config).length > 0 ? config : undefined;
626
+ }
627
+
628
+ /**
629
+ * Builds the Interactions API `transcription_config` (snake_case wire) from
630
+ * provider options; returns undefined when no options are set. Diarization
631
+ * and word timestamps are expressed inside the `mode` object per
632
+ * https://ai.google.dev/gemini-api/docs/transcribe.
633
+ */
634
+ function buildTranscriptionConfig(
635
+ options: GoogleTranscriptionModelOptions | undefined,
636
+ ): Record<string, unknown> | undefined {
637
+ if (options == null) return undefined;
638
+ const config: Record<string, unknown> = {};
639
+ if (options.languageCodes != null) {
640
+ config.language_codes = options.languageCodes;
641
+ }
642
+ if (options.customVocabulary != null) {
643
+ config.custom_vocabulary = options.customVocabulary;
644
+ }
645
+ if (
646
+ options.mode != null ||
647
+ options.diarization === true ||
648
+ options.wordTimestamp === true
649
+ ) {
650
+ config.mode = {
651
+ type: (options.mode ?? 'VERBATIM').toLowerCase(),
652
+ ...(options.diarization === true ? { diarization_mode: 'speaker' } : {}),
653
+ ...(options.wordTimestamp === true
654
+ ? { timestamp_granularities: ['word'] }
655
+ : {}),
656
+ };
657
+ }
658
+ return Object.keys(config).length > 0 ? config : undefined;
659
+ }
660
+
661
+ /** Parses a Google duration offset such as `"1s"` or `"9.400s"` to seconds. */
662
+ function parseOffsetSeconds(
663
+ offset: string | undefined | null,
664
+ ): number | undefined {
665
+ if (offset == null) return undefined;
666
+ const parsed = Number.parseFloat(offset);
667
+ return Number.isFinite(parsed) ? parsed : undefined;
668
+ }
669
+
670
+ function validateLiveInputAudioFormat(
671
+ inputAudioFormat: TranscriptionModelV4StreamOptions['inputAudioFormat'],
672
+ ) {
673
+ if (
674
+ inputAudioFormat.type !== 'audio/pcm' ||
675
+ (inputAudioFormat.rate != null && inputAudioFormat.rate !== 16000)
676
+ ) {
677
+ throw new InvalidArgumentError({
678
+ argument: 'inputAudioFormat',
679
+ message:
680
+ 'The Gemini Live transcription API only supports 16kHz 16-bit PCM input audio.',
681
+ });
682
+ }
683
+ }
684
+
685
+ const googleInteractionsWordAnnotationSchema = z.object({
686
+ type: z.string().nullish(),
687
+ text: z.string().nullish(),
688
+ speaker: z.string().nullish(),
689
+ start_offset: z.string().nullish(),
690
+ end_offset: z.string().nullish(),
691
+ });
692
+
693
+ const googleInteractionsTranscriptionResponseSchema = z.object({
694
+ status: z.string().nullish(),
695
+ steps: z
696
+ .array(
697
+ z.object({
698
+ type: z.string().nullish(),
699
+ content: z
700
+ .array(
701
+ z.object({
702
+ type: z.string().nullish(),
703
+ text: z.string().nullish(),
704
+ annotations: z
705
+ .array(googleInteractionsWordAnnotationSchema)
706
+ .nullish(),
707
+ }),
708
+ )
709
+ .nullish(),
710
+ }),
711
+ )
712
+ .nullish(),
713
+ usage: z.record(z.string(), z.unknown()).nullish(),
714
+ });