# Bilibili Transcript & Subtitles Scraper & API: Video to Text (`quarra/bilibili-transcript-scraper`) Actor

Get Bilibili transcripts (哔哩哔哩) of videos: punctuated simplified Chinese with timestamps, SRT/VTT, optional English translation. Video links, b23.tv share links, BV/av IDs, multi-part videos and creator pages. No login.

- **URL**: https://apify.com/quarra/bilibili-transcript-scraper.md
- **Developed by:** [Quarra Labs](https://apify.com/quarra) (community)
- **Categories:** Social media, Videos, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 transcripts

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

## Bilibili Transcript Scraper: 哔哩哔哩 Video to Text

Turn Bilibili (哔哩哔哩) videos into text, without an account or API key. Paste video links, `b23.tv` share links,
the whole share text copied from the Bilibili app, BV or av IDs, or a creator's page, and each video comes back with
its transcript in punctuated Chinese (as plain text and as timestamped segments), optional SRT and WebVTT subtitle
files, and the video's metadata (title, creator, date, plays, likes, coins, favourites, danmaku, comments, shares,
description, cover). Tick **Translate to English** to get an English translation next to the Chinese (paid option,
see Pricing).

Multi-part videos (分P: courses, series) are read part by part, one record per part. Music-only videos come back as a
free `no_speech` record instead of a made-up transcript. Deleted, private and members-only videos and failures are
free too. You pay only for transcripts delivered.

### What we measured

On the Apify platform on 16 October 2026, with the default settings (512 MB), on 20 inputs: video links, `m.bilibili.com`
and `b23.tv` links, the share text of the Bilibili app, BV and av IDs, a creator's page, and damaged or unsupported links:

- **8 transcripts** from 47 seconds to 1 minute 40: 5 in punctuated Chinese (Mandarin, and a Cantonese lesson), 2 in
  English, 1 song in Korean. 15 minutes billed in all.
- **3 videos without a transcript**, free: a song with no clear words (`no_speech`), a music video whose language
  could not be settled (`uncertain_language`), a 74-minute video over the 30-minute default limit (`too_long`).
- **9 failures** with their reason, free: a private video, a share link to a post, an expired share link, part 200
  of a 155-part course, an anime episode, a live stream, an article, an ID with a missing character and a line of text.
- 0 failures on our side; 20 records in 45 seconds.

Longer videos come back complete too: a 14-minute and a 15-minute video of a science channel, about 4,500 characters
each (15 October 2026, same settings).

### Punctuated Chinese, complete, not guessed

Speech-to-text models write Chinese without punctuation unless they are guided: one block of characters with no
commas and no full stops, unusable for subtitles, quotes or an LLM. We guide the model to punctuate, then put the
punctuation in Chinese form (`，` `。` `？` `！` instead of `,` `.` `?` `!`). Chinese text is joined without spaces,
and `wordCount` counts each Chinese character as a word, and punctuation as none.

**Fast talkers are transcribed in full.** Speech models write only so much text for each 30 seconds of audio:
very dense speech loses the end of each 30 seconds without a trace. We detect it and transcribe the same audio
slowed down by 25% (same pitch), with the timestamps brought back to real time. If the text is still incomplete, a
stronger model checks it; a video is never charged as a fragment.

**Music is not speech.** On songs, the model is known to invent sentences that were never said, often the credits of
the videos it learnt from (「请不吝点赞 订阅 转发 打赏支持明镜与点点栏目」, 「字幕志愿者 …」). We filter those, along
with phrases the model is not confident about and loops. A video whose speech is all filtered out is a free
`no_speech` record; one where more than 20% of the speech had to be left out is a free `incomplete_transcript`
record. Clearly sung lyrics still count as speech.

Speech-to-text runs on third-party AI providers: ElevenLabs Scribe v2, the most accurate model in our tests, for most
videos (directly or through OpenRouter), and OpenAI's Whisper large-v3 turbo run by Groq for the first videos of
some runs and as a backup. The audio is sent to them only to be transcribed. When we call ElevenLabs directly, it stores each transcript on its side: we delete it as soon as we have read it and check
that it is gone; if a deletion cannot be confirmed before the run ends, the run log says so. Through OpenRouter, nothing is stored under our account and there is nothing for us to delete; OpenRouter's and ElevenLabs' own data policies apply.

**Why not Bilibili's own subtitles?** Bilibili shows its subtitles, including its AI subtitles, only to signed-in
users, and we do not use accounts. Every transcript is made from the audio, so videos without subtitles are covered
the same way.

### What you can transcribe

| Input | Examples | What you get |
|---|---|---|
| Videos | `https://www.bilibili.com/video/BV1gHps6yEHL`, `m.bilibili.com/video/…` | that video |
| IDs | `BV1gHps6yEHL`, `av170001` | that video |
| One part | `https://www.bilibili.com/video/BV1Wv411h7kN?p=3` | part 3 of a multi-part video |
| Share links | `https://b23.tv/…`, `b23.tv/…` | the video they lead to |
| Share text from the app | `【影视飓风的直升机？怪东西分享9.0-哔哩哔哩】 https://b23.tv/…` | the video in the text |
| Creators | `https://space.bilibili.com/946974`, `uid:946974` | their latest videos (`maxVideosPerAccount`, default 10); a skipped video is replaced by an older one |

Mix them in one run. A video found in two inputs (a link and its share link, or a creator's page) is transcribed
and charged once.

**Multi-part videos.** Some videos hold several parts (分P), each with its own title and length: a 155-part course,
a series of episodes. Like on the site, a link without `?p=` opens part 1 only; its record says how many parts there
are (`partCount`) and how to get the others (`otherParts`). Add `?p=3` to the link for part 3, or tick **All parts of
multi-part videos** (`allParts: true`) to transcribe every part, up to 50 per video: one record and one charge per
part, with `carouselIndex` as the part number.

**Videos only.** Anime and series episodes (番剧, `bangumi/play/ep…`), live streams (`live.bilibili.com`), articles
(`read/cv…`), posts (`opus/…`), search and channel pages get a free `invalid_url` or `not_a_video` record that says
so. Members-only and paid videos, and videos Bilibili shows only to signed-in users, get a free `unavailable`
record.

**Damaged links are refused, never guessed.** A link with a cut or extra character gets a free `invalid_url` record.

### Languages

Most Bilibili videos are in Mandarin and come back in simplified Chinese (`language: "zh"`). Videos in another
language (English, Japanese, Cantonese…) come back in that language: nothing is translated unless you tick Translate
to English. We do not trust a single language detection when anything says otherwise: when the title's script and
the detected language disagree, a stronger model settles the language and transcribes the video, as far as the run's
own charges cover it. If it still cannot be settled, the video comes back as a free `uncertain_language` record, never
as a charged guess. An English video with a Chinese title, alone in its run, can end this way: run it together with
other videos.

`language` is a preference, never an instruction: it changes neither the models used nor the number of passes, and
never forces a translation (`requestedLanguageFound: false` when the video is in another language).

### English translation (optional)

Tick **Translate to English** (`translateToEnglish: true`) and each transcript also comes back in English, in a
`translation` field. The original text always stays in `transcript`, so you can check a sentence or quote it in
Chinese. The translation is made from the whole punctuated transcript, sentence by sentence and in order, by a
large language model; long talks are translated in full, never summarized.

- It is charged only when it is complete (`translationStatus: "ok"`). If it cannot be completed, you get the
  transcript as usual, `translationStatus: "failed"` with the reason in `translationError`, and nothing is charged
  for the translation.
- A transcript already in English is not translated and not charged (`translationStatus: "notNeeded"`).
- Segments and subtitle files stay in the original language.
- Names of people, brands and shows are only as good as the speech-to-text heard them: when one matters, check it
  against the Chinese.

### Output

One record per video (per part for a multi-part video). A transcript (`status: "ok"`), shortened here:

```json
{
  "status": "ok",
  "platform": "bilibili",
  "videoId": "BV1rrJ66KExT",
  "url": "https://www.bilibili.com/video/BV1rrJ66KExT",
  "carouselIndex": null,
  "author": {"username": "万物60秒", "id": "3747570025236608", "url": "https://space.bilibili.com/3747570025236608"},
  "title": "一分钟走进梵高的一生",
  "partCount": 1,
  "publishedAt": "2026-10-15T01:53:30Z",
  "viewCount": 128,
  "durationSeconds": 61,
  "language": "zh",
  "transcript": "你知道吗，我画的一瓶向日葵后来一度是世界上最贵的画。可我活着的时候穷困潦倒。…如果能对我说一句话，你想说什么？",
  "segments": [
    {"start": 0, "end": 5.04, "duration": 5.04, "text": "你知道吗，我画的一瓶向日葵后来一度是世界上最贵的画。"},
    {"start": 5.68, "end": 8.1, "duration": 2.42, "text": "可我活着的时候穷困潦倒。"}
  ],
  "segmentCount": 13,
  "wordCount": 232,
  "billedMinutes": 2,
  "translation": "Did you know that a vase of sunflowers I painted later became the most expensive painting in the world? …",
  "translationStatus": "ok",
  "translatedMinutes": 2,
  "input": "https://www.bilibili.com/video/BV1rrJ66KExT",
  "inputType": "video"
}
```

A video without a transcript comes back as a free record that holds the reason and nothing else:

```json
{
  "status": "error",
  "platform": "bilibili",
  "videoId": "BV1gHps6yEHM",
  "url": "https://www.bilibili.com/video/BV1gHps6yEHM",
  "errorCode": "private",
  "error": "this video is private (only its creator can see it)",
  "retryable": false,
  "input": "BV1gHps6yEHM",
  "inputType": "video"
}
```

#### Fields that need a word of explanation

- `status`: `ok` (transcript, charged), `missing` (the video exists but has nothing to transcribe: `no_speech`,
  `incomplete_transcript`, `uncertain_language`, `no_audio`, or `too_long` past your limit; free) or `error` (the
  link could not be read; free).
- `errorCode` and `retryable`: `not_found` (deleted video, expired share link, or a part number the video does not
  have), `private`, `not_a_video` (a share link that leads to a post or a creator) and `invalid_url` are final, and
  so is `unavailable` for a members-only or paid video. `unavailable` for a video under review, `blocked`, `network`,
  `transcription_capacity` and `transcription_failed` are worth retrying later (`retryable: true`).
  `connection_failed` means your own connection settings could not be used: fix them, or keep the default.
- `segments`: `start`, `end` and `duration` in seconds. Long Chinese segments are split at the end of sentences (at
  most 40 characters each, two subtitle lines); the time of each piece is shared out by its number of characters,
  so the bounds of the original segment are exact and the cuts inside it are close. `includeSrt` and `includeVtt`
  add ready-to-save `.srt` and `.vtt` files.
- `carouselIndex`, `partTitle`, `partCount`, `otherParts`: the part of a multi-part video (see above);
  `carouselIndex` is `null` for a single video.
- `author.username` is the creator's display name (unique on Bilibili, but the creator can change it); `author.id`
  is the UID, which never changes.
- `viewCount`, `danmakuCount` and the other counts are the video's, as Bilibili shows them when the record is made;
  for a multi-part video they are the totals of all its parts.
- `filteredSegments`: segments of the raw speech-to-text output left out as non-speech (an invented credit over
  music, a loop); minutes are billed on the video's length as usual.
- `billedMinutes`: started minutes of speech-to-text billed for this transcript.
- `translation`, `translationStatus`, `translationError`, `translatedMinutes`: only with Translate to English (see
  above). `translatedMinutes` is the number of started minutes charged for the translation.

The dataset has three views: Overview, Transcripts and Video details. **For a spreadsheet, export the Transcripts
view as Excel (XLSX)**: one row per video, and Excel treats every cell as text. The run's summary is the `OUTPUT`
record of the default key-value store: per input, transcripts, missing videos, errors and duplicates, plus
speech-to-text statistics.

### Input example

```json
{
  "urls": [
    "https://www.bilibili.com/video/BV1gHps6yEHL",
    "https://www.bilibili.com/video/BV1Wv411h7kN?p=3",
    "https://space.bilibili.com/946974"
  ],
  "maxVideosPerAccount": 5,
  "includeSrt": true
}
```

### Pricing

Pay per result: **$0.001 per transcript** plus **$0.01 per started minute** of speech, plus a **start fee of $0.003
per GB of memory per run** ($0.003 at the default 512 MB). A 1-minute video costs $0.011; a 14-minute video costs
$0.141. **Translate to English** adds **$0.03 per started minute** of the video, only for complete translations:
$0.561 for the same 14-minute video with its translation. Each part of a multi-part video (分P) is its own transcript.
Videos without speech, videos over your length limit, private or deleted videos, errors and duplicates are free.
Platform usage is included.

Your maximum charge per run is never exceeded: the length of each video is read before it is transcribed. A video
that would cost more than what is left (with its translation, when you ask for one) gets a free `over_budget` record
and the run goes on with the next ones (a shorter video may still fit); the run stops once not even one more
transcript fits, and its status says both. `maxItems` caps the transcripts of a run: ask for 10, get 10.

Free records are a by-product, not a product: a run stops, and says why, once 100 videos or inputs plus 2 per
transcript have given no transcript, or once the audio transcribed for nothing (music-only videos) passes 15 minutes
of audio plus the start fee and 40% of what the run has charged. A long video is never turned away because of its
length: the run stops only once that much audio has really been transcribed for nothing. `maxDurationMinutes`
(default 30, at most 60) skips longer videos before anything is transcribed.

### Use cases

- Market and trend research in China: what creators in a niche say, searchable, quotable and ready for translation.
- Courses and lectures: a searchable, quotable text of each part of a Bilibili course (知识区), for notes or an LLM.
- Product and game research: what reviewers and players say about a product or a game, video by video.
- Subtitles: punctuated SRT files for your own Bilibili videos, for reposting elsewhere.
- LLM pipelines: summaries, translation and topic analysis across hundreds of Chinese videos.

### Limits

- **Videos only**: no anime or series episodes (番剧), live streams, articles, posts, search or comments. For
  comments, danmaku and search, see our Bilibili Scraper.
- **Private, members-only and paid videos** come back as free `private` or `unavailable` records.
- **Speech or music.** The speech-to-text service does not return exactly the same text on every pass over the same
  video; on clear speech it does not matter, on music it can. A video is charged only when its speech is clear; songs
  are charged only when the lyrics are clear on two passes that agree.
- **Speed.** Count about 10 to 30 seconds per video, more for long ones. Our speech-to-text capacity is shared fairly
  between runs; if it is temporarily used up, videos get free `transcription_capacity` records (`retryable: true`)
  and the status says when to retry.

### Memory

512 MB is the default and the best setting: the run waits on speech-to-text, not on memory, and the Actor used at
most 124 MB in our tests, two videos of 14 and 15 minutes included. The maximum is 1 GB.

### FAQ

**Is it legal to scrape Bilibili transcripts?** This Actor reads only public videos, the way a visitor who is not
signed in sees them, and accepts no Bilibili terms on your behalf. Transcripts reproduce what people say, which may
be protected by copyright and may contain personal data: check that your use (research, analysis, quotation,
accessibility) is allowed where you are, and handle personal data under the rules that apply to you (GDPR in Europe,
PIPL in China). We keep nothing after the run. With Translate to English, the transcript text is sent to a
language-model service that does not keep it to train its models.

**Do I need a Bilibili account, a Chinese phone number or any setup?** No. Keep the default connection: its cost is
included in the price.

**Can it translate?** Yes, into English: tick Translate to English (charged only when the translation is complete).
The Chinese transcript always comes with it.

**What happens when Bilibili changes something?** The Actor runs every day on known videos, so we see a change before
you do. A run that fails on our side ends as Failed and charges no transcript.

**Is anything different on Apify's free plan?** Runs started from an Apify free-plan account are transcribed by our Whisper models only, never by the ElevenLabs model we pay for per use, and the English translation option is off (nothing charged for it). Everything else is the same.

**Does the Actor send anything to Quarra?** Only when a run fails or runs out of time: the Actor, its build, the error type, the line of our code where it happened, and the memory and time the run was given, so we can fix it fast. Never your input, your results, your account or the run ID.

# Actor input Schema

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

One per line: video URLs (bilibili.com/video/BV…, m.bilibili.com, ?p= for one part), share links (b23.tv/…), the whole share text copied from the Bilibili app (the link inside is used), BV or av IDs, or creator pages (space.bilibili.com/…) for their latest videos.

## `maxVideosPerAccount` (type: `integer`):

For creator pages: how many of their latest videos to transcribe.

## `allParts` (type: `boolean`):

Some Bilibili videos have several parts (分P: courses, series). Off: like on the site, a link opens part 1 only (add ?p=3 to the link for part 3). On: every part is transcribed, up to 50, one record and one charge per part.

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

Optional. The language you expect, as a code or a name (zh, yue, en, ja…). It is a preference, not an instruction: each video comes back in the language actually spoken (requestedLanguageFound: false when it differs); it does not translate. For English, tick Translate to English below. Empty: detected for each video. Mandarin comes back in simplified Chinese with Chinese punctuation (，。？).

## `transcriptFormat` (type: `string`):

transcript is the whole text in one string; segments is a list of {start, end, duration, text} in seconds.

## `includeSrt` (type: `boolean`):

Add an srt field: the transcript as a ready-to-save .srt subtitle file.

## `includeVtt` (type: `boolean`):

Add a vtt field: the transcript as a ready-to-save .vtt subtitle file.

## `translateToEnglish` (type: `boolean`):

Add an English translation of each transcript in a translation field, next to the original text (which is always kept). Paid option: $0.03 per started minute of translated video, on top of the transcript. Charged only when the translation is complete; a transcript already in English is not translated and not charged. Subtitles and segments stay in the original language. Available on paid Apify plans only.

## `maxDurationMinutes` (type: `integer`):

Longer videos get a free too_long record instead of a transcript. Their length is read before anything is transcribed.

## `maxItems` (type: `integer`):

Optional cap for the whole run, counted in charged transcripts (one per part for multi-part videos). Respected exactly. Free records (no speech, deleted, private) do not count; they are bounded separately (100 plus 2 per transcript).

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

Connection. Keep the default (automatic). Costs are included in the price.

## Actor input object example

```json
{
  "urls": [
    "https://www.bilibili.com/video/BV1rrJ66KExT"
  ],
  "maxVideosPerAccount": 10,
  "allParts": false,
  "transcriptFormat": "both",
  "includeSrt": false,
  "includeVtt": false,
  "translateToEnglish": false,
  "maxDurationMinutes": 30,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

Every video (every part of a multi-part video): transcript, segments, metadata, and the free missing/error records with their reason.

## `overview` (type: `string`):

One line per video or part: status, creator, language, words, reason.

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

Per-input counts, videos without speech, errors and speech-to-text statistics.

# 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://www.bilibili.com/video/BV1rrJ66KExT"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("quarra/bilibili-transcript-scraper").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://www.bilibili.com/video/BV1rrJ66KExT"],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("quarra/bilibili-transcript-scraper").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://www.bilibili.com/video/BV1rrJ66KExT"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call quarra/bilibili-transcript-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,quarra/bilibili-transcript-scraper"
        }
    }
}
```

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/8dC9mt0gPY5fLUguD/builds/Kh1Kg4kJcsyQR0jJt/openapi.json
