Skip to content

Commit 3e10408

Browse files
samdentyhaydenbleasellgrammel
authored
fix(utils/detect-mimetype): add support for detecting id3 tags (#5737) (#5822)
Co-authored-by: Hayden Bleasel <[email protected]> Co-authored-by: Lars Grammel <[email protected]>
1 parent 06bac05 commit 3e10408

File tree

3 files changed

+115
-4
lines changed

3 files changed

+115
-4
lines changed

.changeset/gentle-toys-smile.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'ai': patch
3+
---
4+
5+
fix(utils/detect-mimetype): add support for detecting id3 tags

packages/ai/core/util/detect-media-type.test.ts

+75
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
detectMediaType,
55
imageMediaTypeSignatures,
66
} from './detect-media-type';
7+
import { convertUint8ArrayToBase64 } from '@ai-sdk/provider-utils';
78

89
describe('detectMediaType', () => {
910
describe('GIF', () => {
@@ -228,6 +229,80 @@ describe('detectMediaType', () => {
228229
}),
229230
).toBe('audio/mpeg');
230231
});
232+
233+
it('should detect MP3 with ID3v2 tags from bytes', () => {
234+
const mp3WithID3Bytes = new Uint8Array([
235+
0x49,
236+
0x44,
237+
0x33, // 'ID3'
238+
0x03,
239+
0x00, // version
240+
0x00, // flags
241+
0x00,
242+
0x00,
243+
0x00,
244+
0x0a, // size (10 bytes)
245+
// 10 bytes of ID3 data
246+
0x00,
247+
0x00,
248+
0x00,
249+
0x00,
250+
0x00,
251+
0x00,
252+
0x00,
253+
0x00,
254+
0x00,
255+
0x00,
256+
// MP3 frame header
257+
0xff,
258+
0xfb,
259+
0x00,
260+
0x00,
261+
]);
262+
expect(
263+
detectMediaType({
264+
data: mp3WithID3Bytes,
265+
signatures: audioMediaTypeSignatures,
266+
}),
267+
).toBe('audio/mpeg');
268+
});
269+
it('should detect MP3 with ID3v2 tags from base64', () => {
270+
const mp3WithID3Bytes = new Uint8Array([
271+
0x49,
272+
0x44,
273+
0x33, // 'ID3'
274+
0x03,
275+
0x00, // version
276+
0x00, // flags
277+
0x00,
278+
0x00,
279+
0x00,
280+
0x0a, // size (10 bytes)
281+
// 10 bytes of ID3 data
282+
0x00,
283+
0x00,
284+
0x00,
285+
0x00,
286+
0x00,
287+
0x00,
288+
0x00,
289+
0x00,
290+
0x00,
291+
0x00,
292+
// MP3 frame header
293+
0xff,
294+
0xfb,
295+
0x00,
296+
0x00,
297+
]);
298+
const mp3WithID3Base64 = convertUint8ArrayToBase64(mp3WithID3Bytes);
299+
expect(
300+
detectMediaType({
301+
data: mp3WithID3Base64,
302+
signatures: audioMediaTypeSignatures,
303+
}),
304+
).toBe('audio/mpeg');
305+
});
231306
});
232307

233308
describe('WAV', () => {

packages/ai/core/util/detect-media-type.ts

+35-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { convertBase64ToUint8Array } from '@ai-sdk/provider-utils';
2+
13
export const imageMediaTypeSignatures = [
24
{
35
mediaType: 'image/gif' as const,
@@ -83,6 +85,31 @@ export const audioMediaTypeSignatures = [
8385
},
8486
] as const;
8587

88+
const stripID3 = (data: Uint8Array | string) => {
89+
const bytes =
90+
typeof data === 'string' ? convertBase64ToUint8Array(data) : data;
91+
const id3Size =
92+
((bytes[6] & 0x7f) << 21) |
93+
((bytes[7] & 0x7f) << 14) |
94+
((bytes[8] & 0x7f) << 7) |
95+
(bytes[9] & 0x7f);
96+
97+
// The raw MP3 starts here
98+
return bytes.slice(id3Size + 10);
99+
};
100+
101+
function stripID3TagsIfPresent(data: Uint8Array | string): Uint8Array | string {
102+
const hasId3 =
103+
(typeof data === 'string' && data.startsWith('SUQz')) ||
104+
(typeof data !== 'string' &&
105+
data.length > 10 &&
106+
data[0] === 0x49 && // 'I'
107+
data[1] === 0x44 && // 'D'
108+
data[2] === 0x33); // '3'
109+
110+
return hasId3 ? stripID3(data) : data;
111+
}
112+
86113
/**
87114
* Detect the media IANA media type of a file using a list of signatures.
88115
*
@@ -97,12 +124,16 @@ export function detectMediaType({
97124
data: Uint8Array | string;
98125
signatures: typeof audioMediaTypeSignatures | typeof imageMediaTypeSignatures;
99126
}): (typeof signatures)[number]['mediaType'] | undefined {
127+
const processedData = stripID3TagsIfPresent(data);
128+
100129
for (const signature of signatures) {
101130
if (
102-
typeof data === 'string'
103-
? data.startsWith(signature.base64Prefix)
104-
: data.length >= signature.bytesPrefix.length &&
105-
signature.bytesPrefix.every((byte, index) => data[index] === byte)
131+
typeof processedData === 'string'
132+
? processedData.startsWith(signature.base64Prefix)
133+
: processedData.length >= signature.bytesPrefix.length &&
134+
signature.bytesPrefix.every(
135+
(byte, index) => processedData[index] === byte,
136+
)
106137
) {
107138
return signature.mediaType;
108139
}

0 commit comments

Comments
 (0)