New: try each tool in your browser, no account needed. Then one API key with 3 free tries of every API. Get your key →
Video6 min read

How to generate YouTube chapters automatically from any video

Chapters make long videos easy to navigate, but writing them means rewatching everything. This use case shows how to turn a video into YouTube-ready chapters, and a WebVTT chapter track for your own player, with the Video API and a few lines of Python.

OpenCorpo Team

Chapters turn a long video into a table of contents. Viewers jump to the part they need instead of scrubbing, the progress bar shows where each topic starts, and Google Search can show those moments as key moments in results. The catch is that someone has to write them: rewatch the video, note when each topic starts, keep the timestamps right.

This use case automates that. You send a video to the Video API. It returns a timestamped description of what happens on screen, the transcript and a summary, and a short script turns them into chapters ready for a YouTube description. The same chapters work as a WebVTT track for your own player.

A video player whose progress bar is split into five chapters, with a chapter list beside it
The result: chapters in the progress bar and a chapter list, generated from the video itself.

Who this is for

  • Creators and channel managers who publish tutorials, talks or reviews and want chapters on every upload without rewatching them.
  • Agencies that publish video for several clients and want chapters to be part of the upload checklist.
  • Course and documentation teams that host video on their own player and want a chapter menu inside every lesson.

YouTube's rules for chapters

YouTube reads chapters from timestamps in the video description, one per line:

text
0:00 Intro
1:05 Create the project
4:12 Configure the build
8:40 Deploy
12:30 Wrap-up

For them to appear:

  • the first timestamp must be 0:00,
  • there must be at least three timestamps, in ascending order,
  • every chapter must be at least 10 seconds long.

Videos of an hour or more use 1:02:03-style timestamps. The script below handles all of this for you.

How it works

Workflow from a video to the Video API, then grouped spans, then chapters published to a YouTube description, a WebVTT track or a CMS
One API call returns the timestamped material; grouping and formatting happen in your code.

The Video API watches the video and returns four possible outputs. Three of them matter here:

Output Why it matters for chapters
description What is on screen, as timestamped spans. Spans break at visual changes (a new slide, a cut, a different screen) and usually last 5 to 20 seconds. Short on-screen text such as slide titles is quoted word for word.
transcript What is said, with timestamps. Useful when the visuals barely change, such as a talking head.
summary One paragraph about the whole video, a good first paragraph for the video description.

Chapters are topics, not shots, so the spans need grouping. The simplest signal is on-screen text: a new slide title or section card usually starts a new topic.

Step 1: Analyze the video

To see what the API returns before writing any code, paste a link into the free video summarizer demo. It needs no account.

Send a public YouTube link, or a file through uploads, and ask for the three outputs. The price is the same whatever outputs you request.

bash
curl -X POST https://api.opencorpo.com/v1/video/analyze \
  -H "X-API-Key: $OPENCORPO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source": {"type": "youtube", "url": "https://www.youtube.com/watch?v=YOUR_VIDEO_ID"},
    "outputs": ["summary", "description", "transcript"],
    "options": {"detail": "high"}
  }'

"detail": "high" reads small on-screen text more reliably, which helps with slides and screen recordings. It costs 2× the default price, so leave it out for camera footage. The call returns a job; wait for it with GET /v1/video/jobs/{id}?wait=60, as shown in the transcript guide.

Here is part of a completed job for a 14-minute tutorial. The video is fictional and the output is abridged:

json
{
  "status": "completed",
  "duration_seconds": 840,
  "results": {
    "summary": "A developer creates a Next.js project, configures the build and deploys it to a hosting platform ...",
    "description": [
      { "start": "00:00", "end": "00:12", "start_seconds": 0.0, "end_seconds": 12.0,
        "text": "A presenter at a desk; a title card reads \"Deploying a Next.js app\"." },
      { "start": "01:05", "end": "01:20", "start_seconds": 65.0, "end_seconds": 80.0,
        "text": "Screen share: a terminal runs \"npx create-next-app\" in an empty folder." },
      { "start": "04:12", "end": "04:30", "start_seconds": 252.0, "end_seconds": 270.0,
        "text": "Slide titled \"Configure the build\" lists output and image settings." }
    ],
    "transcript": [ ... ]
  }
}

Step 2: Draft chapters from the description

Timeline showing many short description spans above five grouped chapters
Dozens of short spans become a handful of chapters. Boundaries follow on-screen titles.

This function walks through the spans. It starts a new chapter when on-screen text is quoted and the current chapter is at least min_len seconds long. It also starts one when a chapter reaches max_len seconds, so long stretches still get split.

python
import re

QUOTED = re.compile(r'"([^"]{3,60})"')


def draft_chapters(spans, min_len=45, max_len=240):
    """Group description spans into chapter drafts."""
    chapters = []
    for span in spans:
        start = int(span["start_seconds"])
        quoted = QUOTED.search(span["text"])
        length = start - chapters[-1]["start"] if chapters else None
        if not chapters or (quoted and length >= min_len) or length >= max_len:
            title = quoted.group(1) if quoted else " ".join(span["text"].split()[:6]) + "…"
            chapters.append({"start": start, "title": title})
    if chapters:
        chapters[0]["start"] = 0
    return chapters


job = ...  # the completed job from step 1
chapters = draft_chapters(job["results"]["description"])

min_len stops a fast run of slides from turning into ten tiny chapters. Raise it for long talks and lower it for short tutorials.

Step 3: Make them YouTube-valid

python
def fmt(seconds, hours=False):
    h, rest = divmod(int(seconds), 3600)
    m, s = divmod(rest, 60)
    return f"{h}:{m:02d}:{s:02d}" if hours else f"{m}:{s:02d}"


def youtube_chapters(chapters, duration):
    """First at 0:00, ascending, each at least 10 s, at least three."""
    kept = []
    for c in sorted(chapters, key=lambda c: c["start"]):
        if kept and c["start"] - kept[-1]["start"] < 10:
            continue
        kept.append(dict(c))
    if not kept:
        raise ValueError("no chapters")
    kept[0]["start"] = 0
    if len(kept) > 1 and duration - kept[-1]["start"] < 10:
        kept.pop()
    if len(kept) < 3:
        raise ValueError("YouTube needs at least three chapters")
    hours = duration >= 3600
    return "\n".join(f"{fmt(c['start'], hours)} {c['title']}" for c in kept)


print(youtube_chapters(chapters, job["duration_seconds"]))

For the example video, that prints:

text
0:00 Deploying a Next.js app
1:05 npx create-next-app
4:12 Configure the build
8:40 Deploy
12:30 Thanks for watching

The timestamps are right, and some titles are better than others. "Configure the build" came from a slide and is perfect. "npx create-next-app" is the command on screen, not the topic. "Thanks for watching" is an end card. Two ways to finish the job:

  • Edit the titles by hand. With the timestamps already right, this takes under a minute.
  • Let a language model name them. Send it the drafts with the matching transcript lines and ask for short titles:
text
You write YouTube chapter titles. Below are chapter start times with the
on-screen description and transcript for each chapter. Return JSON:
[{"start": <seconds>, "title": "<2 to 5 words>"}]. Keep every start time
unchanged. Titles describe the topic, not the on-screen text.

<chapters with their description and transcript excerpts>

Either way, run the final list through youtube_chapters() again, so a renamed or reordered list still meets the rules.

Step 4: Publish the description

Put the summary on top and the chapters below it:

python
description = (
    job["results"]["summary"]
    + "\n\nChapters\n"
    + youtube_chapters(chapters, job["duration_seconds"])
)
print(description)

Paste it into the video description in YouTube Studio. To update descriptions automatically, the YouTube Data API's videos.update method can set them, with OAuth authorization from the channel.

Chapters for your own player

If you host video yourself, the same list becomes a WebVTT chapters file:

python
def webvtt_chapters(chapters, duration):
    def ts(sec):
        h, rest = divmod(int(sec), 3600)
        m, s = divmod(rest, 60)
        return f"{h:02d}:{m:02d}:{s:02d}.000"

    out = ["WEBVTT", ""]
    for i, c in enumerate(chapters):
        end = chapters[i + 1]["start"] if i + 1 < len(chapters) else duration
        out += [str(i + 1), f"{ts(c['start'])} --> {ts(end)}", c["title"], ""]
    return "\n".join(out)
html
<video controls src="/media/deploy-nextjs.mp4">
  <track kind="chapters" src="/media/deploy-nextjs.chapters.vtt" srclang="en" default>
</video>

Browsers' built-in controls mostly ignore chapter tracks, but players such as Video.js turn them into a chapter menu. You can also read the cues in JavaScript and build your own list.

Videos longer than 20 minutes

The API analyses up to 20 minutes per video, which is enough for most tutorials. For a longer recording you own, split it without re-encoding into parts of about 19 minutes, so a part that runs a few seconds over the cut point is not truncated:

bash
ffmpeg -i webinar.mp4 -c copy -f segment -segment_time 1140 -reset_timestamps 1 part_%02d.mp4

Analyse each part through uploads. Then shift each part's spans by the total length of the parts before it, and draft chapters once over the combined list:

python
offset, all_spans = 0, []
for job in part_jobs:  # completed jobs, in order
    for span in job["results"]["description"]:
        all_spans.append({**span,
                          "start_seconds": span["start_seconds"] + offset,
                          "end_seconds": span["end_seconds"] + offset})
    offset += job["duration_seconds"]

chapters = draft_chapters(all_spans, min_len=120, max_len=600)
print(youtube_chapters(chapters, offset))

For a YouTube link, only the first 20 minutes are analysed and the job is marked truncated.

What it costs

Analysis is billed per started minute of video, once per video: €0.05 / minute on the default model. The 14-minute example is billed as 14 minutes, and asking for three outputs instead of one costs nothing extra. "detail": "high" multiplies the price by 2. Grouping, formatting and publishing run in your own code at no cost. Your first 3 analyses are free, but each free try covers only the first 5 minutes of a video, so use a short video to try the workflow. Live prices are on the pricing page.

Limits

  • Titles are drafts. Boundaries are usually right; titles taken from on-screen text sometimes name a command or an end card instead of the topic. Review before publishing.
  • Visual signals need visual changes. In a single-camera talk with no slides, the description changes little. Use the transcript to choose topics, or let a language model do it from the transcript.
  • 20 minutes per video, public YouTube videos or your own uploads. Private videos fail with youtube_private, and a failed job is not charged.
  • Timestamps are to the second. That is the precision chapters need.

Frequently asked questions

What are YouTube's rules for chapters?

Timestamps go in the video description, one per line. The first must be 0:00, there must be at least three in ascending order, and each chapter must be at least 10 seconds long. Chapters can take a little while to appear after you save.

Doesn't YouTube create chapters automatically?

It can, for some videos, if automatic chapters are allowed in the video's settings. Writing your own gives you control over the titles and lets you reuse the same chapters in your own player, a course platform or a CMS.

Does it work for videos without speech?

Yes. The chapters come from the description of what is on screen, which does not depend on speech. For a talking-head video with little visual change, use the transcript to choose topics instead.

How long can the video be?

Up to 20 minutes are analysed per video. For a longer recording you own, split it into parts of about 19 minutes, analyse each and offset the timestamps, as shown in this article.

How much does it cost?

Video analysis is billed per started minute, once per video, whatever outputs you request: €0.05 / minute on the default model. Your first 3 analyses are free and cover up to the first 5 minutes of a video.