@ai-sdk/provider-utils 5.0.11 → 5.0.13
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 +17 -0
- package/dist/index.js +47 -20
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/detect-media-type.ts +68 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/provider-utils",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@standard-schema/spec": "^1.1.0",
|
|
36
36
|
"@workflow/serde": "4.1.0",
|
|
37
37
|
"eventsource-parser": "^3.0.8",
|
|
38
|
-
"@ai-sdk/provider": "4.0.
|
|
38
|
+
"@ai-sdk/provider": "4.0.4"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/node": "22.19.19",
|
package/src/detect-media-type.ts
CHANGED
|
@@ -63,7 +63,7 @@ const documentMediaTypeSignatures = [
|
|
|
63
63
|
},
|
|
64
64
|
] as const;
|
|
65
65
|
|
|
66
|
-
const
|
|
66
|
+
const audioMediaTypeSignaturesWithoutMp4 = [
|
|
67
67
|
{
|
|
68
68
|
mediaType: 'audio/mpeg' as const,
|
|
69
69
|
bytesPrefix: [0xff, 0xfb],
|
|
@@ -117,16 +117,29 @@ const audioMediaTypeSignatures = [
|
|
|
117
117
|
mediaType: 'audio/aac' as const,
|
|
118
118
|
bytesPrefix: [0x40, 0x15, 0x00, 0x00],
|
|
119
119
|
},
|
|
120
|
-
{
|
|
121
|
-
mediaType: 'audio/mp4' as const,
|
|
122
|
-
bytesPrefix: [0x66, 0x74, 0x79, 0x70],
|
|
123
|
-
},
|
|
124
120
|
{
|
|
125
121
|
mediaType: 'audio/webm',
|
|
126
122
|
bytesPrefix: [0x1a, 0x45, 0xdf, 0xa3],
|
|
127
123
|
},
|
|
128
124
|
] as const;
|
|
129
125
|
|
|
126
|
+
const audioMediaTypeSignatures = [
|
|
127
|
+
...audioMediaTypeSignaturesWithoutMp4,
|
|
128
|
+
{
|
|
129
|
+
mediaType: 'audio/mp4' as const,
|
|
130
|
+
bytesPrefix: [
|
|
131
|
+
0x00,
|
|
132
|
+
0x00,
|
|
133
|
+
0x00,
|
|
134
|
+
null,
|
|
135
|
+
0x66,
|
|
136
|
+
0x74,
|
|
137
|
+
0x79,
|
|
138
|
+
0x70, // ftyp
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
] as const;
|
|
142
|
+
|
|
130
143
|
const videoMediaTypeSignatures = [
|
|
131
144
|
{
|
|
132
145
|
mediaType: 'video/mp4' as const,
|
|
@@ -166,31 +179,54 @@ const videoMediaTypeSignatures = [
|
|
|
166
179
|
},
|
|
167
180
|
] as const;
|
|
168
181
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
182
|
+
const DEFAULT_SNIFF_BYTES = 18;
|
|
183
|
+
|
|
184
|
+
// Longest signature prefix in the tables above (e.g. image/avif = 12 bytes).
|
|
185
|
+
const MAX_SIGNATURE_BYTES = 12;
|
|
186
|
+
|
|
187
|
+
// Largest ID3v2 tag (10-byte header + body) skipped to reach the audio frame.
|
|
188
|
+
// Covers typical tags including embedded cover art while keeping the decode
|
|
189
|
+
// bounded and O(1) in the attachment size. Exported for boundary tests.
|
|
190
|
+
export const MAX_ID3_TAG_BYTES = 128 * 1024;
|
|
191
|
+
|
|
192
|
+
// Total prefix decoded when an ID3 tag is present: the tag plus room for the
|
|
193
|
+
// trailing signature, so a tag right at the size limit stays detectable.
|
|
194
|
+
const ID3_SCAN_BYTES = MAX_ID3_TAG_BYTES + MAX_SIGNATURE_BYTES;
|
|
195
|
+
|
|
196
|
+
// Decode/view exactly the first `maxBytes` bytes from the front of the input.
|
|
197
|
+
// The base64 and raw-byte paths yield the same length, so detection does not
|
|
198
|
+
// depend on the input's representation.
|
|
199
|
+
function decodePrefix(data: Uint8Array | string, maxBytes: number): Uint8Array {
|
|
200
|
+
if (typeof data !== 'string') {
|
|
201
|
+
return data.length > maxBytes ? data.subarray(0, maxBytes) : data;
|
|
202
|
+
}
|
|
203
|
+
// base64: 4 chars -> 3 bytes. Decode whole 4-char groups, then trim the 0-2
|
|
204
|
+
// extra bytes so the result matches the raw-byte path exactly.
|
|
205
|
+
const maxChars = Math.ceil(maxBytes / 3) * 4;
|
|
206
|
+
const bytes = convertBase64ToUint8Array(
|
|
207
|
+
data.substring(0, Math.min(data.length, maxChars)),
|
|
208
|
+
);
|
|
209
|
+
return bytes.length > maxBytes ? bytes.subarray(0, maxBytes) : bytes;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function hasID3(bytes: Uint8Array): boolean {
|
|
213
|
+
return (
|
|
214
|
+
bytes.length > 10 &&
|
|
215
|
+
bytes[0] === 0x49 && // 'I'
|
|
216
|
+
bytes[1] === 0x44 && // 'D'
|
|
217
|
+
bytes[2] === 0x33 // '3'
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const stripID3 = (bytes: Uint8Array): Uint8Array => {
|
|
172
222
|
const id3Size =
|
|
173
223
|
((bytes[6] & 0x7f) << 21) |
|
|
174
224
|
((bytes[7] & 0x7f) << 14) |
|
|
175
225
|
((bytes[8] & 0x7f) << 7) |
|
|
176
226
|
(bytes[9] & 0x7f);
|
|
177
|
-
|
|
178
|
-
// The raw MP3 starts here
|
|
179
|
-
return bytes.slice(id3Size + 10);
|
|
227
|
+
return bytes.subarray(id3Size + 10);
|
|
180
228
|
};
|
|
181
229
|
|
|
182
|
-
function stripID3TagsIfPresent(data: Uint8Array | string): Uint8Array | string {
|
|
183
|
-
const hasId3 =
|
|
184
|
-
(typeof data === 'string' && data.startsWith('SUQz')) ||
|
|
185
|
-
(typeof data !== 'string' &&
|
|
186
|
-
data.length > 10 &&
|
|
187
|
-
data[0] === 0x49 && // 'I'
|
|
188
|
-
data[1] === 0x44 && // 'D'
|
|
189
|
-
data[2] === 0x33); // '3'
|
|
190
|
-
|
|
191
|
-
return hasId3 ? stripID3(data) : data;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
230
|
type MediaTypeSignatures = ReadonlyArray<{
|
|
195
231
|
readonly mediaType: string;
|
|
196
232
|
readonly bytesPrefix: ReadonlyArray<number | null>;
|
|
@@ -203,15 +239,13 @@ function detectMediaTypeBySignatures<T extends MediaTypeSignatures>({
|
|
|
203
239
|
data: Uint8Array | string;
|
|
204
240
|
signatures: T;
|
|
205
241
|
}): T[number]['mediaType'] | undefined {
|
|
206
|
-
|
|
242
|
+
let bytes = decodePrefix(data, DEFAULT_SNIFF_BYTES);
|
|
207
243
|
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
)
|
|
214
|
-
: processedData;
|
|
244
|
+
// ID3v2-tagged MP3s carry the audio frame after the tag; scan a bounded
|
|
245
|
+
// prefix past it rather than decoding the whole input.
|
|
246
|
+
if (hasID3(bytes)) {
|
|
247
|
+
bytes = stripID3(decodePrefix(data, ID3_SCAN_BYTES));
|
|
248
|
+
}
|
|
215
249
|
|
|
216
250
|
for (const signature of signatures) {
|
|
217
251
|
if (
|
|
@@ -259,7 +293,9 @@ export function detectMediaType({
|
|
|
259
293
|
signatures: [
|
|
260
294
|
...imageMediaTypeSignatures,
|
|
261
295
|
...documentMediaTypeSignatures,
|
|
262
|
-
|
|
296
|
+
// MP4 containers cannot be distinguished as audio or video by ftyp alone.
|
|
297
|
+
// Preserve the generic detection result as video/mp4.
|
|
298
|
+
...audioMediaTypeSignaturesWithoutMp4,
|
|
263
299
|
...videoMediaTypeSignatures,
|
|
264
300
|
],
|
|
265
301
|
});
|