> ## Documentation Index
> Fetch the complete documentation index at: https://docs.videodb.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Word Counter

> Visualize keyword occurrences in videos

<a href="https://colab.research.google.com/github/video-db/videodb-cookbook/blob/main/examples/Keyword_Search_Counter.ipynb" target="_blank">
  <img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab" noZoom />
</a>

### Introduction

With an endless stream of new video content on our feeds, engaging the audience with dynamic visual elements can make educational and promotional videos much more impactful. VideoDB's suite of features allows you to enhance videos with programmatic editing.

In this tutorial, we'll explore how to create a video that visually counts and displays instances of a specified word as it's spoken. We'll use VideoDB's [Keyword Search](/examples-and-tutorials/video-rag/keyword-search) to index spoken words, and then apply audio and [text overlays](/pages/act/programmable-editing/text-asset) to show a counter updating in real-time with synchronized audio cues.

## Setup

### Installing packages

<CodeGroup>
  ```python Python theme={null}
  !pip install videodb
  ```

  ```javascript Node.js theme={null}
  npm install videodb
  ```
</CodeGroup>

### API Keys

Before proceeding, ensure access to [VideoDB](https://videodb.io) and set up

Get your API key from [VideoDB Console](https://console.videodb.io/auth?utm_source=docs_videodb_io\&utm_medium=docs_link\&utm_campaign=console_auth\&utm_content=docs_link\&id=docs). ( Free for first 50 uploads, No credit card required)

## Steps

### Step 1: Connect to VideoDB

Establish a session for uploading videos. Import the necessary modules from VideoDB library to access functionalities.

<CodeGroup>
  ```python Python theme={null}
  import videodb

  # Set your API key
  api_key = "your_api_key"

  # Connect to VideoDB
  conn = videodb.connect(api_key=api_key)
  coll = conn.get_collection()
  ```

  ```javascript Node.js theme={null}
  import { connect } from 'videodb';

  const conn = await connect({ apiKey: process.env.VIDEO_DB_API_KEY });
  const coll = await conn.getCollection();
  ```
</CodeGroup>

### Step 2: Upload Video

Upload and play the video to ensure it's correctly loaded. We'll be using [this video](https://www.youtube.com/watch?v=Js4rTM2Z1Eg) for the purpose of this tutorial.

<CodeGroup>
  ```python Python theme={null}
  video = coll.upload(url="https://www.youtube.com/watch?v=Js4rTM2Z1Eg")
  video.play()
  ```

  ```javascript Node.js theme={null}
  const video = await coll.uploadURL({ url: "https://www.youtube.com/watch?v=Js4rTM2Z1Eg" });
  console.log(video.playerUrl);
  ```
</CodeGroup>

### Step 3: Understand Spoken Words

Analyze the video with a named spoken-word analyzer, then collect its timed words in chronological order.

<CodeGroup>
  ```python Python theme={null}
  spoken_understanding = video.understand(
      analyzers=[{"type": "spoken_words", "name": "spoken_words"}],
  )
  spoken_understanding.wait_until_complete()

  spoken_words_analyzer = spoken_understanding.get_analyzer("spoken_words")
  spoken_words_output = spoken_words_analyzer.get_output()

  timed_words = [
      {
          "text": word["text"],
          "start": word["start"],
          "end": word["end"],
      }
      for scene in spoken_words_output["scenes"]
      for word in scene["data"]["words"]
  ]

  timed_words.sort(key=lambda word: (word["start"], word["end"]))
  ```

  ```javascript Node.js theme={null}
  const spokenUnderstanding = await video.understand([
      { type: "spoken_words", name: "spoken_words" },
  ]);
  await spokenUnderstanding.waitUntilComplete();

  const spokenWordsAnalyzer = await spokenUnderstanding.getAnalyzer("spoken_words");
  const spokenWordsOutput = await spokenWordsAnalyzer.getOutput();
  const timedWords = spokenWordsOutput.scenes.flatMap((scene) =>
      scene.data.words.map((word) => ({
          text: word.text,
          start: word.start,
          end: word.end,
      }))
  );

  timedWords.sort((left, right) => left.start - right.start || left.end - right.end);
  ```
</CodeGroup>

### Step 4: Keyword Search

Normalize only case and surrounding punctuation, then keep every timed word that exactly matches `education`. The artifact timestamps remain unchanged.

<CodeGroup>
  ```python Python theme={null}
  import unicodedata

  keyword = "education"


  def normalize_word(text):
      normalized = text.strip().casefold()
      while normalized and unicodedata.category(normalized[0]).startswith("P"):
          normalized = normalized[1:]
      while normalized and unicodedata.category(normalized[-1]).startswith("P"):
          normalized = normalized[:-1]
      return normalized


  normalized_keyword = normalize_word(keyword)
  keyword_occurrences = [
      word for word in timed_words if normalize_word(word["text"]) == normalized_keyword
  ]

  keyword_occurrences
  ```

  ```javascript Node.js theme={null}
  const keyword = "education";

  const normalizeWord = (text) => text
      .trim()
      .toLowerCase()
      .replace(/^\p{P}+/u, "")
      .replace(/\p{P}+$/u, "");

  const normalizedKeyword = normalizeWord(keyword);
  const keywordOccurrences = timedWords.filter(
      (word) => normalizeWord(word.text) === normalizedKeyword
  );
  ```
</CodeGroup>

### Step 5: Setup Timeline and Audio

Initialize the timeline and prepare an audio asset to use for each word occurrence.

<CodeGroup>
  ```python Python theme={null}
  from videodb.editor import Timeline, Track, Clip, AudioAsset, VideoAsset, TextAsset
  from videodb.editor import Font, Background, Alignment, HorizontalAlignment, VerticalAlignment, Position, Offset
  from videodb import MediaType

  timeline = Timeline(conn)

  # Upload the twink sound effect
  audio = coll.upload(url="https://github.com/video-db/videodb-cookbook-assets/raw/main/audios/twink.mp3", media_type=MediaType.audio)
  ```

  ```javascript Node.js theme={null}
  import {
      EditorTimeline, Track, Clip, EditorAudioAsset, EditorVideoAsset, EditorTextAsset,
      Font, Background, Alignment, HorizontalAlignment, VerticalAlignment, Position, Offset
  } from 'videodb';
  import { MediaType } from 'videodb';

  const timeline = new EditorTimeline(conn);

  // Upload the twink sound effect
  const audio = await coll.uploadURL({
      url: "https://github.com/video-db/videodb-cookbook-assets/raw/main/audios/twink.mp3",
      mediaType: MediaType.audio
  });
  ```
</CodeGroup>

### Step 6: Overlay Text and Audio

Add text and audio overlays at each instance where the word is spoken using the `Track` and `Clip` pattern.

Note: Adding the 'padding' is an optional step. It helps in adding a little more context to the exact instance identified, thus resulting in a better compiled output.

<CodeGroup>
  ```python Python theme={null}
  video_duration = min(300, int(video.length))  # First 5 minutes only
  audio_offset = 1  # Delay audio/text update by 1 second for better sync

  # Create timeline and tracks
  timeline = Timeline(conn)
  video_track = Track()
  text_track = Track()
  audio_track = Track()

  # Add video clip (first 5 minutes)
  video_clip = Clip(
      asset=VideoAsset(id=video.id, start=0),
      duration=video_duration)
  video_track.add_clip(0, video_clip)

  # Filter keyword occurrences within our duration
  occurrences_in_range = [
      word
      for word in keyword_occurrences
      if int(word["start"]) + audio_offset < video_duration
  ]

  # Add text overlays that update at each word occurrence
  for i, occurrence in enumerate(occurrences_in_range):
      trigger_time = int(occurrence["start"]) + audio_offset

      # Initial "Count-0" from start until first word
      if i == 0 and trigger_time > 0:
          text_asset = TextAsset(
              text="Count-0",
              font=Font(family="Do Hyeon", size=72, color="#000100"),
              background=Background(color="#F702A4", opacity=1.0),
              alignment=Alignment(horizontal=HorizontalAlignment.right, vertical=VerticalAlignment.top),)
          text_clip = Clip(asset=text_asset, duration=trigger_time,
            position=Position.top_right, offset=Offset(x=-0.05, y=0.05))
          text_track.add_clip(0, text_clip)

      # Duration until next word or end of video
      if i + 1 < len(occurrences_in_range):
          next_trigger = int(occurrences_in_range[i + 1]["start"]) + audio_offset
      else:
          next_trigger = video_duration

      text_dur = next_trigger - trigger_time

      # Text overlay with updated count
      text_asset = TextAsset(
          text=f"Count-{i + 1}",
          font=Font(family="Do Hyeon", size=72, color="#000100"),
          background=Background(color="#F702A4", opacity=1.0),
          alignment=Alignment(horizontal=HorizontalAlignment.right, vertical=VerticalAlignment.top),)
      text_clip = Clip(asset=text_asset, duration=text_dur, position=Position.top_right, offset=Offset(x=-0.05, y=0.05))
      text_track.add_clip(trigger_time, text_clip)

      # Audio cue at same trigger time
      if trigger_time < video_duration - 2:
          audio_clip = Clip(asset=AudioAsset(id=audio.id), duration=1)
          audio_track.add_clip(trigger_time, audio_clip)

  # Add all tracks to timeline
  timeline.add_track(video_track)
  timeline.add_track(text_track)
  timeline.add_track(audio_track)
  ```

  ```javascript Node.js theme={null}
  const videoDuration = Math.min(300, parseInt(video.length));  // First 5 minutes only
  const audioOffset = 1;  // Delay audio/text update by 1 second for better sync

  // Create timeline and tracks
  const timeline = new EditorTimeline(conn);
  const videoTrack = new Track();
  const textTrack = new Track();
  const audioTrack = new Track();

  // Add video clip (first 5 minutes)
  const videoClip = new Clip({
      asset: new EditorVideoAsset({ id: video.id, start: 0 }),
      duration: videoDuration
  });
  videoTrack.addClip(0, videoClip);

  // Filter keyword occurrences within our duration
  const occurrencesInRange = keywordOccurrences.filter(
      (word) => parseInt(word.start) + audioOffset < videoDuration
  );

  // Add text overlays that update at each word occurrence
  occurrencesInRange.forEach((occurrence, i) => {
      const triggerTime = parseInt(occurrence.start) + audioOffset;

      // Initial "Count-0" from start until first word
      if (i === 0 && triggerTime > 0) {
          const textAsset = new EditorTextAsset({
              text: "Count-0",
              font: new Font({ family: "Do Hyeon", size: 72, color: "#000100" }),
              background: new Background({ color: "#F702A4", opacity: 1.0 }),
              alignment: new Alignment({ horizontal: HorizontalAlignment.right, vertical: VerticalAlignment.top })
          });
          const textClip = new Clip({
              asset: textAsset,
              duration: triggerTime,
              position: Position.topRight,
              offset: new Offset({ x: -0.05, y: 0.05 })
          });
          textTrack.addClip(0, textClip);
      }

      // Duration until next word or end of video
      const nextTrigger = i + 1 < occurrencesInRange.length
          ? parseInt(occurrencesInRange[i + 1].start) + audioOffset
          : videoDuration;

      const textDur = nextTrigger - triggerTime;

      // Text overlay with updated count
      const textAsset = new EditorTextAsset({
          text: `Count-${i + 1}`,
          font: new Font({ family: "Do Hyeon", size: 72, color: "#000100" }),
          background: new Background({ color: "#F702A4", opacity: 1.0 }),
          alignment: new Alignment({ horizontal: HorizontalAlignment.right, vertical: VerticalAlignment.top })
      });
      const textClip = new Clip({
          asset: textAsset,
          duration: textDur,
          position: Position.topRight,
          offset: new Offset({ x: -0.05, y: 0.05 })
      });
      textTrack.addClip(triggerTime, textClip);

      // Audio cue at same trigger time
      if (triggerTime < videoDuration - 2) {
          const audioClip = new Clip({
              asset: new EditorAudioAsset({ id: audio.id }),
              duration: 1
          });
          audioTrack.addClip(triggerTime, audioClip);
      }
  });

  // Add all tracks to timeline
  timeline.addTrack(videoTrack);
  timeline.addTrack(textTrack);
  timeline.addTrack(audioTrack);
  ```
</CodeGroup>

### Step 7: Generate and Play the Stream

Finally, generate a streaming URL for your edited video and play it.

<CodeGroup>
  ```python Python theme={null}
  from videodb import play_stream

  stream_url = timeline.generate_stream()
  play_stream(stream_url)
  ```

  ```javascript Node.js theme={null}
  const streamUrl = await timeline.generateStream();
  console.log(streamUrl);
  ```
</CodeGroup>

Here's a preview of showing occurrence of the word **Education**.

<iframe className="w-full aspect-video rounded-xl" src="https://www.youtube.com/embed/riw1SZSlsU4" title="Word Counter Video Output" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen />

### Conclusion

This tutorial showcases VideoDB’s capabilities to create a video that programmatically counts and displays the frequency of a specific keyword spoken throughout the video. This method can be adapted for various applications where dynamic text overlays add significant value to video content.

### Tips and Tricks

* Use different text styles and positions based on your video's theme.
* Add background sounds or effects to enhance the viewer's experience.

<Card icon="notebook" title="Explore Full Notebook" href="https://colab.research.google.com/github/video-db/videodb-cookbook/blob/main/examples/Keyword_Search_Counter.ipynb">
  Open the complete implementation in Google Colab with all code examples.
</Card>

## Related Tutorials

<CardGroup cols={2}>
  <Card title="Brand Elements" icon="image" href="/examples-and-tutorials/programmatic-editing/brand-elements">
    Add logos, watermarks, and graphics
  </Card>

  <Card title="Audio Overlay" icon="volume-2" href="/examples-and-tutorials/programmatic-editing/audio-overlay">
    Add background music and sound effects
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.