feat: add audio extraction tool to convert video files to audio formats (AAC, MP3, WAV)

This commit is contained in:
AshAnand34
2025-07-07 14:49:14 -07:00
parent 816a098971
commit 76245edd34
9 changed files with 315 additions and 3 deletions

View File

@@ -0,0 +1,46 @@
import React, { useRef } from 'react';
import { Box, Typography } from '@mui/material';
import BaseFileInput from './BaseFileInput';
import { BaseFileInputProps } from './file-input-utils';
interface AudioFileInputProps extends Omit<BaseFileInputProps, 'accept'> {
accept?: string[];
}
export default function ToolAudioInput({
accept = ['audio/*', '.mp3', '.wav', '.aac'],
...props
}: AudioFileInputProps) {
const audioRef = useRef<HTMLAudioElement>(null);
return (
<BaseFileInput {...props} type={'audio'} accept={accept}>
{({ preview }) => (
<Box
sx={{
position: 'relative',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
}}
>
{preview ? (
<audio
ref={audioRef}
src={preview}
style={{ maxWidth: '100%' }}
controls
/>
) : (
<Typography variant="body2" color="textSecondary">
Drag & drop or import an audio file
</Typography>
)}
</Box>
)}
</BaseFileInput>
);
}

View File

@@ -0,0 +1,50 @@
import { describe, it, expect, vi, beforeAll } from 'vitest';
// Mock the service module BEFORE importing it
vi.mock('./service', () => ({
extractAudioFromVideo: vi.fn(async (input, options) => {
const ext = options.outputFormat;
return new File([new Blob(['audio data'])], `mock_audio.${ext}`, {
type: `audio/${ext}`
});
})
}));
import { extractAudioFromVideo } from './service';
import { InitialValuesType } from './types';
function createMockVideoFile(): File {
return new File(['video data'], 'test.mp4', { type: 'video/mp4' });
}
describe('extractAudioFromVideo (mocked)', () => {
let videoFile: File;
beforeAll(() => {
videoFile = createMockVideoFile();
});
it('should extract audio as AAC', async () => {
const options: InitialValuesType = { outputFormat: 'aac' };
const audioFile = await extractAudioFromVideo(videoFile, options);
expect(audioFile).toBeInstanceOf(File);
expect(audioFile.name.endsWith('.aac')).toBe(true);
expect(audioFile.type).toBe('audio/aac');
});
it('should extract audio as MP3', async () => {
const options: InitialValuesType = { outputFormat: 'mp3' };
const audioFile = await extractAudioFromVideo(videoFile, options);
expect(audioFile).toBeInstanceOf(File);
expect(audioFile.name.endsWith('.mp3')).toBe(true);
expect(audioFile.type).toBe('audio/mp3');
});
it('should extract audio as WAV', async () => {
const options: InitialValuesType = { outputFormat: 'wav' };
const audioFile = await extractAudioFromVideo(videoFile, options);
expect(audioFile).toBeInstanceOf(File);
expect(audioFile.name.endsWith('.wav')).toBe(true);
expect(audioFile.type).toBe('audio/wav');
});
});

View File

@@ -0,0 +1,105 @@
import {
Box,
FormControl,
InputLabel,
MenuItem,
Select,
SelectChangeEvent
} from '@mui/material';
import React, { useState, useEffect, useRef } from 'react';
import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool';
import { extractAudioFromVideo } from './service';
import { InitialValuesType } from './types';
import ToolVideoInput from '@components/input/ToolVideoInput';
import { GetGroupsType } from '@components/options/ToolOptions';
import ToolFileResult from '@components/result/ToolFileResult';
import SelectWithDesc from '@components/options/SelectWithDesc';
const initialValues: InitialValuesType = {
outputFormat: 'aac'
};
export default function ExtractAudio({
title,
longDescription
}: ToolComponentProps) {
const [file, setFile] = useState<File | null>(null);
const [audioFile, setAudioFile] = useState<File | null>(null);
const [loading, setLoading] = useState(false);
// Tool Options section for output format
const getGroups: GetGroupsType<InitialValuesType> = ({
values,
updateField
}) => {
return [
{
title: 'Output Format',
component: (
<Box>
<SelectWithDesc
selected={values.outputFormat}
onChange={(value) => {
updateField('outputFormat', value.toString());
}}
options={[
{ label: 'AAC', value: 'aac' },
{ label: 'MP3', value: 'mp3' },
{ label: 'WAV', value: 'wav' }
]}
description={
'Select the format for the audio to be extracted as.'
}
/>
</Box>
)
}
];
};
// Compute function for ToolContent (no-op, extraction is handled by effect)
const compute = async (values: InitialValuesType, input: File | null) => {
if (!input) return;
try {
setLoading(true);
const audioFileObj = await extractAudioFromVideo(input, values);
await setAudioFile(audioFileObj);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
return (
<ToolContent
title={title}
input={file}
inputComponent={
<ToolVideoInput value={file} onChange={setFile} title={'Input Video'} />
}
resultComponent={
loading ? (
<ToolFileResult
title={'Extracting Audio'}
value={null}
extension={''}
loading={true}
/>
) : (
<ToolFileResult
title={'Extracted Audio'}
value={audioFile}
extension={initialValues.outputFormat}
/>
)
}
initialValues={initialValues}
getGroups={getGroups}
compute={compute}
toolInfo={{ title: `What is ${title}?`, description: longDescription }}
setInput={setFile}
/>
);
}

View File

@@ -0,0 +1,26 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('audio', {
name: 'Extract audio',
path: 'extract-audio',
icon: 'mdi:music-note',
description:
'Extract the audio track from a video file and save it as a separate audio file in your chosen format (AAC, MP3, WAV).',
shortDescription:
'Extract audio from video files (MP4, MOV, etc.) to AAC, MP3, or WAV.',
keywords: [
'extract',
'audio',
'video',
'mp3',
'aac',
'wav',
'audio extraction',
'media',
'convert'
],
longDescription:
'This tool allows you to extract the audio track from a video file (such as MP4, MOV, AVI, etc.) and save it as a standalone audio file in your preferred format (AAC, MP3, or WAV). Useful for podcasts, music, or any scenario where you need just the audio from a video.',
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,70 @@
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile } from '@ffmpeg/util';
import { InitialValuesType } from './types';
const ffmpeg = new FFmpeg();
export async function extractAudioFromVideo(
input: File,
options: InitialValuesType
): Promise<File> {
if (!ffmpeg.loaded) {
await ffmpeg.load({
wasmURL:
'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.9/dist/esm/ffmpeg-core.wasm'
});
}
const inputName = 'input.mp4';
await ffmpeg.writeFile(inputName, await fetchFile(input));
const configuredOutputAudioFormat = options.outputFormat;
const outputName = `output.${configuredOutputAudioFormat}`;
let args: string[] = ['-i', inputName, '-vn'];
if (configuredOutputAudioFormat === 'mp3') {
args.push(
'-ar',
'44100',
'-ac',
'2',
'-b:a',
'192k',
'-f',
'mp3',
outputName
);
} else if (configuredOutputAudioFormat === 'wav') {
args.push(
'-acodec',
'pcm_s16le',
'-ar',
'44100',
'-ac',
'2',
'-f',
'wav',
outputName
);
} else {
// Default to AAC or copy
args.push('-acodec', 'copy', outputName);
}
await ffmpeg.exec(args);
const extractedAudio = await ffmpeg.readFile(outputName);
return new File(
[
new Blob([extractedAudio], {
type: `audio/${configuredOutputAudioFormat}`
})
],
`${input.name.replace(
/\.[^/.]+$/,
''
)}_audio.${configuredOutputAudioFormat}`,
{ type: `audio/${configuredOutputAudioFormat}` }
);
}

View File

@@ -0,0 +1,3 @@
export type InitialValuesType = {
outputFormat: string;
};

View File

@@ -0,0 +1,2 @@
import { tool as audioExtractAudio } from './extract-audio/meta';
export const audioTools = [audioExtractAudio];

View File

@@ -24,7 +24,8 @@ export type ToolCategory =
| 'time'
| 'csv'
| 'pdf'
| 'image-generic';
| 'image-generic'
| 'audio';
export interface DefinedTool {
type: ToolCategory;

View File

@@ -4,6 +4,7 @@ import { DefinedTool, ToolCategory } from './defineTool';
import { capitalizeFirstLetter } from '../utils/string';
import { numberTools } from '../pages/tools/number';
import { videoTools } from '../pages/tools/video';
import { audioTools } from 'pages/tools/audio';
import { listTools } from '../pages/tools/list';
import { Entries } from 'type-fest';
import { jsonTools } from '../pages/tools/json';
@@ -23,7 +24,8 @@ const toolCategoriesOrder: ToolCategory[] = [
'number',
'png',
'time',
'gif'
'gif',
'audio'
];
export const tools: DefinedTool[] = [
...imageTools,
@@ -34,7 +36,8 @@ export const tools: DefinedTool[] = [
...csvTools,
...videoTools,
...numberTools,
...timeTools
...timeTools,
...audioTools
];
const categoriesConfig: {
type: ToolCategory;
@@ -115,6 +118,12 @@ const categoriesConfig: {
icon: 'material-symbols-light:image-outline-rounded',
value:
'Tools for working with pictures compress, resize, crop, convert to JPG, rotate, remove background and much more.'
},
{
type: 'audio',
icon: 'rivet-icons:audio',
value:
'Tools for working with audio extract audio from video, adjusting audio speed, merging multiple audio files and much more.'
}
];
// use for changelogs