REMOTION LOCAL VOICEOVER ON MAC: ORIGINAL TESTED WORKFLOW
Checked October 9, 2026
Guide: https://www.murmurtts.com/blog/resources/remotion-local-voiceover-mac
RESULT AND SCOPE
The original 7.12-second Murmur WAV rendered as 214 frames at 30 fps, 1080x1920,
H.264/yuv420p with AAC stereo 48,000 Hz. Video-track duration: 7.133333 seconds;
container duration: 7.147 seconds; file size: 395,762 bytes. Full audio/video
decode succeeded and representative frames were inspected. An independent
2.25-second input produced 68 frames and a 2.266667-second video track.
The original speech was generated earlier by a development helper reporting
Murmur 1.0.18, Qwen3-TTS Base / Ryan; released-binary parity was not verified.
No assistant model was invoked. Labels summarize the workflow and are not
word-aligned captions. No networking-disabled or listener-preference test ran.
REQUIREMENTS
Murmur setup/license and local automation ready on an Apple Silicon Mac with
macOS 15+. Separately, Node.js/npm plus ffprobe and ffmpeg available in Terminal.
The render environment used Node 26.10.0, npm 11.19.1 and FFmpeg 9.0.2.
All Remotion packages are pinned 4.0.534; React/react-dom 19.3.0.
Initial npm installation and Remotion browser download need network access.
This example references only local narration and original text. Keep the public
asset folder local; exclude private audio before sharing source or web assets.
1. NEW FOLDER AND ORIGINAL NARRATION
Run these commands one step at a time in a fresh directory. Stop if mkdir fails
or the folder already exists; do not continue into an existing project. Do not
paste this entire document as a shell script. Select an installed model and its
own preset voice from your Mac; the fixture's model/voice are not universal defaults.
mkdir remotion-voiceover-01
cd remotion-voiceover-01
mkdir src public
murmur status --json
murmur models --json
murmur voices --model qwen3-base --json
cat > script.txt <<'SCRIPT'
Start with one clear idea. Write a short script. Generate the voice on your Mac, then build the motion around it.
SCRIPT
murmur generate --input script.txt --model qwen3-base --voice Ryan \
--language EN-US --speed 1.0 --output narration.wav --json
cp -n narration.wav public/narration.wav
Alternatively export your approved WAV in Murmur and copy it to
public/narration.wav in the fresh folder. Measure your actual take below.
2. PIN THE RENDER DEPENDENCIES
cat > package.json <<'JSON'
{
"name": "local-remotion-voiceover-fixture",
"version": "1.0.0",
"private": true,
"scripts": {
"preview": "remotion studio src/index.tsx --props=props.json",
"render": "remotion render src/index.tsx LocalVoiceover local-voiceover.mp4 --props=props.json --codec=h264 --audio-codec=aac --pixel-format=yuv420p --concurrency=2 --overwrite=false"
},
"dependencies": {
"@remotion/cli": "4.0.534",
"@remotion/media": "4.0.534",
"remotion": "4.0.534",
"react": "19.3.0",
"react-dom": "19.3.0"
}
}
JSON
npm install --registry=https://registry.npmjs.org --no-audit --no-fund
Keep the generated lockfile with reusable project source. Use npm ci for later
installs from that lockfile. No cloud renderer or speech API is configured here.
3. SAVE THE DURATION PREPARATION SCRIPT
The helper rejects an invalid duration and refuses to replace props.json.
cat > prepare.mjs <<'JS'
import {execFileSync} from 'node:child_process';
import {writeFileSync} from 'node:fs';
const durationSeconds = Number(execFileSync('ffprobe', [
'-v', 'error', '-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1', 'public/narration.wav'
], {encoding: 'utf8'}).trim());
if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) {
throw new Error('Narration must have a positive finite duration.');
}
const props = {audioFile: 'narration.wav', durationSeconds};
writeFileSync('props.json', JSON.stringify(props, null, 2) + '\n', {flag: 'wx'});
console.log(JSON.stringify({...props, fps: 30, durationInFrames: Math.ceil(durationSeconds * 30)}));
JS
4. SAVE THE ORIGINAL REACT COMPOSITION
Audio starts at zero and stays mounted across visual stages. Motion reads the
frame number, so it is deterministic when Remotion seeks directly to a frame.
The three equal stages are a small integration example; use actual cue points
and checked captions for a public product demo.
cat > src/index.tsx <<'TSX'
import React from 'react';
import {Audio} from '@remotion/media';
import {AbsoluteFill, Composition, interpolate, registerRoot, staticFile, useCurrentFrame, useVideoConfig} from 'remotion';
type Props = {audioFile: string; durationSeconds: number};
const fps = 30;
const LocalVoiceover = ({audioFile}: Props) => {
const frame = useCurrentFrame();
const {durationInFrames} = useVideoConfig();
const progress = frame / Math.max(1, durationInFrames - 1);
const stage = Math.min(2, Math.floor(progress * 3));
const stageStart = Math.floor(stage * durationInFrames / 3);
const entrance = interpolate(frame - stageStart, [0, 12], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp'
});
const headings = ['WRITE', 'VOICE', 'MOTION'];
const details = ['Start with one clear idea.', 'Narration from your Mac.', 'Build around the real duration.'];
return ;
};
const Root = () => {
if (!Number.isFinite(props.durationSeconds) || props.durationSeconds <= 0) {
throw new Error('Measure a valid narration duration before rendering.');
}
return {durationInFrames: Math.ceil(props.durationSeconds * fps)};
}}
/>;
registerRoot(Root);
TSX
5. PREPARE, RENDER, AND VERIFY
node prepare.mjs
npm run render
ffprobe -v error -count_frames -show_format -show_streams \
-of json local-voiceover.mp4
ffmpeg -hide_banner -loglevel error -i local-voiceover.mp4 -f null -
node prepare.mjs prints the measured seconds and ceil(seconds * 30) frames.
For the original input: 7.12 seconds -> 214 frames. The composition recalculates
this from props in calculateMetadata. The default 30-frame duration is only a
placeholder; don't replace the measured props with a guessed duration.
Check a video stream with width1080, height1920, fps30/1, codec h264 and the
calculated nb_read_frames. Check an AAC audio stream too. AAC padding can make
the container slightly longer than the 214/30 video-track duration; compare the
individual streams. FFmpeg full decode should return exit0. Watch/listen to
pronunciation, last word, readability, timing and captions before publication.
Your file size can differ. The original source WAV remains separate and intact.
npm run preview opens Remotion Studio with props.json for local editing. Studio
preview was not the completion check here: the actual MP4 export was decoded.
The render script passes --overwrite=false because Remotion defaults to
replacing existing output. A second render correctly refused the existing movie
in our check and left its bytes unchanged. For another version, use a fresh
folder or intentionally choose new source/props/output filenames together.
6. OPTIONAL ASSISTANT PROMPT
Edit this Remotion composition for [product] and [viewer]. Keep narration as an
approved local WAV. Use only verified product claims and original assets in this
workspace. Check Murmur readiness and installed model/voice IDs if a new take is
needed. Save each take under a new name, measure its real duration, and pass that
measurement into calculateMetadata. Keep Audio mounted while visual scenes
change. Render a 1080x1920 H.264/AAC MP4 without replacing existing output, then
return the source, props, ffprobe metadata and full-decode result. I will review
pronunciation, scene cues and captions before publishing.
That prompt needs an agent host with local file/terminal or MCP access. This
fixture did not run Claude Opus 5.5, GPT-6 Astra, or another assistant model.
Hosted assistant charges and data handling are separate from local speech.
LICENSE AND PURCHASE SCOPE
Remotion is not free for every organization. Check the exact published license
and current pricing/collaboration rules for your use case. Murmur's website
edition is $49 one-time with no free trial and a 7-day refund policy. Local
speech has no per-character credits on a licensed install; finite Mac resources,
any assistant/API charges and renderer licensing remain separate.
CHECKED PRIMARY REFERENCES
https://www.remotion.dev/docs/media/audio
https://www.remotion.dev/docs/calculate-metadata
https://www.remotion.dev/docs/cli/render
https://www.remotion.dev/docs/license/pricing
https://github.com/remotion-dev/remotion/blob/main/LICENSE.md
https://ffmpeg.org/ffprobe.html
https://www.murmurtts.com/docs/automation
https://www.murmurtts.com/