# Audio Transcription - Speech to Text, Audio to Text, Whisper (`tidytools/audio-transcriber`) Actor

Speech to text for audio, video, YouTube and podcast RSS, using Whisper. Get text with timestamps plus SRT, VTT, TXT and Markdown files. $0.006 per started audio minute; optional speaker labels $0.015/min. Failed and silent files are free. Up to 1,000 links per run.

- **URL**: https://apify.com/tidytools/audio-transcriber.md
- **Developed by:** [Yukai Lin](https://apify.com/tidytools) (community)
- **Categories:** AI, Videos, Automation
- **Features:** Limited permissions
- **Stats:** 236 total users, 171 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.80 / 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.
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?

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 Transcription: Speech to Text, Video to Text, Podcast Transcription

#### What it does

This Actor turns audio files, video links and podcast RSS episodes into text. You get the text with timestamps, plus downloadable SRT, VTT, TXT and Markdown files. It works for MP3 to text, video to text, YouTube video transcription and podcast transcription. It can also generate SRT/VTT subtitles.

**Example input**

```json
{"urls":["https://webcapture-api.yukailin.workers.dev/samples/speech-sample.wav"]}
```

**What you get back:** one row for the 8.5-second sample, with:

- non-empty `text`
- `durationSeconds` of about `8.5`
- `charged: true`
- `srtUrl`, `vttUrl`, `txtUrl` and `mdUrl` download links

The run's `SUMMARY.status` is `SUCCESS`.

**Price:** $0.006 per started audio minute on the Free and Bronze plans. The sample above is one billed minute, so it costs $0.006. Files that fail, are blocked or have no speech are not charged.

**Hundreds of file links in one run:** put up to 1,000 public media links in `urls`, for example `{"urls":["https://example.com/audio/001.mp3","https://example.com/audio/002.mp3","https://example.com/audio/003.mp3"],"maxTotalMinutes":1000}` (placeholder links; `maxTotalMinutes` is an optional cap on the audio minutes of this run). Each file gets its own row with `charged: true/false`, and failed files are free. Failed rows with `retryable: true` go into the run's `RETRY_INPUT` record: your input with only those links, ready to use as the input of a new run, so finished files are not transcribed or charged again. Rows with `retryable: false` stay out of it; their `error` says what to change. If a limit stops the run, the `REMAINING_INPUT` record holds the files left.

**From the API or a schedule:** this request starts a batch and returns the run's IDs at once.

```
POST https://api.apify.com/v2/acts/tidytools~audio-transcriber/runs
Authorization: Bearer <your Apify API token>
Content-Type: application/json

{ "urls": ["https://example.com/audio/001.mp3", "https://example.com/audio/002.mp3"], "resumeStore": "my-batches" }
```

Check `GET https://api.apify.com/v2/actor-runs/<run id>?waitForFinish=60` until `status` is `SUCCEEDED`, then read `GET https://api.apify.com/v2/datasets/<defaultDatasetId>/items`. To retry, read `GET https://api.apify.com/v2/key-value-stores/<defaultKeyValueStoreId>/records/RETRY_INPUT` (the record is missing when no link is worth retrying) and send it as the body of the same `POST`. With `resumeStore`, a file sent again with the same settings returns its saved transcript with `reused: true` and is not charged again. Send each batch from your own script, n8n or Make, or give an Apify Schedule this input with `"newEpisodesOnly":true` and a `monitorName`, so later runs skip links already transcribed under that name. More in [n8n, Make and Zapier](#use-in-n8n-make-or-zapier-no-25-mb-limit) and [retries](#retry-only-what-may-work-later).

**Check the cost first:** set `"estimateOnly":true` for a free, metadata-only preview of podcast RSS episodes, YouTube playlist/channel videos or files in a public Google Drive folder. Each row has `source`, `title`, `durationSecs`, `estimatedMinutes` and `estimatedUsd`; `SUMMARY` totals known minutes and flags `exceedsMaxTotalMinutes`. Unknown durations stay `null` with `estimate:"unknown"`. This mode does not download media, transcribe or emit charge events. The estimate covers audio-minute events at the current pricing tier; successful transcription and optional extras can change actual cost.

Start with the link you have:

- **Direct audio or video file URL** (such as `.mp3` or `.mp4`): [transcribe a file link](#how-to-transcribe-an-audio-or-video-file-link).
- **Google Drive / Dropbox / OneDrive share link** that opens and downloads without signing in: [transcribe a shared file](#how-to-transcribe-an-audio-or-video-file-link).
- **YouTube video link**: [transcribe a YouTube video](#how-to-transcribe-a-youtube-video-to-text).

**YouTube without captions:** Example input: `{"urls":["https://www.youtube.com/watch?v=CKRxRE7qRzc"]}`. For a public video with accessible audio and speech, the Actor tries to transcribe the audio, not YouTube captions. A successful row includes `text`, timed `segments` and downloadable transcript and subtitle files (with the default save settings). Audio costs $0.006 per started minute, rounded up, only on success; an unreadable video or one with no transcribable speech gets a free failed/no-text row. Live streams still in progress and private or restricted videos are not guaranteed to work.

For medium or long recordings or several links, [process multiple files in one run](#multiple-files-in-one-run). Each file gets its own result row; successful transcripts provide `txtUrl` and `mdUrl` download links when transcript files are saved, plus `srtUrl` and `vttUrl` when subtitles are saved in those formats (both by default).

#### Optional podcast content pack

Set `"contentPack":true` to draft `contentPack.chapters`, `contentPack.showNotes`, `contentPack.socialPosts.x`, and `contentPack.socialPosts.linkedin` for each successful transcript. Add `"contentPackSubtitleLanguages":["Spanish"]` for one translated SRT/VTT language; its download links are in `contentPack.translatedSubtitles`. These are drafts to review against the transcript before publishing. The option is off by default; with it off, output and billing remain the same. A failed pack sets `contentPackError` and has no pack event. On paid Apify plans the pack is written by Gemini (with fallbacks); on the free plan a smaller Workers AI model writes it, so chapters and posts are simpler and a pack with mis-timed chapters is dropped (not charged).

The pack also has `contentPack.clipCandidates`: up to 3 suggested 30–60 second passages worth cutting as short clips, each with `startSeconds`, `endSeconds`, `start`, `end` (e.g. `"4:05"`) and a short `title`. They are timestamp suggestions only, not cut audio or video files and not video links; check them in your editor. A candidate is kept only when it starts on a timestamp from the transcript and fits inside the recording. Untimed transcripts (for example a plain-text publisher transcript) and recordings under 5 minutes always get `[]`, and so does a pack whose suggestions are all invalid; the other pack fields are delivered as usual. Clip candidates are part of the same `content-pack` event below, with no extra charge. When the pack is on, `SUMMARY.clipCandidatesReturned` counts them.

The pack can also include `contentPack.newsletterDraft` with a `subject` (up to 120 characters) and a plain-text `body` (up to 2,000 characters) for an episode-update email: a short intro, key points from the transcript and a closing line asking readers to listen or read. **This is a draft for you to review, not a sent email.** The Actor sends no email and has no mailing list. Its only allowed link is the episode page from your RSS feed (`episodeUrl`). Without that page it has no link. A draft that is empty, too long or contains any other link, web address or email address (also without `https://`, such as `sponsor.example/deal`) is left out, and the other pack fields are delivered as usual. The draft comes from the same AI request and is part of the same `content-pack` event, with no extra charge. When the pack is on, `SUMMARY.newsletterDraftsReturned` counts the drafts that were delivered.

**Charge starts 2026-11-06 or later, after the 14-day notice:** one `content-pack` event at $0.10 per successful episode, in addition to existing audio-minute charges. Until the operator sets `CONTENT_PACK_CHARGE_FROM` to the announced date and that date arrives, the text pack is free; the configured date cannot be earlier than 2026-11-06. Optional translated subtitles use the existing `subtitle-language-minute` event at $0.002 per started minute, charged only on success, including before the pack charge date. `summarize` also remains separate. For long transcripts, AI prompts sample the whole recording, including the final paragraph; the complete transcript stays in `text` and TXT/Markdown files.

Subtitle files are saved to the run's key-value store and linked in each timed transcript row as `srtUrl` and `vttUrl`. To save only one format, set `"outputFormats":["srt"]` or `"outputFormats":["vtt"]`; omitting `outputFormats` keeps both. `"outputFormats":[]` or `"saveSubtitles":false` saves neither original-language subtitle file. The text, segments and audio-minute charge stay the same. `speakerLabels` is optional and off by default; it uses the existing Deepgram nova-3 path and $0.015/min speaker-label event for supported file inputs. YouTube links and some languages do not support it.

#### Try one short recording

The input form starts with one public, 8.5-second spoken WAV file; **Podcast RSS feeds** is empty. Click **Start** to get one transcript row, then replace the sample URL with your own file. The sample needs no login.

Input: `{"urls":["https://webcapture-api.yukailin.workers.dev/samples/speech-sample.wav"]}`

Output: one successful row with nonempty `text`, `durationSeconds` of about `8.5`, `charged: true`, and `srtUrl`, `vttUrl`, `txtUrl` and `mdUrl` download links. The run's `SUMMARY.status` is `SUCCESS`.

**Price:** $0.006 per started audio minute (this sample is one billed minute, $0.006); failed, blocked and speechless files are not charged. Speaker labels are $0.015/min instead, and optional AI summary is $0.01/file.

**WhatsApp / Telegram voice notes:** pass direct `.ogg` / `.opus` download URLs from n8n, uazapi or Evolution API in `urls`. The link must still work without a login when this Actor starts downloading it. One voice note of up to a minute is $0.006, so the Apify Free plan's $5 monthly credit covers about 830 of them; when a flow needs more, a paid Apify plan keeps it running (once the credit is used up, Apify does not start new runs until the monthly reset), and a row that hit your limit says so in its `error`.

n8n HTTP Request JSON body (replace `voiceNoteUrl` with your previous node's direct media URL field): `{"urls":["{{$json.voiceNoteUrl}}"]}`

#### How to transcribe an audio or video file link

1. Paste the file links, one per line, into **Audio or video file URLs** (a direct `.mp3` / `.mp4` link, or a Google Drive / Dropbox / OneDrive share link set to "anyone with the link"). Check that the file downloads in a signed-out or private browser window and that any temporary link is still valid. If your company does not allow public sharing, use **Or upload audio / video files** (`audioFiles`) instead.
2. Optional: set the **Language** and turn on **Speaker labels** ($0.015 per minute instead of $0.006) or **AI summary and chapters** ($0.01 per file).
3. Click **Start**. Each file becomes one row with the text, timestamps and `srtUrl` / `vttUrl` / `txtUrl` / `mdUrl` links to its subtitle and transcript files.
4. Download the files from those links, or call the Actor from the API. A 60-minute recording costs $0.36; a file that fails or has no speech costs nothing.

#### Stereo phone recordings

For a direct audio or video file with exactly two audio channels, set `audioChannelMode` to `left` or `right` when each caller is recorded on a separate side. Only the chosen side is transcribed; the other side is discarded. Run each side separately if you need both, with one transcript and one audio-minute charge based on the original file duration per run. This does not identify speakers or change the `speakerLabels` price.

The default `auto` keeps the normal left/right mix unless a short sample shows one side is nearly silent or both sides nearly cancel when mixed. `mix` always combines both sides. Ordinary two-speaker stereo, mono files and files with more than two channels keep the existing mix in `auto`; `left` or `right` on files without exactly two channels returns a free `unsupported` row. The setting applies to file audio sent to ASR; YouTube service audio and publisher transcripts are handled by their existing paths.

#### Multiple files in one run

1. For three recordings, paste three public links, one per line, into **Audio or video file URLs** (`urls`), or upload three files under **Or upload audio / video files** (`audioFiles`). Example `urls` values: `https://example.com/recording-1.mp3`, `https://example.com/recording-2.mp3`, `https://example.com/recording-3.mp3` (placeholders; replace them with your own links). Links must be downloadable without signing in.
2. Remove the prefilled speech sample from `urls` if you do not want it in this run; it can be transcribed and charged. **Podcast RSS feeds** starts empty. Before **Start**, set Apify's **Maximum cost per run** in Run options or **Max total minutes per run** (`maxTotalMinutes`) in Advanced settings.
3. Each file gets one result row; failed or silent files are free. After the batch, files with a temporary `service_busy`, `timeout` or `network` failure wait 45 seconds and get one more try, one at a time, when at least two minutes remain before the Actor timeout. `SUMMARY.secondPass` reports `tried` and `recovered`. Only the final result produces a row; recovered transcripts use the existing audio-minute event once and still respect the run's maximum cost. Quota, size, no-speech, missing-file and limit failures are not retried in this pass. A YouTube daily limit or paused access may still need a later run: the free row says when the daily speech-to-text capacity resets (00:00 UTC). With at least two saved transcripts, `SUMMARY.combinedTranscript` links to the combined TXT and Markdown files. For a failed row with `retryable: true`, start a new run with that run's key-value store `RETRY_INPUT` record; after a spending or minute limit, use `REMAINING_INPUT` for files still to process. Do not rerun the full original input: completed files can be transcribed and charged again.

#### First run: try the prefilled example

The input form already holds only the short speech sample in **Audio or video file URLs**. Click **Start** without changing anything for one result; the 8.5-second recording is billed as one started minute ($0.006). Each result row shows the text and `charged: true` or `false`. Then replace the example with your own links (a Google Drive / Dropbox / OneDrive share link must be set to "anyone with the link").

Choose a file or video with spoken words. If the only result is `no_text`, the run can still say `SUCCESS`: no speech was detected, no transcript was produced, and the row is free. Check that you selected the track with spoken words, or try another file or link with speech; `SUMMARY.nextStep` and the run status repeat this hint.

#### How to transcribe a podcast to SRT subtitles

1. Paste the podcast's RSS feed (or an Apple Podcasts link) into **Podcast RSS feeds**, or episode file links into **Audio or video file URLs**. Optional: **Episodes per podcast feed** and date or title filters pick the episodes.
   Captivate episode pages and their direct or tracking-prefixed MP3 links can be used when the public player supplies a working file. If Captivate briefly answers 404, the Actor tries another download route. If its episode redirect has no target on every route, the failed row is free and asks you to retry later or use the current MP3 enclosure URL from the show's RSS feed. A genuinely missing file stays a free `not_found` row.
2. Optional: set the **Language** and turn on **Speaker labels** ($0.015 per minute instead of $0.006).
3. Click **Start**. Each episode becomes one row with the text, timestamps and `srtUrl` / `vttUrl` links to its subtitle files.
4. Download the SRT files from those links, or call the Actor from the API for a whole feed. A 60-minute episode costs $0.36.

#### How to transcribe a YouTube video to text

1. Paste YouTube video links (watch, youtu.be or shorts links), one per line, into **Audio or video file URLs**. Videos up to 4 hours work without changing any setting.
2. Click **Start**. The spoken audio is transcribed (not the captions), so videos without captions work too; each video becomes one row with the text, timestamps, the video title and channel, and SRT / VTT / TXT / Markdown files.
3. Same price as files: $0.006 per audio minute (a 30-minute video costs $0.18). Private, removed or silent videos are free rows.

#### Video to text

Paste a public video file link (MP4, WEBM, MOV or MKV, or a Google Drive / Dropbox / OneDrive share link) into **Audio or video file URLs**. The spoken words become text with timestamps, plus SRT and VTT subtitles, TXT and Markdown files. Example input, a 59-second public-domain news interview on Wikimedia Commons:

```json
{ "urls": ["https://upload.wikimedia.org/wikipedia/commons/6/68/Straight_Talk_Africa_-_Exclusive_Interview_With_South_Sudan_Vice_President_James_Wani_Igga.webm"] }
```

Result: one row with `text`, `segments`, `srtUrl`, `vttUrl`, `txtUrl` and `mdUrl`; 1 started minute, $0.006. A video with no speech is a free `no_text` row. For YouTube links, see [How to transcribe a YouTube video to text](#how-to-transcribe-a-youtube-video-to-text).

#### MP3 to text

Paste a public MP3 link (WAV, M4A, OGG, OPUS and FLAC work the same way) into **Audio or video file URLs**, or upload the file under **Or upload audio / video files**. Example input, a 115-second public-domain spoken MP3 on Wikimedia Commons:

```json
{ "urls": ["https://upload.wikimedia.org/wikipedia/commons/5/5b/Wikipedia_-_Lawmaking_%28spoken_by_AI_voice%29.mp3"] }
```

Result: one row with the transcript and timestamps, plus subtitle and transcript files; 2 started minutes, $0.012. A link that needs a login or no longer exists gives a free failed row whose `error` says what to change.

#### Why this one

- **$6 per 1,000 audio minutes** (Gold and above $4.80), with no start fee and no per-file fee. The two most used file transcribers on Apify Store charge $48 (memo23) and $300 on the Free plan ($9 on Gold, sian.agency) per 1,000 minutes ([comparison](#switching-from-another-transcription-actor), checked 2 October 2026).
- **Failed, silent and skipped files are free.** Every row says `charged: true/false`, and the run log shows the cost plan before anything is charged.
- **Apify Free plan ($5 monthly credit) = about 833 audio minutes**, e.g. about 13 one-hour podcast episodes. A run that reaches your limit stops cleanly: files left are free and listed, ready to run again ([details](#how-far-does-the-free-plan-go)).
- **Subtitles and transcript files included**: SRT, VTT, TXT and Markdown for every file, plus paragraphs and timestamps, at no extra charge.
- **No 25 MB limit, no API key, no proxy**: files up to 2 GB and any length, Google Drive / Dropbox / OneDrive share links, podcast RSS feeds, YouTube video links and **Loom, Wistia, Streamable and Reddit (v.redd.it) video links** in the same run.
- **Find every mention** (`searchTerms`): brands, names or topics with the time, the sentence, the speaker and a link that opens the recording at that moment, plus one CSV of all mentions, at no extra charge.
- **Paste the input you already use**: field names of other transcription Actors (`audioUrls`, `mediaUrls`, `datasetItems`, `speakerDiarization`, ...) are mapped automatically.

#### Input (copy and paste)

```json
{
    "urls": ["https://archive.org/download/gerald-ford-inaugural-address-august-9-1974-720p/Gerald%20Ford%20inaugural%20address_%20August%209%2C%201974%20%28720p%29.mp4"],
    "summarize": true
}
```

#### Output (real result, shortened)

The 8-minute video above, transcribed in 33 seconds and billed 9 minutes ($0.054) plus the AI summary ($0.01):

```json
{
    "success": true,
    "durationSeconds": 500.1,
    "billedMinutes": 9,
    "language": "en",
    "text": "Mr. Chief Justice, my dear friends, my fellow Americans, the oath that I have taken is the same oath that was taken by George Washington...",
    "segments": [
        { "start": 1.36, "end": 20.4, "text": "Mr. Chief Justice, my dear friends, my fellow Americans, the oath that I have taken ..." }
    ],
    "chapters": [{ "start": "0:00", "title": "Introduction and Compact with the Nation" }, { "start": "5:04", "title": "Message of Hope and Unity" }],
    "srtUrl": "https://api.apify.com/v2/key-value-stores/.../records/0000-Gerald-20Ford-...mp4.srt",
    "txtUrl": "https://api.apify.com/v2/key-value-stores/.../records/0000-Gerald-20Ford-...mp4.txt"
}
```

All fields, speaker labels and word timestamps: [Output example](#output-example-real-result-shortened).

### How much does it cost?

| Event | Price |
|---|---|
| Audio minute | **$0.006 per minute** ($0.36 per hour) |
| Audio minute with speaker labels (optional) | **$0.015 per minute** ($0.90 per hour), **instead of** the regular audio minute, only when `speakerLabels` is on |
| Translated subtitles (optional) | **$0.002 per minute per language** (e.g. a 30-minute episode in Spanish and German = $0.12), only for languages that succeed |
| AI summary and chapters (optional) | **$0.01 per file**, only when `summarize` is on |

- **No start fee.** You pay only for transcribed audio.
- **SRT and VTT subtitles, paragraphs and timestamps are included.**
- Billed per started minute of audio (a 500-second video is 9 minutes).
- **Word-level timestamps are included in the regular $0.006 per minute** (Whisper word timings). With speaker labels on, words come from nova-3 and the speaker-label price applies, not both.
- **Translated subtitles** (`subtitleLanguages`, up to 5 languages): SRT + VTT per language with the original timings and speaker labels, made right after the transcript. A language that fails is free and reported in `translatedSubtitles`.
- **YouTube video links are billed like files**: the transcribed audio minutes only, no per-video fee. Videos that cannot be transcribed (YouTube access paused, private or removed video, over your limits) are free.
- **Not charged:** files that cannot be downloaded or decoded, files with no speech, files skipped by your limits (`maxMinutesPerFile`, `maxTotalMinutes`), files skipped by `newEpisodesOnly`, episodes removed by the feed filters, and the audio minutes of episodes that use the publisher's own transcript (with `summarize` on, their AI summary is still $0.01).
- Higher Apify plans get volume discounts (Silver 10%, Gold and above 20%).

### How far does the free plan go?

Apify's Free plan includes $5 of credit every month. If you spend it only on this Actor, at $0.006 per audio minute that is **about 833 audio minutes**:

| What you transcribe | Cost each | With $5 |
|---|---|---|
| 60-minute podcast episode | about $0.36 | about 13 episodes |
| 30-minute video | about $0.18 | about 27 videos |
| 60-minute recording with speaker labels ($0.015 per minute) | about $0.90 | about 5 recordings (333 minutes) |

Minutes are billed per started minute of the full source recording in each file, including trailing silence; 180 seconds is 3 billed minutes and 61 seconds is 2. Options cost extra: speaker labels replace the $0.006 minute with $0.015, the AI summary adds $0.01 per file, and translated subtitles add $0.002 per minute and language.

**What happens at the limit.** A file is only started when the run's *Maximum cost per run* still covers all its minutes (and *Max total minutes per run*, if you set it). A file that does not fit gets a free row with `errorType: "limit_reached"`, and the shorter files after it can still run. New files stop once the limit cannot pay for another minute, or after 5 files were skipped for the maximum cost. The run ends normally with `SUMMARY.status` `LIMIT_REACHED`. YouTube videos are checked the same way once their length is known. The row's `error` itself says what the file would cost, what is left of the limit and what to change (raise *Maximum cost per run*, or *Max total minutes per run*; on the Free plan also that the monthly credit may be used up) and to run again with only the files left (the `REMAINING_INPUT` record), for automations that read only the dataset. Files in progress at that moment are finished; minutes are charged only after a transcript row is saved.

**What to do next.** The status message and `SUMMARY.limitReached.message` say how many files were processed, how many were left and that those were not charged. The links left are in `SUMMARY.remainingInputs`, and the `REMAINING_INPUT` record in the run's key-value store is your input with only those links. Then either:

- raise *Maximum cost per run* (Run options) or *Max total minutes per run* and start a run with the `REMAINING_INPUT` record as input, or
- if your monthly credit is used up, upgrade your Apify plan or wait for the monthly reset, then run the `REMAINING_INPUT` record (or paste the links from `SUMMARY.remainingInputs`).

Files that were already finished are not in that input, so they are not transcribed or charged again.

After a successful Free-plan run with a transcribed file, the status message and `SUMMARY.nextStep` say you can keep using this Actor for batches. If your Apify monthly credit is actually used up, upgrade your plan or wait for the monthly reset. The Actor does not read your account's remaining credit.

### Features

It turns **audio files, videos and podcast episodes** into **text with timestamps**, readable **paragraphs**, and ready-to-use **SRT and VTT subtitle files**. It uses OpenAI's open **Whisper large-v3-turbo** model, detects the language automatically and supports 90+ languages.

- 🎙️ **Podcasts, interviews, meetings, lectures, webinars, audiobooks, voice notes**
- 🎬 **Video to text**: MP4, MOV, MKV, WEBM and more. The audio track is extracted automatically
- 📡 **Podcast RSS feeds**: paste a feed (or an Apple Podcasts show link) and the newest episodes are transcribed, with episode title and publish date
- 📝 **Publisher transcripts, no audio minutes**: when a podcast publishes its own transcript in the feed (Podcasting 2.0 `<podcast:transcript>`: VTT, SRT, JSON, HTML or text), it is used instead of transcribing, with the same output and `transcriptSource: "publisher"`
- 🗓️ **Feed filters**: only episodes published since / until a date, or whose title contains (or does not contain) a text
- 📤 **File upload**: upload recordings from your computer in Apify Console (`audioFiles`)
- ▶️ **YouTube video links**: the video's audio is transcribed at the same $0.006 per minute, no per-video fee ([details](#youtube-videos))
- 🎥 **Loom and Wistia links**: paste a Loom share link (sales demos, async meetings, onboarding videos) or a Wistia media link; the file that the host's public player uses is transcribed at the same per-minute price ([details](#loom-and-wistia-links))
- 🎞️ **Streamable and Reddit videos**: a Streamable link (streamable.com/...) or a v.redd.it video link is transcribed from its audio track at the same per-minute price ([details](#streamable-and-reddit-videos))
- 🔎 **Keyword search** (`searchTerms`): every mention of a brand, name or topic with its timestamp, sentence, speaker and a link to that moment, counts per term, and `KEYWORD_MATCHES.csv` for the whole run, e.g. to monitor podcasts for your brand on a schedule (no extra charge, [details](#keyword-search-brand-and-topic-mentions))
- 🔁 **Retry only what may work later**: every failed row says `retryable: true/false`, and the run saves a ready input with only those links (`RETRY_INPUT`) for n8n, Make, Zapier or a webhook ([details](#retry-only-what-may-work-later))
- 🌐 **Web pages with one player**: a page with exactly one audio or video file (e.g. a news, lecture or archive.org page); the file is found and transcribed
- 🔗 **Google Drive, Dropbox and OneDrive share links** work if the file is shared publicly
- ⏱️ **Timestamped segments**, **paragraphs** and full plain text in one result
- 🗣️ **Speaker labels** (`"speakerLabels": true`): who said what, as Speaker 1, Speaker 2… on every segment, paragraph and subtitle line, plus the number of speakers
- 🏷️ **Your names for the speakers** (`"speakerNames": {"1": "Host", "2": "Guest"}`, with speaker labels): adds `namedSpeakerTranscript` (the speaker transcript with your names and the same times) and `namedSpeakers`; everything else keeps Speaker N. Names are only what you give, never guessed (no extra charge, [details](#speaker-names-and-term-corrections))
- 🔠 **Word-level timestamps** (`"wordTimestamps": true`): start and end time of every word, for video editing and precise subtitles (included in the regular price)
- 🌍 **Translated subtitles in one step** (`"subtitleLanguages": ["Spanish", "German"]`): SRT and VTT in up to 5 extra languages, timings and speaker labels kept
- 🔁 **Only new episodes** (`"newEpisodesOnly": true`): scheduled runs skip episodes and files that were already transcribed, and only take episodes released after the newest one already done, so a day without a new episode costs nothing (`backfillOlderEpisodes` works through the back catalogue instead)
- 🧾 **SRT and VTT subtitles** saved for timed transcripts (no extra charge). Each result with saved subtitles includes `subtitleTimingSource`: `segment` when a cue keeps its segment boundaries, `word` when a longer segment uses valid word times, `estimated` when a longer segment is split with interpolated times or a cue's end is adjusted to avoid overlap, `publisher` when a publisher cue keeps its original boundaries, or `mixed` when cues use different sources. This describes the actual subtitle cues, which can use different timing data from the public `segments`. Untimed publisher transcripts and results without saved subtitles omit the field.
- ⏱️ **Subtitle timing check** (`"subtitleQc": true`, off by default, no extra charge): adds `subtitleQc` to each row, a mechanical check of the saved SRT/VTT cues. `counts` gives the number of `overlap` (a cue still on screen when the next starts), `too_short` (under 1 s), `too_fast` (over 20 characters per second; 11 for Chinese, Japanese, Korean and Thai), `line_too_long` (over 42 characters; 16 for those scripts) and `estimated_timing` (times spread by text length, not heard) warnings; `warnings` lists up to 100 of them as `{ cue, start, end, rule, value, limit }` and `warningsTotal` counts all. **It does not proofread the wording.** With `saveSubtitles: false` (or no SRT/VTT format) or a transcript without timed cues, it is `{ "status": "skipped", "reason": … }`; if the check itself fails, the row has `subtitleQcError` and the transcript is unaffected.
- 🎧 **Cleaned MP3** (`"cleanAudio": true`, off by default, no extra charge): for a file, cloud-storage link or podcast episode this run transcribed, also saves a basic cleaned copy of the original audio (light noise reduction and loudness levelling to podcast level, 96 kbps MP3, files up to 2 hours) linked as `cleanedAudioUrl` (size in `cleanedAudioBytes`). This is basic processing, not professional mixing, and it does not make the transcript more accurate: the transcript is always made from the original audio. YouTube links, social and video platform pages (TikTok, Instagram, Loom, …) and publisher transcripts are not processed, and nor are failed or silent files. When the copy cannot be made (too long, too little disk, ffmpeg or storage error), the row keeps its paid transcript and says why in `cleanAudioError`. The price per audio minute is the same.
- ♻️ With `resumeStore`, a reused transcript (free) follows this run's `subtitleQc` and `cleanAudio`: off drops an earlier run's report or MP3 link; `subtitleQc` on gives the report of the reused subtitles, or "skipped" when this run saves no subtitle file (`saveSubtitles: false` or no SRT/VTT format); `cleanAudio` on gives the MP3 the earlier run made, or `cleanAudioError` "skipped" when it made none (a reused transcript downloads no audio).
- 📝 **Meeting notes** (`"meetingNotes": true`): summary, decisions, action items (task, owner, due date) and, with speaker labels, talk time and main points per speaker, as JSON and a Markdown file (no extra charge)
- 🧠 **Optional AI summary and chapters** (`"summarize": true`), with no API key needed
- 🌍 **Automatic language detection**, or set the language yourself
- 🔤 **Vocabulary hints**: give names and terms (e.g. "Apify, Kubernetes") so they are spelled correctly
- ✏️ **Term corrections** (`"termCorrections": {"Cloud flare": "Cloudflare"}`): when a name still comes out wrong, each result also gets `correctedText` with your spellings and `correctionCounts`; `text` stays as transcribed (no extra charge, [details](#speaker-names-and-term-corrections))
- 🀄 **Traditional or Simplified Chinese** (`chineseOutputScript`): `traditional` or `simplified` for Chinese speech in files and podcast audio; `auto` (default) follows the feed language and your vocabulary. Same price ([details](#limitations))
- 🎚️ **Fast or accurate** (`transcriptionEngine`): `fast` (default) uses Cloudflare Workers AI Whisper for the quickest results; `accurate` uses Whisper on our own server, with fewer errors but about twice the time. Same price ([details](#faq))
- 📏 **Large files**: up to 2 GB per file, any length. Long recordings are cut in pauses and transcribed in parallel
- 🔕 **Silent files are free**: files with no speech are reported but not charged
- ⚡ **Fast**: in our tests an 8-minute MP4 video took 33 seconds and a 26-minute MP3 took 109 seconds
- 📄 **TXT and Markdown transcripts** for every file (`txtUrl`, `mdUrl`), plus one combined file per run, ready for Google Docs, Notion or an LLM (no extra charge)
- 🔌 **n8n, Make and Zapier**: one HTTP request, no 25 MB file limit and no OpenAI key ([examples below](#use-in-n8n-make-or-zapier-no-25-mb-limit))

**What you can rely on:**

- **Files, podcast feeds, YouTube, TikTok videos, Instagram Reels, Loom, Wistia, Streamable and Reddit (v.redd.it) videos.** The Actor downloads media files, share links and RSS feeds. YouTube video links go through our own paced YouTube service (no login, no cookies); when YouTube pauses it, those rows are free and say to retry later. Loom and Wistia links use the file of the host's public player, only for public media. Public TikTok video links are transcribed from the video's original sound, and public Instagram Reels from the reel's video (no login, no cookies). Streamable videos are transcribed from their public player MP4, and Reddit's v.redd.it videos from the audio track of their public stream (requests carry our own user agent and only use paths the host's robots.txt allows; Reddit post pages are not read). It does not list TikTok or Instagram profiles, and it does not collect account data.
- **No start fee, no per-run or per-file fee.** You pay per audio minute. Files that fail, have no speech or are skipped by your limits are free, and every row says `charged: true/false`.
- **Stable output.** New fields may be added; existing fields are not renamed or removed, so your parser or automation keeps working.

### Cost control and price comparison

#### Control your cost

- **What is charged**: the minutes of each transcribed file (rounded up per file) and, with `summarize` on, one AI summary per file. Minutes are charged only after the transcript row has been saved.
- **What is free**: lines that are not a URL, duplicate links (also the same YouTube video in two link forms), Spotify / Vimeo / SoundCloud page links, TikTok and Instagram profile links, private Loom and Wistia media, private, removed and licensed-music-only TikTok videos, Instagram photo posts and private or removed reels, Streamable videos that are still uploading or processing, Reddit post links (not read: pass the video's v.redd.it link instead), YouTube list lookups and failed videos, web pages without exactly one media file, files that cannot be downloaded or decoded, files without speech, files skipped by your limits, and publisher transcripts (`billedMinutes: 0`). Every row has `charged: true/false`, and failed rows say "(not charged)".
- **Plan at the start**: the run's status message and log start with the cost plan (audio minutes known from podcast feeds, summaries, and how many files are measured when downloaded); it is also saved as `SUMMARY.costPlan`.
- **Caps**: *Max minutes per file* and *Max total minutes per run* (Advanced settings) skip files before they are transcribed, using the duration read from the file (or from the podcast feed, before downloading).
- **Max charge per run**: set *Maximum cost per run* in the run options (or `maxTotalChargeUsd` in the API). A file is only started when the limit still covers all its minutes. A file that does not fit is skipped with a free row and the run goes on with the files after it; once the limit cannot pay for another minute, or 5 files were skipped this way, no new file starts. `SUMMARY.status` is then `LIMIT_REACHED` and `SUMMARY.notProcessed` lists the files that were not started (count and up to 100 inputs). The status message and `SUMMARY.limitReached` say how many files were processed and how many are left; `SUMMARY.remainingInputs` lists the remaining links and the `REMAINING_INPUT` record is your input with only those. To continue, raise the maximum cost per run and run that input: finished files are not in it, so they are not charged again (running the same full input again would transcribe and charge them again, unless `newEpisodesOnly` or `resumeStore` is on). The AI summary is only made when the limit also covers it.
- **If Apify restarts the run** (server migration or Resurrect), items already finished are skipped and not charged again (`SUMMARY.resumedSkipped`).
- **Time limit per file** (`fileTimeoutSecs`, default 1,800 seconds): a file whose download, conversion and transcription take longer gets a row with `errorType: "timeout"` and is not charged, and the run goes on with the next file. Raise it for recordings of several hours. YouTube videos get at least their length x 15 seconds + 20 minutes.
- **Files in parallel** (`fileConcurrency`, default 3, up to 10): several files are downloaded and transcribed at the same time, so podcast batches finish sooner. Rows are saved in the order files finish; `inputIndex` is each file's position in the input. Your max charge per run and *Max total minutes per run* still hold: before a file is transcribed, its cost is reserved together with the files already in progress, so files running side by side can never go over the limit. With little run memory fewer files run at once (about 3 at the default 1,024 MB); the time a file waits for budget held by other files does not count against `fileTimeoutSecs`.

#### Price comparison (checked 2 October 2026)

Per-minute prices of other transcription Actors on Apify Store, Free plan unless noted; "not stated" means we did not find it in that Actor's input or README:

| Actor | Price | Speaker labels |
|---|---|---|
| **This Actor** | **$0.006 per minute**, no start fee (Gold+ $0.0048) | $0.015 per minute instead of $0.006 |
| steadyfetch / media-transcriber | $0.003 per minute | not stated |
| amanatools / whisper-transcriber | $0.008 per minute, plus $0.005 per file | not stated |
| kaz_kakyo / audio-transcriber | $0.01 per minute; chapters $0.01 per file | yes (Deepgram nova-3) |
| viralanalyzer / video-transcriber | $0.015 per minute | not stated |
| sauliusautomatesit / media-transcriber | $0.035 to $0.12 per minute | not stated |
| hgservices / speech-to-text | $0.042 per minute (Gold $0.024), plus $0.09 per YouTube video | yes |
| memo23 / video-audio-transcriber | $0.048 per minute (billed per second, $0.0008 per second), plus $0.005 per run | yes |
| tictechid / vanzi-universal-transcriber | $0.0025 per second ($0.15 per minute), plus $0.01 per run | not stated |
| sian.agency / INCREDIBLY-FAST-audio-transcriber | $0.30 per minute ($0.005 per second) on the Free plan (Bronze $0.015, Gold $0.009), plus $0.005 per run | extra $0.001 per second on the Free plan, $0.0001 per second on paid plans |
| parseforge / audio-transcriber | $0.44 per file, plus $0.012 per run | not stated |

This is not the lowest per-minute price: steadyfetch charges half as much. What the $0.006 includes: no start fee and no per-file fee; failed, silent and skipped files free; SRT/VTT subtitles and TXT/Markdown files; podcast RSS feeds with date and title filters, publisher transcripts (0 audio minutes) and only-new-episode schedules; Google Drive, Dropbox and OneDrive share links; files up to 2 GB. Speaker diarization, translated subtitles and AI summaries are optional. YouTube video links, public TikTok video links, public Instagram Reels, Loom, Wistia, Streamable and Reddit (v.redd.it) video links are transcribed at the same per-minute price, with no per-video fee; keyword search (`searchTerms`) is included.

#### Switching from another transcription Actor

You can paste the input you already use: field names of the most used transcription Actors are accepted and mapped to this Actor's fields (the run log lists each one under "Input compatibility"). Options that have no equivalent here are listed in the log too, never silently dropped.

Comparison with the two most used audio/video file transcribers on Apify Store, checked on 2 October 2026 from each Actor's public pricing and README (Free plan; prices can change, check their pages):

| | **This Actor** | sian.agency / INCREDIBLY-FAST-audio-transcriber | memo23 / video-audio-transcriber |
|---|---|---|---|
| Price per 1,000 audio minutes | **$6** (Gold+ $4.80) | $300 on the Free plan ($0.005 per second); $15 Bronze, $9 Gold+ | $48 ($0.0008 per second) |
| Start fee | **none** | $0.005 per GB of run memory | $0.005 per GB of run memory ($0.01 at the default 2 GB) |
| Billing unit | started minute per file | second | second |
| Failed or silent files free? | yes, every row has `charged` | files over the Free-plan cap are not charged; other failures not stated | yes, only delivered transcripts are charged |
| Files per run | up to 1,000 | 1 file of up to 5 MB or 60 seconds on the Free plan; unlimited on paid plans | up to 50 URLs |
| Speaker labels | $0.015 per minute instead of $0.006 | extra $0.001 per second on the Free plan | included |
| AI summary and chapters | $0.01 per file | not offered | included |
| Podcast RSS feeds | yes, with date/title filters and publisher transcripts | not stated | yes, with publisher transcripts |
| YouTube / TikTok / Instagram links | YouTube, public TikTok video links and public Instagram Reels yes, same per-minute price (no speaker labels on YouTube) | yes (paid plans) | yes |
| Model | Whisper large-v3-turbo | not stated | Whisper tiny, base or small |
| Max file size | 2 GB | 1 GB on paid plans | not stated |

If you transcribe files, recordings, podcast feeds, YouTube, TikTok or Instagram Reels, this one costs less per minute. For YouTube videos that already have captions, our [YouTube Transcript Scraper](https://apify.com/tidytools/youtube-transcript) is cheaper still.

**Paste your existing input.** Both of these work unchanged:

```json
{
    "audioUrls": ["https://example.com/episode.mp3"],
    "language": "english",
    "speakerDiarization": true
}
```

```json
{
    "mediaUrls": ["https://example.com/meeting.mp4", "https://feeds.npr.org/510289/podcast.xml"],
    "maxEpisodesPerFeed": 1,
    "maxMinutesPerItem": 120,
    "publishedAfter": "2026-09-01",
    "newItemsOnly": true,
    "watchlistId": "my-show"
}
```

| Field from another Actor | Read here as |
|---|---|
| `audioUrls`, `startUrls` (strings or `{ "url": ... }`), `mediaUrls`, `media_urls`, `videoUrls`, `fileUrls` | `urls` (podcast feed links in these lists go to `podcastFeeds`) |
| `podcastUrls`, `feedUrls`, `rssUrls` | `podcastFeeds` |
| `maxEpisodesPerShow`, `rss_episodes` | `maxEpisodesPerFeed` |
| `maxMinutesPerItem`, `max_minutes_per_file` | `maxMinutesPerFile` (0 = no limit) |
| `publishedAfter` / `episodeTitleContains` | `episodesSince` / `titleIncludes` |
| `newItemsOnly` / `watchlistId` | `newEpisodesOnly` / `monitorName` |
| `speakerDiarization`, `diarize` | `speakerLabels` (the speaker-label price applies) |
| `includeWords`, `word_timestamps` | `wordTimestamps` |
| `keyterms` | `vocabulary` |
| `outputFormats` containing `srt` and/or `vtt` | Save only the selected original-language subtitle formats; omitting it keeps both, and a list without either saves neither |
| `language: "auto"`, `"english"`, `"en-US"` | auto-detect, `"en"`, `"en"` |

Not used here (noted in the log): `translateToEnglish` (the transcript stays in the spoken language; for English subtitles set `"subtitleLanguages": ["English"]`), `useEuServers`, `model`, `cookies`, `deepgramApiKey`. When both a field of this Actor and another Actor's name for it are set, this Actor's field wins.

### Supported input

| Input | Details |
|---|---|
| Audio files | MP3, WAV, M4A, AAC, OGG, OPUS, FLAC, AIFF, WMA, AMR and other formats ffmpeg can read |
| Video files | MP4, MOV, MKV, WEBM, AVI and more. Only the audio track is used |
| Size | Up to **2 GB** per file, any length |
| Share links | Google Drive files and public folders (every audio/video file in the folder; large files without the virus-scan page), Dropbox shared links (also a file opened inside a shared folder), OneDrive (`1drv.ms`), SharePoint / OneDrive for Business, Box shared links, WeTransfer and iCloud Drive links. The file must open without a sign-in ("Anyone with the link"); see [How do I share a cloud link so it works?](#faq) |
| Temporary and presigned links | Dropbox temporary links (`files/get_temporary_link`, 4 hours), presigned URLs of Amazon S3 and S3-compatible storage (Cloudflare R2, Backblaze B2, Wasabi, MinIO, DigitalOcean Spaces), Google Cloud Storage signed URLs, Azure SAS URLs, Oracle pre-authenticated requests, signed Apify key-value store links, Mux signed playback links (`stream.mux.com/...?token=`). An expired link fails at once with `errorType: "link_expired"` (free) |
| Uploads | `audioFiles`: files uploaded in Apify Console (stored in a key-value store of your account). In API calls, pass file URLs in `urls` |
| Web pages | A page with exactly one `<audio>` / `<video>` file, or one `og:audio` / `og:video` / JSON-LD `contentUrl` file link; the found file is in `mediaUrl` |
| YouTube videos and lists | Public video, playlist, channel and `@handle` links; each selected video's audio is transcribed and billed per minute ([details](#youtube-videos)). List lookup itself is free |
| Loom, Wistia | Loom share and embed links (`loom.com/share/<id>`, also with a title, `?sid=`, a `<workspace>.loom.com` or `useloom.com` host, the library address `loom.com/looms/videos/<title>-<id>` and the video's thumbnail link `cdn.loom.com/sessions/thumbnails/<id>-...`), Wistia media links (`<account>.wistia.com/medias/<id>`, `fast.wistia.com/embed/medias/<id>`, `fast.wistia.net/embed/iframe/<id>`): public media only, billed per minute like files ([details](#loom-and-wistia-links)) |
| TikTok videos | `tiktok.com/@<user>/video/<id>` links of public videos and short links (`vm.tiktok.com/...`, `tiktok.com/t/...`): the video's original sound is transcribed, billed per minute like files ([details](#tiktok-videos)). Profile links are not expanded |
| Instagram Reels | `instagram.com/reel/<code>/`, `/reels/<code>/`, `/p/<code>/`, `/tv/<code>/` and `/<user>/reel/<code>/` links of public reels and video posts: the reel's sound is transcribed, billed per minute like files ([details](#instagram-reels)). Photo and carousel posts are free rows; profile and story links are not expanded |
| Streamable videos | `streamable.com/<code>` links of public videos (also the `/s/<code>` and `/e/<code>` forms): the MP4 of the host's public player is transcribed, billed per minute like files ([details](#streamable-and-reddit-videos)). A video that is still uploading or processing is a free row saying to run it again later |
| Reddit videos | `v.redd.it` video links (`v.redd.it/<id>`, or the video's HLS playlist such as `v.redd.it/<id>/HLS_PLAYLIST.m3u8`, the `hls_url` of the post): the audio track of the video is transcribed, billed per minute like files ([details](#streamable-and-reddit-videos)). Reddit post and profile pages are not read (Reddit's robots.txt does not allow it): pass the video's v.redd.it link, or a Reddit scraper Actor's dataset via `datasetId` |
| Podcast feeds | Any podcast RSS feed, or an Apple Podcasts show or episode link. Publisher transcripts are used when the feed has them; date and title filters are optional |
| Dataset | The `datasetId` of another Actor run, or its rows pasted into `datasetItems`: `datasetId` detects a field from the first row and uses it for the dataset, while `datasetItems` detects a field per row (or set `datasetField`; dot paths work); pasted rows without a usable link are free `invalid_input` rows that name their index |

Links must be downloadable without logging in. Facebook, X, Vimeo and Spotify page links are web pages, not files: each comes back as a free row with `errorType: "unsupported_platform"` and a hint. Download the media first (for example with a downloader Actor from Apify Store) and pass the file URL, or that Actor's dataset via `datasetId`. Vimeo streams its videos encrypted, so for Vimeo use the file behind the video's Download button when the owner allows downloads; direct file links, such as a Vimeo "video file link" (`player.vimeo.com/external/...` or `.../progressive_redirect/...`), are transcribed normally. Reddit post pages are not read either (Reddit's robots.txt does not allow it): pass the video's v.redd.it link or its direct audio file URL instead.

**Web pages**: for any other web page, the page is read and its media file is looked up: the one `<audio>` or `<video>` element (its `src` or its first playable `<source>`; several sources of one element are one file), or else one direct file named by `og:audio`, `og:video`, `twitter:player:stream` or JSON-LD `contentUrl`. Only direct files count: streaming playlists (HLS `.m3u8`, DASH `.mpd`) and player pages are not used. When the page has no such file (`errorType: "no_media"`) or several (`"multiple_media"`, listed in `mediaCandidates`), the row is free; pass the file you want as a direct URL.

A line that is not a URL does not stop the run: it becomes one free row with `errorType: "invalid_input"` and the other files are processed. Use `urls` (a plain list of strings) in API calls; the request-list field `audioUrls` from earlier versions still works.

### How to use it

1. Paste **audio or video file URLs** (the most used input; Google Drive, Dropbox and OneDrive share links work, or pages with one player), **upload files**, and/or add **YouTube links** and **podcast feeds** (optionally with date and title filters).
2. Optional: set the **language** (e.g. `en`, `es`, `ja`), **vocabulary hints**, and turn on **AI summary and chapters**, **speaker labels** or **word-level timestamps**.
3. Optional (Advanced settings): cap the cost with **Max minutes per file** and **Max total minutes per run**; for schedules, turn on **Only new episodes** with a **monitor name**.
4. Click **Start**. Each file becomes one result with the text, segments, paragraphs and links to its SRT/VTT files.

#### Input example

File links first (the most common input), with a language, vocabulary hints, an AI summary and cost caps:

```json
{
    "urls": ["https://example.com/meeting-recording.mp4", "https://drive.google.com/file/d/FILE_ID/view?usp=sharing"],
    "language": "en",
    "vocabulary": "Apify, Cloudflare, TidyTools",
    "summarize": true,
    "maxMinutesPerFile": 90,
    "maxTotalMinutes": 300
}
```

Add a podcast feed (the newest 3 episodes) and a YouTube video to the same run:

```json
{
    "urls": ["https://www.youtube.com/watch?v=CKRxRE7qRzc"],
    "podcastFeeds": ["https://feeds.npr.org/510289/podcast.xml"],
    "maxEpisodesPerFeed": 3
}
```

#### Output example (real result, shortened)

An 8-minute MP4 video with `"summarize": true`:

```json
{
    "url": "https://archive.org/download/gerald-ford-inaugural-address-august-9-1974-720p/Gerald%20Ford%20inaugural%20address_%20August%209%2C%201974%20%28720p%29.mp4",
    "success": true,
    "format": "mp4",
    "fileSizeBytes": 48964530,
    "durationSeconds": 500.1,
    "billedMinutes": 9,
    "language": "en",
    "wordCount": 876,
    "text": "Mr. Chief Justice, my dear friends, my fellow Americans, the oath that I have taken is the same oath that was taken by George Washington...",
    "segments": [
        { "start": 1.36, "end": 20.4, "text": "Mr. Chief Justice, my dear friends, my fellow Americans, the oath that I have taken is the same oath that was taken by George Washington and by every president under the Constitution." },
        { "start": 20.4, "end": 31.32, "text": "But I assume the presidency under extraordinary circumstances never before experienced by Americans." }
    ],
    "paragraphs": [
        { "start": 1.36, "end": 39.68, "text": "Mr. Chief Justice, my dear friends, ... This is an hour of history that troubles our minds and hurts our hearts." },
        { "start": 41.44, "end": 73.64, "text": "Therefore, I feel it is my first duty to make an unprecedented compact with my countrymen. ..." }
    ],
    "summary": "The president assumes office under extraordinary circumstances ... and asks for prayers for Richard Nixon and his family.",
    "chapters": [
        { "startSeconds": 0, "start": "0:00", "title": "Introduction and Compact with the Nation" },
        { "startSeconds": 304, "start": "5:04", "title": "Message of Hope and Unity" }
    ],
    "srtUrl": "https://api.apify.com/v2/key-value-stores/.../records/0000-Gerald-20Ford-...mp4.srt",
    "vttUrl": "https://api.apify.com/v2/key-value-stores/.../records/0000-Gerald-20Ford-...mp4.vtt",
    "txtUrl": "https://api.apify.com/v2/key-value-stores/.../records/0000-Gerald-20Ford-...mp4.txt",
    "mdUrl": "https://api.apify.com/v2/key-value-stores/.../records/0000-Gerald-20Ford-...mp4.md",
    "processingSeconds": 33
}
```

With `"speakerLabels": true` (a real 74-minute Changelog & Friends episode: 3 speakers found, 13,069 words, 195 seconds of processing, 74 minutes billed at $0.015):

```json
{
    "success": true,
    "model": "nova-3",
    "durationSeconds": 4390.7,
    "billedMinutes": 74,
    "speakers": 3,
    "segments": [
        { "start": 16.14, "end": 23.34, "speaker": 1, "text": "Welcome to Changelog and Friends, a weekly talk show about thinking outside the Dropbox." }
    ],
    "paragraphs": [
        { "start": 16.14, "end": 34.87, "speaker": 1, "text": "Welcome to Changelog and Friends, a weekly talk show about thinking outside the Dropbox. Thanks as always to our partners at fly.io, ..." },
        { "start": 43.38, "end": 69.23, "speaker": 2, "text": "This is the year we almost break the database. Let me explain. Where do agents actually store their stuff? ..." }
    ],
    "speakerTranscript": "Speaker 1 [0:16]: Welcome to Changelog and Friends, ...\n\nSpeaker 2 [0:43]: This is the year we almost break the database. ..."
}
```

With `"wordTimestamps": true`, every segment also gets its words (real result, a 5-minute news episode):

```json
{ "start": 4.16, "end": 7.52, "speaker": 1, "text": "What's up, friends? Adam here. Big week here at ChangeLog.",
  "words": [{ "word": "What's", "start": 4.16, "end": 4.4, "speaker": 1 }, { "word": "up,", "start": 4.4, "end": 4.56, "speaker": 1 }, { "word": "friends?", "start": 4.56, "end": 4.96, "speaker": 1 }] }
```

In the SRT file a line starts with `Speaker 2: ` when the speaker changes; the VTT file uses `<v Speaker 2>` voice tags. Files longer than 50 minutes are sent in overlapping pieces, and the speakers of each piece are matched by the words in the overlap, so Speaker 1 stays Speaker 1 for the whole file.

#### Speaker names and term corrections

Two optional tables, applied to the finished transcript (no extra charge, no new event, no second transcription):

- `speakerNames` (with `speakerLabels` on): your names for the speaker numbers of a first run, e.g. `{"1": "Host", "2": "Guest"}` (up to 20; names 1-40 characters, no line breaks, control characters or `<` `>`). A row with speakers gets `namedSpeakerTranscript` and `namedSpeakers`; a speaker you did not name stays `Speaker N` (`source: "default"`). `speakerTranscript`, `segments`, `paragraphs`, the SRT/VTT subtitles and the TXT/Markdown files keep `Speaker N`. Who is who is never guessed: check the numbers in `speakerTranscript` first. Without speaker labels the table is ignored.
- `termCorrections`: heard form → correct spelling, e.g. `{"Cloud flare": "Cloudflare", "Kuber Netties": "Kubernetes"}` (up to 50; each side 1-80 characters). Every successful row gets `correctedText` (a corrected copy of `text`) and `correctionCounts` (replacements per entry, `0` when not found). `text`, `segments`, subtitles, transcript files, the AI summary and keyword search keep the words as transcribed. Matching is literal and case-sensitive (no patterns), whole words in Latin text (`AI` does not change `said`), while a Chinese or Japanese neighbour counts as a word boundary; the longest entry wins and the text is read once. An entry whose correct spelling is another entry (`A → B`, `B → A`) is refused.

An invalid table ends the run before anything is downloaded, with one free `invalid_input` row saying what to change. Failed and no-speech rows get neither field. With `resumeStore`, both tables are applied to the saved transcript, so you can rename speakers or fix terms in a later run for free (the row is `reused: true`, nothing is transcribed or charged again).

```json
{
    "speakerTranscript": "Speaker 1 [0:00]: Welcome back to the show.\n\nSpeaker 2 [0:04]: Thanks, we moved everything to Cloud flare.",
    "namedSpeakerTranscript": "Host [0:00]: Welcome back to the show.\n\nGuest [0:04]: Thanks, we moved everything to Cloud flare.",
    "namedSpeakers": [{ "speaker": 1, "name": "Host", "source": "user" }, { "speaker": 2, "name": "Guest", "source": "user" }],
    "text": "Welcome back to the show. Thanks, we moved everything to Cloud flare.",
    "correctedText": "Welcome back to the show. Thanks, we moved everything to Cloudflare.",
    "correctionCounts": { "Cloud flare": 1 }
}
```

#### Transcript files (TXT and Markdown)

Every transcribed file also gets two download links (no extra charge; turn off with `"saveTranscriptFiles": false`):

- `txtUrl`: plain text, one paragraph per block. With speaker labels: `Speaker 1 [0:16]: ...`.
- `mdUrl`: Markdown with the title (episode title or file name), source link, duration, language, the AI summary and chapters when `summarize` is on, then the transcript with a `[m:ss]` timestamp per paragraph.

When a run has two or more transcripts, all of them are also joined in input order into `transcripts.txt` and `transcripts.md` in the run's key-value store; the links are in `SUMMARY.combinedTranscript` and in the run log.

A podcast episode from an RSS feed also carries the episode details:

For each successful podcast episode with a title, `saveTranscriptFiles` (on by default) also saves two separate publication drafts in the run's key-value store, at no extra charge:

- `podcastPageHtmlUrl`: downloadable HTML with the title, source episode link and transcript. When `summarize` produced a summary or chapters, those appear too.
- `podcastPageCmsUrl`: downloadable JSON with `title`, `podcastTitle`, `sourceUrl`, `excerpt`, `chapters` and `bodyHtml` for copying into a CMS.

These are **drafts to review and edit** before publishing. The Actor does not publish to a CMS or send email. With `summarize: false`, the excerpt and chapters are empty; no summary is invented. Failed, silent and skipped files, other media, and runs with `saveTranscriptFiles: false` get no page links. If either page file cannot be saved, `podcastPageError` explains the problem and neither page link is returned. The original TXT/MD transcript and SRT/VTT subtitle links remain available under their usual conditions.

```json
{
    "url": "https://tracking.swap.fm/track/.../default.mp3?...",
    "podcastTitle": "Planet Money",
    "episodeTitle": "Middlegarchs are the new Oligarchs",
    "pubDate": "2026-09-25T21:46:44.000Z",
    "episodeUrl": "https://www.npr.org/2026/09/25/nx-s1-5981194/stealthy-wealthy-everywhere-millionaires-pass-through",
    "feedUrl": "https://feeds.npr.org/510289/podcast.xml",
    "success": true,
    "format": "mp3",
    "durationSeconds": 2065.2,
    "billedMinutes": 35,
    "wordCount": 5505
}
```

#### Meeting notes

Turn on `"meetingNotes": true` to get structured notes for meetings, calls and interviews (no extra charge):

- `meetingNotes.summary`, `meetingNotes.decisions` and `meetingNotes.actionItems` (`task`, plus `owner` and `due` when they are said in the recording). Only decisions and tasks that are actually said are listed; the lists are empty when there are none.
- Optional `meetingNotes.evidence[]` cites a decision or action item with `kind` (`decision` or `actionItem`), zero-based `index` in that list, `startSeconds`, and an exact transcript `quote`. A quote is included only when it matches a timed transcript paragraph. Notes without a verifiable timed quote remain in the lists without evidence; the Markdown file shows the time and quote beside cited items. Repeated quotes use the first matching paragraph.
- With `speakerLabels` on: `meetingNotes.speakerStats` (talk time, share of talk time, words and turns per speaker, computed from the timings, not by AI) and `meetingNotes.speakerHighlights` (each speaker's main points).
- `meetingNotesUrl`: the same notes as a Markdown file with a checklist of action items. The notes are also added to the `mdUrl` transcript.
- `ACTION_ITEMS.csv` in the run key-value store: one row per AI-suggested task across successful files, with `inputIndex,title,sourceUrl,task,owner,due`. Missing owner or deadline stays blank; if there are no tasks, the file contains only the header. `SUMMARY.actionItemsCsvUrl` links to it and `SUMMARY.actionItemsCount` counts task rows. Check tasks against the original transcript before acting on them.

For long recordings, the AI notes prompt samples the beginning, evenly spaced passages, and the final spoken paragraph in time order. If the final paragraph itself is too long, its beginning and end are kept so a closing commitment can reach the notes model. The `text` transcript and saved transcript files remain complete; sampling applies only to the AI notes prompt.

```json
{
    "meetingNotes": {
        "summary": "The team reviewed the launch plan and agreed on the date.",
        "decisions": ["Ship version 2 on Friday"],
        "actionItems": [{ "task": "Update the user guide", "owner": "Speaker 2", "due": "Thursday" }],
        "speakerStats": [{ "speaker": 1, "talkTime": "12:40", "talkShare": 61.2, "words": 1830, "turns": 24 }],
        "speakerHighlights": [{ "speaker": 1, "points": ["Wants to launch before the conference"] }]
    },
    "meetingNotesUrl": "https://api.apify.com/v2/key-value-stores/.../records/0000-meeting.notes.md"
}
```

The notes are written by an AI model from the transcript and can miss or misread a point: check them against the transcript before you send them on. Episodes that use the publisher's transcript (no audio minutes) get the speaker statistics only, unless `summarize` is on.

#### Publisher transcripts (no audio minutes)

Many podcasts put their own transcript in the feed (`<podcast:transcript>`, Podcasting 2.0). With `usePublisherTranscripts` on (the default), such an episode is not transcribed: the publisher's file is downloaded and converted to the same output. Timed formats (VTT, SRT, JSON) give segments with timestamps, paragraphs and our SRT/VTT files; HTML and plain text give the text and paragraphs only (`timestamps: false`, no subtitle files). Speaker names from the transcript are kept as `speakerName` on each segment. No audio minutes are charged (`billedMinutes: 0`); the AI summary, if you turn it on, is charged as usual.

The transcript is checked first: it must have at least 20 words, cover at least half of the episode's duration (from the feed) without running far past it, and have a plausible number of words per minute. A transcript that fails the check, or cannot be downloaded, is ignored and the episode is transcribed normally; the row then says why in `publisherTranscriptError`. Set `"usePublisherTranscripts": false` to always transcribe the audio yourself.

Real result (Podnews Daily, whose feed has a VTT transcript per episode; with `"summarize": true`, so only the $0.01 summary was charged):

```json
{
    "podcastTitle": "Podnews Daily - podcast industry and podcasting news",
    "episodeTitle": "Was Inception Point AI just a PR stunt?",
    "pubDate": "2026-09-29T10:30:00.000Z",
    "success": true,
    "transcriptSource": "publisher",
    "transcriptUrl": "https://podnews.net/audio/podnews260929.mp3.vtt",
    "transcriptFormat": "vtt",
    "timestamps": true,
    "model": "publisher",
    "language": "en",
    "durationSeconds": 273,
    "billedMinutes": 0,
    "wordCount": 642,
    "segments": [
        { "start": 0.56, "end": 4.96, "text": "From Singapore Airport, the latest from podnews.net with The Podglomerate.", "speakerName": "James Cridland" }
    ],
    "summary": "Inception Point AI's CEO Jeanine Wright has suggested that the company's thousands of AI-made podcasts may have been perceived as a publicity stunt, ...",
    "summaryCharged": true
}
```

In the same test, a 63-minute Buzzcast episode (12,067 words, 810 segments, 10 chapters) was also taken from the publisher's transcript: 0 audio minutes instead of 64.

#### Feed filters

```json
{
    "podcastFeeds": ["https://podnews.net/rss"],
    "episodesSince": "2026-09-20",
    "episodesUntil": "2026-09-28",
    "titleIncludes": ["patreon", "spotify*video"],
    "titleExcludes": ["weekly"],
    "maxEpisodesPerFeed": 1
}
```

- `episodesSince` and `episodesUntil` apply only to RSS/Podcast feeds. `episodesSince` accepts a date (`YYYY-MM-DD`) or a relative time such as `30 days`, `2 weeks`, `6 months`; `episodesUntil` accepts a date, including the whole day.
- `titleIncludes`: keep an episode when its title contains one of the texts; `titleExcludes`: drop it when its title contains one. Case does not matter; `*` matches anything (`spotify*video`).
- Filters are applied first, then `newEpisodesOnly`, then `maxEpisodesPerFeed`. With a date filter, episodes without a publish date are dropped. A link to one Apple Podcasts episode is not filtered.
- Filtered episodes get **no row** and are not charged; `SUMMARY.episodesFiltered` counts them. In our test above, 150 of 151 episodes were filtered and the one match ("Patreon removes per-creation memberships", 4.6 minutes) was transcribed.

#### Web page with one player

```json
{ "urls": ["https://archive.org/details/nasa_tv-ScienceCast_-_Record-Setting_Asteroid_Flyby"] }
```

Real result (shortened): the page's `og:video` file was found and transcribed, 262 seconds, 5 minutes billed:

```json
{
    "url": "https://archive.org/details/nasa_tv-ScienceCast_-_Record-Setting_Asteroid_Flyby",
    "mediaUrl": "https://archive.org/download/nasa_tv-ScienceCast_-_Record-Setting_Asteroid_Flyby/ScienceCast_-_Record-Setting_Asteroid_Flyby.mp4",
    "success": true,
    "transcriptSource": "asr",
    "format": "mp4",
    "billedMinutes": 5,
    "segments": [{ "start": 3.7, "end": 8.48, "text": "Record-setting asteroid flyby, presented by Science at NASA." }]
}
```

#### Loom and Wistia links

```json
{ "urls": ["https://www.loom.com/share/3690d29d5e3c426eb0f7ea00f9d3ae7b", "https://fast.wistia.com/embed/medias/j38ihh83m5"] }
```

Paste the link you would share with a colleague. The Actor asks the host for the file its own public player uses (Loom's video file, or for newer Loom videos that are only streamed, the audio track of the stream; Wistia's smallest MP4 or audio file, never an audio-description track), downloads it and transcribes it like any file, **billed per audio minute, no per-link fee**. The row adds `mediaTitle` (the title on Loom or Wistia), `mediaUrl` (the file that was transcribed) and `sourceKind` (`loom` or `wistia`); `sourceFileId` is the host's id (`loom:<id>`), so `resumeStore` recognises the same video in another link form.

Real result (shortened; 53-second Loom video, 1 minute billed, 7 seconds end to end):

```json
{
    "url": "https://www.loom.com/share/3690d29d5e3c426eb0f7ea00f9d3ae7b",
    "mediaUrl": "https://cdn.loom.com/sessions/transcoded/3690d29d5e3c426eb0f7ea00f9d3ae7b.mp4?Policy=...",
    "mediaTitle": "Sell Faster and Smarter with Loom",
    "sourceKind": "loom",
    "success": true,
    "durationSeconds": 53.4,
    "billedMinutes": 1,
    "text": "Ready to prospect more efficiently, close more pipeline, and create loyal champions? Loom helps you stand out in a sea of text. ..."
}
```

Only public media is read: a private, password-protected or workspace-only Loom or Wistia video and a removed link each come back as a free row (`private_media` or `not_found`) that says why. Encrypted streams are not read (free row, `unsupported_platform`). A Loom folder, space or library link is a free `folder_link` row that asks for the share link of each video; another loom.com page is a free `unsupported_platform` row. Loom's free rows also carry `reasonDetail` (`loom_folder`, `loom_unrecognized_path`, `loom_encrypted`, `loom_no_audio_playlist` or `loom_no_segments`). Requests carry our own user agent and only use paths that the hosts' robots.txt allows.

#### TikTok videos

Paste public TikTok video links (`https://www.tiktok.com/@<user>/video/<id>`, the `m.tiktok.com` form, or a short link from the app's Share button such as `https://vm.tiktok.com/...`), one per line. The Actor reads the public video page, takes the video's sound track and transcribes it like any file, **billed per audio minute, no per-video fee**. The row adds `mediaTitle` (the video's caption), `mediaUrl` (the sound file that was transcribed) and `sourceKind: "tiktok"`; `sourceFileId` is `tiktok:<video id>`, so `resumeStore` recognises the same video in another link form.

- **Only the creator's original sound is transcribed.** A video whose sound is licensed music or another creator's sound has no voice track of its own to transcribe: it is a free row (`errorType: "unsupported"`) that says so.
- Private and removed videos are free rows (`private_media`, `not_found`). When TikTok does not give Apify's network the video (a refusal, a verification or empty page, or an unknown TikTok status; this can happen from data-centre networks, and with videos shown only in some countries), the page is read again through our other networks (our backend, our server, our home line; `downloadVia` limits which, `"direct"` = Apify's network only). If none of them gets it, the row is free (`blocked`), lists each network's answer, and is worth running again later.
- No login and no cookies are used, and the video file itself is not downloaded. Profile, sound, tag and photo links are not expanded: pass the link of each video.
- **Direct file links from TikTok's servers** (for example from another tool's export: `https://v16m.tiktokcdn-us.com/...`, `https://v16m.tiktokcdn.com/...`, `https://v16-webapp-prime.tiktok.com/video/tos/...`) are downloaded like any file and billed per minute. TikTok signs these links for about a day, and the `*-webapp-prime.tiktok.com` ones (a video's `playAddr` / `downloadAddr`) only work in the browser session that loaded the video. An expired or refused one is a free row (`errorType: "link_expired"`, or `"link_invalid"` when the link was changed or cut short) that tells you to pass the video page link (`tiktok.com/@user/video/...`) instead, which is read fresh on every run.

#### Instagram Reels

Paste links of public Instagram Reels or video posts (`https://www.instagram.com/reel/<code>/`, `/reels/<code>/`, `/p/<code>/`, `/tv/<code>/` or `/<user>/reel/<code>/`), one per line. The Actor reads the reel's public embed player, downloads its video right away and transcribes the sound like any file, **billed per audio minute, no per-reel fee**. The row adds `mediaTitle` (the reel's caption) and `sourceKind: "instagram"`; `sourceFileId` is `instagram:<code>`, so `resumeStore` recognises the same reel in another link form. The video file link that Instagram signs for the player expires, so it is not saved in the row.

- Only the reel itself is used (its sound, caption and length). The owner's account, likes, comments and view counts are not read into the output.
- Photo and carousel posts are free rows (`errorType: "no_media"`, "this post is not a video"). A video Instagram has blocked for copyright is a free row (`unsupported`).
- A reel whose sound is music from Instagram's library is still transcribed; when no speech is heard, the row is free (`no_text`) and says the sound is library music.
- Private and removed reels are free rows (`private_media`, `not_found`). When Instagram refuses the request (this can happen from data-centre networks), the row is free (`blocked`) and worth running again later.
- No login and no cookies are used. Profile, story, highlight and share links (`instagram.com/share/...`) are not expanded: pass the link of each reel.

#### Streamable and Reddit videos

```json
{ "urls": ["https://streamable.com/dnd1", "https://v.redd.it/zv89llsvexdz/HLSPlaylist.m3u8"] }
```

Paste a public Streamable link (`streamable.com/<code>`, the share, `/s/<code>` or embed form) or the link of a Reddit-hosted video (`https://v.redd.it/<id>`, or the video's HLS playlist such as `https://v.redd.it/<id>/HLS_PLAYLIST.m3u8` — the `hls_url` of its post). Streamable videos are transcribed from the MP4 of their public player, Reddit videos from the audio track of their public HLS stream, **billed per audio minute like files, no per-video fee**. The row adds `sourceKind` (`streamable` / `reddit`); `sourceFileId` is `streamable:<code>` / `reddit:<video id>`, so `resumeStore` recognises the same video in another link form. **A Streamable row never contains the file's CDN link** (`mediaUrl` / `downloadUrl`): Streamable signs that link for a short time and it expires, so the row keeps your original link and the stable fields instead, and the signed link is only used to download the file right away. A Reddit row keeps `mediaUrl` (the audio playlist that was transcribed) as before.

- **A Streamable video that is still uploading or processing is a free `service_busy` row**: run that link again later; it is only transcribed (and charged) when it is ready. A video that failed on Streamable's side is a free `not_found` row, and one whose file link is not a usable HTTPS URL (a malformed CDN link on Streamable's side) is a free `unsupported_platform` row.
- **Reddit post and profile links are not read**: Reddit's robots.txt does not allow reading them with our user agent, and no cookies are used. Pass the video's v.redd.it link or HLS playlist instead, or a Reddit scraper Actor's dataset via `datasetId` (its media URL fields are detected automatically). Direct video and audio files on v.redd.it (for example `DASH_AUDIO_128.mp4`) are transcribed as plain files too.
- Requests carry our own user agent, use no cookies and only touch paths that the host's robots.txt allows (Streamable's API host; Reddit's video host v.redd.it). A video that was removed, or that our network is refused to read, is a free row (`not_found`, `blocked`) that says so.

#### Keyword search (brand and topic mentions)

```json
{
    "podcastFeeds": ["https://feeds.npr.org/510289/podcast.xml"],
    "maxEpisodesPerFeed": 5,
    "searchTerms": ["inflation", "Federal Reserve", "tariff*"]
}
```

Add `searchTerms` to any run (files, feeds, YouTube, Loom...). Each transcript gets every mention with its time, the sentence it was said in, the speaker (with speaker labels or a publisher transcript that names them) and a link that opens the recording at that moment, plus a count per term. **No extra charge**: it runs on the transcript you already pay for.

Real result (shortened, `"searchTerms": ["Loom", "prospect*", "pipeline", "video"]` on the Loom video above):

```json
{
    "keywordsFound": ["Loom", "prospect*", "pipeline", "video"],
    "keywordCounts": { "Loom": 7, "prospect*": 2, "pipeline": 1, "video": 2 },
    "keywordMatches": [
        { "term": "prospect*", "matched": "prospect", "start": 0.5, "end": 5.7, "time": "0:00", "link": "https://www.loom.com/share/3690d29d5e3c426eb0f7ea00f9d3ae7b?t=0", "text": "Ready to prospect more efficiently, close more pipeline, and create loyal champions?" },
        { "term": "Loom", "matched": "loom", "start": 6.86, "end": 9.7, "time": "0:06", "link": "https://www.loom.com/share/3690d29d5e3c426eb0f7ea00f9d3ae7b?t=6", "text": "Loom helps you stand out in a sea of text." },
        { "term": "prospect*", "matched": "prospects", "start": 18.3, "end": 20.26, "time": "0:18", "link": "https://www.loom.com/share/3690d29d5e3c426eb0f7ea00f9d3ae7b?t=18", "text": "Prospects can book a meeting instantly." }
    ]
}
```

- **Matching**: case and accents are ignored (`cafe` finds "café"); whole words only, so `AI` does not match "said" (Chinese, Japanese, Korean and Thai terms match anywhere, as those languages have no spaces); a phrase may run across two segments; a trailing `*` matches word endings (`tariff*` finds "tariffs"). Up to 50 terms, up to 500 listed mentions per file (counts stay exact; `keywordMatchesTruncated: true` when more).
- **Links to the moment**: YouTube `&t=`, Loom `?t=`, Wistia `?wtime=`, and `#t=` on the media file for files and podcast episodes (browsers' own players start there).
- **One list for the whole run**: `KEYWORD_MATCHES.csv` (opens in Excel and Google Sheets) and `KEYWORD_MATCHES.json` in the run's key-value store, one line per mention with the episode or video title; totals per term in `SUMMARY.keywords`; and the *Keyword mentions* view of the dataset.
- **Brand monitoring on a schedule**: combine `searchTerms`, `newEpisodesOnly`, a podcast feed list and a stable `monitorName`. Create a recurring Apify Schedule yourself. Optionally set the secret `mentionWebhookUrl` to a JSON endpoint or Slack/Discord webhook. The first run transcribes the latest episodes as a baseline and sends no alert; later runs send at most one alert for newly completed podcast episodes with matches. No new episode, no match or a failed file sends none. The alert includes the term, episode title, time link when available, short excerpt and run link. `SUMMARY.mentionAlert` reports `sent`, `failed` or `attempted` without the webhook URL; delivery failure does not alter the transcript or charges. A send attempt is marked before delivery to avoid a duplicate after restart, so an uncertain delivery is not automatically retried. With `resumeStore`, transcripts done before are searched again with new terms without paying again (`reused: true`).

The shareable `RETRY_INPUT` and `REMAINING_INPUT` records omit `mentionWebhookUrl`. If you reuse either record, add any needed monitoring settings and webhook separately in the new run's input.

#### YouTube videos

```json
{ "urls": ["https://www.youtube.com/watch?v=CKRxRE7qRzc", "https://youtu.be/...", "https://www.youtube.com/shorts/..."] }
```

Paste YouTube video links like any other file: `youtube.com/watch?v=`, `youtu.be/`, `/shorts/`, `/live/` and `/embed/` links all work (a watch link with `&list=` is that one video). The audio of the video is transcribed with the same Whisper large-v3-turbo model and **billed exactly like a file: $0.006 per started audio minute, no per-video fee**. Only transcribed minutes are charged; a video that cannot be transcribed is a free row.

Real result (shortened; 89-second video, 2 minutes billed, 30 seconds end to end):

```json
{
    "url": "https://www.youtube.com/watch?v=CKRxRE7qRzc",
    "sourceKind": "youtube",
    "youtubeVideoId": "CKRxRE7qRzc",
    "videoTitle": "特朗普與習近平在美國會面：有哪些關鍵議題？－ BBC News 中文",
    "success": true,
    "transcriptSource": "asr",
    "asrBackend": "whisper-local",
    "durationSeconds": 89.4,
    "billedMinutes": 2,
    "language": "en",
    "text": "Presidents Trump and Xi are about to meet here in the US. So what's at stake? ..."
}
```

How YouTube links differ from files:

- **Captions are not used**: the spoken audio is always transcribed (Whisper quality, your language settings). **If the video already has captions and you only need the text, our [YouTube Transcript Scraper](https://apify.com/tidytools/youtube-transcript) is cheaper** (one small fee per video instead of per minute), and it also lists whole channels and playlists.
- **Playlists, channels and `@handle` links**: paste the public YouTube URL into `urls`. The newest `maxVideosPerList` videos (default 20, maximum 500) enter the same transcription path as individual video links. Before trying a playlist or channel, set `maxVideosPerList` to 1–3 or use `estimateOnly` to bound first-run minute charges. A list lookup or a video skipped by the cap is free; a failed lookup gives one free `source_unavailable` row. `SUMMARY.listsExpanded`, `videosFromLists` and `videosSkippedByLimit` report the counts. `titleIncludes` and `titleExcludes` filter list titles before the cap; date filters (`episodesSince`/`episodesUntil`) apply only to RSS/Podcast feeds. A YouTube list with either date field returns one free `invalid_input` row; remove the date field and use `maxVideosPerList`, title filters or `newEpisodesOnly` instead. `newEpisodesOnly` remembers completed videos by monitor name; on a monitor's first run, older videos beyond the cap become its baseline. A video also pasted individually is transcribed once.
- **Per-video limit**: `maxMinutesPerFile` (default and maximum for YouTube: 240 minutes, so a 3-hour podcast video works without changing anything). Longer videos are a free `too_large` row that says what to do: raise your own `maxMinutesPerFile`, or, over 240 minutes, use the [YouTube Transcript Scraper](https://apify.com/tidytools/youtube-transcript) when the video has captions, or pass the recording as a file link (files have no length limit). Your max charge per run and `maxTotalMinutes` limit each video the same way as files. A long video gets at least its length x 15 seconds + 20 minutes of `fileTimeoutSecs` (a 4-hour video: 80 minutes), because the YouTube queue on our side decides how long it takes.
- **Speaker labels are not available** for YouTube links: with `speakerLabels` on, a YouTube link is a free `unsupported` row (turn speaker labels off, or pass a downloaded file). `wordTimestamps` (segment timings only) and `vocabulary` are not applied; the row lists them in `optionsNotApplied`. Subtitles (SRT/VTT), paragraphs, transcript files, AI summary, meeting notes and translated subtitles work as for files.
- **`language`**: the spoken language is always detected and the video is transcribed in it. When you set `language` and the video is spoken in another language, you still get the transcript (charged as usual) and the row says so in `languageNote`; for a transcript in your language, add it to `subtitleLanguages`. A Chinese `language` (`zh`, `zh-TW`, `zh-CN`) also picks the script (Traditional or Simplified).
- **Paced access, retry later**: YouTube is accessed at a steady pace from our own service, without login or cookies. When YouTube challenges it, YouTube access pauses for a few hours; videos in that time get a free row (`errorType: "service_busy"`, "please try again later"). Private, removed, members-only, age-restricted and region-blocked videos are free rows too, each with its own reason (`private_media`, `not_found`, `needs_login`, `unsupported`). Videos are transcribed one at a time on our side, so a busy queue can make a run wait; the wait counts toward the video's time limit.
- **Time limits in long batches**: a video still not finished when its time limit runs out is a free `timeout` row that says so (`retryable: true`), and it is tried once more at the end of the run by reading the same job on our side (it is not transcribed or charged twice). Before the run's own timeout (Run options > Timeout), the run stops waiting 2 minutes early and does not start a YouTube video with less than 5 minutes left (free `run_time_limit` rows): every video still gets its row, the transcribed ones are charged as usual, and `RETRY_INPUT` holds the links to run again. For a large batch, give the run a long timeout or split it into smaller runs.
- **Automatic retries**: when YouTube refuses an audio download for a moment (HTTP 403), the video is tried again in the same run (up to 2 more times, 20 to 40 seconds apart); only a video that still fails is a free `download_failed` row that says to run the link again later. A video skipped only because other videos running in parallel held your max charge is tried again with its real length once that budget is free. When our YouTube service is busy or cannot be reached for a moment, the video is sent again (up to 2 more times, 20 and 40 seconds apart), and a video already sent is waited for through a short outage of our service (3 to 15 minutes, longer for longer videos); only a video that still does not get through is a free `service_busy` row, tried once more at the end of the run. A video with only music or background sound is a free "no speech" row (`noSpeech: true`).
- **If our transcription server stops** while processing YouTube audio, the video gets one free `transcription_unavailable` row (`charged: false`, `billedMinutes: 0`) and is not submitted to the same server again in that run. Try again later, or download and split a long recording into shorter clips. The 240-minute setting is an input limit, not a guarantee that every long recording can finish.
- Extra fields on YouTube rows: `sourceKind: "youtube"`, `youtubeVideoId`, `videoTitle`, `channel`, `publishDate`, `videoDurationSeconds`; `sourceFileId` is `youtube:<video id>`.

#### Results that are not charged

Every row has `success: true` or `false`. Rows with `success: false` are free and carry an `errorType` and `retryable` (whether running them again later may work, see [Retry only what may work later](#retry-only-what-may-work-later)). When there is a specific fix, the row also has `howToFix`: one sentence with the exact setting to change for that link's host (for example "In Dropbox: Share > "Copy link" with "Anyone with the link can view" ..."). Loom rows can also carry `reasonDetail`, a short code for which Loom case it was (for example `loom_folder`). Many `too_large`, `no_text` and `not_audio` rows can carry it too, when the cause is one of these: `too_large_max_minutes` (your own `maxMinutesPerFile`: raise it or leave it empty), `too_large_youtube_max`, `too_large_size_limit`, `too_large_route_limit` or `too_large_disk`; `no_text_no_speech`, `no_text_filler_only` (only filler words such as "Thank you." over music, which a second engine did not hear as speech), `no_text_empty_download` or `no_text_empty_audio`; `not_audio_unreadable` or `not_audio_no_audio_track` (for example a video-only file).

| errorType | Meaning |
|---|---|
| `no_text` | No speech in the audio (silence or music only); the row also has `noSpeech: true`. This is a free, valid answer, not a transcript or a download failure: it does not make the run `PARTIAL_RESULTS` and is counted in `SUMMARY.noSpeech`. If every file has `no_text`, check the spoken audio track or use another file or link with speech; the run status and `SUMMARY.nextStep` say so. |
| `too_large` | Longer than `maxMinutesPerFile`, or larger than 2 GB |
| `limit_reached` | Would go over `maxTotalMinutes` or your Apify spending limit |
| `run_time_limit` | A YouTube video not started because less than 5 minutes of the run's timeout were left (`retryable: true`, in `RETRY_INPUT`); give the run a longer timeout or run those links again |
| `private_link` | A share link that is not public: shared only with invited people or your organisation, protected by a password, or with downloads turned off (Box, OneDrive "Block download"). `howToFix` names the sharing setting to change |
| `needs_login` | The link opened a sign-in page instead of the file (Google, Microsoft, Dropbox or Box sign-in): share the file as "Anyone with the link", or upload it with `audioFiles` |
| `quota_exceeded` | The host's download limit for this file was reached (Google Drive: too many downloads in 24 hours; Dropbox: too much traffic). `retryable: true`: run it again later |
| `not_audio` | The link is a file, but not audio or video (a Google Docs file, a PDF or ZIP shared from OneDrive or Box, an image or document that a Google Drive link downloads, a file without an audio track) |
| `folder_link` | A Dropbox, OneDrive, SharePoint, Box or Loom folder, or a ZIP download: pass the link of each recording (Google Drive folder links are expanded instead) |
| `unsupported` | Not an audio or video file (e.g. a web page), a TikTok video whose sound is licensed music (no original voice track), an Instagram video blocked for copyright, a language that speaker labels do not support (e.g. Chinese), or speaker labels on a YouTube link |
| `unsupported_platform` | A Facebook, X, Vimeo or Spotify page link (not a file), a TikTok or Instagram profile, story or other non-video page, a YouTube non-video page such as search, a SoundCloud link (pass a podcast RSS feed or direct audio file), a Reddit post page, a loom.com page that is not a video, or an encrypted-only Loom stream (see `reasonDetail`); the error says what to do instead |
| `private_media` | A private Loom, Wistia, TikTok, Instagram or YouTube video; ask the owner to make it public, or upload a recording you can access |
| `link_expired`, `link_invalid` | A signed or temporary link that no longer works (`link_expired`: a presigned or Dropbox temporary link past its time, a TikTok file link that expired or only works in the browser session that made it) or whose signature was refused (`link_invalid`: changed or cut short when copied). Running it again will not help: make a new link, or for TikTok pass the video page link (`tiktok.com/@user/video/...`) |
| `needs_login` (YouTube) | An age-gated or members-only video needs sign-in, which this Actor does not use; choose a public video or upload a recording you can access |
| `service_busy` | YouTube links: YouTube access is paused after a YouTube challenge, the daily YouTube limit is reached, or speech-to-text is busy. Streamable videos that are still uploading or processing (run the link again later). Please try again later |
| `no_media` | A web page without an audio or video file we can use (no player, or only a streaming playlist or player page), or an Instagram photo or carousel post |
| `multiple_media` | A web page with several audio or video files; they are listed in `mediaCandidates` |
| `invalid_input` | A line that is not a URL, or a setting that cannot be used (an unknown `language` or subtitle language, a wrong episode filter, no input at all): then the run ends with this one free row and says what to change |
| `not_found`, `blocked`, `network`, `timeout`, `http_error`, `download_failed` | The file could not be downloaded. A YouTube video that is unavailable, removed or from a terminated account is `not_found` (use another video; running it again will not help); a YouTube video not finished within its time limit or before the run's timeout is `timeout` (run it again). A Google Drive link that downloads an empty or tiny file of an unknown type instead of the recording is `download_failed`, not `no_text` (an image or a document is `not_audio`): check the sharing and that the link points to the recording, or pass a direct download link or upload the file. A `blocked` row was refused by the file host on every route we tried; `downloadAttempts` lists them (see [Files the host refuses](#files-the-host-refuses)) |
| `rate_limited` | The file host asked us to wait more than 2 minutes before the next download (HTTP 429 with Retry-After); run that file again later |
| `failed` | Processing stopped with an error, for example speech-to-text failed on one part of a file with speaker labels. This does not mean the recording has no speech. No partial transcript is delivered and nothing is charged. `retryable: true`: run it again |

A batch never fails as a whole because some files could not be downloaded: the other files are transcribed and charged, the failed ones are free rows, and `RETRY_INPUT` holds the links worth running again.

#### Files the host refuses

Some file hosts refuse downloads from data-center networks, refuse non-browser clients, or stop answering after many files in a row (HTTP 401, 403, 429 or a bot-check page). With the defaults, a refused file is tried again:

1. on Apify's network with a standard desktop browser User-Agent instead of ours (`downloadUserAgent: "auto"`; set `"tidytools"` to never switch);
2. through our backend (Cloudflare, any file size), our Oracle server in the US (files up to 15 MB) and our home line in Taiwan (files up to 10 MB) (`downloadVia: "auto"`; pick one route to use only that one).

Each row says how the file came through (`downloadRoute`: `direct`, `backend`, `vm` or `residential`; `downloadUserAgent`: `tidytools` or `browser`), and a file that was refused at first also lists every attempt in `downloadAttempts`. `SUMMARY.downloads` counts them. There is no extra charge for any route. We never send cookies or log in, never solve CAPTCHAs or use browser automation to get past a bot check, and when a host sends `Retry-After` we wait that long before asking it again from any route. Files on the same host in one run start with the route that worked for that host; a host that refused everything for two files is asked only once for each later file.

If at least 10 files in a run are blocked and every result row is blocked, the status suggests trying fewer simultaneous batches later when several batches came from the same host. Retry only the blocked links with `RETRY_INPUT` when that record was saved. In a mixed batch, check each other row separately; its outcome is not implied by the blocked status note.

The run's key-value store also has a `SUMMARY` record with `status` (`SUCCESS`, `PARTIAL_RESULTS`, `FAILED`, `NO_RESULTS` or `LIMIT_REACHED`), files transcribed, files without speech (`noSpeech`, counted as successes), minutes billed and more. When files failed, the same record also says what to do with them: `failuresByReason` counts the failed files by `errorType` (e.g. `{ "private_link": 2, "needs_login": 1, "blocked": 1 }`), `filesNeedNewInput` is how many of them will not work until the input changes, and `failureHint` is the same one-line next step as in the run's status message. Files without speech are not in these counts (still a valid answer, counted in `noSpeech`), and neither are `limit_reached` rows (the run's stop, not a file failure: `SUMMARY.remainingInputs` and the `REMAINING_INPUT` record say how to continue).

### Use in n8n, Make or Zapier (no 25 MB limit)

OpenAI's Whisper API, and the Whisper nodes in n8n and Make, reject files over 25 MB, so long recordings have to be compressed or split first. Here you only send the file link: the Actor downloads the file (up to 2 GB, any length), cuts it at pauses and returns the whole transcript. No OpenAI key and no ffmpeg step.

**One HTTP request.** `run-sync-get-dataset-items` starts the run, waits for it and returns the results (one item per file):

```
POST https://api.apify.com/v2/acts/tidytools~audio-transcriber/run-sync-get-dataset-items
Authorization: Bearer <your Apify API token>
Content-Type: application/json

{ "urls": ["https://drive.google.com/file/d/FILE_ID/view?usp=sharing"] }
```

Use `text`, `paragraphs`, `srtUrl`, `txtUrl` or `mdUrl` from the response. Google Drive, Dropbox and OneDrive files must be shared as "anyone with the link".

**n8n**: paste this node into a workflow (Ctrl+V), then add a *Header Auth* credential with name `Authorization` and value `Bearer <your Apify API token>`. It reads the file link from the previous node's `webViewLink` (Google Drive trigger); change that expression for other sources.

```json
{
  "nodes": [
    {
      "name": "Transcribe (Apify)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [0, 0],
      "parameters": {
        "method": "POST",
        "url": "https://api.apify.com/v2/acts/tidytools~audio-transcriber/run-sync-get-dataset-items",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ urls: [$json.webViewLink] }) }}",
        "options": { "timeout": 300000 }
      }
    }
  ],
  "connections": {}
}
```

You can also use the official **Apify** node (*Run actor and get dataset*, Actor `tidytools/audio-transcriber`) with the same JSON input; it waits for long runs without the 300-second limit below.

**Make**: add an *HTTP > Make a request* module:

| Field | Value |
|---|---|
| URL | `https://api.apify.com/v2/acts/tidytools~audio-transcriber/run-sync-get-dataset-items` |
| Method | POST |
| Headers | `Authorization`: `Bearer <your Apify API token>` |
| Body type | Raw, content type JSON (application/json) |
| Request content | `{ "urls": ["{{1.webContentLink}}"] }` (the file link from your trigger module) |
| Parse response | Yes |

Or use Make's **Apify** app: *Run an Actor* (with *Run synchronously* on), then *Get Dataset Items*. Zapier works the same way with *Webhooks by Zapier > Custom Request* or the Apify app.

**Ready-made n8n workflows:** [Google Drive recording to transcript Google Doc, and recording link (webhook or Dropbox) to transcript Doc](https://github.com/tidytools/apify-web-data-recipes/tree/main/n8n). Import the JSON file, add your own credentials, and pick the folder.

**Large files (200 MB and more) and safe retries.** The file never passes through n8n or Make: send its link and the Actor streams it to disk (no memory limit in your workflow), continues an interrupted download where it stopped, and explains Drive or storage refusals (`private_link`, `needs_login`, `quota_exceeded`, `link_expired`, `link_invalid`) instead of transcribing an error page. Measured on 2 October 2026 with NASA press-conference videos behind signed links, at the default 1 GB memory: 100 MB / 40 min in 2.4 minutes, 250 MB / 55 min in 4.3 minutes, 500 MB / 43 min in 2.6 minutes, from start of the run to the finished transcript. Every row has `sourceFileId` (yours, or derived from the link) and `runId`. Set `resumeStore` (any name) and a retried workflow gets the finished transcript back without paying again (`reused: true`); if the first run is still going, the retry waits for it.

#### Retry only what may work later

Every free failure row has `retryable`: `true` when the same input may work if run again later (YouTube or the host was busy, a network error, a time limit, a Drive download quota, your spending limit), `false` when it will not until the input changes (no speech, private, removed, not a media file, too long for your `maxMinutesPerFile`). When a run has retryable failures, it saves `RETRY_INPUT` in its key-value store: your input with `urls` set to just those links (feeds, dataset and uploads removed, so nothing done is transcribed twice), and `SUMMARY.retry` lists them. This also holds for a run with nothing to transcribe: if your only input is a public Google Drive folder that Drive does not list right now (`quota_exceeded`, HTTP 429), the run ends with that free row, `RETRY_INPUT` holds the folder link and `SUMMARY.retry` lists it, so you can run it again later without pasting it again. In n8n or Make, read \`https://api.apify.com/v

# Changelog

This Actor's version history is a separate document: https://apify.com/tidytools/audio-transcriber/changelog.md

# Actor input Schema

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

Paste public media links, one per line, including YouTube videos, playlists, channels and @handle URLs. Playlist videos are transcribed individually at the regular per-minute price; see maxVideosPerList. Also accepts audio/video files, public Drive/Dropbox/OneDrive shares and TikTok, Instagram Reel, Loom, Wistia, Streamable or v.redd.it videos. Files up to 2 GB.

## `audioFiles` (type: `array`):

Upload recordings from your computer in Apify Console (they are stored in a key-value store of your account and read from there). Same formats and 2 GB limit as the URLs. In API calls, pass file URLs in "urls" instead.

## `podcastFeeds` (type: `array`):

Podcast RSS feed URLs or Apple Podcasts show links. The newest episodes are transcribed (see the next field). Each result includes the episode title and publish date. Leave blank to try only the short speech sample in Audio or video file URLs; add your own feed when ready.

## `audioUrls` (type: `array`):

Same as "Audio or video file URLs", in Apify's request list format (also accepts a link to a text file with URLs). Kept for existing integrations; new integrations should use "urls".

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

How many of the newest episodes to transcribe from each feed.

## `maxVideosPerList` (type: `integer`):

Maximum number of videos to transcribe from each YouTube playlist, channel or @handle URL. Each successful video is charged its audio minutes; listing and skipped videos are free.

## `usePublisherTranscripts` (type: `boolean`):

Many podcasts publish their own transcript in the feed (Podcasting 2.0 <podcast:transcript>: VTT, SRT, JSON, HTML or text). When an episode has one, it is downloaded and converted to the same output (text, segments, SRT/VTT for timed formats) with transcriptSource "publisher", and no audio minutes are charged. A transcript that is empty or does not match the episode length is ignored and the audio is transcribed as usual.

## `episodesSince` (type: `string`):

Only for RSS/Podcast feeds: keep episodes published on or after this date (YYYY-MM-DD), or within e.g. "30 days". Applied before the per-feed limit. YouTube playlists, channels and @handles do not support date filters; with one set, each list gives one free invalid_input row. Filtered podcast episodes get no row; SUMMARY.episodesFiltered counts them.

## `episodesUntil` (type: `string`):

Only for RSS/Podcast feeds: keep episodes published on or before this date (YYYY-MM-DD, the whole day counts). YouTube playlists, channels and @handles do not support date filters; with one set, each list gives one free invalid_input row.

## `titleIncludes` (type: `array`):

Only podcast episodes or YouTube list videos whose title contains one of these texts (case-insensitive). \* matches anything, e.g. "interview\*ceo".

## `titleExcludes` (type: `array`):

Skip podcast episodes or YouTube list videos whose title contains one of these texts (case-insensitive), e.g. "trailer", "rerun", "best of".

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

ISO 639-1 code such as en, es, de, ja or zh (names such as English also work). Leave empty to detect automatically. YouTube links are always transcribed in the spoken language; when it differs from this, the row says so in languageNote.

## `vocabulary` (type: `string`):

Names, brands or terms that appear in the audio, e.g. "Apify, Cloudflare, Kubernetes". Helps spell them correctly.

## `termCorrections` (type: `object`):

Up to 50 heard forms and their correct spelling, e.g. {"Cloud flare": "Cloudflare"} (each side 1-80 characters). Successful rows get correctedText and correctionCounts (0 when not found); text, segments, subtitles, transcript files and the summary stay as transcribed. Literal, case-sensitive, whole-word match, longest entry first, one pass. With resumeStore, a changed table is applied to the saved transcript without transcribing again. No extra charge.

## `chineseOutputScript` (type: `string`):

For Chinese speech in files and podcast audio: "traditional" or "simplified" guides the transcription to those characters (also in the SRT/VTT/TXT files); most characters follow, a few may not, and the text is not converted afterwards. "auto" (default) uses the podcast feed's language and your vocabulary. Other languages are not translated; YouTube links and speaker labels ignore it. With resumeStore, a file saved in another script is transcribed again (charged as usual).

## `transcriptionEngine` (type: `string`):

Choose "fast" (default, Cloudflare Workers AI Whisper) or "accurate" (self-hosted Whisper with more thorough decoding), e.g. {"transcriptionEngine":"accurate"}. On paid plans, Accurate may fall back to Fast when its server is unavailable unless allowWorkersAiFallback is false. Free plans do not divert Accurate requests. Both cost the same. Speaker labels use Deepgram; YouTube links use the YouTube service regardless of this setting.

## `allowWorkersAiFallback` (type: `boolean`):

On by default. When Accurate is selected on a paid plan and the self-hosted Whisper server cannot answer, use Cloudflare Workers AI Whisper for that file piece. Turn off to get a free failure row instead. Fast always uses Workers AI. This switch affects only files and audio URLs. YouTube links always use the YouTube service's local Whisper; when that service is busy, wait or get a free retryable failure, and Workers AI fallback is never used. Results identify the ASR backend.

## `audioChannelMode` (type: `string`):

For two-channel files: auto (default) keeps the normal mix but picks the useful side when the other is nearly silent or the channels nearly cancel out; mix always combines both; left or right transcribes only that side. Left/right on mono or 3+ channels gives a free unsupported result. Does not identify speakers. One file is still one transcript, charged audio-minutes by its original duration. YouTube service and publisher transcripts ignore this setting.

## `searchTerms` (type: `array`):

JSON array of up to 50 brands, names or topics to find, e.g. {"searchTerms":\["Apify","customer churn"]}; in the form, enter one per line. Matches include timestamp, sentence, speaker and a link to that moment; the run also saves KEYWORD_MATCHES.csv. Matching ignores case and accents; a trailing \* matches word endings, e.g. "transcri\*". No extra charge.

## `mentionWebhookUrl` (type: `string`):

With searchTerms and newEpisodesOnly, send one alert per run when newly completed podcast episodes contain matches. The first run builds a baseline without alerts. Accepts a JSON endpoint or Slack/Discord webhook. Create a recurring Apify Schedule for this Actor separately.

## `saveSubtitles` (type: `boolean`):

Save subtitle files for every timed transcript (no extra charge). Use outputFormats to choose SRT, VTT, or both; turn this off to save neither.

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

Enter srt and/or vtt, one per line. Omit to keep the default of both; \["srt"] saves only SRT and \["vtt"] only VTT. An empty list or a list without either format saves no original-language subtitle file. The transcript row and its charges are unchanged. saveSubtitles:false turns both off.

## `saveTranscriptFiles` (type: `boolean`):

Also save a plain-text (.txt) and a Markdown (.md) transcript for every file, linked as txtUrl and mdUrl, plus one combined file for the whole run when there are 2 or more transcripts (no extra charge).

## `subtitleQc` (type: `boolean`):

Off by default. Adds subtitleQc to each row: a mechanical check of the saved SRT/VTT cues that lists overlapping cues, cues under 1 s, reading speed over 20 characters per second (11 for Chinese, Japanese, Korean and Thai), lines over 42 characters (16 for those scripts) and estimated cue times, with up to 100 warnings (cue number, times, rule, measured value). It does not proofread the wording. Skipped when no subtitle file is saved. No extra charge.

## `cleanAudio` (type: `boolean`):

Off by default. For files you transcribe from a file link, cloud-storage link or podcast feed (not YouTube, social video pages or publisher transcripts), also saves a basic cleaned 96 kbps MP3 (light noise reduction, loudness levelling) as cleanedAudioUrl. Not professional mixing; the transcript still uses the original audio. Files up to 2 hours. If no copy can be made, the row keeps its transcript and cleanAudioError says why. No extra charge.

## `summarize` (type: `boolean`):

Add a short summary and timestamped chapters to each transcript. Charged as a separate event per file (see Pricing). Recordings shorter than 2 minutes get a summary only.

## `contentPack` (type: `boolean`):

Off by default. Creates chapters, show notes, X and LinkedIn draft posts, up to 3 suggested 30-60 second clip times (clipCandidates; 5+ minute timed transcripts; suggestions, not cut media) and a newsletterDraft subject and body to review (never sent). Optionally add one translated subtitle language via contentPackSubtitleLanguages. One content-pack event per successful episode from 2026-11-06 ($0.10); free before then. Translated subtitles use the subtitle-language-minute event.

## `contentPackSubtitleLanguages` (type: `array`):

When contentPack is on, also create SRT and VTT subtitles in one language, e.g. Spanish or ja. Billed by the existing subtitle-language-minute event ($0.002 per started minute). Leave empty for text assets only.

## `meetingNotes` (type: `boolean`):

Add structured meeting notes to each transcript: a short summary, decisions, action items (task, owner, due date) and, with speaker labels on, talk time per speaker and each speaker's main points. Saved in the row (meetingNotes) and as a Markdown file (meetingNotesUrl). No extra charge.

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

Label each segment and paragraph with Speaker 1, Speaker 2… and count the speakers. Uses Deepgram nova-3 instead of Whisper and is billed as "audio minute with speaker labels" ($0.015/min) instead of the regular audio minute. Some languages (e.g. Chinese) are not available; those files fail with errorType "unsupported" and are not charged. Not available for YouTube links (free "unsupported" row).

## `speakerNames` (type: `object`):

With speaker labels on: names for the numbered speakers, e.g. {"1": "Host", "2": "Guest"} (up to 20, 1-40 characters, no line breaks or HTML). Rows with speakers get namedSpeakerTranscript and namedSpeakers (source "user", or "default" for unnamed speakers, who stay Speaker N). speakerTranscript, subtitles and transcript files keep Speaker N. Names are never guessed: check the numbers first. With resumeStore, new names are applied without transcribing again. No extra charge.

## `wordTimestamps` (type: `boolean`):

Add a words array \[{word, start, end}] to every segment (for video editing and precise subtitle alignment). Included in the regular $0.006/min price. With speaker labels on, the words come from nova-3 and the speaker-label price applies (not charged twice).

## `subtitleLanguages` (type: `array`):

Also make SRT + VTT subtitles in these languages (up to 5), e.g. Spanish, German, Traditional Chinese, ja. Timings and speaker labels are kept. $0.002 per audio minute per language, charged only for languages that succeed.

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

A dataset from another Actor run, e.g. a podcast or video scraper. Its media URLs are added to the list above. Have the rows themselves instead? Paste them into "datasetItems" below.

## `datasetField` (type: `string`):

Field that holds the media URL, e.g. audioUrl or media.url (a dot path, for nested fields). Leave empty to detect it (audioUrl, mediaUrl, videoUrl, url...). Used for the dataset above and for pasted dataset rows.

## `datasetItems` (type: `array`):

JSON array of rows from another Actor dataset, e.g. {"datasetItems":\[{"audioUrl":"https://example.com/episode.mp3"}]}. Each row needs a media URL field (audioUrl, mediaUrl, videoUrl, url, etc.), or set datasetField to its field name; dot paths work. A row may itself be a URL. Rows without usable links get free error rows with their index; duplicate media is transcribed once. Other source columns are not copied to results.

## `maxMinutesPerFile` (type: `integer`):

Skip files longer than this. Skipped files get a result row with errorType "too_large" and are not charged. Empty = no limit (0 also means no limit). YouTube videos: empty = 240 minutes (the maximum).

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

Stop charging for new files once this many audio minutes were transcribed. Files that would go over it get a row with errorType "limit_reached" and are not charged. Empty or 0 = no limit (your Apify spending limit still applies).

## `estimateOnly` (type: `boolean`):

Expand podcast RSS feeds, YouTube playlists/channels and public Google Drive folders, then list metadata-based audio-minute cost estimates. Never downloads media, transcribes or charges an event. Unknown durations stay unknown; optional summaries, content packs and translated subtitles are excluded.

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

Long files are split into 90-second pieces that are transcribed in parallel. More is faster.

## `fileConcurrency` (type: `integer`):

How many files (podcast episodes, URLs) are downloaded and transcribed at the same time. More is faster for batches; rows then arrive in the order files finish (inputIndex gives the input position). Your max charge per run and maxTotalMinutes are still respected: the cost of every file in progress is reserved before it is transcribed. With little run memory fewer files run at once (about 3 at 1024 MB).

## `fileTimeoutSecs` (type: `integer`):

A file whose download, conversion and transcription take longer than this is reported with errorType "timeout" and not charged; the run goes on with the next file. Raise it for recordings of several hours. YouTube videos get at least their length x 15 s + 20 min (our YouTube queue decides how long they take). 0 = no limit.

## `proxyConfiguration` (type: `object`):

Only used for requests sent from Apify's network (file downloads and podcast feeds). Apify proxy usage is billed to your Apify account.

## `downloadVia` (type: `string`):

Some file hosts refuse downloads from data centers or after many files in a row (HTTP 401/403/429 or a bot-check page). Auto then tries our backend (any size), our Oracle server (up to 15 MB) and our home line in Taiwan (up to 10 MB) before giving up. Each failed file is a free row that lists what was tried. No extra charge. We never use cookies or logins and never solve CAPTCHAs; when a host sends Retry-After we wait that long.

## `downloadUserAgent` (type: `string`):

Every file is first requested with our own User-Agent (TidyToolsTranscriber). Some hosts serve media files only to browsers: with Auto, a file that was refused is requested once more with a standard desktop browser User-Agent string (a plain header; no cookies, logins, CAPTCHA solving or browser automation). Choose TidyTools only to never switch. Only used for the audio/video file links you give.

## `newEpisodesOnly` (type: `boolean`):

Remember what was transcribed (podcast episode GUID, YouTube video URL or file URL) and skip it in later runs with the same monitor name. From podcast feeds, only newer episodes are taken. From YouTube lists, the first run starts with the latest videos; later runs skip completed videos. Failed files are retried next time.

## `backfillOlderEpisodes` (type: `boolean`):

With "Only new episodes": also take older episodes that were never transcribed (up to Max episodes per feed per run), to work through a back catalogue over several runs. Off by default, so scheduled runs only pick up new releases.

## `monitorName` (type: `string`):

Runs with the same name share the list of transcribed files, e.g. one name per podcast schedule. Letters, digits and dashes.

## `sourceFileId` (type: `string`):

Your own id of the file (Drive file id, Dropbox file id `id:...`, storage object key...). Used for runs with one file; it is copied to the row so your workflow can match results, and it is the key of `resumeStore`. Without it, an id is derived from the link (Drive file id, Dropbox shared link id, the bucket and object of a presigned URL). Every row also has this run's `runId`.

## `resumeStore` (type: `string`):

Optional. A name such as `n8n-recordings`: finished transcripts are remembered by source file id in a named key-value store of your Apify account. When the same file is sent again with the same settings (a workflow retry), its saved transcript is returned again with `reused: true` and is not charged again; if another run is still transcribing that file, this run waits for it. Leave empty to turn off.

## `tiktokOnly` (type: `boolean`):

Only public TikTok video links are accepted; other links get a free invalid_input row. The TikTok Transcript variant always enables this mode.

## Actor input object example

```json
{
  "urls": [
    "https://webcapture-api.yukailin.workers.dev/samples/speech-sample.wav"
  ],
  "maxEpisodesPerFeed": 1,
  "maxVideosPerList": 20,
  "usePublisherTranscripts": true,
  "chineseOutputScript": "auto",
  "transcriptionEngine": "fast",
  "allowWorkersAiFallback": true,
  "audioChannelMode": "auto",
  "saveSubtitles": true,
  "outputFormats": [
    "srt",
    "vtt"
  ],
  "saveTranscriptFiles": true,
  "subtitleQc": false,
  "cleanAudio": false,
  "summarize": false,
  "contentPack": false,
  "contentPackSubtitleLanguages": [],
  "meetingNotes": false,
  "speakerLabels": false,
  "wordTimestamps": false,
  "subtitleLanguages": [],
  "estimateOnly": false,
  "maxConcurrency": 3,
  "fileConcurrency": 3,
  "fileTimeoutSecs": 1800,
  "downloadVia": "auto",
  "downloadUserAgent": "auto",
  "newEpisodesOnly": false,
  "backfillOlderEpisodes": false,
  "tiktokOnly": false
}
```

# Actor output Schema

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

No description

## `full` (type: `string`):

No description

## `subtitles` (type: `string`):

No description

## `details` (type: `string`):

No description

## `actionItems` (type: `string`):

No description

## `keywordMatches` (type: `string`):

No description

# 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://webcapture-api.yukailin.workers.dev/samples/speech-sample.wav"
    ],
    "maxEpisodesPerFeed": 1,
    "contentPackSubtitleLanguages": [],
    "subtitleLanguages": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("tidytools/audio-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 = {
    "urls": ["https://webcapture-api.yukailin.workers.dev/samples/speech-sample.wav"],
    "maxEpisodesPerFeed": 1,
    "contentPackSubtitleLanguages": [],
    "subtitleLanguages": [],
}

# Run the Actor and wait for it to finish
run = client.actor("tidytools/audio-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 '{
  "urls": [
    "https://webcapture-api.yukailin.workers.dev/samples/speech-sample.wav"
  ],
  "maxEpisodesPerFeed": 1,
  "contentPackSubtitleLanguages": [],
  "subtitleLanguages": []
}' |
apify call tidytools/audio-transcriber --silent --output-dataset

```

## MCP server setup

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