# Bilibili (哔哩哔哩) Video Transcript & Subtitle Extractor (`lanternlab318/bilibili-transcript`) Actor

Turn any Bilibili (哔哩哔哩) video link or BV number into clean, LLM-ready text with timestamps.

- **URL**: https://apify.com/lanternlab318/bilibili-transcript.md
- **Developed by:** [Lantern Lab](https://apify.com/lanternlab318) (community)
- **Categories:** Social media, Videos, AI
- **Stats:** 4 total users, 3 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

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

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 Video Transcript & Subtitle Extractor

Hand it a Bilibili (哔哩哔哩) video link, BV number or b23.tv share link and get the spoken
words back as clean text you can paste straight into a model, with a timestamp on every
line. When Bilibili publishes a caption track for the video, that track is used. When it
does not — which, signed out, is nearly always — the Actor downloads the public audio and
transcribes it with open-source SenseVoice-Small running inside the Actor itself. No
Bilibili login, no cookies, no account of your own, and nothing sent to a transcription
service.
**$1.99 per 1,000 transcripts, plus $0.001 for each 10 seconds of speech transcribed,
plus $0.005 per run** — a ten-minute talk costs about $0.067. See
[Pricing](#pricing).

Input:

```json
{
  "videoUrls": ["https://www.bilibili.com/video/BV1z9nt6aE5H/"],
  "allowAsr": true,
  "maxDurationSec": 600
}
```

Output — one row per video part. `text` is the whole transcript; `segments` carries the
same words with timings. Sample from a real run, transcript trimmed:

```json
{
  "recordType": "transcript",
  "platform": "bilibili",
  "videoId": "BV1DSnK6REdy",
  "bvid": "BV1DSnK6REdy",
  "aid": 117475559611144,
  "cid": 42910354047,
  "part": 1,
  "partCount": 1,
  "url": "https://www.bilibili.com/video/BV1DSnK6REdy/",
  "title": "电脑总是卡顿掉帧？原来是ACE 根本没有彻底卸载干净！",
  "authorName": "宇浩玩数码",
  "publishedAt": "2026-10-20T22:20:03.000Z",
  "durationSec": 52,
  "viewCount": 2191286,
  "language": "zh",
  "transcriptSource": "asr",
  "asrModel": "sensevoice-small",
  "text": "只要你电脑上玩过藤子杰的任何一款游戏，那么你以后在玩别的游戏的时候，都会卡到怀疑人生。我也是今天才发现的…",
  "wordCount": 299,
  "segments": [
    { "start": 0.2, "end": 12.44, "text": "只要你电脑上玩过藤子杰的任何一款游戏，那么你以后在玩别的游戏的时候，都会卡到怀疑人生。" },
    { "start": 12.74, "end": 17.01, "text": "我不能忍受的是给你看一下，他居然把自节的反作弊捆绑到steam上了。" }
  ],
  "segmentCount": 5,
  "availableCaptionLanguages": [],
  "audioSec": 51.04,
  "speechSec": 49.49,
  "asrBlocksCharged": 5,
  "partial": false,
  "partialReason": null,
  "collectedAt": "2026-10-24T10:50:00.882Z"
}
```

### What you get

- **The transcript, twice over.** `text` is one block of plain text, ready for a prompt.
  `segments` is the same content as `{ start, end, text }`, so you can link to a moment.
- **Where the words came from.** `transcriptSource` is `platform_caption` or `asr`, and
  `asrModel` names the model when it was recognised. You always know what you are reading.
- **What you are paying for, on the row.** `speechSec` is the talking the model found and
  `asrBlocksCharged` is what that was billed as, so any invoice can be checked against
  the rows that produced it.
- **The video around it.** `title`, `authorName` (the uploader's public display name),
  `publishedAt`, `durationSec`, `viewCount`, `bvid`, `cid` and `part`, so a transcript can
  be traced back to the exact part it came from.
- **Multi-part (分P) videos.** Add `?p=3` to a link for one part, or turn on
  `includeAllParts` to get every part as its own row.
- **Honest gaps.** A video with no transcript is still a row, with `partial: true`, a
  `partialReason` and a sentence in `statusText` saying what happened. A blocked request,
  a deleted video and a video with no speech are three different reasons and never look
  the same. The run never reports "nothing found" for collection it did not finish.
- **A run summary.** The last dataset row, and the `RUN_SUMMARY` key, hold the totals:
  transcripts by source, every partial reason, audio recognised, requests sent to
  Bilibili and why the run stopped.

### Input

| Field | Default | What it does |
| --- | --- | --- |
| `videoUrls` | — | One video per line: full link, `BV…`, `av…` or a b23.tv link. `?p=3` picks a part. |
| `preferPlatformCaptions` | `true` | Read Bilibili's own caption track first when there is one. |
| `allowAsr` | `true` | Transcribe the audio when there is no caption track. |
| `includeSegments` | `true` | Include the time-coded `segments` array. |
| `includeAllParts` | `false` | Write a row for every part of a multi-part video. |
| `maxDurationSec` | `600` | Skip (and mark) a part longer than this instead of transcribing it. |
| `maxItems` | `50` | Upper bound on transcripts written and charged. |
| `asrModel` | `sensevoice-small` | The only model carried, running inside the Actor. |
| `asrLanguage` | auto | Force the spoken language, for example `zh` or `en`. |
| `maxUpstreamRequests` | derived | Hard ceiling on requests to Bilibili. Left empty, it is worked out from your input. |
| `proxyConfiguration` | Apify Proxy | Bilibili refuses some datacenter addresses; the Actor retries once through a Chinese residential address by itself. |

### Limits worth knowing before you run it

- **Bilibili serves its caption list to signed-in callers only.** This Actor signs in to
  nothing, so `preferPlatformCaptions` finds a track only on the rare video that exposes
  one. On a sample of 40 popular videos taken on 2026-10-24, none did. Plan on speech
  recognition, and on its price.
- **Speech recognition is not free and not instant.** It costs CPU in proportion to how
  much talking there is, which is also how it is priced. `maxDurationSec` is there so a
  long video cannot surprise you; raise it deliberately.
- **Recognition makes mistakes**, more of them on music, dialect, overlapping speakers and
  technical names. `segments` keeps the timeline, so any line can be checked against the
  video.
- **A video with no speech** — a music video, a clip that is all sound effects — comes
  back with `partialReason: "no_speech_detected"`, no text, and **no charge**. That
  includes the case where the model only looped one short syllable over the music. It is
  an answer about the video, not a failure of the run.
- **One awkward video cannot hold up your list.** Recognition is stopped if it passes
  twice the length of the audio; that video is returned with `asr_failed` and is not
  charged, and the rest of the list carries on.
- **Public videos only.** Nothing behind a login, a paywall or an age gate is collected.

### Pricing

Pay per event: **$1.99 per 1,000 transcripts, plus $0.001 for each 10 seconds of speech
the model transcribes, plus $0.005 for each run.** A video Bilibili already captioned
costs no audio events at all.

| Event | Price | Charged for |
| --- | --- | --- |
| `result` | $0.00199 ($1.99 per 1,000) | Each row that carries words. |
| `asr-10s` | $0.001 per started 10 seconds of speech | The stretches of a video the model actually found a voice in, added up and rounded up per video — silence, intro music and music-only passages are not charged. Not charged for a platform caption, and not charged when the model found no words. |
| `run-start` | $0.005, once per run | The signing keys and signed-out session every run sets up before it reads a video. |

What that works out to, per video of someone talking from start to finish (a video that
is half music costs about half the audio part):

| Typical video | Price |
| --- | --- |
| 1 min | $0.008 |
| 3 min | $0.025 |
| 10 min | $0.067 |
| any length, Bilibili captioned it | $0.002 |

Rows with no transcript — blocked, deleted, too long, no speech, a duplicate, over a
limit — are written **free**. A run that collects nothing still pays the $0.005 session.

To cap a run, set **Maximum cost per run** in the run options. At the cap the Actor stops
before the next video, marks what it did not reach `partial` with `charge_limit_reached`,
and says so in `RUN_SUMMARY`.

### Notes on how it works

Requests go to Bilibili's public web API, signed with the same WBI signature the website
uses, with no cookie jar at all. A refused request is retried once with fresh signing
keys and then once through a Chinese residential address; if it is still refused, the row
says `blocked` rather than pretending the video has no transcript. The audio itself comes
off Bilibili's CDN, which refuses nobody, so it is fetched straight off the Actor and only
retried through your proxy if the CDN turns that down - `downloadSec` and `asrSec` on each
row say where a slow video spent its time. Bilibili lists several mirrors per stream and
some of them only answer viewers inside China, so the global ones are always tried first.
The next video is fetched while the current one is being transcribed, so you are not
charged for a machine waiting on a CDN. If Apify moves a run to another machine
mid-collection, it carries on from the videos it had already written instead of collecting
and charging for them twice. Speech recognition is
[SenseVoice-Small](https://github.com/FunAudioLLM/SenseVoice) through
[sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx) on the CPU, with
[Silero VAD](https://github.com/snakers4/silero-vad) cutting the audio into utterances so
every line keeps a real start and end — and so the quiet stretches are never transcribed
or billed. The weights are baked into the Actor's image, so a run downloads no model and
calls no third-party service.

# Actor input Schema

## `videoUrls` (type: `array`):

One Bilibili video per line: a full link (https://www.bilibili.com/video/BV1z9nt6aE5H/), a BV number (BV1z9nt6aE5H), an av number (av117489384035786) or a b23.tv short link. Share text copied from the app works too. For a multi-part (分P) video, add ?p=3 to pick a part; without it the first part is transcribed.

## `preferPlatformCaptions` (type: `boolean`):

Read Bilibili's own caption track first, which is faster and cheaper than recognising the audio. Note that Bilibili serves its caption list to signed-in callers only, and this Actor signs in to nothing, so in practice almost every video falls through to speech recognition. Leave this on anyway: when a track is there, it is the better text.

## `allowAsr` (type: `boolean`):

Run open-source speech recognition (SenseVoice-Small) inside this Actor on the video's public audio stream. Nothing is sent to a transcription service and no key is needed. Turn this off to get only the videos that publish a caption track; the rest are then returned with partialReason "no_captions" and cost nothing.

## `includeSegments` (type: `boolean`):

Add segments\[{start, end, text}] to every row alongside the plain text, so you can jump to a moment in the video. Turn it off for smaller rows when you only need the text.

## `includeAllParts` (type: `boolean`):

A Bilibili video can hold many parts (分P). With this off, a link without ?p= gives you the first part only. With it on, every part becomes its own row, bounded by maxItems.

## `maxDurationSec` (type: `integer`):

Speech recognition is charged by how much talking it transcribes, so a video longer than this is skipped instead of surprising you with the bill. The row is still written, with partialReason "too_long", and is not charged. Videos that publish a caption track are not affected.

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

Upper bound on the transcripts this run writes and charges for. Videos past it are written with partialReason "max_items_reached" and are not charged.

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

SenseVoice-Small, running on the CPU inside this Actor. It reads Mandarin and Cantonese well, writes punctuation, and names the language it heard. It is the only model this Actor carries, and the one the price below was measured on.

## `asrLanguage` (type: `string`):

Force the language of the speech, as a short code such as "zh", "yue", "en" or "ja". Leave empty - which is what you usually want on Bilibili - and the model names the language itself; language on each row then says what it decided.

## `maxUpstreamRequests` (type: `integer`):

Hard ceiling on the HTTP requests this run sends to Bilibili: video details, the player, caption files, audio downloads and retries together. At the ceiling collection stops - the videos not reached are written with partialReason "upstream_request_cap" and the run ends normally. Leave empty and the Actor works a ceiling out from your links and limits, so a run is never unbounded.

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

Bilibili refuses some datacenter addresses. If a run comes back with rows marked "blocked", the Actor retries once through a Chinese residential address by itself; you can also pick a different proxy here.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.bilibili.com/video/BV1z9nt6aE5H/"
  ],
  "preferPlatformCaptions": true,
  "allowAsr": true,
  "includeSegments": true,
  "includeAllParts": false,
  "maxDurationSec": 600,
  "maxItems": 50,
  "asrModel": "sensevoice-small",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

No description

## `runSummary` (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 = {
    "videoUrls": [
        "https://www.bilibili.com/video/BV1z9nt6aE5H/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("lanternlab318/bilibili-transcript").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 = { "videoUrls": ["https://www.bilibili.com/video/BV1z9nt6aE5H/"] }

# Run the Actor and wait for it to finish
run = client.actor("lanternlab318/bilibili-transcript").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 '{
  "videoUrls": [
    "https://www.bilibili.com/video/BV1z9nt6aE5H/"
  ]
}' |
apify call lanternlab318/bilibili-transcript --silent --output-dataset

```

## MCP server setup

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

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/ohg0aL8ozRzNnK2zF/builds/3GKud94XPpPTHMPp2/openapi.json
