# Audio & Video Transcriber (Whisper, your own files) (`mathtec/whisper-media-transcriber`) Actor

Transcribe audio and video files from direct URLs with open-source Whisper (faster-whisper, CPU). Text, timestamped segments, SRT or VTT. Silence and music are filtered before decoding, and each transcript carries quality flags. Memory stays flat for long files. Pay per audio minute.

- **URL**: https://apify.com/mathtec/whisper-media-transcriber.md
- **Developed by:** [Adam Rudd](https://apify.com/mathtec) (community)
- **Categories:** AI, Videos, Developer tools
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 2 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

$60.00 / 1,000 audio minute transcribeds

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

## Audio & Video Transcriber (Whisper, your own files)

Turn audio and video files into text, timestamped segments, or SRT/VTT subtitles. It runs open-source Whisper
([faster-whisper](https://github.com/SYSTRAN/faster-whisper), MIT) on CPU inside the Actor, so your audio is not
sent to any third-party speech service.

- **Input:** direct links to media files (mp3, wav, m4a, aac, ogg, opus, flac, mp4, mov, mkv, webm and anything
  else FFmpeg can decode). Only the audio track of a video is used.
- **Output:** one dataset item per URL: plain text plus, if you choose, timestamped segments, SRT or WebVTT. Every
  item says which engine and model (with its exact revision) produced it, whether silence filtering was on, and
  whether the transcript tripped a quality check.
- **Silence and music are filtered out before decoding** (voice activity detection, on by default), because
  Whisper invents text such as "Thank you." on non-speech audio. See *Measured*.
- **Quality flags:** each transcript is checked for the known failure shapes (repeated loops, one phrase repeated
  many times, uniform 30 s segments, long stretches with no text, a missing tail). A tripped check sets
  `quality_degraded: true` and says what and where in `quality_flags`.
- **Vocabulary prompt:** give names and terms and they come out spelled your way (measured below).
- **Long files:** audio is decoded in windows, so memory stays flat however long the file is (tested to 70 minutes).
- **Pricing:** pay per audio minute. See *Pricing*.

### What it does NOT do

- It does not download from web pages. The URL must return the media file itself; a URL that returns a web page
  fails with "URL returned a web page, not an audio or video file", and one that returns anything else that is
  not audio fails with "the URL did not return an audio or video file". Neither is charged.
- No speaker diarisation and no word-level timestamps in this version.

### Input

| Field | Default | Notes |
|---|---|---|
| `mediaUrls` | *(required)* | Direct media-file URLs |
| `model` | `small` | `tiny`, `base` or `small`. `small` is built in; `tiny` and `base` download in the run that uses them. (`medium` and `large-v3-turbo` appear in the measurements below but are not offered at the current price.) |
| `language` | auto | ISO code such as `en`. Empty = detect. **Auto-detect listens to only about the first 30 s of speech**, so a file that opens with music or another language can be detected wrongly. Set it if you know it. |
| `outputFormat` | `segments` | `text`, `segments`, `srt` or `vtt`. `text` is always included. |
| `vocabulary_prompt` | empty | Names and terms, spelled as you want them. **Only the last 224 tokens are used** (Whisper's limit): put the important terms at the end. **A prompt that does not match the audio can bleed into the transcript.** |
| `vadFilter` | `true` | Silero voice-activity detection (faster-whisper's `vad_filter`). |
| `vadThreshold`, `vadNegThreshold`, `vadMinSpeechDurationMs`, `vadMaxSpeechDurationS`, `vadMinSilenceDurationMs`, `vadSpeechPadMs` | 0.5, auto, 0, none, 2000, 400 | faster-whisper's own defaults, each explained in the input form |
| `engine` | `self_hosted` | The only engine: Whisper inside the Actor. |
| `windowSeconds` | `600` | Decoding window, 30 to 3600 |
| `beamSize` | `5` | 1 = greedy (faster) |
| `carryContext` | `true` | Prompt each window with the previous window's last ~200 characters. Keeps punctuation and quieter speakers across window boundaries (measured on a 2-speaker interview: 3.1% vs 5.4% WER off) |
| `maxDownloadMb` / `downloadTimeoutSecs` / `cpuThreads` | 2000 / 300 / 0 (auto) | |

### Output

```json
{
  "url": "https://.../episode-12.mp3",
  "status": "ok",
  "engine": "self_hosted",
  "model": "small",
  "modelRevision": "Systran/faster-whisper-small@536b0662742c02347bc0e980a01041f333bce120",
  "vad": {"enabled": true, "threshold": 0.5, "min_speech_duration_ms": 0, "min_silence_duration_ms": 2000,
          "speech_pad_ms": 400, "speechSeconds": 797.43},
  "quality_degraded": false,
  "quality_flags": [],
  "language": "en",
  "durationSeconds": 802.54,
  "transcribedSeconds": 802.54,
  "chargedUnits": 14,
  "text": "One dollar and eighty-seven cents. That was all. ...",
  "segments": [{"start": 0.0, "end": 3.6, "text": "One dollar and eighty-seven cents."}]
}
```

`status`: `ok`; `partial` (your maximum charge was reached or the run was aborted: the transcript covers what was
paid for); `failed` (404, a web page or non-media file, or a decoding error part-way: then the transcript covers
only the audio before the error and the error says so); `rejected` (not an http(s) URL, or a private address);
`not_processed` (did not fit in your maximum charge, or the run had stopped).

`quality_flags` entries are `{"gate", "detail"}`. Gates: `loops`, `repeats`, `uniform`, `gaps`, `coverage` (thresholds
from an established transcription checklist: 3 identical segments in a row, one text more than 25 times, 10+
segments of 28–31 s, more than 8 s with no text, the last segment ending 30 s or more before the audio does),
plus `decode_errors` and `window_failed`. `quality_degraded` is `null` for items that were never transcribed.
**`gaps` also trips on real pauses longer than 8 s** (for example 90 s of silence between two talks); the detail
says where, so you can judge.

An item larger than Apify's dataset limit (9 MB of JSON, [Apify docs](https://docs.apify.com/platform/storage/dataset))
keeps its metadata in the dataset and stores the transcript in the run's key-value store, linked by
`transcriptKey` / `transcriptUrl`.

### Pricing

**One price per minute of audio transcribed**, shown on the Pricing tab; no start fee. A partial minute at the end of each
file is rounded up. Rejected and failed URLs are not charged. If you set a maximum cost per run, a file that does
not fit is skipped without charge (`not_processed`); if the limit is reached mid-file you get the transcript up to the
last paid minute (`partial`). The price is the same for every model, and larger models take longer.

### Measured (this Actor's own tests, 2026-09-30)

One shared 16-vCPU KVM host (Haswell, no AVX-512), often busy with other work. **Every figure is one run (n=1)**, and
Apify's hardware differs. WER uses Whisper's English normaliser.

#### Accuracy depends heavily on the audio

Word error rate by condition and model, using the Actor's default settings (600 s windows, beam 5, VAD on, language
`en`, no vocabulary prompt). **On this set, `small` (the default) ranged from 0.3% on clean synthetic speech to 20.7%
on a clip full of unusual names. Real recordings (phone calls, crosstalk, heavy accents, poor microphones) were not
tested and may be worse.**

| Condition | tiny | base | small | medium | large-v3-turbo |
|---|---|---|---|---|---|
| TTS, clean (6 voices) | 1.4% | 0.7% | 0.3% | 0.3% | 0.6% |
| LibriVox story, one reader | 5.1% | 3.7% | 2.0% | 1.7% | 1.1% |
| LibriVox sonnet, 17 readers pooled \[range] | 9.2% \[4.0-16.9] | 6.7% \[3.2-12.1] | 5.0% \[3.2-8.1]\* | 8.4% \[3.2-32.3]\* | 3.6% \[1.6-7.3]\* |
| TTS + pink noise, 20 dB | 1.2% | 1.2% | 0.4% | 0.3% | 0.2% |
| TTS + pink noise, 10 dB | 2.2% | 1.4% | 0.4% | 0.3% | 0.2% |
| TTS + pink noise, 5 dB | 4.4% | 3.5% | 0.9% | 0.9% | 0.6% |
| TTS + pink noise, 0 dB | 18.5% | 9.9% | 4.3% | 2.2% | 1.8% |
| TTS + music, 10 dB | 1.4% | 0.9% | 16.5%\* | 0.2% | 0.2% |
| TTS + music, 0 dB | 9.7%\* | 7.9%\* | 7.2% | 10.7%\* | 10.2%\* |
| LibriVox story + pink noise, 10 dB | 7.9% | 5.4% | 3.0% | 1.7% | 1.5% |
| LibriVox story + music, 10 dB | 6.9% | 4.9% | 2.7% | 2.2% | 1.5% |
| TTS + competing talker, 10 dB | 11.1% | 4.6% | 1.1% | 0.6% | 4.5% |
| TTS through a phone channel | 10.9%\* | 8.8%\* | 7.8% | 7.0%\* | 7.2%\* |
| Unusual proper nouns, no prompt | 29.6% | 28.1% | 20.7% | 18.5% | 20.7% |

WER in percent, lower is better; VAD on, n=1 run per cell. `*` = at least one quality gate tripped on that cell (see `quality_flags` in `results/accuracy.jsonl`).

How the set was built (only audio we have rights to):

- **LibriVox** readings are public domain: *The Gift of the Magi* (one reader, 13.4 min) and Shakespeare's *Sonnet 130*
  read by **17 different volunteer readers** (different voices, accents and microphones, 56–91 s each). The sonnet
  reference is the Project Gutenberg text; its "damask'd" normalises to "damask would", so each reader carries up to
  two unavoidable errors (the sonnet figures are upper bounds). Readers' spoken introductions are not scored.
- **TTS**: our own 5.5-minute meeting script read by six Kokoro-82M voices (two British, four American). Clean synthetic
  speech is the easy case.
- **Noise and music** were synthesised (pink noise; a chord-and-melody loop) and mixed at the stated signal-to-noise
  ratio (speech level over its non-silent frames vs. the added sound). **Talker**: the LibriVox reading mixed 10 dB
  under the TTS speech. **Phone**: a synthetic phone channel (300–3400 Hz, 8 kHz, 8-bit mu-law).
- **tech_clean**: a 1-minute Kokoro clip with 15 invented or rare proper nouns (24 occurrences), scored without a
  vocabulary prompt. With the prompt, see below.
- A notable failure: `small` at music 10 dB dropped a whole ~120-word paragraph (and inserted a near-duplicate sentence).
  This happened with VAD on and with VAD off. The `gaps` check flagged it.

#### Silence and music (VAD)

Synthetic test file: 120 s of music, 45 s of speech, 90 s of digital silence, 45 s of speech. Segments whose
midpoint falls in the music or silence are invented text:

| model | VAD off | VAD on (default) |
|---|---|---|
| large-v3-turbo | 22 invented segments ("Thank you." ×6 in 30 s blocks, then invented sentences such as "What do we do with the new site?"), `loops` flag | 0 |
| small | 3 × "you" in the silence | 0 |
| tiny | 9 ("Thanks for watching." ×2, then repeated real sentences placed in the silence) | 0 |

Pure silence (60 s): "you" ×2 with VAD off for every model, nothing with VAD on. Pure music (120 s): large-v3-turbo wrote
"Thank you." ×4 with VAD off, nothing with VAD on. The cost: on the clean LibriVox reading, `small` scored 1.77% WER
with VAD off and 1.96–2.0% with it on.

#### Vocabulary prompt

One-minute clip with 15 unusual names and terms (24 occurrences), e.g. "Oleksandra Wyrzykowski", "Zephyrine",
"Hraunfoss". Exact spelling counts.

| model | no prompt | with prompt (80 tokens) | terms first, then filler (464 tokens, cut to last 224) | filler first, terms last (465 tokens, cut to last 224) | prompt words in unrelated audio |
|---|---|---|---|---|---|
| tiny | 1/15 terms, 2/28 occurrences, WER 29.6% | 9/15 terms, 16/28 occurrences, WER 17.8% | 1/15 terms, 2/28 occurrences, WER 31.1% | 10/15 terms, 18/28 occurrences, WER 13.3% | 0 |
| base | 0/15 terms, 0/28 occurrences, WER 28.1% | 13/15 terms, 25/28 occurrences, WER 7.4% | 0/15 terms, 0/28 occurrences, WER 32.6% | 12/15 terms, 23/28 occurrences, WER 8.9% | 0 |
| small | 2/15 terms, 4/28 occurrences, WER 20.7% | 14/15 terms, 26/28 occurrences, WER 1.5% | 3/15 terms, 6/28 occurrences, WER 23.0% | 14/15 terms, 27/28 occurrences, WER 3.7% | 0 |
| medium | 3/15 terms, 6/28 occurrences, WER 18.5% | 15/15 terms, 28/28 occurrences, WER 0.0% | 3/15 terms, 6/28 occurrences, WER 18.5% | 15/15 terms, 27/28 occurrences, WER 0.7% | 0 |
| large-v3-turbo | 4/15 terms, 8/28 occurrences, WER 23.0% | 14/15 terms, 26/28 occurrences, WER 2.2% | 5/15 terms, 11/28 occurrences, WER 12.6% | 15/15 terms, 28/28 occurrences, WER 5.9% | 0 |

Terms first then filler: the prompt is over 224 tokens, so the terms are cut off and it behaves like no prompt. Filler first, terms last: the terms survive the cut. The 5th column counts prompt words that appeared in a different recording containing none of them.

The same prompt, applied to a different recording with none of those words, put 0 of them into the transcript for all
five models (the bleed test). That is one test, not proof that bleed cannot happen.

#### Memory

Peak memory depends on the window length and the model, not on file length (70-minute file, `small`, 600 s windows:
1.6 GB for the whole Actor process). `large-v3-turbo` peaked at 2.1 GB on the Actor's own path, so it needs the 4 GB
setting; it does not fit 2 GB.

### Rights and responsible use

Only submit media that you own or are licensed to process, with any consent the recording requires.

### About this Actor

Built, tested and maintained with an AI coding agent working for its publisher. Dependencies: faster-whisper 1.2.1
(MIT), CTranslate2, PyAV 15.1.0 (bundled FFmpeg), Silero VAD (via faster-whisper), OpenAI Whisper weights converted to
CTranslate2 (MIT; revisions pinned and checked at build), Apify SDK for Python 4.0.2.

# Actor input Schema

## `mediaUrls` (type: `array`):

Direct links to audio or video FILES (mp3, wav, m4a, mp4, mov, webm, ogg, flac, ...). Each URL must return the media file itself. A URL that returns a web page (for example a page with a player on it) fails with 'URL returned a web page, not an audio or video file'. One dataset item is produced per URL.

## `model` (type: `string`):

Larger = more accurate and slower. 'small' is built into the image (no download); tiny (78 MB) and base (148 MB) are downloaded in the run that uses them. Accuracy depends heavily on the audio: see the accuracy table in the README.

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

ISO 639-1 code such as 'en', 'de', 'es'. Leave empty to auto-detect. Auto-detect listens only to about the first 30 seconds of speech in each file, so a recording that opens with music, a jingle or a different language can be detected wrongly, and the whole transcript then comes out in the wrong language. If you know the language, set it.

## `outputFormat` (type: `string`):

Every item has the plain 'text'. 'segments' adds a list of {start, end, text} with timestamps in seconds; 'srt' and 'vtt' add a subtitle file as a string.

## `vocabulary_prompt` (type: `string`):

Names, product names and technical terms that occur in the recording, written the way you want them spelled, for example: 'Speakers: Oleksandra Wyrzykowski, Tjark Ihlenfeld. Terms: Zephyrine, Kubernetes, QLoRA.' It is given to Whisper as its initial prompt for every decoding window. LIMIT: Whisper uses at most 224 tokens. If your prompt is longer, only the LAST 224 tokens are used and the beginning is dropped, so put the most important terms at the end. WARNING: a prompt that does not match the audio can bleed into the transcript: Whisper may write words from the prompt that were never spoken. Leave it empty unless you know what is in the recording.

## `vadFilter` (type: `boolean`):

On by default. Silero voice-activity detection (faster-whisper's vad_filter) removes silence, music and noise before Whisper decodes, because Whisper invents text such as 'Thank you.' or 'Thanks for watching.' on non-speech audio. Turn it off only if speech is being skipped (very quiet speech, or speech under loud music).

## `engine` (type: `string`):

Which transcription engine runs. 'self_hosted' runs open-source Whisper inside this Actor; your audio is not sent to any other service. It is the only engine.

## `windowSeconds` (type: `integer`):

Audio is decoded and transcribed this many seconds at a time, so memory stays flat however long the file is. Longer windows use a little more memory (raw audio is window x 64 KB, plus Whisper's features for it) and cut the audio less often. The segment that straddles a window edge is re-decoded with the next window, so no words are dropped. See the README for measurements.

## `beamSize` (type: `integer`):

Whisper beam search width. 1 is greedy decoding (faster, slightly less accurate); 5 is the library default.

## `carryContext` (type: `boolean`):

Pass the last ~200 characters of the previous window as the prompt for the next one (helps keep names/spelling consistent). Measured effect on accuracy is in the README. If a vocabulary prompt is also set, the two are joined and the last 224 tokens are used.

## `vadThreshold` (type: `number`):

Silero gives each 32 ms of audio a speech probability; above this value it counts as speech. Lower = keeps more (quiet speech, but also more noise); higher = removes more. Library default 0.5.

## `vadNegThreshold` (type: `number`):

Probability below which speech is considered to have ended. Leave empty to let the library compute it (speech threshold minus 0.15, at least 0.01).

## `vadMinSpeechDurationMs` (type: `integer`):

Speech chunks shorter than this are thrown away. Library default 0 (keep all).

## `vadMaxSpeechDurationS` (type: `number`):

Speech chunks longer than this are split at a short silence. Leave empty for no limit (the library default).

## `vadMinSilenceDurationMs` (type: `integer`):

Non-speech must last at least this long before it is cut out. Library default 2000. Lower = cuts shorter pauses.

## `vadSpeechPadMs` (type: `integer`):

Audio kept before and after each speech chunk so word beginnings and endings are not clipped. Library default 400.

## `maxDownloadMb` (type: `integer`):

Files larger than this are not downloaded. The file is stored on the run's disk while it is transcribed (disk = 2 x the run's memory).

## `downloadTimeoutSecs` (type: `integer`):

Read timeout for each file download.

## `cpuThreads` (type: `integer`):

0 = use all CPUs the run was given (1 CPU per 4 GB of memory on Apify).

## Actor input object example

```json
{
  "mediaUrls": [
    "https://archive.org/download/sonnet_130_librivox/sonnet130_shakespeare_kmcq_64kb.mp3"
  ],
  "model": "small",
  "language": "",
  "outputFormat": "segments",
  "vocabulary_prompt": "",
  "vadFilter": true,
  "engine": "self_hosted",
  "windowSeconds": 600,
  "beamSize": 5,
  "carryContext": true,
  "vadThreshold": 0.5,
  "vadMinSpeechDurationMs": 0,
  "vadMinSilenceDurationMs": 2000,
  "vadSpeechPadMs": 400,
  "maxDownloadMb": 2000,
  "downloadTimeoutSecs": 300,
  "cpuThreads": 0
}
```

# Actor output Schema

## `transcripts` (type: `string`):

Dataset items: url, status (ok | partial | failed | rejected | not_processed), error, engine, model, modelRevision, vad, quality_degraded, quality_flags, language, languageProbability, durationSeconds, transcribedSeconds, chargedUnits, processingSeconds, rtf, text, and segments / srt / vtt depending on outputFormat. An item too large for a dataset (over 9 MB of JSON) has transcriptKey / transcriptUrl instead of the transcript fields; the transcript is then a record in the run's default key-value store.

# 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 = {
    "mediaUrls": [
        "https://archive.org/download/sonnet_130_librivox/sonnet130_shakespeare_kmcq_64kb.mp3"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("mathtec/whisper-media-transcriber").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 = { "mediaUrls": ["https://archive.org/download/sonnet_130_librivox/sonnet130_shakespeare_kmcq_64kb.mp3"] }

# Run the Actor and wait for it to finish
run = client.actor("mathtec/whisper-media-transcriber").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 '{
  "mediaUrls": [
    "https://archive.org/download/sonnet_130_librivox/sonnet130_shakespeare_kmcq_64kb.mp3"
  ]
}' |
apify call mathtec/whisper-media-transcriber --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mathtec/whisper-media-transcriber"
        }
    }
}
```

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/VaEKDeOgA6W8owRAz/builds/SUOSMoWnVNFaIZ66p/openapi.json
