npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ffmpeg-framecraft

v1.0.4

Published

FFmpeg-based video processing: crop, slice, transitions, subtitles, audio extraction

Readme

ffmpeg-framecraft – FFmpeg toolkit for 9:16 & short-form video

A Node.js-friendly FFmpeg toolkit for video processing: crop to 9:16, slice by time, add transitions between clips, subtitles, background music, and extract audio. Designed for vertical/short-form output (e.g. YouTube Shorts, TikTok, Reels) and general video pipelines.


Table of contents


Prerequisites

FFmpeg and ffprobe must be installed:

| Platform | Command | |-----------|--------| | macOS | brew install ffmpeg | | Ubuntu/Debian | sudo apt install ffmpeg | | Windows | FFmpeg download |

If the binaries are not in PATH, set:

  • FFMPEG_PATH — path to ffmpeg
  • FFPROBE_PATH — path to ffprobe

Installation

npm install ffmpeg-framecraft

Then:

const { FramecraftEngine } = require('ffmpeg-framecraft');

Quick start

const { FramecraftEngine } = require('ffmpeg-framecraft');

const engine = new FramecraftEngine();

// Crop a landscape video to vertical 9:16
await engine.cropTo916('input.mp4', 'shorts.mp4');

// Extract one segment
await engine.slice('input.mp4', 'clip.mp4', { start: 10, end: 30 });

// Combine several segments with a fade between them
await engine.slicesWithTransitions('input.mp4', 'highlight.mp4', {
  slices: [
    { start: 0, end: 10 },
    { start: 45, end: 55 },
    { start: 120, end: 130 },
  ],
  transition: 'fade',
});

Features

1. Crop to 9:16

Converts horizontal video to vertical (9:16) and scales to 720×1280 (baseline-friendly).

  • Default (static): centered crop (fast, no extra dependencies).
  • Smart (dynamic, Node): content-aware crop that tracks people, supports two-person shots, and biases framing toward the “speaking” person using a lightweight mouth-motion heuristic. No Python required, but does rely on optional JS AI deps.
  • Python (AutoCrop-vertical, recommended for advanced): delegates to the original Autocrop-vertical Python script for scene-aware TRACK/LETTERBOX, VFR handling, per-frame processing, audio start-time compensation, and hardware encoding. Best quality and behavior for complex content (interviews, podcasts, multi-person scenes).

| Parameter | Type | Description | |---------------|--------|-------------| | inputPath | string | Source video path | | outputPath | string | Output path (e.g. .mp4) | | opts.onProgress | function | (progress) => {}; progress.percent is 0–100 | | opts.smart | boolean | Enable smart (dynamic) crop (default: false) | | opts.smartSampleEvery | number | Seconds between analysis samples (default: 0.25) | | opts.smartTwoShot | boolean | When 2+ people are present, keep both in-frame if possible (default: true) | | opts.smartSpeakerBias | boolean | Bias framing toward the speaking person (heuristic) (default: true) |

Example

await engine.cropTo916('landscape.mp4', 'vertical.mp4');

await engine.cropTo916('landscape.mp4', 'vertical.mp4', {
  onProgress: (p) => console.log(`${p.percent?.toFixed(1) ?? '-'}%`),
});

// Smart (dynamic) crop
await engine.cropTo916('landscape.mp4', 'vertical_smart.mp4', {
  smart: true,
  smartSampleEvery: 0.25,
});

// Smart crop tuned for interviews/podcasts (keep 2 people + speaker bias)
await engine.cropTo916('input.mp4', 'vertical_podcast.mp4', {
  smart: true,
  smartTwoShot: true,
  smartSpeakerBias: true,
  smartSampleEvery: 0.15,
});

// Python-backed AutoCrop-vertical (exact behavior, recommended for advanced use)
await engine.cropTo916AutoCropVertical('input.mp4', 'vertical_autocrop.mp4', {
  ratio: '9:16',       // same as --ratio
  quality: 'balanced', // 'fast' | 'balanced' | 'high'
  encoder: 'auto',     // 'auto' | 'hw' | specific encoder name
  // frameSkip: 0,
  // downscale: 0,
});

2. Slice by timestamp

Extracts a single segment from the video. Start/end can be seconds or time strings ("MM:SS", "HH:MM:SS").

| Parameter | Type | Description | |-------------|--------|-------------| | inputPath | string | Source video | | outputPath| string | Output path | | range | object | { start, end } in seconds or time string | | opts.onProgress | function | Optional progress callback |

Example

// Seconds
await engine.slice('input.mp4', 'clip.mp4', { start: 10, end: 30 });

// Time strings
await engine.slice('input.mp4', 'clip.mp4', { start: '0:10', end: '1:30' });
await engine.slice('input.mp4', 'clip.mp4', { start: '1:00:00', end: '1:05:00' });

3. Multiple slices with transitions

Trims several time ranges from one video and concatenates them with a transition (xfade + acrossfade). Progress is based on output duration.

| Parameter | Type | Description | |---------------|--------|-------------| | inputPath | string | Source video | | outputPath | string | Output path | | options.slices | array | [{ start, end }, ...]; times in seconds or "MM:SS" | | options.transition | string | object | Preset name (e.g. 'fade') or { type, duration } (seconds) | | options.preset | string | object | Platform preset name or object; sets default transition | | opts.onProgress | function | Optional progress callback |

Per-slice transition: Any slice (except the first) can override the transition:

{ start: 10, end: 20, transition: 'wipeleft' }  // only this boundary uses wipeleft

Example — same transition for all boundaries

await engine.slicesWithTransitions('input.mp4', 'highlight.mp4', {
  slices: [
    { start: 0, end: 8 },
    { start: 20, end: 28 },
    { start: 60, end: 68 },
  ],
  transition: 'fade',
});

Example — custom duration

await engine.slicesWithTransitions('input.mp4', 'out.mp4', {
  slices: [{ start: 0, end: 5 }, { start: 10, end: 15 }],
  transition: { type: 'dissolve', duration: 1.5 },
});

Example — platform preset (transition from preset)

await engine.slicesWithTransitions('input.mp4', 'out.mp4', {
  slices: [{ start: 0, end: 10 }, { start: 30, end: 40 }],
  preset: 'youtubeShort',  // uses preset's transition + duration
});

Example — per-slice transitions

await engine.slicesWithTransitions('input.mp4', 'out.mp4', {
  slices: [
    { start: 0, end: 9 },
    { start: 38, end: 62, transition: 'fadewhite', duration: 2 },
    { start: 172, end: 176.4, transition: 'dissolve', duration: 1 },
  ],
  transition: 'fade',  // default for boundaries that don't specify one
});

Slices are clamped to the file duration; invalid or zero-length ranges throw.


4. Subtitles

Burns an SRT file into the video. Optional ASS-style options for font/size/colour (useful for AI-generated captions).

| Parameter | Type | Description | |---------------|--------|-------------| | inputPath | string | Source video | | outputPath | string | Output path | | srtPath | string | Path to .srt file | | opts | object | onProgress + optional style: fontName, fontSize, primaryColour, outlineColour, backColour, outline, shadow |

Example

await engine.addSubtitles('input.mp4', 'with_subs.mp4', 'captions.srt');

await engine.addSubtitles('input.mp4', 'with_subs.mp4', 'captions.srt', {
  fontSize: 28,
  primaryColour: '&Hffffff',
  outline: 2,
  onProgress: (p) => console.log(p.percent?.toFixed(1) + '%'),
});

5. Background music

Mixes the video’s audio with a second audio file (e.g. music). If the video has no audio, only the music track is used.

| Parameter | Type | Description | |---------------|--------|-------------| | inputPath | string | Source video | | outputPath | string | Output path | | musicPath | string | Path to music/audio (e.g. .mp3, .m4a) | | opts.onProgress | function | Optional progress callback |

Example

await engine.addBackgroundMusic('talk.mp4', 'with_music.mp4', 'background.mp3');

6. Extract thumbnail

Exports a single frame as an image. Format is taken from the file extension (.jpg, .png).

| Parameter | Type | Description | |---------------|--------|-------------| | inputPath | string | Source video | | outputPath | string | Output image path (e.g. frame.jpg, poster.png) | | time | number | string | Time in seconds or "MM:SS" / "HH:MM:SS" (default: 0) |

Example

await engine.extractThumbnail('input.mp4', 'frame.jpg', 5);
await engine.extractThumbnail('input.mp4', 'poster.png', '0:01:30');

7. Extract audio only

Re-encodes audio to AAC (.m4a) or MP3 (.mp3) based on the output extension. No video stream.

| Parameter | Type | Description | |---------------|--------|-------------| | inputPath | string | Source video | | outputPath | string | Output path (.m4a or .mp3) | | opts.bitrate| number | kbps (default: 256 for AAC, 320 for MP3) | | opts.sampleRate | number | Hz (default: 48000) | | opts.onProgress | function | Optional progress callback |

Example

await engine.extractAudioOnly('video.mp4', 'audio.m4a');
await engine.extractAudioOnly('video.mp4', 'audio.mp3');
await engine.extractAudioOnly('video.mp4', 'audio.mp3', { bitrate: 256 });

Pipelines (compose)

Run several operations in sequence with a single call. Intermediate files are written to the same directory as the final output and deleted after the pipeline finishes.

Preset: one named operation.

await engine.compose('input.mp4', 'output.mp4', 'shorts');
// Same as: cropTo916(input, output)

Pipeline: array of steps. Each step has an op and step-specific options.

| Step op | Required options | Description | |-----------|------------------|-------------| | crop916 | — | Crop to 9:16 | | slice | start, end | Extract segment | | subtitles | srtPath | Burn SRT | | music | musicPath | Add background music | | slicesWithTransitions | slices | Multiple slices + transitions; optional transition, preset | | audioOnly | outputPath | Extract audio to given path (no video output file from compose) |

Example — crop then slice

await engine.compose('input.mp4', 'output.mp4', [
  { op: 'crop916' },
  { op: 'slice', start: 0, end: 60 },
]);

Example — slice then extract audio only

await engine.compose('input.mp4', 'output.mp4', [
  { op: 'slice', start: 10, end: 20 },
  { op: 'audioOnly', outputPath: path.join(__dirname, 'data', 'clip_audio.mp3') },
], { onProgress: (p) => console.log(p.percent?.toFixed(1) + '%') });
// Only clip_audio.mp3 is kept; intermediate slice file is removed.

Example — full pipeline

await engine.compose('input.mp4', 'output.mp4', [
  { op: 'slicesWithTransitions', slices: [{ start: 0, end: 10 }, { start: 30, end: 40 }], preset: 'youtubeShort' },
  { op: 'subtitles', srtPath: 'subs.srt' },
  { op: 'music', musicPath: 'bgm.mp3' },
], { onProgress: (p) => console.log(p.percent?.toFixed(1) + '%') });

Platform presets

Presets define aspect ratio, resolution, transition, and codec hints. Use preset name (string) or the object from the package.

| Preset | Name | Notes | |----------------------|------------------|--------| | YouTube Shorts | youtubeShort | 9:16, 1080×1920, fade, subtitles/watermark flags | | TikTok | tiktok | 9:16, 1080×1920, fade | | Instagram Reels | instagramReels | 9:16, 1080×1920, dissolve | | Shorts (720p) | shorts | 9:16, 720×1280, baseline profile |

Example

const { youtubeShortPreset, getPreset } = require('ffmpeg-framecraft');

console.log(youtubeShortPreset.transition);  // 'fade'
console.log(getPreset('tiktok').aspectRatio); // '9:16'

await engine.slicesWithTransitions('input.mp4', 'out.mp4', {
  slices: [...],
  preset: 'youtubeShort',
});

Transition presets

Used by slicesWithTransitions. You can pass a preset name or { type, duration } (duration in seconds).

Available names:
fade, fadeLong, wipeleft, wiperight, wipeup, wipedown, slideleft, slideright, slideup, slidedown, circleopen, circleclose, rectcrop, distance, fadeblack, fadewhite, radial, dissolve, pixelize, zoomin, zoomout.

Example

const { TRANSITION_PRESETS, getTransition } = require('ffmpeg-framecraft');

// Preset name
transition: 'dissolve'

// Custom type + duration
transition: { type: 'fade', duration: 1 }

// Resolve preset to object
getTransition('fadeLong');  // { type: 'fade', duration: 1 }

Transition duration is automatically clamped so it does not exceed the length of either segment at that boundary.


Progress reporting

All operations that take opts.onProgress report:

  • percent — 0–100, based on output duration when known (slice, slicesWithTransitions, cropTo916, addBackgroundMusic). Otherwise from FFmpeg input duration.
  • timemark — current output time (e.g. "0:00:12.50").
  • frames, currentFps, currentKbps when available.

On completion, the executor emits a final onProgress({ percent: 100 }) so the UI can show 100% before “Compose completed”.


API reference

| Method | Description | |--------|-------------| | cropTo916(inputPath, outputPath, opts?) | Crop to 720×1280 vertical (9:16). | | slice(inputPath, outputPath, { start, end }, opts?) | Extract one segment; start/end in seconds or time string. | | addSubtitles(inputPath, outputPath, srtPath, opts?) | Burn SRT; optional ASS style options. | | extractThumbnail(inputPath, outputPath, time?) | Single frame to image (.jpg / .png). | | addBackgroundMusic(inputPath, outputPath, musicPath, opts?) | Mix video audio with music. | | extractAudioOnly(inputPath, outputPath, opts?) | Audio only → .m4a (AAC) or .mp3. | | slicesWithTransitions(inputPath, outputPath, { slices, transition?, preset? }, opts?) | Multiple slices joined with xfade/acrossfade. | | compose(inputPath, outputPath, pipeline?, opts?) | Run preset 'shorts' or pipeline array. |

Common opts: onProgress: (progress) => {}.


Advanced: filter builders

For tests or custom FFmpeg graphs you can use the low-level filter helpers.

const {
  cropTo916Filter,
  subtitleFilter,
  amixFilter,
  buildSlicesWithTransitionsFilter,
  SHORTS_WIDTH,
  SHORTS_HEIGHT,
} = require('ffmpeg-framecraft');

// Build -vf style filter strings (no execution)
cropTo916Filter(1920, 1080);
// => "crop=607:1080:656:0,scale=720:1280"

subtitleFilter('/path/to/subs.srt', { fontSize: 24 });
// => "subtitles='...':force_style='FontSize=24'"

amixFilter(true);
// => "[0:a][1:a]amix=inputs=2:duration=shortest[aout]"

// Slices + transitions: pass normalized slices and hasAudio
const slices = [
  { startSeconds: 0, endSeconds: 10, transition: null },
  { startSeconds: 20, endSeconds: 30, transition: { type: 'fade', duration: 0.5 } },
];
const { filterComplex, mapVideo, mapAudio } = buildSlicesWithTransitionsFilter(slices, true);
// Use filterComplex with -filter_complex and mapVideo/mapAudio with -map

Advanced AutoCrop-vertical integration

For the most advanced, scene-aware cropping (mirroring the behavior of Autocrop-vertical), ffmpeg-framecraft can delegate cropping to the Python tool.

  • When to use this (recommended):

    • Multi-person scenes (interviews, podcasts, group shots).
    • You want the exact TRACK vs LETTERBOX behavior, scene detection, VFR normalization, and audio sync that AutoCrop-vertical provides.
    • You’re OK installing Python dependencies once on your machine or server.
  • What it does:

    • Uses PySceneDetect to split the video into scenes.
    • For each scene, uses YOLOv8 + face detection to decide:
      • TRACK (tight crop on subject) vs
      • LETTERBOX (preserve full shot with black bars).
    • Processes frames with OpenCV and encodes via FFmpeg.
    • Normalizes VFR → CFR and compensates audio start-time offsets.

Recommended setup (one-time)

Run the built-in setup command from your project root:

npx ffmpeg-framecraft-setup-autocrop \
  --py-command python3.9 \
  --py-dir .
  • Python requirement: The underlying Autocrop-vertical tool officially supports Python 3.8+. For best compatibility use a Python 3.8–3.11 interpreter when choosing --py-command (for example python3.9 or python3.10).
  • --py-command: Python executable to use for the venv (e.g. python3, python3.9 or a full path).
  • --py-dir: Base directory where autocrop-vertical/ should be cloned (default: current directory).

What this does:

  • Clones Autocrop-vertical into <py-dir>/autocrop-vertical (if not already present).
  • Creates a virtualenv in autocrop-vertical/.venv.
  • Upgrades pip, pins numpy<2, and installs requirements.txt.
  • Writes .autocrop-config.json in py-dir with:
    • pythonDir — path to the AutoCrop-vertical repo.
    • pythonCommand — path to the venv’s Python binary.

After this, you can call cropTo916AutoCropVertical without passing Python paths explicitly.

Using AutoCrop-vertical from Node

const { FramecraftEngine } = require('ffmpeg-framecraft');
const engine = new FramecraftEngine();

await engine.cropTo916AutoCropVertical('input.mp4', 'vertical_autocrop.mp4', {
  // All options are optional if you ran the setup script:
  // pythonDir: '/custom/path/to/autocrop-vertical',
  // pythonCommand: '/custom/path/to/python',
  ratio: '9:16',        // maps to --ratio
  quality: 'balanced',  // 'fast' | 'balanced' | 'high'
  encoder: 'auto',      // 'auto' | 'hw' | explicit encoder name
  // frameSkip: 0,
  // downscale: 0,
  // planOnly: true,
});

Resolution order for Python settings:

  • opts.pythonDir / opts.pythonCommand (explicit in code)
  • Environment:
    • AUTO_CROP_VERTICAL_DIR
    • AUTO_CROP_VERTICAL_PY
  • .autocrop-config.json written by ffmpeg-framecraft-setup-autocrop

If pythonDir is still missing, the engine throws with a clear message suggesting running the setup command or setting environment variables.


License

ISC License

Copyright (c) 2025, the ffmpeg-framecraft contributors

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.