# Ad Creative Intelligence — Transcript, Hook, On-Screen Text (`kurilov_ai/ad-creative-intelligence`) Actor

Turns ad video URLs into transcripts with timestamps, on-screen text, the first-3-seconds hook and the detected CTA. Pay per analyzed video, no login, no API key.

- **URL**: https://apify.com/kurilov\_ai/ad-creative-intelligence.md
- **Developed by:** [Andrey Kurilov](https://apify.com/kurilov_ai) (community)
- **Categories:** Videos, AI, Social media
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $37.50 / 1,000 video\_analyzeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Ad Creative Intelligence — Transcript, On-Screen Text, Hook & CTA | $50/1k videos | No Login

Turn a list of ad video URLs into structured creative intelligence: a full transcript with
timestamps, the text burned onto the screen, the first-3-seconds hook and the call to
action — one JSON object per creative, $0.03 per analyzed video.

### What it solves

Getting the ads themselves is a commodity — a dozen Ad Library scrapers do it for
$0.15–1.00 per 1 000. The question your team actually asks is one level up: *what is said
in the creative, what is written on it, and what is the hook that stops the scroll?*

This actor is the second step in that pipeline. It takes the media URLs you already have
and gives back the analysis layer:

- **Hook** — what the creative opens with in the first 3 seconds, the window performance
  buyers benchmark. Both channels are reported: what is spoken there and what is burned onto
  the opening frames, because a modern creative usually carries the hook in both at once.
  Cut on word timings, so a single long spoken segment does not spill the whole script into
  the hook. A creative that opens on music has no hook in the window — you get an empty
  `hook.text` with `emptyReason` and `firstSpeechStartSec`, the hook delay, rather than a
  line quoted from the middle of the ad.
- **Transcript** — segments with timestamps, plus a flat `transcriptPlain` string to pipe into an LLM.
- **On-screen text** — offers, prices and captions burned into the video, read off key frames.
- **CTA** — the detected call to action and where it appears.
- **Pace and identity** — scene-change count and a perceptual hash of the creative.

Typical recipe: `Meta Ad Library Scraper ($0.15/1k) → this actor → your hook library`.

Built for performance agencies and media buyers, UGC studios building hook libraries,
brands monitoring competitors, and ad-intelligence tools that need a speech layer without
running the infrastructure.

### Input example

```json
{
  "videos": [
    {
      "url": "https://archive.org/download/OlayTotalEffectsAdvert/Olay%20Total%20Effects%20Advert.ia.mp4",
      "meta": { "adId": "OlayTotalEffectsAdvert" }
    },
    {
      "url": "https://archive.org/download/LOrealElviveDavidGinolaAdvert1998/L%27Oreal%20Elvive%20David%20Ginola%20Advert.ia.mp4",
      "meta": { "adId": "LOrealElviveDavidGinolaAdvert1998" }
    }
  ],
  "language": "auto",
  "asrModel": "fast",
  "extractOnScreenText": true,
  "maxFrames": 12,
  "hookWindowSeconds": 3,
  "skipIfNoSpeech": false,
  "maxVideoDurationSeconds": 300,
  "maxConcurrency": 6
}
```

Only `videos` is required. **Send batches**, not single URLs: every run has a fixed start-up cost that is
billable, so 20 videos is the smallest sensible run and 100–500 is the sweet spot.

#### Input fields

| Field | Type | Default | Description |
|---|---|---|---|
| `videos` | array | — | Items with a direct media `url` and optional `meta` passed through to the output. Required. |
| `language` | string | `auto` | Keep `auto`. Forcing the wrong language is the most common source of garbage transcripts. |
| `asrModel` | enum | `fast` | `fast` (4 GB) or `accurate` (8 GB, better on noisy or music-heavy ads, doubles the compute cost). |
| `extractOnScreenText` | boolean | `true` | Run OCR over key frames. Turn off for a cheaper audio-only pass. |
| `maxFrames` | integer | `12` | Cap on key frames per video, picked by scene-change detection. |
| `hookWindowSeconds` | integer | `3` | Length of the opening window reported as the hook. |
| `skipIfNoSpeech` | boolean | `false` | Report silent videos as `no_content` without running OCR. |
| `maxVideoDurationSeconds` | integer | `300` | Hard ceiling. Longer videos fail for $0. Can be lowered, never raised. |
| `maxConcurrency` | integer | `6` | Upper bound on videos in flight. The run lowers it to what the memory you gave it can keep busy. |

### Output example

Real output for the first video of the input above — this is what the actor returned on the
platform, not an illustration.

```json
{
  "sourceUrl": "https://archive.org/download/OlayTotalEffectsAdvert/Olay%20Total%20Effects%20Advert.ia.mp4",
  "adId": "OlayTotalEffectsAdvert",
  "status": "ok",
  "failureReason": null,
  "durationSec": 19.621,
  "language": "en",
  "hasSpeech": true,
  "hook": {
    "text": "You can either buy an anti-aging cream that works in lines and wrinkles",
    "windowSec": 3,
    "spokenWords": 13,
    "source": "speech",
    "spokenText": "You can either buy an anti-aging cream that works in lines and wrinkles",
    "onScreenText": null,
    "firstSpeechStartSec": 0.14,
    "emptyReason": null
  },
  "transcript": [
    {
      "start": 0.14,
      "end": 9.2,
      "text": "You can either buy an anti-aging cream that works in lines and wrinkles or one designed to work on pores, blotches, tone, texture, radiance and lines and wrinkles.",
      "confidence": 0.8313
    },
    { "start": 9.48, "end": 14.4, "text": "Only one cream guarantees to reduce the appearance of all seven signs of aging or your money back.", "confidence": 0.8313 },
    { "start": 14.92, "end": 15.44, "text": "Total Effects.", "confidence": 0.8313 }
  ],
  "transcriptPlain": "You can either buy an anti-aging cream that works in lines and wrinkles or one designed to work on pores, blotches, tone, texture, radiance and lines and wrinkles. Only one cream guarantees to reduce the appearance of all seven signs of aging or your money back. Total Effects. Not a difficult choice. Total Effects from Ole.",
  "onScreenText": [
    { "atSec": 1.96, "text": "A", "confidence": 0.7936 }
  ],
  "cta": null,
  "sceneChanges": 6,
  "perceptualHash": "01694949418a0400",
  "meta": { "adId": "OlayTotalEffectsAdvert", "sourceDurationSec": 19.6, "bytes": 1918100 },
  "processedAt": "2026-09-14T11:17:05.746Z"
}
```

This creative opens on the voiceover, so `source` is `speech`. On a creative that opens on a
text card with the presenter a beat behind, `source` is `both` and the card is in
`onScreenText` — 10 of the 40 creatives in the measured batch were that shape. On one that
opens on music, `text` is `null`, `emptyReason` is `speech_starts_later`, and
`firstSpeechStartSec` tells you how late: that was 8 of the 40.

#### Output fields

| Field | Type | Description |
|---|---|---|
| `sourceUrl` | string | The submitted media URL. Normalized, it is also the charging key. |
| `adId` | string | null | Copied from `meta.adId`. |
| `status` | `ok` | `no_content` | `failed` | `ok` — speech or on-screen text found; `no_content` — processed but empty; `failed` — unusable media. |
| `failureReason` | string | null | `unreachable`, `unsupported_media`, `too_long`, `timeout`, `internal_error`. |
| `durationSec` | number | null | Video duration. |
| `language` | string | null | ISO 639-1 code detected from the audio. |
| `hasSpeech` | boolean | Whether any speech was recognized. |
| `hook` | object | null | The opening window: `text`, `spokenText`, `onScreenText`, `windowSec`, `spokenWords`, `source` (`speech`, `on_screen`, `both` or null), `firstSpeechStartSec`, `emptyReason`. Null only when the video failed. On a `no_content` video the object is present and `emptyReason` says why it is empty. |
| `transcript` | array | `{ start, end, text, confidence }` segments in chronological order. |
| `transcriptPlain` | string | null | The whole transcript as one flat string, for LLM input. |
| `onScreenText` | array | `{ atSec, text, confidence }`, deduplicated across frames. |
| `cta` | object | null | `{ text, atSec, source }` where source is `on_screen` or `speech`. |
| `sceneChanges` | integer | null | Detected scene changes — a proxy for editing pace. |
| `perceptualHash` | string | null | Fingerprint of the creative, for spotting the same ad under another advertiser. |
| `meta` | object | null | Your input `meta`, unchanged. |
| `processedAt` | string | UTC ISO-8601 timestamp. |

The full typed schema lives in [`.actor/dataset_schema.json`](.actor/dataset_schema.json), so
MCP clients and LLM agents can pick the actor up without guessing.

### Pricing

Pay per event. You are never charged for a failure.

Billed per **started 30 seconds** of video, because that is how the work scales. A
15-second creative costs less than it did under the old flat rate; a two-minute one finally
pays for the two minutes of transcription it takes.

| Event | Price | When |
|---|---|---|
| `actor-start` | $0.010 | Once per run. |
| `video_analyzed` | $0.050 | Video processed, speech or on-screen text found. Covers the first 30 s. |
| `video_extra_block` | $0.050 | Each started 30 s beyond the first. |
| `video_no_content` | $0.010 | Video processed, nothing found. Covers the first 30 s. |
| `video_failed` | **$0.000** | Media unreachable, broken, longer than the duration limit — or a stage of ours failed and produced nothing. No event is reported at all, so it cannot appear on your invoice. |

That is **$50 per 1,000 creatives** up to 30 seconds, which is how ad creatives are
actually counted. Pricing per media-minute would be the wrong unit here and misleading:
because a started block is billed whole, a 15-second creative works out at $100 per 1,000
real minutes and an 8-second one at $188. You buy analyses of creatives, not minutes of
audio.

**Calculator** — a typical batch of 500 creatives, 470 with content, most of them short:

```
actor-start                    1 × $0.010  =  $0.01
video_analyzed               470 × $0.050  =  $23.50
video_extra_block   90 over 30 s × $0.050  =  $4.50
video_no_content              20 × $0.010  =  $0.20
video_failed                  10 × $0.000  =  $0.00
                                              ───────
500 creatives                                  $28.21   (≈ $0.056 per creative)
```

For comparison, the closest listing in the store returns a transcript and on-screen text —
no hook, no CTA, no scene count, no perceptual hash — and charges $75 per 1,000.

Set `ACTOR_MAX_TOTAL_CHARGE_USD` on the run to cap the spend: when the cap is hit the run
finishes cleanly and keeps everything already stored in the dataset — it does not crash,
and it does not charge twice for a retried video.

### What you are guaranteed

**Nothing leaves the run.** Everything is computed on the Apify machine you pay for — no
third-party transcription service, no external AI provider, no key of yours or of ours.
There is nothing to sign up for, nothing to rate-limit you, and your creatives are not
handed to anyone.

**A partial result is still a result.** If one half of the analysis cannot be produced, you
get the other half rather than an error — and you are charged the normal rate, because a
transcript without on-screen text is still worth having.

**Our breakage is never your bill.** An empty result looks the same whether the creative
carried nothing or something on our side failed to read it. The two are told apart
explicitly: nothing found and everything worked → `no_content` from $0.010; nothing found
because something broke → `failed` / `internal_error` at $0.000.

**More memory is faster at the same price.** Doubling the memory roughly halves the time and
costs the same in total. Pick memory for how quickly you want the batch back, not to save
money — and leave `maxConcurrency` alone unless the media host is slow.

### FAQ

**Do I need a Facebook/TikTok login, cookies or an API key?**
No. The actor takes media URLs you already have and downloads them over plain HTTP.

**Does it scrape the Ad Library?**
No, by design. Ad Library scrapers are a commodity at $0.15/1k — use one of those and feed
its output here. That also keeps this actor complementary to the tools you already run.

**What if the same video appears twice in a batch?**
The charging key is the normalized URL, so you are billed once per creative even across
retries of the same run.

**Which languages?**
Transcription covers ~99 languages and the language is auto-detected from the audio — do not
set it by hand unless you are certain. CTA detection covers English, Spanish, Portuguese and
German.

**How long may a video be?**
300 seconds by default, and that is a hard cap. Anything longer is reported as
`failed` / `too_long` and charged $0, so one stray long-form video cannot eat your budget.

**Can I run it on a schedule?**
Yes. A failed result reports no billable event at all and an empty one starts at $0.010, so polling is cheap.

### Limitations

- Direct media URLs only; resolving an ad ID to a media URL is not implemented in v1.
- The `cta` field is the call to action **inside the creative** — burned into a frame or
  spoken in the voiceover. It is not the Ad Library button ("Shop now", "Learn more"):
  that is rendered by Meta outside the video and cannot be recovered from the file. If you
  need it, it comes from the Ad Library scraper that fed you these URLs, in its `ctaText`.
  Measured against Meta's own labels on 5 live creatives, our field agreed 0 times — by
  construction, not by failure.
- No face recognition, no biometric processing, no personal-data extraction — not now, not on request.
- Transcripts are produced for analysis; the actor never republishes the source media.
- Quality on heavily music-covered voiceovers is lower — check `confidence` rather than assuming.
- Videos above 256 MB are reported as `unsupported_media`.
- A caption that flashes on and off very briefly can be missed. Raise `maxFrames` for
  text-dense creatives.
- The same caption seen more than once is reported once, near-identical misreadings
  included. A caption whose wording genuinely changes later stays a separate entry.

### Roadmap

- **v1** — this scope: transcript, on-screen text, hook, CTA, scene changes, perceptual hash.
- **v2** — hook typology (question / shock-stat / problem / demo / testimonial), sentiment,
  offer and price extraction, creative deduplication by perceptual hash.
- **v3** — Standby mode for agents, MCP tool, cross-platform comparison of one creative.

### Disclaimer

You are responsible for complying with the terms of service of the platforms your media
comes from and with applicable data-protection law. This actor analyzes advertising
creatives; do not use it on private, personal or paywalled content.

# Actor input Schema

## `videos` (type: `array`):

Videos to analyze. Each item needs a direct media `url`; optional `meta` is copied to the output untouched, use it to carry your own identifiers such as `adId`. The actor does not scrape Ad Library itself — feed it URLs you already have.

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

Leave `auto` — the language is detected from the audio, and forcing a wrong language is the most common source of garbage transcripts. Set an ISO 639-1 code only when you know every video in the batch is in that language.

## `asrModel` (type: `string`):

How hard to work on the audio. `fast` is the default and runs in 4 GB of memory; `accurate` reads noisy or music-heavy ads better but needs 8 GB, which doubles the compute cost of the run.

## `extractOnScreenText` (type: `boolean`):

Run OCR over the key frames to capture captions, offers and prices burned into the video. Turn off for an audio-only, cheaper and faster pass.

## `maxFrames` (type: `integer`):

Upper bound on key frames sent to OCR. Frames are picked by scene-change detection, not by a fixed interval — 12 covers a typical 15–60 second ad.

## `hookWindowSeconds` (type: `integer`):

Length of the opening window reported separately as the hook. Three seconds is what performance buyers benchmark against.

## `skipIfNoSpeech` (type: `boolean`):

When on, a video with no speech is reported as `no_content` and OCR is skipped. Keep it off if you also want on-screen text from music-only creatives.

## `maxVideoDurationSeconds` (type: `integer`):

Hard per-video limit. Anything longer is reported as `failed` with reason `too_long` and charged $0 — this is what stops one stray 20-minute video from eating the whole run's budget. The platform cap is 300 seconds; you can only lower it.

## `maxConcurrency` (type: `integer`):

Upper bound on how many videos are in flight at once. The run lowers it to what the memory you gave it can actually keep busy, so raising it mostly helps when the media host is slow.

## Actor input object example

```json
{
  "videos": [
    {
      "url": "https://archive.org/download/OlayTotalEffectsAdvert/Olay%20Total%20Effects%20Advert.ia.mp4",
      "meta": {
        "adId": "OlayTotalEffectsAdvert"
      }
    },
    {
      "url": "https://archive.org/download/LOrealElviveDavidGinolaAdvert1998/L%27Oreal%20Elvive%20David%20Ginola%20Advert.ia.mp4",
      "meta": {
        "adId": "LOrealElviveDavidGinolaAdvert1998"
      }
    }
  ],
  "language": "auto",
  "asrModel": "fast",
  "extractOnScreenText": true,
  "maxFrames": 12,
  "hookWindowSeconds": 3,
  "skipIfNoSpeech": false,
  "maxVideoDurationSeconds": 300,
  "maxConcurrency": 6
}
```

# Actor output Schema

## `dataset` (type: `string`):

One row per video, in the order they were submitted.

# 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 = {
    "videos": [
        {
            "url": "https://archive.org/download/OlayTotalEffectsAdvert/Olay%20Total%20Effects%20Advert.ia.mp4",
            "meta": {
                "adId": "OlayTotalEffectsAdvert"
            }
        },
        {
            "url": "https://archive.org/download/LOrealElviveDavidGinolaAdvert1998/L%27Oreal%20Elvive%20David%20Ginola%20Advert.ia.mp4",
            "meta": {
                "adId": "LOrealElviveDavidGinolaAdvert1998"
            }
        }
    ],
    "language": "auto",
    "asrModel": "fast",
    "extractOnScreenText": true,
    "maxFrames": 12,
    "hookWindowSeconds": 3,
    "skipIfNoSpeech": false,
    "maxVideoDurationSeconds": 300,
    "maxConcurrency": 6
};

// Run the Actor and wait for it to finish
const run = await client.actor("kurilov_ai/ad-creative-intelligence").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 = {
    "videos": [
        {
            "url": "https://archive.org/download/OlayTotalEffectsAdvert/Olay%20Total%20Effects%20Advert.ia.mp4",
            "meta": { "adId": "OlayTotalEffectsAdvert" },
        },
        {
            "url": "https://archive.org/download/LOrealElviveDavidGinolaAdvert1998/L%27Oreal%20Elvive%20David%20Ginola%20Advert.ia.mp4",
            "meta": { "adId": "LOrealElviveDavidGinolaAdvert1998" },
        },
    ],
    "language": "auto",
    "asrModel": "fast",
    "extractOnScreenText": True,
    "maxFrames": 12,
    "hookWindowSeconds": 3,
    "skipIfNoSpeech": False,
    "maxVideoDurationSeconds": 300,
    "maxConcurrency": 6,
}

# Run the Actor and wait for it to finish
run = client.actor("kurilov_ai/ad-creative-intelligence").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 '{
  "videos": [
    {
      "url": "https://archive.org/download/OlayTotalEffectsAdvert/Olay%20Total%20Effects%20Advert.ia.mp4",
      "meta": {
        "adId": "OlayTotalEffectsAdvert"
      }
    },
    {
      "url": "https://archive.org/download/LOrealElviveDavidGinolaAdvert1998/L%27Oreal%20Elvive%20David%20Ginola%20Advert.ia.mp4",
      "meta": {
        "adId": "LOrealElviveDavidGinolaAdvert1998"
      }
    }
  ],
  "language": "auto",
  "asrModel": "fast",
  "extractOnScreenText": true,
  "maxFrames": 12,
  "hookWindowSeconds": 3,
  "skipIfNoSpeech": false,
  "maxVideoDurationSeconds": 300,
  "maxConcurrency": 6
}' |
apify call kurilov_ai/ad-creative-intelligence --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kurilov_ai/ad-creative-intelligence"
        }
    }
}
```

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/GPepVSlqdto3caxwE/builds/eOE92L2aljgn6Vobw/openapi.json
