ai 7.0.37 → 7.0.38
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 +12 -0
- package/dist/index.d.ts +263 -40
- package/dist/index.js +684 -423
- package/dist/index.js.map +1 -1
- package/dist/internal/index.js +1 -1
- package/dist/internal/index.js.map +1 -1
- package/dist/test/index.d.ts +14 -2
- package/dist/test/index.js +15 -0
- package/dist/test/index.js.map +1 -1
- package/docs/03-ai-sdk-core/36-transcription.mdx +40 -35
- package/docs/03-ai-sdk-core/36-translation.mdx +196 -0
- package/docs/03-ai-sdk-core/index.mdx +5 -0
- package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +1 -1
- package/docs/07-reference/01-ai-sdk-core/11-stream-translate.mdx +176 -0
- package/docs/07-reference/01-ai-sdk-core/index.mdx +5 -0
- package/docs/07-reference/05-ai-sdk-errors/ai-no-translation-generated-error.mdx +26 -0
- package/docs/07-reference/05-ai-sdk-errors/index.mdx +1 -0
- package/package.json +4 -4
- package/src/error/index.ts +1 -0
- package/src/error/no-translation-generated-error.ts +28 -0
- package/src/generate-text/generate-text.ts +4 -1
- package/src/generate-text/stream-language-model-call.ts +14 -12
- package/src/generate-text/stream-text-result.ts +3 -2
- package/src/index.ts +1 -0
- package/src/model/resolve-model.ts +35 -0
- package/src/test/mock-speech-translation-model-v4.ts +24 -0
- package/src/transcribe/stream-transcribe.ts +2 -11
- package/src/translate/index.ts +5 -0
- package/src/translate/stream-translate-result.ts +146 -0
- package/src/translate/stream-translate.ts +374 -0
- package/src/types/speech-translation-model-response-metadata.ts +22 -0
- package/src/types/speech-translation-model.ts +11 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Experimental_SpeechTranslationModelV4StreamPart,
|
|
3
|
+
Experimental_SpeechTranslationModelV4Usage,
|
|
4
|
+
JSONObject,
|
|
5
|
+
SharedV4AudioFormat,
|
|
6
|
+
} from '@ai-sdk/provider';
|
|
7
|
+
import {
|
|
8
|
+
DelayedPromise,
|
|
9
|
+
withUserAgentSuffix,
|
|
10
|
+
type ProviderOptions,
|
|
11
|
+
} from '@ai-sdk/provider-utils';
|
|
12
|
+
import { NoTranslationGeneratedError } from '../error/no-translation-generated-error';
|
|
13
|
+
import { logWarnings } from '../logger/log-warnings';
|
|
14
|
+
import { resolveSpeechTranslationModel } from '../model/resolve-model';
|
|
15
|
+
import type { SpeechTranslationModel } from '../types/speech-translation-model';
|
|
16
|
+
import type { SpeechTranslationModelResponseMetadata } from '../types/speech-translation-model-response-metadata';
|
|
17
|
+
import type { Warning } from '../types/warning';
|
|
18
|
+
import { asAsyncIterableStream } from '../util/async-iterable-stream';
|
|
19
|
+
import { mergeAbortSignals } from '../util/merge-abort-signals';
|
|
20
|
+
import { VERSION } from '../version';
|
|
21
|
+
import type {
|
|
22
|
+
StreamTranslationResult,
|
|
23
|
+
TranslationStreamPart,
|
|
24
|
+
} from './stream-translate-result';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Streams speech-to-speech translations using a speech translation model.
|
|
28
|
+
*
|
|
29
|
+
* @param model - The speech translation model to use.
|
|
30
|
+
* @param audio - Raw audio chunks to translate.
|
|
31
|
+
* @param inputAudioFormat - The input audio format for the raw audio chunks.
|
|
32
|
+
* @param targetLanguage - The language to translate the audio into.
|
|
33
|
+
* @param sourceLanguage - The language of the source audio. Auto-detected when absent.
|
|
34
|
+
* @param outputAudioFormat - The desired audio format for translated audio chunks.
|
|
35
|
+
* @param providerOptions - Additional provider-specific options.
|
|
36
|
+
* @param abortSignal - An optional abort signal that can be used to cancel the call.
|
|
37
|
+
* @param headers - Additional HTTP/WebSocket headers to send when supported by the provider.
|
|
38
|
+
* @param includeRawChunks - When true, providers include raw provider chunks in the stream as `raw` parts.
|
|
39
|
+
*
|
|
40
|
+
* @returns A result object that contains the streaming translation and final translation metadata.
|
|
41
|
+
*/
|
|
42
|
+
export function streamTranslate({
|
|
43
|
+
model,
|
|
44
|
+
audio,
|
|
45
|
+
inputAudioFormat,
|
|
46
|
+
targetLanguage,
|
|
47
|
+
sourceLanguage,
|
|
48
|
+
outputAudioFormat,
|
|
49
|
+
providerOptions = {},
|
|
50
|
+
abortSignal,
|
|
51
|
+
headers,
|
|
52
|
+
includeRawChunks,
|
|
53
|
+
_internal: { currentDate = () => new Date() } = {},
|
|
54
|
+
}: {
|
|
55
|
+
/**
|
|
56
|
+
* The speech translation model to use.
|
|
57
|
+
*/
|
|
58
|
+
model: SpeechTranslationModel;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Raw audio chunks to translate.
|
|
62
|
+
*/
|
|
63
|
+
audio: ReadableStream<Uint8Array | string>;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The input audio format for the raw audio chunks.
|
|
67
|
+
*/
|
|
68
|
+
inputAudioFormat: SharedV4AudioFormat;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The language to translate the audio into, as a BCP-47-style language
|
|
72
|
+
* tag (e.g. `en`, `es`, `fr-CA`). Supported values are provider-specific
|
|
73
|
+
* and validated by the provider.
|
|
74
|
+
*/
|
|
75
|
+
targetLanguage: string;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The language of the source audio, as a BCP-47-style language tag.
|
|
79
|
+
* When absent, providers auto-detect the source language.
|
|
80
|
+
*/
|
|
81
|
+
sourceLanguage?: string;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The desired audio format for translated audio chunks.
|
|
85
|
+
* When absent, the provider default output format is used.
|
|
86
|
+
*/
|
|
87
|
+
outputAudioFormat?: SharedV4AudioFormat;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Additional provider-specific options.
|
|
91
|
+
*/
|
|
92
|
+
providerOptions?: ProviderOptions;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Abort signal.
|
|
96
|
+
*/
|
|
97
|
+
abortSignal?: AbortSignal;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Additional headers to include in the request, if supported by the provider.
|
|
101
|
+
*/
|
|
102
|
+
headers?: Record<string, string>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* When true, providers should include raw provider chunks in the stream.
|
|
106
|
+
*/
|
|
107
|
+
includeRawChunks?: boolean;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Internal test hooks.
|
|
111
|
+
*/
|
|
112
|
+
_internal?: {
|
|
113
|
+
currentDate?: () => Date;
|
|
114
|
+
};
|
|
115
|
+
}): StreamTranslationResult {
|
|
116
|
+
const resolvedModel = resolveSpeechTranslationModel(model);
|
|
117
|
+
|
|
118
|
+
const doStream = resolvedModel.doStream.bind(resolvedModel);
|
|
119
|
+
|
|
120
|
+
const headersWithUserAgent = withUserAgentSuffix(
|
|
121
|
+
headers ?? {},
|
|
122
|
+
`ai/${VERSION}`,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const sourceTextPromise = new DelayedPromise<string>();
|
|
126
|
+
const translationTextPromise = new DelayedPromise<string>();
|
|
127
|
+
const durationInSecondsPromise = new DelayedPromise<number | undefined>();
|
|
128
|
+
const usagePromise = new DelayedPromise<
|
|
129
|
+
Experimental_SpeechTranslationModelV4Usage | undefined
|
|
130
|
+
>();
|
|
131
|
+
const warningsPromise = new DelayedPromise<Array<Warning>>();
|
|
132
|
+
const responsePromise =
|
|
133
|
+
new DelayedPromise<SpeechTranslationModelResponseMetadata>();
|
|
134
|
+
const providerMetadataPromise = new DelayedPromise<
|
|
135
|
+
Record<string, JSONObject>
|
|
136
|
+
>();
|
|
137
|
+
|
|
138
|
+
const rejectPendingPromises = (error: unknown) => {
|
|
139
|
+
for (const promise of [
|
|
140
|
+
sourceTextPromise,
|
|
141
|
+
translationTextPromise,
|
|
142
|
+
durationInSecondsPromise,
|
|
143
|
+
usagePromise,
|
|
144
|
+
warningsPromise,
|
|
145
|
+
responsePromise,
|
|
146
|
+
providerMetadataPromise,
|
|
147
|
+
]) {
|
|
148
|
+
if (promise.isPending()) {
|
|
149
|
+
promise.reject(error);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const startedAt = currentDate();
|
|
155
|
+
let response: SpeechTranslationModelResponseMetadata | undefined;
|
|
156
|
+
const currentResponseMetadata = () =>
|
|
157
|
+
response ?? { timestamp: startedAt, modelId: resolvedModel.modelId };
|
|
158
|
+
|
|
159
|
+
const resolveWarnings = (warnings: Array<Warning>) => {
|
|
160
|
+
warningsPromise.resolve(warnings);
|
|
161
|
+
logWarnings({
|
|
162
|
+
warnings,
|
|
163
|
+
provider: resolvedModel.provider,
|
|
164
|
+
model: resolvedModel.modelId,
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// When the consumer cancels `fullStream` early, we abort the model pipe below
|
|
169
|
+
// with a defined reason via this controller. Relying on the default cancel
|
|
170
|
+
// cascade aborts the pipe with an `undefined` reason, which surfaces as a
|
|
171
|
+
// spurious unhandled rejection on Node.js 26 when the transform drops chunks.
|
|
172
|
+
const pipeAbortController = new AbortController();
|
|
173
|
+
|
|
174
|
+
// A stream is successful when at least one `audio` part was emitted or the
|
|
175
|
+
// final `outputText` is non-empty (audio-only providers may report an empty
|
|
176
|
+
// `outputText`).
|
|
177
|
+
let hasAudioOutput = false;
|
|
178
|
+
|
|
179
|
+
// `Transformer.cancel` is part of the Streams spec (and supported at runtime),
|
|
180
|
+
// but not yet reflected in the ambient `Transformer` type, so widen it here.
|
|
181
|
+
const transformer: Transformer<
|
|
182
|
+
Experimental_SpeechTranslationModelV4StreamPart,
|
|
183
|
+
TranslationStreamPart
|
|
184
|
+
> & { cancel?: (reason?: unknown) => void } = {
|
|
185
|
+
transform(value, controller) {
|
|
186
|
+
switch (value.type) {
|
|
187
|
+
case 'stream-start': {
|
|
188
|
+
resolveWarnings(value.warnings);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case 'response-metadata': {
|
|
193
|
+
response = {
|
|
194
|
+
timestamp: value.timestamp ?? currentResponseMetadata().timestamp,
|
|
195
|
+
modelId: value.modelId ?? currentResponseMetadata().modelId,
|
|
196
|
+
headers: value.headers ?? response?.headers,
|
|
197
|
+
};
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
case 'audio': {
|
|
202
|
+
hasAudioOutput = true;
|
|
203
|
+
controller.enqueue(value);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
case 'output-text-delta':
|
|
208
|
+
case 'output-text-final':
|
|
209
|
+
case 'source-transcript-delta':
|
|
210
|
+
case 'source-transcript-partial':
|
|
211
|
+
case 'source-transcript-final':
|
|
212
|
+
case 'raw':
|
|
213
|
+
case 'error': {
|
|
214
|
+
controller.enqueue(value);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
case 'finish': {
|
|
219
|
+
if (!warningsPromise.isResolved()) {
|
|
220
|
+
resolveWarnings([]);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!hasAudioOutput && !value.outputText) {
|
|
224
|
+
throw new NoTranslationGeneratedError({
|
|
225
|
+
response: currentResponseMetadata(),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
sourceTextPromise.resolve(value.sourceText);
|
|
230
|
+
translationTextPromise.resolve(value.outputText);
|
|
231
|
+
durationInSecondsPromise.resolve(value.durationInSeconds);
|
|
232
|
+
usagePromise.resolve(value.usage);
|
|
233
|
+
responsePromise.resolve(currentResponseMetadata());
|
|
234
|
+
providerMetadataPromise.resolve(value.providerMetadata ?? {});
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
default: {
|
|
239
|
+
const _exhaustiveCheck: never = value;
|
|
240
|
+
throw new Error(`Unsupported part type: ${_exhaustiveCheck}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
flush() {
|
|
246
|
+
if (translationTextPromise.isPending()) {
|
|
247
|
+
throw new NoTranslationGeneratedError({
|
|
248
|
+
response: currentResponseMetadata(),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
cancel(reason) {
|
|
254
|
+
pipeAbortController.abort(
|
|
255
|
+
reason ?? new Error('Translation stream was cancelled.'),
|
|
256
|
+
);
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const transform = new TransformStream<
|
|
261
|
+
Experimental_SpeechTranslationModelV4StreamPart,
|
|
262
|
+
TranslationStreamPart
|
|
263
|
+
>(transformer);
|
|
264
|
+
|
|
265
|
+
// Piping (instead of an eager read loop) preserves consumer backpressure
|
|
266
|
+
// and propagates cancellation of `fullStream` to the model stream.
|
|
267
|
+
void (async () => {
|
|
268
|
+
const result = await doStream({
|
|
269
|
+
audio,
|
|
270
|
+
inputAudioFormat,
|
|
271
|
+
targetLanguage,
|
|
272
|
+
sourceLanguage,
|
|
273
|
+
outputAudioFormat,
|
|
274
|
+
providerOptions,
|
|
275
|
+
// merged so cancelling fullStream also aborts a still-pending doStream
|
|
276
|
+
abortSignal: mergeAbortSignals(abortSignal, pipeAbortController.signal),
|
|
277
|
+
headers: headersWithUserAgent,
|
|
278
|
+
includeRawChunks,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
response = {
|
|
282
|
+
timestamp: result.response?.timestamp ?? startedAt,
|
|
283
|
+
modelId: result.response?.modelId ?? resolvedModel.modelId,
|
|
284
|
+
headers: result.response?.headers,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
await result.stream.pipeTo(transform.writable, {
|
|
288
|
+
signal: pipeAbortController.signal,
|
|
289
|
+
});
|
|
290
|
+
})().catch(error => {
|
|
291
|
+
const reason =
|
|
292
|
+
error ?? new Error('Translation stream was cancelled or errored.');
|
|
293
|
+
rejectPendingPromises(reason);
|
|
294
|
+
// When `doStream` rejects before the model stream exists (e.g. auth or
|
|
295
|
+
// header resolution failure), nothing has taken ownership of `audio` yet,
|
|
296
|
+
// so cancel it directly — otherwise an upstream producer piping into it
|
|
297
|
+
// hangs forever. When the model did take a reader, `audio` is locked and
|
|
298
|
+
// the cancel rejects, which is fine: the model's cleanup owns it then.
|
|
299
|
+
audio.cancel(reason).catch(() => {});
|
|
300
|
+
transform.writable.abort(reason).catch(() => {
|
|
301
|
+
// the writable is already errored when the model stream failed mid-pipe
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Translation streams can be unbounded (live microphone + raw chunks), so
|
|
306
|
+
// unlike streamText we cannot retain an unread tee branch for replay. The
|
|
307
|
+
// output stream has one owner: either fullStream, or the first result promise
|
|
308
|
+
// getter which claims and drains it internally.
|
|
309
|
+
let streamOwner: 'unclaimed' | 'full-stream' | 'result-promises' =
|
|
310
|
+
'unclaimed';
|
|
311
|
+
|
|
312
|
+
function consumeStream() {
|
|
313
|
+
if (streamOwner === 'full-stream' || streamOwner === 'result-promises') {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
streamOwner = 'result-promises';
|
|
317
|
+
const reader = transform.readable.getReader();
|
|
318
|
+
void (async () => {
|
|
319
|
+
while (!(await reader.read()).done) {
|
|
320
|
+
// drain; results surface via the promises
|
|
321
|
+
}
|
|
322
|
+
})().catch(() => {
|
|
323
|
+
// stream errors reject the promises via the pipe handler above
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function getFullStream() {
|
|
328
|
+
if (streamOwner !== 'unclaimed') {
|
|
329
|
+
throw new Error(
|
|
330
|
+
streamOwner === 'full-stream'
|
|
331
|
+
? 'fullStream can only be accessed once.'
|
|
332
|
+
: 'fullStream cannot be accessed after a result promise.',
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
streamOwner = 'full-stream';
|
|
336
|
+
// Direct ownership preserves backpressure and cancellation: cancelling
|
|
337
|
+
// this stream reaches Transformer.cancel and aborts the model pipe.
|
|
338
|
+
return asAsyncIterableStream(transform.readable);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
get sourceText() {
|
|
343
|
+
consumeStream();
|
|
344
|
+
return sourceTextPromise.promise;
|
|
345
|
+
},
|
|
346
|
+
get translationText() {
|
|
347
|
+
consumeStream();
|
|
348
|
+
return translationTextPromise.promise;
|
|
349
|
+
},
|
|
350
|
+
get durationInSeconds() {
|
|
351
|
+
consumeStream();
|
|
352
|
+
return durationInSecondsPromise.promise;
|
|
353
|
+
},
|
|
354
|
+
get usage() {
|
|
355
|
+
consumeStream();
|
|
356
|
+
return usagePromise.promise;
|
|
357
|
+
},
|
|
358
|
+
get warnings() {
|
|
359
|
+
consumeStream();
|
|
360
|
+
return warningsPromise.promise;
|
|
361
|
+
},
|
|
362
|
+
get response() {
|
|
363
|
+
consumeStream();
|
|
364
|
+
return responsePromise.promise;
|
|
365
|
+
},
|
|
366
|
+
get providerMetadata() {
|
|
367
|
+
consumeStream();
|
|
368
|
+
return providerMetadataPromise.promise;
|
|
369
|
+
},
|
|
370
|
+
get fullStream() {
|
|
371
|
+
return getFullStream();
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response metadata for speech translation model calls.
|
|
3
|
+
*
|
|
4
|
+
* Experimental: part of the experimental speech translation modality and may
|
|
5
|
+
* change in patch releases.
|
|
6
|
+
*/
|
|
7
|
+
export type SpeechTranslationModelResponseMetadata = {
|
|
8
|
+
/**
|
|
9
|
+
* Timestamp for the start of the generated response.
|
|
10
|
+
*/
|
|
11
|
+
timestamp: Date;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The ID of the response model that was used to generate the response.
|
|
15
|
+
*/
|
|
16
|
+
modelId: string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Response headers.
|
|
20
|
+
*/
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Experimental_SpeechTranslationModelV4 } from '@ai-sdk/provider';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Speech translation model that is used by the AI SDK.
|
|
5
|
+
*
|
|
6
|
+
* Experimental: part of the experimental speech translation modality and may
|
|
7
|
+
* change in patch releases.
|
|
8
|
+
*/
|
|
9
|
+
export type SpeechTranslationModel =
|
|
10
|
+
| string
|
|
11
|
+
| Experimental_SpeechTranslationModelV4;
|