Experiments
This page aims to collect experiments and hacks that were made to the Remotion Recorder
Swear word detection
Here is a demonstration by Matt McGillivray of how swear words can be automatically censored and replaced with a sound effect.
AI audio enhancement
Another experiment by Matt McGillivray is this one where he uses the ai|coustics API to enhance the audio of his recording.
enhance-audio.tstsimport {$} from'bun';import {WEBCAM_PREFIX} from'./config/cameras';constAPI_URL='https://api.ai-coustics.com/v1';constAPI_KEY= process.env.AI_COUSTICS_API_KEY;if (!API_KEY) {console.error('AI_COUSTICS_API_KEY environment variable is required');process.exit(1);}asyncfunctionuploadAndEnhance(audioBuffer:ArrayBuffer,fileName:string,options: {loudness_target_level?:number;loudness_peak_limit?:number;enhancement_level?:number;transcode_kind?:string;} = {},) {const {loudness_target_level=-14, loudness_peak_limit=-1, enhancement_level=0.7, transcode_kind='MP3'} = options;constformData=newFormData();formData.append('loudness_target_level', loudness_target_level.toString());formData.append('loudness_peak_limit', loudness_peak_limit.toString());formData.append('enhancement_level', enhancement_level.toString());formData.append('transcode_kind', transcode_kind);formData.append('model_arch', 'FINCH');constaudioBlob=newBlob([audioBuffer], {type: 'application/octet-stream',});formData.append('file', audioBlob, fileName);if (!API_KEY) {thrownewError('API_KEY is undefined');}try {constresponse=awaitfetch(`${API_URL}/media/enhance`, {method: 'POST',headers: {'X-API-Key': API_KEY,},body: formData,});if (response.status !==201) {constresponseText=await response.text();thrownewError(`API error: ${responseText}`);}constresponseJson=await response.json();constgeneratedName= responseJson.generated_name;console.log(`Uploaded file's generated name: ${generatedName}`);return generatedName;} catch (error) {thrownewError(`Failed to enhance audio: ${error}`);}}asyncfunctiondownloadEnhancedMedia(generatedName:string, outputFilePath:string, maxRetries=60, retryDelayMs=5000) {consturl=`${API_URL}/media/${generatedName}`;if (!API_KEY) {thrownewError('API_KEY is undefined');}for (let attempt =1; attempt <= maxRetries; attempt++) {try {constresponse=awaitfetch(url, {headers: {'X-API-Key': API_KEY,},});if (response.status ===200) {constarrayBuffer=await response.arrayBuffer();await Bun.write(outputFilePath, newUint8Array(arrayBuffer));console.log(`✓ Downloaded enhanced audio to: ${outputFilePath}`);return;} elseif (response.status ===202) {console.log(`⏳ Audio still processing... (attempt ${attempt}/${maxRetries})`);if (attempt < maxRetries) {awaitnewPromise((resolve) =>setTimeout(resolve, retryDelayMs));continue;}} else {constresponseText=await response.text();thrownewError(`API error: ${responseText}`);}} catch (error) {if (attempt === maxRetries) {thrownewError(`Failed to download after ${maxRetries} attempts: ${error}`);}console.log(`⚠️ Download attempt ${attempt} failed, retrying...`);awaitnewPromise((resolve) =>setTimeout(resolve, retryDelayMs));}}thrownewError(`Audio still processing after ${(maxRetries*retryDelayMs) /1000} seconds`);}asyncfunctionextractAudioForAPI(videoPath:string,options: {outputFormat?:'mp3';bitrate?:number;sampleRate?:number;} = {},) {const {outputFormat='mp3', bitrate=128, sampleRate=44100} = options;constfileName=videoPath.split('/').pop()?.replace(/\.[^/.]+$/, '') ||'audio';constoutputDir= videoPath.replace(/\/[^/]+$/, '/audio');constoutputPath=`${outputDir}/${fileName}.${outputFormat}`;await$`mkdir -p ${outputDir}`.quiet();try {await$`ffmpeg -hide_banner -i ${videoPath} -vn -acodec libmp3lame -ab ${bitrate}k -ar ${sampleRate} ${outputPath} -y`.quiet();constaudioBuffer=await Bun.file(outputPath).arrayBuffer();return {audioBuffer, outputPath, fileName: `${fileName}.${outputFormat}`};} catch (error) {thrownewError(`Failed to extract audio from ${videoPath}: ${error}`);}}asyncfunctionreplaceAudioInVideo(originalVideoPath:string, enhancedAudioPath:string, outputVideoPath:string) {try {await$`ffmpeg -hide_banner -i ${originalVideoPath} -i ${enhancedAudioPath} -c:v copy -c:a libopus -map 0:v:0 -map 1:a:0 ${outputVideoPath} -y`;console.log(`✓ Replaced audio in video: ${outputVideoPath}`);} catch (error) {console.error('FFmpeg stderr:', error.stderr?.toString());console.error('FFmpeg stdout:', error.stdout?.toString());thrownewError(`Failed to replace audio in video: ${error}`);}}constid= process.argv[2];if (!id) {console.error('Please provide a composition ID');console.error('Usage: bun enhanceAudio.ts <composition-id>');process.exit(1);}constfiles=await$`ls public/${id}`.quiet();constwebcamFiles= files.stdout.toString('utf8').split('\n').filter((f) => f.startsWith(WEBCAM_PREFIX));if (webcamFiles.length===0) {console.log(`No webcam files found in public/${id}`);process.exit(0);}console.log(`Found ${webcamFiles.length} webcam files to process`);constrawDir=`public/${id}/raw`;await$`mkdir -p ${rawDir}`.quiet();for (constfileof webcamFiles) {constvideoPath=`public/${id}/${file}`;constrawVideoPath=`${rawDir}/${file}`;console.log(`Processing ${file}...`);try {await$`cp ${videoPath} ${rawVideoPath}`.quiet();console.log(`✓ Backed up original to raw/`);const {audioBuffer, outputPath, fileName} =awaitextractAudioForAPI(videoPath);console.log(`✓ Extracted audio: ${outputPath} (${audioBuffer.byteLength} bytes)`);console.log(`Enhancing audio with AI-coustics...`);constgeneratedName=awaituploadAndEnhance(audioBuffer, fileName);console.log(`✓ Enhanced audio uploaded: ${generatedName}`);constenhancedOutputPath= outputPath.replace('.mp3', '_enhanced.mp3');awaitdownloadEnhancedMedia(generatedName, enhancedOutputPath);awaitreplaceAudioInVideo(rawVideoPath, enhancedOutputPath, videoPath);await$`rm ${outputPath}`.quiet();await$`rm ${enhancedOutputPath}`.quiet();console.log(`✓ Cleaned up temporary audio files`);} catch (error) {console.error(`✗ Failed to process ${file}:`, error);}}
Code Hike integration
In a branch, we experimented with using Code snippets instead of videos as a source for the display.