# Video to Scenes — Scene Detection, Keyframes, Transcript (`saliga/video-to-scenes`) Actor

Split videos into scenes: cut detection, one keyframe per scene, contact sheet and, optionally, what is said in each scene. Frames every N seconds or at timestamps. Markdown storyboard and RAG chunks for LLMs.

- **URL**: https://apify.com/saliga/video-to-scenes.md
- **Developed by:** [Salih Küçükkayış](https://apify.com/saliga) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 frames

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

Split videos into scenes and keyframes for AI vision, video search, thumbnails and editing. Give it video file links (or Google Drive and Dropbox share links) and get, for each video, every **scene cut** with its time range, **one keyframe per scene**, a **contact sheet**, and, if you want, **what is said in each scene**: a transcript attached to the matching keyframe, ready for multimodal RAG and LLM video understanding.

Other modes: one frame **every N seconds**, **N frames spread evenly**, or frames **at the timestamps you list**.

**$2 per 1,000 frames**, **$5 per 1,000 video minutes scanned for scenes** (Scenes mode only), **$5 per 1,000 audio minutes transcribed** (only with the transcript on), plus $0.005 per run. Contact sheets, metadata and videos that fail are free. No subscription, no API key, no ffmpeg to install.

### Quick start

Press **Start** with the example input. It splits the 52-second Sintel trailer into scenes:

```json
{
  "videoUrls": ["https://media.w3.org/2010/05/sintel/trailer.mp4"]
}
```

Each video becomes one dataset row (real output of that run, lists shortened):

```json
{
  "url": "https://media.w3.org/2010/05/sintel/trailer.mp4",
  "fileName": "trailer.mp4",
  "durationSec": 52.208,
  "width": 854, "height": 480, "fps": 24,
  "videoCodec": "h264", "audioCodec": "aac", "hasAudio": true, "container": "mov", "bitrateKbps": 670,
  "mode": "scenes",
  "analyzedSec": 52.208,
  "sceneCount": 17,
  "frameCount": 17,
  "contactSheetUrl": "https://api.apify.com/v2/key-value-stores/.../records/000-trailer-contact-sheet.jpg?signature=...",
  "scenes": [
    { "scene": 0, "label": "Scene 1", "startSec": 0, "endSec": 1.333, "durationSec": 1.333, "cutScore": null, "timestampSec": 0.875,
      "imageUrl": "https://api.apify.com/v2/key-value-stores/.../records/000-trailer-001.jpg?signature=..." },
    { "scene": 1, "label": "Scene 2", "startSec": 1.333, "endSec": 5.083, "durationSec": 3.75, "cutScore": 0.386, "timestampSec": 3.208, "imageUrl": "..." },
    { "scene": 2, "label": "Scene 3", "startSec": 5.083, "endSec": 8.125, "durationSec": 3.042, "cutScore": 0.653, "timestampSec": 6.625, "imageUrl": "..." }
  ],
  "frames": [
    { "index": 0, "timestampSec": 0.875, "timecode": "00:00:00.875", "scene": 0, "imageUrl": "...", "width": 854, "height": 480 }
  ],
  "markdown": "# trailer\n\nSource: https://media.w3.org/2010/05/sintel/trailer.mp4 · Duration: 00:52\n\n## Scene 1 (00:00–00:01)\n\n![Scene 1 at 00:00](...)\n\n...",
  "billed": { "videoMinutes": 1, "frames": 17, "transcriptMinutes": 0 }
}
```

With *Transcript* on, each scene also gets the words spoken in it, and `chunks` holds one RAG chunk per scene with speech. From the full 15-minute Sintel film:

```json
{ "index": 0, "chars": 151, "text": "You're a fool for traveling alone so completely unprepared. You're lucky your blood's still flowing. So what brings you to the land or the gatekeepers?",
  "startSec": 109.5, "endSec": 134.375, "startTime": "01:49", "imageUrl": "...", "scene": 22 }
```

### What it does

- **Scene detection**: compares the colour histogram of the picture 4 times a second and marks a cut where it changes sharply. Colour, not only brightness, so a cut between two shots of similar brightness (navy to dark red) is still found. *Scene cut sensitivity* and *Shortest scene* tune it; *Fast scan* looks at keyframes only for long videos.
- **Keyframes**: one still from the middle of each scene, so it shows the scene, not the transition. When there are more scenes than *Max frames per video*, every cut is still listed and the stills are spread evenly over the scenes.
- **Black frames** (fades, cuts to black) are replaced by the next frame with content in the same scene. Turn *Skip black frames* off for exact times.
- **Contact sheet**: one grid image of all frames with timecodes, free.
- **Transcript per scene** (optional): the audio is transcribed with Whisper on CPU and each sentence is attached to the scene it falls in. You also get the full transcript with timestamps, and `chunks`: one RAG chunk per scene with its text, time range and keyframe link.
- **Markdown storyboard**: a heading per scene with its time range, keyframe image and what is said. Paste it into an LLM or a notes app.
- **Images**: JPEG, WebP or PNG, scaled to *Max image width* (1280 px by default) or kept at full size. Every image has a public link that works without an Apify login.
- **Metadata**: duration, resolution, frame rate, codecs, bitrate and container.

### Pricing

| Event | Price | When |
|---|---|---|
| Frame | $2 / 1,000 | each still saved |
| Video minute | $5 / 1,000 | each started minute scanned for cuts, Scenes mode only |
| Transcript minute | $5 / 1,000 | each started minute of audio transcribed, only with the transcript on |
| Run start | $0.005 | per run, at the default 4 GB memory |

Cost examples:

| Job | Cost |
|---|---|
| 100 short videos, 8 frames each (Count mode) | $1.61 |
| A 10-minute video split into 40 scenes | $0.14 |
| The same with the transcript of every scene | $0.19 |
| A 15-minute 2K film: 98 scenes, 50 keyframes and the transcript (measured) | $0.26 |
| A 2-hour lecture, one frame a minute (Interval mode, *Max frames* 120) | $0.25 |

Set a spending limit on the run (*Max total charge* in Apify) and the Actor stops cleanly at it: the scan, the frames and the transcript are cut to what still fits. *Max minutes per video* (default 120) caps each video.

### Use it from code, n8n, Make or an AI agent

```bash
curl -X POST "https://api.apify.com/v2/acts/saliga~video-to-scenes/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"videoUrls": ["https://example.com/demo.mp4"], "transcribe": true}'
```

Feed the `imageUrl` of each scene to a vision model together with the scene's `transcript` to describe, tag or search the video scene by scene.

### Limits

- **Platform pages are not supported**: YouTube, TikTok, Instagram, Facebook, X, Vimeo and similar pages are refused with a clear error. Use a direct link to the video file.
- Scene detection finds hard cuts. Slow cross-fades and dissolves may not be split; lower the sensitivity value to catch more of them (and some false cuts).
- Up to 300 frames and 600 minutes per video, and 4 GB per download. Live streams are not supported.
- The transcript uses Whisper's base model: good for clear speech, weaker on names and noisy audio. For a transcript alone, see [Audio to Text](https://apify.com/saliga/audio-to-text).

### FAQ

**Is my video sent anywhere else?** No. It is downloaded into the run and processed there with ffmpeg (PyAV) and an open-source Whisper model.

**Why was something not charged?** Videos that cannot be downloaded or decoded, frames that cannot be read, timestamps past the end of the video and audio without speech are never charged. The `error` or `note` field says why.

**Other tools from the same developer**: [Career Site Jobs](https://apify.com/saliga/career-site-jobs), [Audio to Text](https://apify.com/saliga/audio-to-text), [Document to Markdown](https://apify.com/saliga/document-to-markdown), [Bulk HTTP Requests](https://apify.com/saliga/bulk-http-requests), [Website Screenshot + Change Detection](https://apify.com/saliga/website-screenshot-change-detection), [Keyword Search Volume](https://apify.com/saliga/keyword-search-volume).

# Actor input Schema

## `videoUrls` (type: `array`):

Direct links to video files (mp4, mov, webm, mkv, avi, ts, ...) or Google Drive and Dropbox share links. One result per video. YouTube, TikTok, Instagram and other platform pages are not supported: use a link to the file itself.

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

Scenes: find every cut and take one keyframe from the middle of each scene. Interval: one frame every N seconds. Count: N frames spread evenly. Timestamps: frames at the times you list.

## `sceneThreshold` (type: `number`):

How different two moments must look (colour histogram, 0-1) to count as a cut. Lower finds more cuts, including softer ones; higher finds only hard cuts.

## `minSceneSeconds` (type: `number`):

Cuts closer together than this are merged into one scene (flashes, quick montages).

## `fastScan` (type: `boolean`):

Look only at the video's keyframes. Several times faster on long videos, but a cut is placed at the next keyframe, usually within a few seconds.

## `intervalSeconds` (type: `number`):

For the "Every N seconds" mode.

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

For the "N frames spread evenly" mode.

## `timestamps` (type: `array`):

For the "At given timestamps" mode: seconds or clock times, for example 15, 1:30 or 00:02:05.5.

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

Cap on the stills saved per video. In Scenes mode every cut is still listed; when there are more scenes than this, the stills are spread evenly over them.

## `skipBlankFrames` (type: `boolean`):

When a planned frame is black (a fade or a cut to black), take the next frame with content instead, within the same scene. Turn off to get frames at exactly the planned times.

## `transcribe` (type: `boolean`):

Transcribe the audio with Whisper (base model) and attach what is said in each scene or between frames. Adds RAG chunks with the scene's time range and keyframe link. Charged per audio minute.

## `language` (type: `string`):

Two-letter code (en, es, de, fr, tr, ...) or auto to detect it.

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

Format of the saved frames.

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

Frames wider than this are scaled down, keeping the aspect ratio. 0 keeps the original size.

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

Higher is sharper and larger.

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

Also save one grid image of all frames with their timecodes (free).

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

Only the first this many minutes of each video are analysed and charged.

## `maxFileSizeMb` (type: `integer`):

Larger files are skipped with an error (not charged).

## `requestHeaders` (type: `object`):

Extra HTTP headers for downloading, for example {"Authorization": "Bearer …"}. Sent only to the host of each URL you gave, never to a redirect on another host.

## Actor input object example

```json
{
  "videoUrls": [
    "https://media.w3.org/2010/05/sintel/trailer.mp4"
  ],
  "mode": "scenes",
  "sceneThreshold": 0.3,
  "minSceneSeconds": 1,
  "fastScan": false,
  "intervalSeconds": 10,
  "frameCount": 12,
  "maxFramesPerVideo": 50,
  "skipBlankFrames": true,
  "transcribe": false,
  "language": "auto",
  "imageFormat": "jpeg",
  "maxWidth": 1280,
  "quality": 85,
  "contactSheet": true,
  "maxVideoMinutes": 120,
  "maxFileSizeMb": 1000,
  "requestHeaders": {}
}
```

# Actor output Schema

## `results` (type: `string`):

One row per video: duration, scene and frame counts, contact sheet and any error.

## `fullResults` (type: `string`):

Every field: scenes with time ranges, frames with image links, transcript per scene, Markdown storyboard and RAG chunks.

## `images` (type: `string`):

The saved images.

# 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 = {
    "videoUrls": [
        "https://media.w3.org/2010/05/sintel/trailer.mp4"
    ],
    "requestHeaders": {}
};

// Run the Actor and wait for it to finish
const run = await client.actor("saliga/video-to-scenes").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 = {
    "videoUrls": ["https://media.w3.org/2010/05/sintel/trailer.mp4"],
    "requestHeaders": {},
}

# Run the Actor and wait for it to finish
run = client.actor("saliga/video-to-scenes").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 '{
  "videoUrls": [
    "https://media.w3.org/2010/05/sintel/trailer.mp4"
  ],
  "requestHeaders": {}
}' |
apify call saliga/video-to-scenes --silent --output-dataset

```

## MCP server setup

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

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/jRcBzOkODgrcI1crX/builds/0aDBuOev6TSLOmZel/openapi.json
