# Smart Video Keyframes: Best Frame per Scene for AI (`nookforgestudio/video-keyframe-extractor`) Actor

Turn videos into a clean set of keyframes: one sharp frame per scene, with blurry and duplicate shots removed. Timestamps, contact sheet and optional on-screen text. Ready for vision LLMs, RAG and thumbnails. Pay per video minute and frame.

- **URL**: https://apify.com/nookforgestudio/video-keyframe-extractor.md
- **Developed by:** [Nookforgestudio](https://apify.com/nookforgestudio) (community)
- **Categories:** AI, Videos, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 frame extracteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

### Smart Video Keyframes: Best Frame per Scene for AI

Turn any video into a **small, clean set of keyframes**: one sharp frame per scene, with **blurry and duplicate shots removed**. Each frame comes with its timestamp, scene boundaries and a public image link, plus a **contact sheet** of the whole video. Optionally read the **on-screen text** of every frame (slides, captions, signs, UI).

Built for feeding videos to **vision LLMs, RAG pipelines, search indexes and thumbnail pickers**, where 300 near-identical frames cost more and say less than 7 good ones.

Measured on public videos:

| Video | Every 10 s | **This Actor** |
|---|---|---|
| 4:44 conference talk with slides (High sensitivity) | 28 frames, 7 distinct slides | **7 frames, one per slide, each with all bullets visible, plus slide text** |
| 12:14 film, 1080p (Tears of Steel) | 73 frames | **159 scenes found, 24 blurry picks dropped, best 100 kept (cap)** |
| 32 s trailer (Big Buck Bunny) | 3 frames | **16 frames, one per shot** |

- 🎬 **One frame per scene**: hard cuts plus subtle changes like slide transitions, with an adaptive threshold that ignores camera motion
- 🔍 **Sharpest frame of each scene**: avoids motion blur, focus pulls and transition frames
- 🧹 **No blurry or duplicate frames**: shots the video cuts back to are kept once
- ⏱️ **Timestamps and scene ranges** for every frame (`00:01:50.750`, scene start and end)
- 🖼️ **Contact sheet** per video with all keyframes and timecodes
- 🔤 **Optional on-screen text** (OCR) for slides, captions and screen recordings
- 🔒 **Runs inside the Actor**: videos are never sent to a third-party AI API
- 💸 **Pay per video minute and frame; failed videos are free**

#### Ready-made examples

- [Turn a conference talk into one frame per slide, with slide text](https://apify.com/nookforgestudio/video-keyframe-extractor/examples/conference-talk-to-slide-frames)
- [Pick the best frame of every scene in a film or trailer](https://apify.com/nookforgestudio/video-keyframe-extractor/examples/best-frame-per-scene)

#### How it compares

| | Fixed-interval sampling | **Smart Video Keyframes** |
|---|---|---|
| Frames per video | Grows with length, mostly repeats | **One per scene, capped** |
| Blurry and transition frames | Kept | **Dropped or avoided** |
| Repeated shots and static slides | Kept every time | **Kept once** |
| Text on screen | Not included | **Optional OCR per frame** |
| Data sent to a third party | Varies | **No, runs inside the Actor** |

#### How to use

1. Paste direct video file links into **Video URLs** (or read them from a downloader's dataset).
2. Keep **One per scene**. For lectures, webinars and screen recordings set **Scene sensitivity** to **High** and turn on **Read on-screen text**.
3. Click **Start**. Frames appear in the **Output** tab and the contact sheet in the run's storage.

Page links (YouTube, TikTok, Instagram) are not downloaded. Run a downloader Actor first and pass its file links or its dataset ID.

#### Input example

```json
{
  "urls": ["https://upload.wikimedia.org/wikipedia/commons/2/29/Research_and_Privacy_on_Wikipedia_-_Wikimania_2025_Lightning_Talk_-_Eli_Asikin-Garmager.webm"],
  "mode": "scenes",
  "sceneSensitivity": "high",
  "extractText": true,
  "maxFramesPerVideo": 50
}
```

#### Output example

Real output for the talk above (text truncated):

```json
{
  "videoUrl": "https://upload.wikimedia.org/.../Research_and_Privacy_on_Wikipedia_-_Wikimania_2025_Lightning_Talk_-_Eli_Asikin-Garmager.webm",
  "frameIndex": 5,
  "timestamp": 164.25,
  "timecode": "00:02:44.250",
  "sceneIndex": 4,
  "sceneStart": 118.5,
  "sceneEnd": 173.75,
  "sharpnessScore": 0.954,
  "imageUrl": "https://api.apify.com/v2/key-value-stores/.../records/frame-001-0005.jpg",
  "width": 1280,
  "height": 720,
  "text": "Key takeaways for\nresearchers\nShare plans early\nKnow the norms and guidelines\nRespect privacy\nRecognize the significance ...",
  "contactSheetUrl": "https://api.apify.com/v2/key-value-stores/.../records/contact-001.jpg"
}
```

A `SUMMARY` record in the run's key-value store lists each video's duration, billed minutes, scenes found and frames skipped as blurry or duplicate.

#### Modes

| Mode | Picks | Good for |
|---|---|---|
| **One per scene** (default) | Sharpest frame of each shot or slide | Films, ads, talks, tutorials, AI summaries |
| **Every N seconds** | Sharpest frame in each N-second window | Long static shots, surveillance, timelapses |
| **Fixed number** | N evenly spread sharp frames | Thumbnails, previews, quick overviews |

#### Pricing

| Event | Price | When |
|---|---|---|
| Frame extracted | **$0.002** | Each keyframe saved |
| Video minute | **$0.005** | Each started minute of video scanned |
| Frame text | **$0.003** | Each frame read with OCR (only when turned on) |
| Actor start | $0.00005 per GB | Once per run |

| Example | Cost |
|---|---|
| 5-minute talk, 7 slide frames, with text | $0.025 + $0.014 + $0.021 = **$0.06** |
| 12-minute film, 100 keyframes | $0.065 + $0.20 = **$0.27** |
| 30-second ad, 15 keyframes | $0.005 + $0.03 = **$0.035** |

Platform usage is included. Videos that fail to download or decode are not charged, and videos over **Max video length** are skipped for free. Cap cost with **Max frames per video** or the run's **maximum cost**; the Actor stops cleanly at the limit.

#### Use with AI agents (MCP)

```bash
claude mcp add --transport http apify "https://mcp.apify.com?tools=nookforgestudio/video-keyframe-extractor"
```

Then ask: *"Get one frame per slide from this talk and summarize each slide: https://..."*

#### Use via API

```bash
curl -X POST "https://api.apify.com/v2/acts/nookforgestudio~video-keyframe-extractor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"urls": ["https://example.com/video.mp4"], "maxFramesPerVideo": 20}'
```

Also works from Make, n8n and Zapier through Apify's integrations.

#### Supported videos and limits

- MP4, MOV, WebM, MKV, AVI, OGV and most other formats ffmpeg reads
- Direct file links only; up to **Max video file size** (default 1.5 GB) and **Max video length** (default 120 min)
- On-screen text works best on printed English and Chinese; handwriting is not reliable
- Very dark or very flat frames (fades, black screens) are never picked

#### FAQ

**Why not just take a frame every few seconds?** Fixed sampling returns many near-identical frames for static shots and slides and misses short scenes. Scene-based picking returns fewer, more useful frames, which keeps LLM and storage costs down.

**What does High sensitivity change?** It splits on smaller changes, such as a slide whose title changes but layout stays the same. Use it for talks, webinars and screen recordings.

**Can I keep every scene even if it is blurry?** Turn off **Skip blurry frames**. Each frame still includes a `sharpnessScore` (1.0 = as sharp as the video gets) and a `blurry` flag.

#### Custom work

Need object detection on video, tracking, highlight reels or a custom pipeline? Contact [Nookforge Studio](https://apify.com/nookforgestudio).

#### Credits

Example videos from Wikimedia Commons: *Tears of Steel* and *Big Buck Bunny* (Blender Foundation, CC BY 3.0), *Research and Privacy on Wikipedia* lightning talk by Eli Asikin-Garmager (CC0).

# Actor input Schema

## `urls` (type: `array`):

Direct links to video files (MP4, MOV, WebM, MKV, AVI and most other formats). Page links such as YouTube or TikTok are not supported; use a downloader Actor first and pass its file links or dataset.

## `datasetId` (type: `string`):

Optional. ID of an Apify dataset (for example a downloader or scraper output) that contains video file URLs.

## `urlField` (type: `string`):

Dot path to the field holding the video URL(s), e.g. 'videoUrl' or 'media.url'. Arrays are expanded.

## `mode` (type: `string`):

'Scenes': the sharpest frame of each shot. 'Interval': the sharpest frame in every N-second window. 'Count': a fixed number of evenly spread frames.

## `sceneSensitivity` (type: `string`):

Higher finds more, subtler cuts (slides, slow pans). Lower only splits on hard cuts.

## `intervalSeconds` (type: `integer`):

Used with 'Every N seconds'.

## `frameCount` (type: `integer`):

Used with 'Fixed number of frames'.

## `maxFramesPerVideo` (type: `integer`):

Caps frames and cost per video. When a video has more scenes, the longest scenes are kept.

## `skipBlurry` (type: `boolean`):

Drop picks that are much softer than the video's typical sharpness (motion blur, focus pulls, transitions).

## `skipDuplicates` (type: `boolean`):

Drop frames that look nearly identical to one already kept, such as a shot the video cuts back to.

## `extractText` (type: `boolean`):

Run OCR on every kept frame and return its text (slides, captions, signs, UI). Charged per frame.

## `contactSheet` (type: `boolean`):

Save one overview image per video with all keyframes and timecodes.

## `imageFormat` (type: `string`):

Format of saved frames.

## `maxWidth` (type: `integer`):

Frames wider than this are downscaled. Use 3840 to keep 4K.

## `quality` (type: `integer`):

30 to 100.

## `minSceneSeconds` (type: `integer`):

Cuts closer together than this are merged.

## `maxVideoMinutes` (type: `integer`):

Longer videos are skipped and not charged.

## `maxVideoMB` (type: `integer`):

Larger files are skipped and not charged.

## `maxVideos` (type: `integer`):

Safety cap on videos per run.

## Actor input object example

```json
{
  "urls": [
    "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"
  ],
  "urlField": "url",
  "mode": "scenes",
  "sceneSensitivity": "normal",
  "intervalSeconds": 10,
  "frameCount": 12,
  "maxFramesPerVideo": 50,
  "skipBlurry": true,
  "skipDuplicates": true,
  "extractText": false,
  "contactSheet": true,
  "imageFormat": "jpg",
  "maxWidth": 1280,
  "quality": 85,
  "minSceneSeconds": 1,
  "maxVideoMinutes": 120,
  "maxVideoMB": 1500,
  "maxVideos": 100
}
```

# Actor output Schema

## `frames` (type: `string`):

One row per keyframe with image link, timestamp, scene and sharpness.

## `contactSheets` (type: `string`):

One overview image per video.

## `summary` (type: `string`):

Per-video stats.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urls": [
        "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("nookforgestudio/video-keyframe-extractor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "urls": ["https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"] }

# Run the Actor and wait for it to finish
run = client.actor("nookforgestudio/video-keyframe-extractor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"
  ]
}' |
apify call nookforgestudio/video-keyframe-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nookforgestudio/video-keyframe-extractor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/0Bsgo9Q1oX44Xhwhk/builds/IbB6XWXmrh8f5grIK/openapi.json
