Skip to main content

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.ts
ts
import {$} 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.

AltStyle によって変換されたページ (->オリジナル) /