# Media & Podcast Transcriber: Audio/Video to Text, SRT, VTT (`stubborn_otter/media-transcriber`) Actor

Transcribe audio/video links, podcast RSS feeds, Apple Podcasts, Dropbox and Google Drive files into text, speaker paragraphs, SRT and VTT. $0.015 per audio minute; links that fail are free.

- **URL**: https://apify.com/stubborn_otter/media-transcriber.md
- **Developed by:** [Stubborn Otter Software](https://apify.com/stubborn_otter) (community)
- **Categories:** AI, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $15.00 / 1,000 audio minutes

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

## Media & Podcast Transcriber: Audio/Video to Text, SRT, VTT

Paste links to audio or video files, podcast RSS feeds, Apple Podcasts shows, or Dropbox and Google Drive files. You get back the **full transcript, timestamped paragraphs with speaker labels, SRT and VTT subtitle files**, and the detected language and length of each file. **$0.015 per audio minute** on every plan, speaker labels included. **You're never charged for links it can't reach, sites it doesn't support, or files with no speech.**

| You give it | You get back, per file |
|---|---|
| `https://example.com/episode-12.mp3` | `{"status": "ok", "title": "episode-12", "durationSeconds": 3612.4, "chargedMinutes": 61, "language": "en", "speakers": 2, "text": "Speaker 1: Welcome back…", "paragraphs": [{"start": 0.4, "end": 12.4, "speaker": 1, "text": "Welcome back…"}], "srtUrl": "…/transcript-001-episode-12.srt", "vttUrl": "…", "txtUrl": "…"}` |
| `https://feeds.example.com/show.xml` | One row per episode, newest first (5 by default) |
| `https://youtu.be/…` | `{"status": "unsupported_site", "reason": "YouTube links aren't supported: …", "chargedMinutes": 0}` |

### What you can transcribe

| Works | Examples |
|---|---|
| **Direct audio and video files** | `.mp3`, `.m4a`, `.wav`, `.flac`, `.ogg`, `.opus`, `.webm`, `.mp4`, `.mov`, `.aac`: any public link that downloads the file |
| **Podcast feeds** | RSS or Atom feed links, or the podcast's web page if it links its feed. Episodes are taken newest first |
| **Apple Podcasts** | Show links (`podcasts.apple.com/…/id123…`) and episode links (`…?i=1000…`), resolved to the show's public RSS feed |
| **Dropbox** | File share links (`dropbox.com/scl/fi/…`); folder links aren't supported |
| **Google Drive** | Files shared with **"Anyone with the link"**; private files come back with instructions to share them |

**Refused at no charge:** YouTube, TikTok, Instagram, Facebook, X (Twitter), LinkedIn, Twitch, Loom, Vimeo, Zoom and Spotify links, including links that redirect to them and their video CDNs. These platforms' terms don't allow automated downloading, so this Actor never contacts them. Live radio streams and streaming playlists (HLS, DASH, M3U) are refused too, because they have no end to transcribe.

### How to use it

1. **Paste your links** into *Media links*, one per line, up to 100 per run. A feed counts as one link, however many episodes it has.
2. **Optional: set limits.** *Max episodes per feed* (5), *max minutes per file* (180) and *max minutes per run* (300). Every limit is applied **before** anything is sent for transcription.
3. **Run.** Each file becomes one dataset row, in the order you gave them. The `.txt`, `.srt` and `.vtt` files go to the run's key-value store, and each row links to its files.

**Scheduled podcast transcripts:** schedule a run with your feed. *Skip episodes already transcribed* is on by default, so each run transcribes and charges only the new episodes. Runs with no new episodes succeed without charging for transcription.

**From n8n, Make, Zapier or the API:** "URL in, transcript out". Start the Actor with `{"mediaUrls": ["https://…/file.mp3"]}` and read the dataset items.

### Pricing

| Event | Price | When |
|---|---|---|
| Audio minute | **$0.015** | Each started minute of a file that came back with words |
| Audio minute, your own Deepgram key | **$0.005** | The same, when you add your own Deepgram API key (Deepgram bills you for the audio directly) |
| Actor start | $0.0003 | Once per run |

Some example prices:

- A 60-minute podcast episode costs **$0.90**, or $0.30 with your own key.
- Ten 30-minute episodes cost $4.50.
- A 90-second clip counts as 2 started minutes and costs $0.03.

Speaker labels, subtitles and language detection are included.

**Always free:**

- links refused or unreachable;
- web pages and other files that aren't audio or video;
- files over your limits;
- files with no speech (silence or music only);
- episodes skipped as already transcribed;
- the same file listed twice in one run (charged once).

**Your maximum cost per run is respected before transcription starts.** The Actor reserves each file's length against your limit before sending it. A file that doesn't fit is skipped with status `over_budget`, and you aren't charged for it. When a file's length can't be read in advance, the Actor reserves what its size allows. You're never charged more than it reserved.

**Free Apify plans:** add your own Deepgram API key (new Deepgram accounts get **$200 of free credit**, no card needed, at [console.deepgram.com](https://console.deepgram.com)), or upgrade your Apify plan. The demo clip in the example input works on any plan.

### What each row tells you

| Field | Meaning |
|---|---|
| `status` | `ok` (charged) or, all free: `no_speech`, `duplicate`, `unsupported_site`, `not_media`, `unreachable`, `too_long`, `over_budget`, `needs_api_key`, `provider_error` |
| `reason` | Why a file wasn't transcribed, in one sentence with the fix. For example: *"This Google Drive file isn't shared publicly. In Drive choose Share, set General access to 'Anyone with the link', then run again."* |
| `title`, `sourceType`, `feedTitle`, `publishedAt` | What the file is and where it came from (`direct`, `feed`, `apple_podcasts`, `dropbox`, `google_drive`) |
| `durationSeconds`, `chargedMinutes`, `charged` | The file's length and what you paid for it |
| `language`, `speakers`, `wordCount` | The language detected (or the one you set), the number of speakers and the number of words |
| `text` | The full transcript, never truncated, with "Speaker 1:" / "Speaker 2:" when there are several speakers |
| `paragraphs` | `[{start, end, speaker, text}]`, with times in seconds |
| `txtUrl`, `srtUrl`, `vttUrl`, `wordsUrl` | Links to the files (word timings only if you pick that format) |
| `note` | Anything unusual. For example, the file's host blocked the speech service, so the Actor uploaded the file itself |

The run summary is always in the run's `OUTPUT` record. It has counts per status, minutes processed, minutes charged, feeds expanded and the commonest failure. Turn on *Add a summary row* to also get it as the last dataset item.

### Reliability

- **One row per file, in your order.** A file that can't be transcribed still gets a row with its status and reason. You never get 7 rows back from 10 links with no explanation.
- **Links are checked before transcription.** The Actor follows redirects (podcast analytics links included), checks that the link is audio or video, and reads the file's length from its first few kilobytes.
- **No local AI model, no memory errors.** Files are transcribed by Deepgram's Nova-3 speech model, which downloads them directly. The Actor runs at 512 MB, and long podcasts don't need more memory.
- **Retries with backoff** when the speech service is busy. Four files are transcribed in parallel.
- **The run fails only when nothing could be transcribed**, and the error says why. A scheduled feed run with no new episodes isn't a failure.

### Limits

- **Links per run:** 100. A feed adds up to *max episodes per feed* (1–500) episodes.
- **File length:** up to 600 minutes per file (*max minutes per file*, default 180). The speech service stops processing any single file after 10 minutes of work. Very long files may hit that limit and come back as `provider_error`, free.
- **File size:** up to 2 GB.
- **Languages:** detected automatically, or set one with a code such as `en`, `es`, `de`, `pt-BR` or `ja`. Use `multi` for conversations that switch languages.

### Your data and your rights

- **Where your audio goes:** each file goes to [Deepgram](https://deepgram.com)'s speech API, through Deepgram's own download of your link or, if the host blocks it, through an upload by this Actor.
- **No model training:** every request is sent with Deepgram's opt-out from model training (`mip_opt_out=true`).
- **With your own key:** requests run on your Deepgram account under your agreement with Deepgram.
- **What the Actor keeps:** it never stores your media. Transcripts are saved only in your own run's dataset and key-value store. The Actor's only memory between runs is the record of episodes already transcribed. That is a list of episode IDs, kept in a key-value store in your Apify account.
- **Your responsibility:** only transcribe media you have the right to process, and get consent where recording laws require it.

This Actor is made by Stubborn Otter Software. It isn't affiliated with or endorsed by Deepgram, Apple, Dropbox or Google.

### Questions and issues

Open an issue on the **Issues** tab. Issues are answered within a day.

# Actor input Schema

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

One link per line, up to 100: direct audio or video files (.mp3, .m4a, .wav, .mp4, .ogg, .webm, ...), podcast RSS or Atom feeds, podcast web pages that link their feed, Apple Podcasts show or episode links, Dropbox file links, and Google Drive files shared with 'Anyone with the link'. YouTube, TikTok, Instagram, Facebook, X, LinkedIn, Twitch, Loom, Vimeo, Zoom and Spotify links are refused at no charge.

## `maxEpisodesPerFeed` (type: `integer`):

For each podcast feed, transcribe at most this many episodes, newest first.

## `maxMinutesPerItem` (type: `integer`):

Files longer than this are skipped with status too_long and not charged.

## `maxTotalMinutes` (type: `integer`):

Stop adding files once their total length would pass this. Later files get status over_budget and aren't charged. The run's 'Maximum cost per run' setting is respected too.

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

Leave empty to detect each file's language automatically. Or give a language code (en, en-US, es, fr, de, pt-BR, ja, ...) to force one, or 'multi' for conversations that switch between languages.

## `speakerLabels` (type: `boolean`):

Tell speakers apart (Speaker 1, Speaker 2, ...) in the text, paragraphs and subtitles. Included in the price.

## `outputFormats` (type: `array`):

What to produce for each transcribed file. Files go to the run's key-value store; each dataset row links them.

## `skipAlreadyTranscribed` (type: `boolean`):

For feeds and Apple Podcasts shows: skip episodes this Actor already transcribed for you in an earlier run (status duplicate, not charged), so a scheduled run only pays for new episodes. Direct file links are always transcribed.

## `includeSummaryRow` (type: `boolean`):

Also push the run summary as the last dataset item. Off by default so the dataset stays one row per file. The summary is always in the OUTPUT record.

## `deepgramApiKey` (type: `string`):

A Deepgram API key from console.deepgram.com (API Keys, role Member). Files are then transcribed on your Deepgram account.

## Actor input object example

```json
{
  "mediaUrls": [
    "https://api.apify.com/v2/key-value-stores/t6g66CNDpKtZcuneO/records/sample-60s.mp3"
  ],
  "maxEpisodesPerFeed": 5,
  "maxMinutesPerItem": 180,
  "maxTotalMinutes": 300,
  "speakerLabels": true,
  "outputFormats": [
    "text",
    "paragraphs",
    "srt",
    "vtt"
  ],
  "skipAlreadyTranscribed": true,
  "includeSummaryRow": false
}
```

# Actor output Schema

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

One row per media item, in input order: status, reason, duration, charged minutes, language, speakers, full text, speaker paragraphs and links to the .txt, .srt and .vtt files.

## `files` (type: `string`):

The .txt, .srt, .vtt (and optional word-timing .json) files, one set per transcribed item.

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

Counts per status, minutes processed, charged minutes and price, feeds expanded and the commonest failure.

# 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://api.apify.com/v2/key-value-stores/t6g66CNDpKtZcuneO/records/sample-60s.mp3"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("stubborn_otter/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://api.apify.com/v2/key-value-stores/t6g66CNDpKtZcuneO/records/sample-60s.mp3"] }

# Run the Actor and wait for it to finish
run = client.actor("stubborn_otter/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://api.apify.com/v2/key-value-stores/t6g66CNDpKtZcuneO/records/sample-60s.mp3"
  ]
}' |
apify call stubborn_otter/media-transcriber --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,stubborn_otter/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/LrmbNilcpxJbt4zKh/builds/EfM6Naiqa9vyGDfLM/openapi.json
