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

framewise

v0.1.0

Published

Turn a video into a lean, deduplicated, high-quality set of frames — ideal for feeding vision LLMs within a token budget

Readme

framewise

Turn a video into a lean, deduplicated, high-quality set of frames — ideal for feeding vision LLMs within a token budget.

CI npm version License: MIT

framewise is a smarter successor to naive frame extraction. Instead of uniform sampling plus a crude pixel diff, it combines perceptual-hash deduplication, blur filtering, and optional scene-cut detection so you get the fewest, most representative frames — perfect for vision/multimodal LLM prompts where every image costs tokens.

Features

  • Perceptual-hash dedup — dHash + Hamming distance drops near-duplicate frames, not just pixel-identical ones.
  • Blur filtering — optional Laplacian-variance sharpness gate discards out-of-focus / motion-blurred frames.
  • Scene detection — optionally force-keep frames at detected scene cuts so transitions are never lost.
  • VLM token-budget friendlymaxFrames caps the kept set with deterministic, evenly-spaced sampling.
  • Zero-config ffmpeg — uses system ffmpeg if present, otherwise falls back to the optional ffmpeg-static binary.
  • Safe by construction — shells out via execFile with an argument array (no shell), so paths can never inject commands.
  • TypeScript-native — full types, ESM, works as a library and a CLI.

Install

pnpm add framewise

framewise needs an ffmpeg binary. Either install ffmpeg and put it on your PATH, or install the optional bundled binary:

pnpm add ffmpeg-static

Quick start

Library

import { extractFrames } from 'framewise';

const result = await extractFrames('demo.mp4', './frames', {
  fps: 2,
  maxFrames: 40,
  blurThreshold: 50,
  sceneDetection: true,
});

console.log(`kept ${result.keptCount} of ${result.extractedCount} frames`);
for (const frame of result.frames) {
  console.log(frame.index, frame.path, frame.keptReason, frame.timestampSec);
}

CLI

# Simplest form — writes to ./demo-frames next to the video
framewise demo.mp4

# Sample at 2 fps, cap at 40 frames
framewise demo.mp4 ./frames --fps 2 --max-frames 40

# Drop blurry frames and keep scene cuts
framewise demo.mp4 --blur-threshold 50 --scene-detection

# WebP output, machine-readable result
framewise demo.mp4 --format webp --quality 70 --json

API reference

extractFrames(videoPath, outputDir, options?)

Extracts, filters and renumbers frames into outputDir (created recursively). Returns an ExtractResult.

Options

| Option | Type | Default | Description | | ------------------ | ------------------------------------- | ----------- | --------------------------------------------------------------------- | | fps | number | 5 | Frames per second to sample from the video. | | format | 'jpeg' \| 'png' \| 'webp' | 'jpeg' | Output image format. | | quality | number (0–100) | 85 | Output quality, mapped to ffmpeg -q:v. | | maxFrames | number | undefined | Caps discretionary frames via even sampling; always retains first, last and scene-cut transitions. | | dedupe | boolean | true | Enable dHash + Hamming-distance deduplication. | | dedupeThreshold | number | 8 | Min Hamming distance from the last kept frame required to keep. | | blurThreshold | number | undefined | Min Laplacian variance to keep; disabled when unset. | | sceneDetection | boolean | false | Force-keep frames at detected scene cuts. | | sceneThreshold | number | 0.08 | Normalized mean-abs-diff threshold for a scene cut. | | includeFirstLast | boolean | true | Always include the video's first and last frames. | | ffmpegPath | string | undefined | Override the ffmpeg binary path. | | onProgress | (e) => void | undefined | Progress callback fired at extracting/analyzing/selecting/writing. |

ExtractResult

| Field | Type | Description | | ------------------- | ------------- | -------------------------------------------------- | | outputDir | string | Directory the final frames were written to. | | frames | FrameInfo[] | Kept frames, renumbered frame-0001…. | | extractedCount | number | Raw frames pulled from ffmpeg. | | keptCount | number | Frames kept after selection. | | droppedDuplicates | number | Frames dropped as near-duplicates. | | droppedBlurry | number | Frames dropped by the blur gate. |

Each FrameInfo has index, path, timestampSec, dHash (16-hex-char), sharpness (Laplacian variance) and keptReason ('first' | 'last' | 'scene-cut' | 'unique' | 'sampled').

Building blocks

Individually exported and unit-tested:

  • isVideoFile(filename): boolean
  • checkFfmpeg(ffmpegPath?): Promise<boolean>
  • resolveFfmpegBinary(ffmpegPath?): Promise<string | null>
  • computeDHash(imagePath): Promise<string>
  • hammingDistance(hexA, hexB): number
  • computeSharpness(imagePath): Promise<number>

How selection works

  1. Extract — one ffmpeg pass samples frames at fps; first/last frames are pulled separately when includeFirstLast is on.
  2. Analyze — compute a dHash and a Laplacian-variance sharpness score for every extracted frame.
  3. Blur gate — if blurThreshold is set, drop frames below it (first/last are never dropped).
  4. Scene cuts — if sceneDetection is on, mark frames whose downscaled mean-abs-diff from the previous frame ≥ sceneThreshold as force-keeps.
  5. Dedup — sequentially keep a frame only if its Hamming distance from the last kept frame ≥ dedupeThreshold; force-keeps always survive.
  6. Budget — if maxFrames is set, it caps only discretionary frames via evenly-spaced sampling; first, last and scene-cut transitions are always retained (so the kept count may exceed maxFrames when forced frames alone do).
  7. Write — kept frames are renumbered to frame-0001.<ext> and all raw files are removed, so the output directory holds only the final set.

Requirements

  • Node.js >= 20
  • An ffmpeg binary on PATH, or the optional ffmpeg-static dependency.

Contributing

Contributions are welcome! Please read the contributing guide for development setup and the PR process, and note the Code of Conduct. To report a security issue, see the security policy.

License

MIT © Dominik Rycharski

See LICENSE for details.