Skip to content

fix(utils/detect-mimetype): add support for detecting id3 tags (#5737) #5822

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/gentle-toys-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'ai': patch
---

fix(utils/detect-mimetype): add support for detecting id3 tags
75 changes: 75 additions & 0 deletions packages/ai/core/util/detect-media-type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
detectMediaType,
imageMediaTypeSignatures,
} from './detect-media-type';
import { convertUint8ArrayToBase64 } from '@ai-sdk/provider-utils';

describe('detectMediaType', () => {
describe('GIF', () => {
Expand Down Expand Up @@ -228,6 +229,80 @@ describe('detectMediaType', () => {
}),
).toBe('audio/mpeg');
});

it('should detect MP3 with ID3v2 tags from bytes', () => {
const mp3WithID3Bytes = new Uint8Array([
0x49,
0x44,
0x33, // 'ID3'
0x03,
0x00, // version
0x00, // flags
0x00,
0x00,
0x00,
0x0a, // size (10 bytes)
// 10 bytes of ID3 data
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
// MP3 frame header
0xff,
0xfb,
0x00,
0x00,
]);
expect(
detectMediaType({
data: mp3WithID3Bytes,
signatures: audioMediaTypeSignatures,
}),
).toBe('audio/mpeg');
});
it('should detect MP3 with ID3v2 tags from base64', () => {
const mp3WithID3Bytes = new Uint8Array([
0x49,
0x44,
0x33, // 'ID3'
0x03,
0x00, // version
0x00, // flags
0x00,
0x00,
0x00,
0x0a, // size (10 bytes)
// 10 bytes of ID3 data
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
// MP3 frame header
0xff,
0xfb,
0x00,
0x00,
]);
const mp3WithID3Base64 = convertUint8ArrayToBase64(mp3WithID3Bytes);
expect(
detectMediaType({
data: mp3WithID3Base64,
signatures: audioMediaTypeSignatures,
}),
).toBe('audio/mpeg');
});
});

describe('WAV', () => {
Expand Down
39 changes: 35 additions & 4 deletions packages/ai/core/util/detect-media-type.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { convertBase64ToUint8Array } from '@ai-sdk/provider-utils';

export const imageMediaTypeSignatures = [
{
mediaType: 'image/gif' as const,
Expand Down Expand Up @@ -83,6 +85,31 @@ export const audioMediaTypeSignatures = [
},
] as const;

const stripID3 = (data: Uint8Array | string) => {
const bytes =
typeof data === 'string' ? convertBase64ToUint8Array(data) : data;
const id3Size =
((bytes[6] & 0x7f) << 21) |
((bytes[7] & 0x7f) << 14) |
((bytes[8] & 0x7f) << 7) |
(bytes[9] & 0x7f);

// The raw MP3 starts here
return bytes.slice(id3Size + 10);
};

function stripID3TagsIfPresent(data: Uint8Array | string): Uint8Array | string {
const hasId3 =
(typeof data === 'string' && data.startsWith('SUQz')) ||
(typeof data !== 'string' &&
data.length > 10 &&
data[0] === 0x49 && // 'I'
data[1] === 0x44 && // 'D'
data[2] === 0x33); // '3'

return hasId3 ? stripID3(data) : data;
}

/**
* Detect the media IANA media type of a file using a list of signatures.
*
Expand All @@ -97,12 +124,16 @@ export function detectMediaType({
data: Uint8Array | string;
signatures: typeof audioMediaTypeSignatures | typeof imageMediaTypeSignatures;
}): (typeof signatures)[number]['mediaType'] | undefined {
const processedData = stripID3TagsIfPresent(data);

for (const signature of signatures) {
if (
typeof data === 'string'
? data.startsWith(signature.base64Prefix)
: data.length >= signature.bytesPrefix.length &&
signature.bytesPrefix.every((byte, index) => data[index] === byte)
typeof processedData === 'string'
? processedData.startsWith(signature.base64Prefix)
: processedData.length >= signature.bytesPrefix.length &&
signature.bytesPrefix.every(
(byte, index) => processedData[index] === byte,
)
) {
return signature.mediaType;
}
Expand Down
Loading