チュートリアル

Twelve Labs APIを使用して、機械学習(ML)のトレーニング不要で簡単に動画を分類する方法!

アンキット・カレ

開発者は、機械学習モデルをトレーニングすることなく、Twelve Labs Classification APIを使用してビデオをカスタムカテゴリに分類できます。このチュートリアルでは、ビデオのアップロード、自然言語のプロンプトによるカスタム分類基準の定義、そして結果をFlaskアプリに表示する方法について説明します。

開発者は、機械学習モデルをトレーニングすることなく、Twelve Labs Classification APIを使用してビデオをカスタムカテゴリに分類できます。このチュートリアルでは、ビデオのアップロード、自然言語のプロンプトによるカスタム分類基準の定義、そして結果をFlaskアプリに表示する方法について説明します。

この記事の内容

No headings found on page

ニュースレターに登録する

ニュースレターに登録する

ビデオ理解に関する最新の技術進歩、チュートリアル、業界の動向をお届けします

ビデオ理解に関する最新の技術進歩、チュートリアル、業界の動向をお届けします

AIを活用してビデオを検索、分析、探索します。

2023/05/09

14分

記事へのリンクをコピー

ビデオ分類とは、動画のコンテンツに基づいて、1つ以上の定義済みのカテゴリまたはラベルを自動的に割り当てるプロセスを指します。このタスクには、動画に存在するイベント、アクション、オブジェクト、またはその他の特徴を認識して理解するために、動画の視覚的情報(場合によっては音声情報も)を分析することが含まれます。ビデオ分類は、コンピュータビジョンにおける重要な研究領域であり、ビデオインデックス作成、コンテンツベースのビデオ検索、ビデオ推薦、ビデオ監視、人間の行動認識など、数多くの実用的なアプリケーションがあります。

かつて、ビデオ分類は定義済みのカテゴリやラベルに限定されており、イベント、アクション、オブジェクト、その他の特徴の特定に焦点を当てていました。モデルを再トレーニングしたり基準を更新したりすることなく、分類基準をカスタマイズすることは、遠い夢のように思われました。しかし、ここでTwelve Labs分類APIが登場し、トレーニング不要で、ほぼリアルタイムで、カスタム基準に基づいて動画を簡単かつ強力に分類できるようにすることで、その難題を解決します。まさにゲームチェンジャーと言えるでしょう!

Twelve Labs分類API - コンセプトの概要

Twelve Labs分類APIは、各動画内でクラスラベルが占める時間に基づいて、インデックス化された動画にラベルを付けるように設計されています。その時間が50%未満の場合、クラスラベルは適用されません。そのため、特に大きな動画をアップロードする場合は、クラスとそのプロンプトを慎重に設計することが重要です。APIサービスは任意の数のクラスに対応できるため、クラス内に必要な数だけプロンプトを追加できます。

たとえば、あなたの愛犬ブルーノと愛猫カーラがさまざまなイタズラをしている面白い動画のコレクションがあるとします。これらの動画をTwelve Labsのプラットフォームにアップロードし、毛むくじゃらの友達の愉快な冒険に合わせてカスタマイズした独自の分類基準を作成できます。

<pre><code class="json">"classes": [
      {
            "name": "Doge_Bruno",
            "prompts": [
                "playing with my dog",
                "my dog doing funny things",
                "dog playing with water"
            ]
        },
        {
            "name": "Kitty_Karla",
            "prompts": [
                "cat jumping",
                "cat playing with toys"
            ]
        }
  ]
  </code></pre>

たった1回のAPIコールで、作成した基準を使用してアップロードした動画を分類できます。プロンプトをいくつか忘れてしまったり、新しいクラスを導入したくなったりした場合は、JSONにクラスとプロンプトを追加するだけで簡単に対応できます。新しいモデルをトレーニングしたり、既存のモデルを再トレーニングしたりする必要はないため、プロセス全体が非常にスムーズです。

分類結果

クイック概要

前提条件:このチュートリアルをスムーズに進めるために、Twelve Labs APIスイートへの登録を済ませ、必要なパッケージをインストールしてください。基本に慣れるために、最初2番目のチュートリアルを読んでおくことをお勧めします 🤓。

動画のアップロード:Twelve Labsプラットフォームに動画を送信すると、動画が自動的にインデックス化され、カスタムの分類基準を追加してコンテンツをその場で管理できるようになります。うれしいことに、機械学習(ML)モデルをトレーニングする必要すらありません 😆😁😊。

動画の分類:いよいよ本番です!独自のカスタムクラスと、各クラス内のさまざまなプロンプトを作成します。基準を定義したら、構築したシステムをすぐに使って結果を取得できます。遅延なく、すぐに欲しい情報が手に入ります! 🍿✌️🥳

デモアプリの構築: Flaskベースのアプリを作成して、分類APIからの結果を活用し、コンピューターのローカルフォルダーに保存されている動画にアクセスします。そして、カスタムデザインされたスマートなHTMLページをレンダリングして、分類結果をスタイリッシュに表示します 🔍💻🎨。👨‍🎨

前提条件

最初のチュートリアルでは、シンプルな自然言語クエリを使用して動画内の特定の瞬間を見つける基本について説明しました。わかりやすくするために、プラットフォームには動画を1つだけアップロードし、インデックスの作成、インデックスの構成、タスクAPIの定義、動画インデックス作成タスクの基本的な監視、Flaskベースのデモアプリの作成手順などの重要な概念について説明しました。

2番目のチュートリアルではさらに一歩進め、複数の検索クエリを組み合わせて、より正確でターゲットを絞った検索を作成する方法を検討しました。複数の動画を非同期でアップロードし、複数のインデックスを作成し、動画インデックス作成タスクを監視してタスク完了の推定時間などの詳細を取得するためのコードを追加実装しました。また、複数の動画に対応し、HTMLテンプレートを使用して表示するようにFlaskアプリを構成しました。

この流れを引き継ぎ、今回のチュートリアルでは、Pythonに組み込まれているconcurrent.futuresライブラリを使用した同期動画アップロードについて説明します。動画のインデックス作成ステータスを監視し、それをCSVファイルに記録します。さらに、入力した分類基準と関連する分類APIレスポンスのフィールドをHTMLテンプレートに表示させて、結果を解釈しやすくします。

このチュートリアルや以前のチュートリアルを読んでいる中で、何か問題が発生した場合は、遠慮なくサポートまでご連絡ください。私たちはDiscordサーバーを通じて、快速急行よりも速いレスポンスタイムで迅速なサポートを提供していることを誇りに思っています 🚅🏎️⚡️。または、メールでのご連絡も可能です。Twelve Labsは現在オープンベータ版であるため、Twelve Labsアカウントを作成してAPIダッシュボードにアクセスし、APIキーを取得できます。無料クレジットを使用すれば、最大10時間の動画コンテンツを分類できます。

<pre><code class="python">%env API_KEY=<your_API_key>
%env API_URL=https://api.twelvelabs.io/v1.1

!pip install requests
!pip install flask

import os
import requests
import glob
from pprint import pprint


#Retrieve the URL of the API and the API key
API_URL = os.getenv("API_URL")
assert API_URL

API_KEY = os.getenv("API_KEY")
assert API_KEY
</code></pre>

動画のアップロード

インデックスを作成し、動画アップロード用に構成します:

<pre><code class="python"># Construct the URL of the `/indexes` endpoint
INDEXES_URL = f"{API_URL}/indexes"

# Set the header of the request
default_header = {
    "x-api-key": API_KEY
}

# Define a function to create an index with a given name
def create_index(index_name, index_options, engine):
    # Declare a dictionary named data
    data = {
        "engine_id": engine,
        "index_options": index_options,
        "index_name": index_name,
    }

    # Create an index
    response = requests.post(INDEXES_URL, headers=default_header, json=data)

    # Store the unique identifier of your index
    INDEX_ID = response.json().get('_id')

    # Check if the status code is 201 and print success
    if response.status_code == 201:
        print(f"Status code: {response.status_code} - The request was successful and a new index was created.")
    else:
        print(f"Status code: {response.status_code}")
    pprint(response.json())
    return INDEX_ID


# Create the indexes
index_id_content_classification = create_index(index_name = "insta+tiktok", index_options=["visual", "conversation", "text_in_video", "logo"], engine = "marengo2.5")

# Print the created index IDs
print(f"Created index IDs: {index_id_content_classification}")
</code></pre>
<pre><code class="bash">Status code: 201 - The request was successful and a new index was created.
{'_id': '64544b858b1dd6cde172af77'}
Created index IDs: 64544b858b1dd6cde172af77
</code></pre>

アップロード関数の記述

今回は、指定されたフォルダーからすべての動画を自動的に取得し、動画ファイルと同じ名前を割り当て、プラットフォームにアップロードするコードを作成しました。これは、Pythonライブラリを使用して同期的に実行されます。インデックスを作成したいすべての動画を1つのフォルダーに入れるだけで準備完了です。インデックス作成プロセス全体には、最も長い動画の長さの約40%の時間がかかります。後で同じインデックスにさらに動画を追加したい場合も簡単です。新しいフォルダーは必要ありません。既存のフォルダーに放り込むだけです。コードが機能して、プロセスを開始する前に、同じ名前のインデックス付き動画や保留中のインデックス作成タスクがないかチェックします。これにより、面倒な重複を避けることができます。非常に便利ですよね? 😄

<pre><code class="python">import os
import requests
from concurrent.futures import ThreadPoolExecutor

TASKS_URL = f"{API_URL}/tasks"
TASK_ID_LIST = []
video_folder = 'classify'  # folder containing the video files
INDEX_ID = '64544b858b1dd6cde172af77'

def upload_video(file_name):
    # Validate if a video already exists in the index
    task_list_response = requests.get(
        TASKS_URL,
        headers=default_header,
        params={"index_id": INDEX_ID, "filename": file_name},
    )
    if "data" in task_list_response.json():
        task_list = task_list_response.json()["data"]
        if len(task_list) > 0:
            if task_list[0]['status'] == 'ready': 
                print(f"Video '{file_name}' already exists in index {INDEX_ID}")
            else:
                print("task pending or validating")
            return

    # Proceed further to create a new task to index the current video if the video didn't exist in the index already
    print("Entering task creation code for the file: ", file_name)
    
    if file_name.endswith('.mp4'):  # Make sure the file is an MP4 video
        file_path = os.path.join(video_folder, file_name)  # Get the full path of the video file
        with open(file_path, "rb") as file_stream:
            data = {
                "index_id": INDEX_ID,
                "language": "en"
            }
            file_param = [
                ("video_file", (file_name, file_stream, "application/octet-stream")),] #The video will be indexed on the platform using the same name as the video file itself.
            response = requests.post(TASKS_URL, headers=default_header, data=data, files=file_param)
            TASK_ID = response.json().get("_id")
            TASK_ID_LIST.append(TASK_ID)
            # Check if the status code is 201 and print success
            if response.status_code == 201:
                print(f"Status code: {response.status_code} - The request was successful and a new resource was created.")
            else:
                print(f"Status code: {response.status_code}")
            print(f"File name: {file_name}")
            pprint(response.json())
            print("\n")

# Get list of video files
video_files = [f for f in os.listdir(video_folder) if f.endswith('.mp4')]

# Create a ThreadPoolExecutor
with ThreadPoolExecutor() as executor:
    # Use executor to run upload_video in parallel for all video files
    executor.map(upload_video, video_files)
</code></pre>

インデックス作成プロセスの監視

アップロード関数と同様に、同時に発生するすべてのタスクを追跡する監視関数を設計しました。同時にインデックス化されている各動画の推定残り時間とアップロードのパーセンテージを、整理されたCSVファイルに忠実に記録します。この気の利いた関数は、フォルダー内のすべての動画のインデックス作成が完了するまで実行され続けます。最後に、同期インデックス作成プロセスにかかった合計時間を秒単位でわかりやすく表示します。素晴らしい効率性だと思いませんか?

<pre><code class="python">import time
import csv
from concurrent.futures import ThreadPoolExecutor, as_completed

def monitor_upload_status(task_id):
    TASK_STATUS_URL = f"{API_URL}/tasks/{task_id}"
    while True:
        response = requests.get(TASK_STATUS_URL, headers=default_header)
        STATUS = response.json().get("status")
        if STATUS == "ready":
            return task_id, STATUS
        remain_seconds = response.json().get('process', {}).get('remain_seconds', None)
        upload_percentage = response.json().get('process', {}).get('upload_percentage', None)
        if remain_seconds is not None:
            print(f"Task ID: {task_id}, Remaining seconds: {remain_seconds}, Upload Percentage: {upload_percentage}")
        else:
             print(f"Task ID: {task_id}, Status: {STATUS}")
        time.sleep(10)

# Define starting time
start = time.time()
print("Starting to monitor...")

# Monitor the indexing process for all tasks
with ThreadPoolExecutor() as executor:
    futures = {executor.submit(monitor_upload_status, task_id) for task_id in TASK_ID_LIST}
    with open('upload_status.csv', 'w', newline='') as file:
        writer = csv.writer(file)
        writer.writerow(["Task ID", "Status"])
        for future in as_completed(futures):
            task_id, status = future.result()
            writer.writerow([task_id, status])

# Define ending time
end = time.time()
print("Monitoring finished")
print("Time elapsed (in seconds): ", end - start)
</code></pre>

出力:

<pre><code class="language-plaintext">Starting to monitor...
Monitoring finished
Time elapsed (in seconds):  253.00311
</code></pre>

インデックス内のすべての動画のリスト:

必要な動画がすべてインデックス化されていることを確認するために、インデックス内のすべての動画をリストして徹底的なダブルチェックを行いましょう。さらに、すべての動画IDとそれに対応する名前を含む便利なリストを作成します。このリストは、後で分類APIから返される動画クリップ(分類基準に一致するセグメント)の適切な動画名を取得する必要があるときに役立ちます。

なお、インデックス化された動画が11個あるため、ページ制限を20に変更しました。デフォルトでは、APIは1ページにつき10個の結果を返すため、制限を更新しないと、1つの結果がページ2に隠れてしまい、video_id_name_listの作成に使用するresponse_jsonに含まれなくなってしまいます。そのため、すべてを1ページにまとめておきましょう!

<pre><code class="python"># List all the videos in an index
INDEX_ID='64544b858b1dd6cde172af77'
default_header = {
    "x-api-key": API_KEY
}
# INDEX_ID='64502d238b1dd6cde172a9c5' #movies
# INDEX_ID= '64399bc25b65d57eaecafb35' #lex
INDEXES_VIDEOS_URL = f"{API_URL}/indexes/{INDEX_ID}/videos?page_limit=20"
response = requests.get(INDEXES_VIDEOS_URL, headers=default_header)

response_json = response.json()
pprint(response_json)

video_id_name_list = [{'video_id': video['_id'], 'video_name': video['metadata']['filename']} for video in response_json['data']]

pprint(video_id_name_list)
</code></pre>

出力:

<pre><code class="python">{'data': [{'_id': '64544bb486daab572f3494a0',
           'created_at': '2023-05-05T00:19:33Z',
           'metadata': {'duration': 507.5,
                        'engine_id': 'marengo2.5',
                        'filename': 'JetTila.mp4',
                        'fps': 30,
                        'height': 720,
                        'size': 44891944,
                        'width': 1280},
           'updated_at': '2023-05-05T00:20:04Z'},
          {'_id': '64544bad86daab572f34949f',
           'created_at': '2023-05-05T00:19:32Z',
           'metadata': {'duration': 516.682833,
                        'engine_id': 'marengo2.5',
                        'filename': 'Kylie.mp4',
                        'fps': 23.976023976023978,
                        'height': 720,
                        'size': 37594080,
                        'width': 1280},
           'updated_at': '2023-05-05T00:19:57Z'},
          {'_id': '64544b9286daab572f34949e',
           'created_at': '2023-05-05T00:19:27Z',
           'metadata': {'duration': 34.333333,
                        'engine_id': 'marengo2.5',
                        'filename': 'Oh_my.mp4',
                        'fps': 30,
                        'height': 1280,
                        'size': 10480126,
                        'width': 720},
           'updated_at': '2023-05-05T00:19:30Z'},
          .
					.
					.
					{'_id': '64544b8786daab572f349496',
           'created_at': '2023-05-05T00:19:18Z',
           'metadata': {'duration': 14.333333,
                        'engine_id': 'marengo2.5',
                        'filename': 'cats.mp4',
                        'fps': 30,
                        'height': 1280,
                        'size': 1304438,
                        'width': 720},
           'updated_at': '2023-05-05T00:19:19Z'}],
 'page_info': {'limit_per_page': 20,
               'page': 1,
               'total_duration': 1363.925599,
               'total_page': 1,
               'total_results': 11}}
[{'video_id': '64544bb486daab572f3494a0', 'video_name': 'JetTila.mp4'},
 {'video_id': '64544bad86daab572f34949f', 'video_name': 'Kylie.mp4'},
 {'video_id': '64544b9286daab572f34949e', 'video_name': 'Oh_my.mp4'},
 {'video_id': '64544b8e86daab572f34949c', 'video_name': 'Pitbull.mp4'},
 {'video_id': '64544b9286daab572f34949d', 'video_name': 'She.mp4'},
 {'video_id': '64544b8d86daab572f34949b', 'video_name': 'fun.mp4'},
 {'video_id': '64544b8986daab572f349497', 'video_name': 'Dance.mp4'},
 {'video_id': '64544b8986daab572f349498', 'video_name': 'Jennie.mp4'},
 {'video_id': '64544b8a86daab572f349499', 'video_name': 'McDonald.mp4'},
 {'video_id': '64544b8c86daab572f34949a', 'video_name': 'Orangutan.mp4'},
 {'video_id': '64544b8786daab572f349496', 'video_name': 'cats.mp4'}]
 </code></pre>

動画の分類

コードに入る前に、開発の背景にある理論を簡単に確認しておきましょう。コードのみに興味がある場合は、読み飛ばしていただいて構いません。分類に関して、以下のパラメーターを使用して動作を制御できます。

  • classes:プラットフォームが識別する必要があるエンティティまたはアクションの名前と定義を概説するオブジェクトの配列。各オブジェクトには以下のフィールドが含まれます:

  • name:このクラスに割り当てたい名前を表す文字列。

  • prompts:クラスに含まれる内容を説明する文字列の配列。プラットフォームは、動画を分類するためにここで提供する値に従います。

  • threshold(しきい値):リクエストで説明したプロンプトに沿った信頼度レベルに基づいて、結果を微調整するためにしきい値パラメーターを使用します。範囲は0〜100で、設定しない場合はデフォルト値の75が適用されます。このパラメーターを使用して最も関連性の高い結果のみを取得することで、返されるレスポンスを絞り込むことができます。

分類基準を設定し、Twelve Labsの分類APIを使用して分類リクエストを行いましょう。今回のデモではデフォルトのしきい値設定を使用します:

<pre><code class="python"># Construct the URL of the `/classify/bulk` endpoint
CLASSIFY_BULK_URL = f"{API_URL}/classify/bulk"

# Set the header of the request
headers = {
    "x-api-key": API_KEY
}

# Declare a dictionary named `data`
data =  {
  "conversation_option": "semantic",
  "options": ["visual", "conversation"],
  "index_id" : INDEX_ID,
  "include_clips": False,
  "classes": [
      {
            "name": "BeautyTok",
            "prompts": [
                "Makeup",
                "Skincare",
                "cosmetic products",
                "doing nails",
                "doing hair",
                "DanceTok",
                "dance tutorial",
                "dance competition",
                "dance challenge",
                "dance trend",
                "dancing with friends"
            ]
        },
        {
            "name": "CookTok",
            "prompts": [
                "cooking tutorial",
                "cooking utensils",
                "baking tutorials",
                "recipes",
                "restaurants",
                "food",
                "pasta"
            ]
        },
        {
            "name": "AnimalTok",
            "prompts": [
                "dog",
                "cat",
                "birds",
                "fish",
                "playing with pets",
                "pets doing funny things"
            ]
        },
        {
            "name": "ArtTok",
            "prompts": [
                "handicraft",
                "drawing",
                "graffiti",
                "sketching",
                "digital art",
                "coloring",
                "sketchbook",
                "artwork",
                "artists"
            ]
        }
  ]
}

# Make a classification request
response = requests.post(CLASSIFY_BULK_URL, headers=headers, json=data)
print (f'Status code: {response.status_code}')
pprint(response.json())
duration_data = response.json()
</code></pre>

クラスに含まれるプロンプトに基づいて、動画全体にクラスラベルが割り当てられます。適切な動画セグメント(プロンプトに関連するクリップ)をピンポイントで特定し、正確な動画ラベル付けを実現するには、関連する多数のプロンプトを提供することが重要です。クラスラベルに一致する動画の長さが動画の合計の長さの50%を超えた場合にのみ、そのクラスラベルが割り当てられます。この全体の長さは、プロンプトに一致する動画クリップを結合することによって確定されます。

実行した分類APIコールの結果は以下の通りです。「duration ratio」は動画全体の長さに対する動画セグメントの割合を表し、「score」はモデルの信頼度を示し、「name」はクラスラベルを指します。一致するすべての動画が信頼度スコアの降順で表示されます:

<pre><code class="python">Status code: 200
{'data': [{'classes': [{'duration_ratio': 1,
                        'name': 'AnimalTok',
                        'score': 95.35111111111111}],
           'video_id': '64544b8786daab572f349496'},
          {'classes': [{'duration_ratio': 1,
                        'name': 'AnimalTok',
                        'score': 95.14666666666668}],
           'video_id': '64544b8e86daab572f34949c'},
          .
			.
			.
          {'classes': [{'duration_ratio': 0.8175611166393244,
                        'name': 'ArtTok',
                        'score': 89.45777777777778}],
           'video_id': '64544b9286daab572f34949d'}],
 'page_info': {'limit_per_page': 10,
               'next_page_token': '',
               'page_expired_at': '2023-05-05T17:37:30Z',
               'prev_page_token': '',
               'total_results': 10}}
 </code></pre>

それでは、同じコードを書き直して分類APIをコールしてみましょう。しかし今回は1点だけ異なる仕様を実装します、include_clipsをTrueに設定します。これにより、クラス内で提供されたプロンプトに一致するすべての関連動画クリップと、それぞれのメタデータを取得します:

<pre><code class="python">
# Construct the URL of the `/classify/bulk` endpoint
CLASSIFY_BULK_URL = f"{API_URL}/classify/bulk"

# Set the header of the request
headers = {
    "x-api-key": API_KEY
}

# Declare a dictionary named `data`
data =  {
  "conversation_option": "semantic",
  "options": ["visual", "conversation"],
  "index_id" : INDEX_ID,
  "include_clips": True,
  "classes": [
      {
            "name": "BeautyTok",
            "prompts": [
                "Makeup",
                "Skincare",
                "cosmetic products",
                "doing nails",
                "doing hair",
                "DanceTok",
                "dance tutorial",
                "dance competition",
                "dance challenge",
                "dance trend",
                "dancing with friends"
            ]
        },
        {
            "name": "CookTok",
            "prompts": [
                "cooking tutorial",
                "cooking utensils",
                "baking tutorials",
                "recipes",
                "restaurants",
                "food",
                "pasta"
            ]
        },
        {
            "name": "AnimalTok",
            "prompts": [
                "dog",
                "cat",
                "birds",
                "fish",
                "playing with pets",
                "pets doing funny things"
            ]
        },
        {
            "name": "ArtTok",
            "prompts": [
                "handicraft",
                "drawing",
                "graffiti",
                "sketching",
                "digital art",
                "coloring",
                "sketchbook",
                "artwork",
                "artists"
            ]
        }
  ]
}

# Make a classification request
response = requests.post(CLASSIFY_BULK_URL, headers=headers, json=data)
print (f'Status code: {response.status_code}')
pprint(response.json())
duration_data = response.json()
</code></pre>

出力:

<pre><code class="python">{'data': [{'classes': [{'clips': [{'end': 14,
                                   'option': '',
                                   'prompt': 'cat',
                                   'score': 84.77,
                                   'start': 0},
                                  {'end': 14,
                                   'option': '',
                                   'prompt': 'pets doing funny things',
                                   'score': 83.56,
                                   'start': 8.34375},
                                  {'end': 14,
                                   'option': '',
                                   'prompt': 'playing with pets',
                                   'score': 68.11,
                                   'start': 8.34375},
                                  {'end': 8.34375,
                                   'option': '',
                                   'prompt': 'pets doing funny things',
                                   'score': 58.26,
                                   'start': 0}],
                        'duration_ratio': 1,
                        'name': 'AnimalTok',
                        'score': 95.35111111111111}],
			.
			.
			.	
           'video_id': '64544b8786daab572f349496'},
          {'classes': [{'clips': [{'end': 49.189,
                                   'option': '',
                                   'prompt': 'artists',
                                   'score': 78.14,
                                   'start': 0.55},
                                  {'end': 23.659,
                                   'option': '',
                                   'prompt': 'sketching',
                                   'score': 58.85,
                                   'start': 0.55}],
                        'duration_ratio': 0.8175611166393244,
                        'name': 'ArtTok',
                        'score': 89.45777777777778}],
			.
			.
			.
			'video_id': '64544b9286daab572f34949d'}],
 'page_info': {'limit_per_page': 10,
               'next_page_token': '',
               'page_expired_at': '2023-05-05T17:37:30Z',
               'prev_page_token': '',
               'total_results': 10}}
               </code></pre>

分かりやすくするために、出力をトリミングしています。出力に、開始タイムスタンプと終了タイムスタンプ、特定のクリップの信頼度スコア、およびそれに関連するプロンプトを含む、各動画のクリップデータが表示されるようになったことに注目してください。各プロンプトに関連付けられた分類オプション(例:visualとconversation。visualは音声と視覚の一致、conversationは対話の一致を表します)を統合するために、APIエンドポイントの改良を進めています。

それでは、JSONの結果と、前に作成したvideo_id_name_listをシリアル化(pickle)してファイルに保存しましょう:

<pre><code class="python">import pickle

with open('video_id_name_list.pickle', 'wb') as handle:
    pickle.dump(video_id_name_list, handle, protocol=pickle.HIGHEST_PROTOCOL)

with open('duration_data.pickle', 'wb') as handle:
    pickle.dump(duration_data, handle, protocol=pickle.HIGHEST_PROTOCOL)

with open('clips_data.pickle', 'wb') as handle:
    pickle.dump(clips_data, handle, protocol=pickle.HIGHEST_PROTOCOL)
    </code></pre>

デモアプリの構築

以前のチュートリアルと同様に、Webページをホストし、シリアル化されたデータを利用するFlaskベースのデモアプリを作成します。このデータをローカルドライブから取得した動画に適用することで、見栄えの良い分類結果Webページを作成します。これにより、ビデオ分類APIがアプリケーションをどのように強化し、印象的な結果をもたらすかを直接体験できます。

ディレクトリ構造は以下のようになります:

<pre><code class="markdown">my_flask_app/
│   app.py
│   sample_notebook.ipynb
└───templates/
│	│   index.html
└───classify/
	│   <your_video_1.mp4><your_video_2.mp4><your_video_3.mp4>
			.
			.
			.
</code></pre>

Flaskアプリのコード

このチュートリアルでは、ローカルディレクトリから動画ファイルを提供し、HTML5ビデオプレーヤーを使用して特定のセグメントを再生する方法を少し工夫しています。Flaskアプリケーションで使用されるserve_video関数は、Flaskアプリケーションスクリプトと同じディレクトリにあるclassify_tryディレクトリからビデオファイルを提供します。HTMLテンプレートの url_for('serve_video', filename=video_mapping[video.video_id]) 式は、提供されたビデオファイルのURLを生成します。

「include_clips」をTrueに設定したときの分類APIの出力からお気づきかもしれませんが、APIは多数のクリップとそのメタデータを返しました。シンプルにするため、またこれらのクリップを含む結果を示すために、get_top_clips関数を含めました。この関数は3つの固有のプロンプトを見つけ、それらに関連付けられたすべてのクリップのメタデータを返すため、分類結果をより包括的に見ることができます。

以下は、「app.py」 ファイルのコードです:

<pre><code class="python">from flask import Flask, render_template, send_from_directory
import pickle
import os
from collections import defaultdict

app = Flask(__name__)

# Replace the following dictionaries with your data
with open('video_id_name_list.pickle', 'rb') as handle:
    video_id_name_list = pickle.load(handle)

with open('duration_data.pickle', 'rb') as handle:
    duration_data = pickle.load(handle)

with open('clips_data.pickle', 'rb') as handle:
    clips_data = pickle.load(handle)

VIDEO_DIRECTORY = os.path.join(os.path.dirname(os.path.realpath(__file__)), "classify")

@app.route('/<path:filename>')
def serve_video(filename):
    print(VIDEO_DIRECTORY, filename)
    return send_from_directory(directory=VIDEO_DIRECTORY, path=filename)

def get_top_clips(clips_data, num_clips=3):
    top_clips = defaultdict(list)
    for video in clips_data['data']:
        video_id = video['video_id']
        unique_prompts = set()
        for clip_class in video['classes']:
            for clip in clip_class['clips']:
                if clip['prompt'] not in unique_prompts and len(unique_prompts) < num_clips:
                    top_clips[video_id].append(clip)
                    unique_prompts.add(clip['prompt'])
    return top_clips

@app.route('/')
def home():
    video_id_name_dict = {video['video_id']: video['video_name'] for video in video_id_name_list}
    top_clips = get_top_clips(clips_data)
    return render_template('index.html', video_mapping=video_id_name_dict, duration_data=duration_data, top_clips=top_clips)


if __name__ == '__main__':
    app.run(debug=True)
</code></pre>

HTMLテンプレート

以下はJinja2ベースのHTMLテンプレートのサンプルです。HTMLファイル内にコードを組み込み、準備して渡したシリアル化データのフィールドを使用して反復処理します。このテンプレートは、ローカルドライブから必要な動画を取得し、分類基準に応じて結果を表示します:

<pre><code class="language-html"><!doctype html>
<html lang="en">
<head>
    <link rel="shortcut icon" href="#" />
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Classification Output</title>
    <style>
        body {
            background-color: #f0e7cc;
            font-family: Arial, sans-serif;
        }
        .container {
            width: 80%;
            margin: 0 auto;
            padding-bottom: 30px;
        }
        video {
            display: block;
            margin: 0 auto;
        }
        .progress-bar {
            width: 20%;
            height: 20px;
            background-color: #f0f0f0;
            margin-bottom: 10px;
            display: inline-block;
            vertical-align: middle;
            margin-left: 10px;
        }
        .progress {
            height: 100%;
            background-color: #4caf50;
        }
        .score {
            display: inline-block;
            vertical-align: middle;
            font-weight: bold;
        }
        .video-category {
            padding-bottom: 30px;
            margin-bottom: 20px;
        }
        button {
            background-color: #008CBA;
            border: none;
            color: white;
            padding: 8px 16px;
            text-align: center;
            text-decoration: none;
            display: inline-block;
            font-size: 14px;
            margin: 4px 2px;
            cursor: pointer;
            border-radius: 4px;
            box-shadow: 0 1px 2px rgba(0,0,0,0.2);
            transition: background-color 0.2s ease;
        }
        button:hover {
            background-color: #006494;
        }
        h1 {
            color: #070707;
            text-align: center;
            background-color: #cb6a16; /* Change the background color to your desired color */
            padding: 10px;
            margin-bottom: 20px;  
        }
        .clips-container {
            text-align: center;
        }

    </style>
    <script>
        function playClip(videoPlayer, start, end) {
            videoPlayer.currentTime = start;
            videoPlayer.play();
            videoPlayer.ontimeupdate = function () {
                if (videoPlayer.currentTime >= end) {
                    videoPlayer.pause();
                }
            };
        }
    </script>
</head>
<body>
    <div class="container">
        <h1>Video Classification</h1>
        {% for video in duration_data.data %}
            {% set video_id = video.video_id %}
            {% set video_path = video_mapping[video_id] %}
            <div class="video-category">
                <h2>{{ video.classes[0].name }}</h2>
                <p>Video file: {{video_path}}</p>
                <div>
                    <span class="score">Score: {{ '%.2f'|format(video.classes[0].score) }}</span>
                    <div class="progress-bar">
                        <div class="progress" style="width: {{ video.classes[0].score }}%;"></div>
                    </div>
                </div>
                <div>
                    <span class="score">Duration: {{ '%.2f%%'|format(video.classes[0].duration_ratio * 100) }}</span>
                    <div class="progress-bar">
                        <div class="progress" style="width: {{ video.classes[0].duration_ratio * 100 }}%;"></div>
                    </div>
                </div>
                <video id="video-{{ video_id }}" width="480" height="320" controls>
                    <source src="{{ url_for('serve_video', filename=video_path) }}" type="video/mp4">
                    Your browser does not support the video tag.
                </video>
                <div class="clips-container">
                    <h3>Sample Clips</h3>
                    {% for clip in top_clips[video_id] %}
                    <button onclick="playClip(document.getElementById('video-{{ video_id }}'), {{ clip.start }}, {{ clip.end }})">Clip: {{ clip.prompt }} | Start {{ '%.2f'|format(clip.start) }}s - End {{ '%.2f'|format(clip.end) }}s - Score {{ '%.2f'|format(clip.score) }}</button>
                    {% endfor %}
            </div>
            <br /><br />
        {% endfor %}
    </div>
</body>
</html>
</code></pre>

Flaskアプリの起動

素晴らしいですね!それでは、Jupyter Notebookの最後のセルを実行してFlaskアプリを起動しましょう:

<pre><code class="python">%run app.py
</code></pre>

すべてが想定通りに進んだことを確認する、以下のような出力が表示されるはずです 😊:

URLリンク http://127.0.0.1:5000 をクリックすると、以下のような画面が表示されます:

このチュートリアルで作成した完全なコードが含まれるJupyter Notebookはこちらからご確認いただけます - https://tinyurl.com/classifyNotebook

さらに高度な構築に挑戦してみませんか?

以下のようなアイデアをぜひ試してみてください:

  1. 3つの個別のプロンプトを選択する代わりに、ページネーションと遅延読み込みを実装して、すべてのクリップを表示します。これにより、分類されたクリップのより広い範囲を探索し、ビデオ分類結果のより包括的な視覚を得ることができます。

  2. ショートテール、ミディアムテール、ロングテールのプロンプトを実験し、しきい値を微調整して、Discordチャネルで他のマルチモーダル愛好家と分析結果を共有しましょう。

結び

これからの展開にもぜひご期待ください!まだ参加されていない方は、マルチモーダルAIに情熱を持つ同じ志を持った仲間とつながることができる、活気あるDiscordコミュニティにぜひご参加ください。

ビデオ分類とは、動画のコンテンツに基づいて、1つ以上の定義済みのカテゴリまたはラベルを自動的に割り当てるプロセスを指します。このタスクには、動画に存在するイベント、アクション、オブジェクト、またはその他の特徴を認識して理解するために、動画の視覚的情報(場合によっては音声情報も)を分析することが含まれます。ビデオ分類は、コンピュータビジョンにおける重要な研究領域であり、ビデオインデックス作成、コンテンツベースのビデオ検索、ビデオ推薦、ビデオ監視、人間の行動認識など、数多くの実用的なアプリケーションがあります。

かつて、ビデオ分類は定義済みのカテゴリやラベルに限定されており、イベント、アクション、オブジェクト、その他の特徴の特定に焦点を当てていました。モデルを再トレーニングしたり基準を更新したりすることなく、分類基準をカスタマイズすることは、遠い夢のように思われました。しかし、ここでTwelve Labs分類APIが登場し、トレーニング不要で、ほぼリアルタイムで、カスタム基準に基づいて動画を簡単かつ強力に分類できるようにすることで、その難題を解決します。まさにゲームチェンジャーと言えるでしょう!

Twelve Labs分類API - コンセプトの概要

Twelve Labs分類APIは、各動画内でクラスラベルが占める時間に基づいて、インデックス化された動画にラベルを付けるように設計されています。その時間が50%未満の場合、クラスラベルは適用されません。そのため、特に大きな動画をアップロードする場合は、クラスとそのプロンプトを慎重に設計することが重要です。APIサービスは任意の数のクラスに対応できるため、クラス内に必要な数だけプロンプトを追加できます。

たとえば、あなたの愛犬ブルーノと愛猫カーラがさまざまなイタズラをしている面白い動画のコレクションがあるとします。これらの動画をTwelve Labsのプラットフォームにアップロードし、毛むくじゃらの友達の愉快な冒険に合わせてカスタマイズした独自の分類基準を作成できます。

<pre><code class="json">"classes": [
      {
            "name": "Doge_Bruno",
            "prompts": [
                "playing with my dog",
                "my dog doing funny things",
                "dog playing with water"
            ]
        },
        {
            "name": "Kitty_Karla",
            "prompts": [
                "cat jumping",
                "cat playing with toys"
            ]
        }
  ]
  </code></pre>

たった1回のAPIコールで、作成した基準を使用してアップロードした動画を分類できます。プロンプトをいくつか忘れてしまったり、新しいクラスを導入したくなったりした場合は、JSONにクラスとプロンプトを追加するだけで簡単に対応できます。新しいモデルをトレーニングしたり、既存のモデルを再トレーニングしたりする必要はないため、プロセス全体が非常にスムーズです。

分類結果

クイック概要

前提条件:このチュートリアルをスムーズに進めるために、Twelve Labs APIスイートへの登録を済ませ、必要なパッケージをインストールしてください。基本に慣れるために、最初2番目のチュートリアルを読んでおくことをお勧めします 🤓。

動画のアップロード:Twelve Labsプラットフォームに動画を送信すると、動画が自動的にインデックス化され、カスタムの分類基準を追加してコンテンツをその場で管理できるようになります。うれしいことに、機械学習(ML)モデルをトレーニングする必要すらありません 😆😁😊。

動画の分類:いよいよ本番です!独自のカスタムクラスと、各クラス内のさまざまなプロンプトを作成します。基準を定義したら、構築したシステムをすぐに使って結果を取得できます。遅延なく、すぐに欲しい情報が手に入ります! 🍿✌️🥳

デモアプリの構築: Flaskベースのアプリを作成して、分類APIからの結果を活用し、コンピューターのローカルフォルダーに保存されている動画にアクセスします。そして、カスタムデザインされたスマートなHTMLページをレンダリングして、分類結果をスタイリッシュに表示します 🔍💻🎨。👨‍🎨

前提条件

最初のチュートリアルでは、シンプルな自然言語クエリを使用して動画内の特定の瞬間を見つける基本について説明しました。わかりやすくするために、プラットフォームには動画を1つだけアップロードし、インデックスの作成、インデックスの構成、タスクAPIの定義、動画インデックス作成タスクの基本的な監視、Flaskベースのデモアプリの作成手順などの重要な概念について説明しました。

2番目のチュートリアルではさらに一歩進め、複数の検索クエリを組み合わせて、より正確でターゲットを絞った検索を作成する方法を検討しました。複数の動画を非同期でアップロードし、複数のインデックスを作成し、動画インデックス作成タスクを監視してタスク完了の推定時間などの詳細を取得するためのコードを追加実装しました。また、複数の動画に対応し、HTMLテンプレートを使用して表示するようにFlaskアプリを構成しました。

この流れを引き継ぎ、今回のチュートリアルでは、Pythonに組み込まれているconcurrent.futuresライブラリを使用した同期動画アップロードについて説明します。動画のインデックス作成ステータスを監視し、それをCSVファイルに記録します。さらに、入力した分類基準と関連する分類APIレスポンスのフィールドをHTMLテンプレートに表示させて、結果を解釈しやすくします。

このチュートリアルや以前のチュートリアルを読んでいる中で、何か問題が発生した場合は、遠慮なくサポートまでご連絡ください。私たちはDiscordサーバーを通じて、快速急行よりも速いレスポンスタイムで迅速なサポートを提供していることを誇りに思っています 🚅🏎️⚡️。または、メールでのご連絡も可能です。Twelve Labsは現在オープンベータ版であるため、Twelve Labsアカウントを作成してAPIダッシュボードにアクセスし、APIキーを取得できます。無料クレジットを使用すれば、最大10時間の動画コンテンツを分類できます。

<pre><code class="python">%env API_KEY=<your_API_key>
%env API_URL=https://api.twelvelabs.io/v1.1

!pip install requests
!pip install flask

import os
import requests
import glob
from pprint import pprint


#Retrieve the URL of the API and the API key
API_URL = os.getenv("API_URL")
assert API_URL

API_KEY = os.getenv("API_KEY")
assert API_KEY
</code></pre>

動画のアップロード

インデックスを作成し、動画アップロード用に構成します:

<pre><code class="python"># Construct the URL of the `/indexes` endpoint
INDEXES_URL = f"{API_URL}/indexes"

# Set the header of the request
default_header = {
    "x-api-key": API_KEY
}

# Define a function to create an index with a given name
def create_index(index_name, index_options, engine):
    # Declare a dictionary named data
    data = {
        "engine_id": engine,
        "index_options": index_options,
        "index_name": index_name,
    }

    # Create an index
    response = requests.post(INDEXES_URL, headers=default_header, json=data)

    # Store the unique identifier of your index
    INDEX_ID = response.json().get('_id')

    # Check if the status code is 201 and print success
    if response.status_code == 201:
        print(f"Status code: {response.status_code} - The request was successful and a new index was created.")
    else:
        print(f"Status code: {response.status_code}")
    pprint(response.json())
    return INDEX_ID


# Create the indexes
index_id_content_classification = create_index(index_name = "insta+tiktok", index_options=["visual", "conversation", "text_in_video", "logo"], engine = "marengo2.5")

# Print the created index IDs
print(f"Created index IDs: {index_id_content_classification}")
</code></pre>
<pre><code class="bash">Status code: 201 - The request was successful and a new index was created.
{'_id': '64544b858b1dd6cde172af77'}
Created index IDs: 64544b858b1dd6cde172af77
</code></pre>

アップロード関数の記述

今回は、指定されたフォルダーからすべての動画を自動的に取得し、動画ファイルと同じ名前を割り当て、プラットフォームにアップロードするコードを作成しました。これは、Pythonライブラリを使用して同期的に実行されます。インデックスを作成したいすべての動画を1つのフォルダーに入れるだけで準備完了です。インデックス作成プロセス全体には、最も長い動画の長さの約40%の時間がかかります。後で同じインデックスにさらに動画を追加したい場合も簡単です。新しいフォルダーは必要ありません。既存のフォルダーに放り込むだけです。コードが機能して、プロセスを開始する前に、同じ名前のインデックス付き動画や保留中のインデックス作成タスクがないかチェックします。これにより、面倒な重複を避けることができます。非常に便利ですよね? 😄

<pre><code class="python">import os
import requests
from concurrent.futures import ThreadPoolExecutor

TASKS_URL = f"{API_URL}/tasks"
TASK_ID_LIST = []
video_folder = 'classify'  # folder containing the video files
INDEX_ID = '64544b858b1dd6cde172af77'

def upload_video(file_name):
    # Validate if a video already exists in the index
    task_list_response = requests.get(
        TASKS_URL,
        headers=default_header,
        params={"index_id": INDEX_ID, "filename": file_name},
    )
    if "data" in task_list_response.json():
        task_list = task_list_response.json()["data"]
        if len(task_list) > 0:
            if task_list[0]['status'] == 'ready': 
                print(f"Video '{file_name}' already exists in index {INDEX_ID}")
            else:
                print("task pending or validating")
            return

    # Proceed further to create a new task to index the current video if the video didn't exist in the index already
    print("Entering task creation code for the file: ", file_name)
    
    if file_name.endswith('.mp4'):  # Make sure the file is an MP4 video
        file_path = os.path.join(video_folder, file_name)  # Get the full path of the video file
        with open(file_path, "rb") as file_stream:
            data = {
                "index_id": INDEX_ID,
                "language": "en"
            }
            file_param = [
                ("video_file", (file_name, file_stream, "application/octet-stream")),] #The video will be indexed on the platform using the same name as the video file itself.
            response = requests.post(TASKS_URL, headers=default_header, data=data, files=file_param)
            TASK_ID = response.json().get("_id")
            TASK_ID_LIST.append(TASK_ID)
            # Check if the status code is 201 and print success
            if response.status_code == 201:
                print(f"Status code: {response.status_code} - The request was successful and a new resource was created.")
            else:
                print(f"Status code: {response.status_code}")
            print(f"File name: {file_name}")
            pprint(response.json())
            print("\n")

# Get list of video files
video_files = [f for f in os.listdir(video_folder) if f.endswith('.mp4')]

# Create a ThreadPoolExecutor
with ThreadPoolExecutor() as executor:
    # Use executor to run upload_video in parallel for all video files
    executor.map(upload_video, video_files)
</code></pre>

インデックス作成プロセスの監視

アップロード関数と同様に、同時に発生するすべてのタスクを追跡する監視関数を設計しました。同時にインデックス化されている各動画の推定残り時間とアップロードのパーセンテージを、整理されたCSVファイルに忠実に記録します。この気の利いた関数は、フォルダー内のすべての動画のインデックス作成が完了するまで実行され続けます。最後に、同期インデックス作成プロセスにかかった合計時間を秒単位でわかりやすく表示します。素晴らしい効率性だと思いませんか?

<pre><code class="python">import time
import csv
from concurrent.futures import ThreadPoolExecutor, as_completed

def monitor_upload_status(task_id):
    TASK_STATUS_URL = f"{API_URL}/tasks/{task_id}"
    while True:
        response = requests.get(TASK_STATUS_URL, headers=default_header)
        STATUS = response.json().get("status")
        if STATUS == "ready":
            return task_id, STATUS
        remain_seconds = response.json().get('process', {}).get('remain_seconds', None)
        upload_percentage = response.json().get('process', {}).get('upload_percentage', None)
        if remain_seconds is not None:
            print(f"Task ID: {task_id}, Remaining seconds: {remain_seconds}, Upload Percentage: {upload_percentage}")
        else:
             print(f"Task ID: {task_id}, Status: {STATUS}")
        time.sleep(10)

# Define starting time
start = time.time()
print("Starting to monitor...")

# Monitor the indexing process for all tasks
with ThreadPoolExecutor() as executor:
    futures = {executor.submit(monitor_upload_status, task_id) for task_id in TASK_ID_LIST}
    with open('upload_status.csv', 'w', newline='') as file:
        writer = csv.writer(file)
        writer.writerow(["Task ID", "Status"])
        for future in as_completed(futures):
            task_id, status = future.result()
            writer.writerow([task_id, status])

# Define ending time
end = time.time()
print("Monitoring finished")
print("Time elapsed (in seconds): ", end - start)
</code></pre>

出力:

<pre><code class="language-plaintext">Starting to monitor...
Monitoring finished
Time elapsed (in seconds):  253.00311
</code></pre>

インデックス内のすべての動画のリスト:

必要な動画がすべてインデックス化されていることを確認するために、インデックス内のすべての動画をリストして徹底的なダブルチェックを行いましょう。さらに、すべての動画IDとそれに対応する名前を含む便利なリストを作成します。このリストは、後で分類APIから返される動画クリップ(分類基準に一致するセグメント)の適切な動画名を取得する必要があるときに役立ちます。

なお、インデックス化された動画が11個あるため、ページ制限を20に変更しました。デフォルトでは、APIは1ページにつき10個の結果を返すため、制限を更新しないと、1つの結果がページ2に隠れてしまい、video_id_name_listの作成に使用するresponse_jsonに含まれなくなってしまいます。そのため、すべてを1ページにまとめておきましょう!

<pre><code class="python"># List all the videos in an index
INDEX_ID='64544b858b1dd6cde172af77'
default_header = {
    "x-api-key": API_KEY
}
# INDEX_ID='64502d238b1dd6cde172a9c5' #movies
# INDEX_ID= '64399bc25b65d57eaecafb35' #lex
INDEXES_VIDEOS_URL = f"{API_URL}/indexes/{INDEX_ID}/videos?page_limit=20"
response = requests.get(INDEXES_VIDEOS_URL, headers=default_header)

response_json = response.json()
pprint(response_json)

video_id_name_list = [{'video_id': video['_id'], 'video_name': video['metadata']['filename']} for video in response_json['data']]

pprint(video_id_name_list)
</code></pre>

出力:

<pre><code class="python">{'data': [{'_id': '64544bb486daab572f3494a0',
           'created_at': '2023-05-05T00:19:33Z',
           'metadata': {'duration': 507.5,
                        'engine_id': 'marengo2.5',
                        'filename': 'JetTila.mp4',
                        'fps': 30,
                        'height': 720,
                        'size': 44891944,
                        'width': 1280},
           'updated_at': '2023-05-05T00:20:04Z'},
          {'_id': '64544bad86daab572f34949f',
           'created_at': '2023-05-05T00:19:32Z',
           'metadata': {'duration': 516.682833,
                        'engine_id': 'marengo2.5',
                        'filename': 'Kylie.mp4',
                        'fps': 23.976023976023978,
                        'height': 720,
                        'size': 37594080,
                        'width': 1280},
           'updated_at': '2023-05-05T00:19:57Z'},
          {'_id': '64544b9286daab572f34949e',
           'created_at': '2023-05-05T00:19:27Z',
           'metadata': {'duration': 34.333333,
                        'engine_id': 'marengo2.5',
                        'filename': 'Oh_my.mp4',
                        'fps': 30,
                        'height': 1280,
                        'size': 10480126,
                        'width': 720},
           'updated_at': '2023-05-05T00:19:30Z'},
          .
					.
					.
					{'_id': '64544b8786daab572f349496',
           'created_at': '2023-05-05T00:19:18Z',
           'metadata': {'duration': 14.333333,
                        'engine_id': 'marengo2.5',
                        'filename': 'cats.mp4',
                        'fps': 30,
                        'height': 1280,
                        'size': 1304438,
                        'width': 720},
           'updated_at': '2023-05-05T00:19:19Z'}],
 'page_info': {'limit_per_page': 20,
               'page': 1,
               'total_duration': 1363.925599,
               'total_page': 1,
               'total_results': 11}}
[{'video_id': '64544bb486daab572f3494a0', 'video_name': 'JetTila.mp4'},
 {'video_id': '64544bad86daab572f34949f', 'video_name': 'Kylie.mp4'},
 {'video_id': '64544b9286daab572f34949e', 'video_name': 'Oh_my.mp4'},
 {'video_id': '64544b8e86daab572f34949c', 'video_name': 'Pitbull.mp4'},
 {'video_id': '64544b9286daab572f34949d', 'video_name': 'She.mp4'},
 {'video_id': '64544b8d86daab572f34949b', 'video_name': 'fun.mp4'},
 {'video_id': '64544b8986daab572f349497', 'video_name': 'Dance.mp4'},
 {'video_id': '64544b8986daab572f349498', 'video_name': 'Jennie.mp4'},
 {'video_id': '64544b8a86daab572f349499', 'video_name': 'McDonald.mp4'},
 {'video_id': '64544b8c86daab572f34949a', 'video_name': 'Orangutan.mp4'},
 {'video_id': '64544b8786daab572f349496', 'video_name': 'cats.mp4'}]
 </code></pre>

動画の分類

コードに入る前に、開発の背景にある理論を簡単に確認しておきましょう。コードのみに興味がある場合は、読み飛ばしていただいて構いません。分類に関して、以下のパラメーターを使用して動作を制御できます。

  • classes:プラットフォームが識別する必要があるエンティティまたはアクションの名前と定義を概説するオブジェクトの配列。各オブジェクトには以下のフィールドが含まれます:

  • name:このクラスに割り当てたい名前を表す文字列。

  • prompts:クラスに含まれる内容を説明する文字列の配列。プラットフォームは、動画を分類するためにここで提供する値に従います。

  • threshold(しきい値):リクエストで説明したプロンプトに沿った信頼度レベルに基づいて、結果を微調整するためにしきい値パラメーターを使用します。範囲は0〜100で、設定しない場合はデフォルト値の75が適用されます。このパラメーターを使用して最も関連性の高い結果のみを取得することで、返されるレスポンスを絞り込むことができます。

分類基準を設定し、Twelve Labsの分類APIを使用して分類リクエストを行いましょう。今回のデモではデフォルトのしきい値設定を使用します:

<pre><code class="python"># Construct the URL of the `/classify/bulk` endpoint
CLASSIFY_BULK_URL = f"{API_URL}/classify/bulk"

# Set the header of the request
headers = {
    "x-api-key": API_KEY
}

# Declare a dictionary named `data`
data =  {
  "conversation_option": "semantic",
  "options": ["visual", "conversation"],
  "index_id" : INDEX_ID,
  "include_clips": False,
  "classes": [
      {
            "name": "BeautyTok",
            "prompts": [
                "Makeup",
                "Skincare",
                "cosmetic products",
                "doing nails",
                "doing hair",
                "DanceTok",
                "dance tutorial",
                "dance competition",
                "dance challenge",
                "dance trend",
                "dancing with friends"
            ]
        },
        {
            "name": "CookTok",
            "prompts": [
                "cooking tutorial",
                "cooking utensils",
                "baking tutorials",
                "recipes",
                "restaurants",
                "food",
                "pasta"
            ]
        },
        {
            "name": "AnimalTok",
            "prompts": [
                "dog",
                "cat",
                "birds",
                "fish",
                "playing with pets",
                "pets doing funny things"
            ]
        },
        {
            "name": "ArtTok",
            "prompts": [
                "handicraft",
                "drawing",
                "graffiti",
                "sketching",
                "digital art",
                "coloring",
                "sketchbook",
                "artwork",
                "artists"
            ]
        }
  ]
}

# Make a classification request
response = requests.post(CLASSIFY_BULK_URL, headers=headers, json=data)
print (f'Status code: {response.status_code}')
pprint(response.json())
duration_data = response.json()
</code></pre>

クラスに含まれるプロンプトに基づいて、動画全体にクラスラベルが割り当てられます。適切な動画セグメント(プロンプトに関連するクリップ)をピンポイントで特定し、正確な動画ラベル付けを実現するには、関連する多数のプロンプトを提供することが重要です。クラスラベルに一致する動画の長さが動画の合計の長さの50%を超えた場合にのみ、そのクラスラベルが割り当てられます。この全体の長さは、プロンプトに一致する動画クリップを結合することによって確定されます。

実行した分類APIコールの結果は以下の通りです。「duration ratio」は動画全体の長さに対する動画セグメントの割合を表し、「score」はモデルの信頼度を示し、「name」はクラスラベルを指します。一致するすべての動画が信頼度スコアの降順で表示されます:

<pre><code class="python">Status code: 200
{'data': [{'classes': [{'duration_ratio': 1,
                        'name': 'AnimalTok',
                        'score': 95.35111111111111}],
           'video_id': '64544b8786daab572f349496'},
          {'classes': [{'duration_ratio': 1,
                        'name': 'AnimalTok',
                        'score': 95.14666666666668}],
           'video_id': '64544b8e86daab572f34949c'},
          .
			.
			.
          {'classes': [{'duration_ratio': 0.8175611166393244,
                        'name': 'ArtTok',
                        'score': 89.45777777777778}],
           'video_id': '64544b9286daab572f34949d'}],
 'page_info': {'limit_per_page': 10,
               'next_page_token': '',
               'page_expired_at': '2023-05-05T17:37:30Z',
               'prev_page_token': '',
               'total_results': 10}}
 </code></pre>

それでは、同じコードを書き直して分類APIをコールしてみましょう。しかし今回は1点だけ異なる仕様を実装します、include_clipsをTrueに設定します。これにより、クラス内で提供されたプロンプトに一致するすべての関連動画クリップと、それぞれのメタデータを取得します:

<pre><code class="python">
# Construct the URL of the `/classify/bulk` endpoint
CLASSIFY_BULK_URL = f"{API_URL}/classify/bulk"

# Set the header of the request
headers = {
    "x-api-key": API_KEY
}

# Declare a dictionary named `data`
data =  {
  "conversation_option": "semantic",
  "options": ["visual", "conversation"],
  "index_id" : INDEX_ID,
  "include_clips": True,
  "classes": [
      {
            "name": "BeautyTok",
            "prompts": [
                "Makeup",
                "Skincare",
                "cosmetic products",
                "doing nails",
                "doing hair",
                "DanceTok",
                "dance tutorial",
                "dance competition",
                "dance challenge",
                "dance trend",
                "dancing with friends"
            ]
        },
        {
            "name": "CookTok",
            "prompts": [
                "cooking tutorial",
                "cooking utensils",
                "baking tutorials",
                "recipes",
                "restaurants",
                "food",
                "pasta"
            ]
        },
        {
            "name": "AnimalTok",
            "prompts": [
                "dog",
                "cat",
                "birds",
                "fish",
                "playing with pets",
                "pets doing funny things"
            ]
        },
        {
            "name": "ArtTok",
            "prompts": [
                "handicraft",
                "drawing",
                "graffiti",
                "sketching",
                "digital art",
                "coloring",
                "sketchbook",
                "artwork",
                "artists"
            ]
        }
  ]
}

# Make a classification request
response = requests.post(CLASSIFY_BULK_URL, headers=headers, json=data)
print (f'Status code: {response.status_code}')
pprint(response.json())
duration_data = response.json()
</code></pre>

出力:

<pre><code class="python">{'data': [{'classes': [{'clips': [{'end': 14,
                                   'option': '',
                                   'prompt': 'cat',
                                   'score': 84.77,
                                   'start': 0},
                                  {'end': 14,
                                   'option': '',
                                   'prompt': 'pets doing funny things',
                                   'score': 83.56,
                                   'start': 8.34375},
                                  {'end': 14,
                                   'option': '',
                                   'prompt': 'playing with pets',
                                   'score': 68.11,
                                   'start': 8.34375},
                                  {'end': 8.34375,
                                   'option': '',
                                   'prompt': 'pets doing funny things',
                                   'score': 58.26,
                                   'start': 0}],
                        'duration_ratio': 1,
                        'name': 'AnimalTok',
                        'score': 95.35111111111111}],
			.
			.
			.	
           'video_id': '64544b8786daab572f349496'},
          {'classes': [{'clips': [{'end': 49.189,
                                   'option': '',
                                   'prompt': 'artists',
                                   'score': 78.14,
                                   'start': 0.55},
                                  {'end': 23.659,
                                   'option': '',
                                   'prompt': 'sketching',
                                   'score': 58.85,
                                   'start': 0.55}],
                        'duration_ratio': 0.8175611166393244,
                        'name': 'ArtTok',
                        'score': 89.45777777777778}],
			.
			.
			.
			'video_id': '64544b9286daab572f34949d'}],
 'page_info': {'limit_per_page': 10,
               'next_page_token': '',
               'page_expired_at': '2023-05-05T17:37:30Z',
               'prev_page_token': '',
               'total_results': 10}}
               </code></pre>

分かりやすくするために、出力をトリミングしています。出力に、開始タイムスタンプと終了タイムスタンプ、特定のクリップの信頼度スコア、およびそれに関連するプロンプトを含む、各動画のクリップデータが表示されるようになったことに注目してください。各プロンプトに関連付けられた分類オプション(例:visualとconversation。visualは音声と視覚の一致、conversationは対話の一致を表します)を統合するために、APIエンドポイントの改良を進めています。

それでは、JSONの結果と、前に作成したvideo_id_name_listをシリアル化(pickle)してファイルに保存しましょう:

<pre><code class="python">import pickle

with open('video_id_name_list.pickle', 'wb') as handle:
    pickle.dump(video_id_name_list, handle, protocol=pickle.HIGHEST_PROTOCOL)

with open('duration_data.pickle', 'wb') as handle:
    pickle.dump(duration_data, handle, protocol=pickle.HIGHEST_PROTOCOL)

with open('clips_data.pickle', 'wb') as handle:
    pickle.dump(clips_data, handle, protocol=pickle.HIGHEST_PROTOCOL)
    </code></pre>

デモアプリの構築

以前のチュートリアルと同様に、Webページをホストし、シリアル化されたデータを利用するFlaskベースのデモアプリを作成します。このデータをローカルドライブから取得した動画に適用することで、見栄えの良い分類結果Webページを作成します。これにより、ビデオ分類APIがアプリケーションをどのように強化し、印象的な結果をもたらすかを直接体験できます。

ディレクトリ構造は以下のようになります:

<pre><code class="markdown">my_flask_app/
│   app.py
│   sample_notebook.ipynb
└───templates/
│	│   index.html
└───classify/
	│   <your_video_1.mp4><your_video_2.mp4><your_video_3.mp4>
			.
			.
			.
</code></pre>

Flaskアプリのコード

このチュートリアルでは、ローカルディレクトリから動画ファイルを提供し、HTML5ビデオプレーヤーを使用して特定のセグメントを再生する方法を少し工夫しています。Flaskアプリケーションで使用されるserve_video関数は、Flaskアプリケーションスクリプトと同じディレクトリにあるclassify_tryディレクトリからビデオファイルを提供します。HTMLテンプレートの url_for('serve_video', filename=video_mapping[video.video_id]) 式は、提供されたビデオファイルのURLを生成します。

「include_clips」をTrueに設定したときの分類APIの出力からお気づきかもしれませんが、APIは多数のクリップとそのメタデータを返しました。シンプルにするため、またこれらのクリップを含む結果を示すために、get_top_clips関数を含めました。この関数は3つの固有のプロンプトを見つけ、それらに関連付けられたすべてのクリップのメタデータを返すため、分類結果をより包括的に見ることができます。

以下は、「app.py」 ファイルのコードです:

<pre><code class="python">from flask import Flask, render_template, send_from_directory
import pickle
import os
from collections import defaultdict

app = Flask(__name__)

# Replace the following dictionaries with your data
with open('video_id_name_list.pickle', 'rb') as handle:
    video_id_name_list = pickle.load(handle)

with open('duration_data.pickle', 'rb') as handle:
    duration_data = pickle.load(handle)

with open('clips_data.pickle', 'rb') as handle:
    clips_data = pickle.load(handle)

VIDEO_DIRECTORY = os.path.join(os.path.dirname(os.path.realpath(__file__)), "classify")

@app.route('/<path:filename>')
def serve_video(filename):
    print(VIDEO_DIRECTORY, filename)
    return send_from_directory(directory=VIDEO_DIRECTORY, path=filename)

def get_top_clips(clips_data, num_clips=3):
    top_clips = defaultdict(list)
    for video in clips_data['data']:
        video_id = video['video_id']
        unique_prompts = set()
        for clip_class in video['classes']:
            for clip in clip_class['clips']:
                if clip['prompt'] not in unique_prompts and len(unique_prompts) < num_clips:
                    top_clips[video_id].append(clip)
                    unique_prompts.add(clip['prompt'])
    return top_clips

@app.route('/')
def home():
    video_id_name_dict = {video['video_id']: video['video_name'] for video in video_id_name_list}
    top_clips = get_top_clips(clips_data)
    return render_template('index.html', video_mapping=video_id_name_dict, duration_data=duration_data, top_clips=top_clips)


if __name__ == '__main__':
    app.run(debug=True)
</code></pre>

HTMLテンプレート

以下はJinja2ベースのHTMLテンプレートのサンプルです。HTMLファイル内にコードを組み込み、準備して渡したシリアル化データのフィールドを使用して反復処理します。このテンプレートは、ローカルドライブから必要な動画を取得し、分類基準に応じて結果を表示します:

<pre><code class="language-html"><!doctype html>
<html lang="en">
<head>
    <link rel="shortcut icon" href="#" />
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Classification Output</title>
    <style>
        body {
            background-color: #f0e7cc;
            font-family: Arial, sans-serif;
        }
        .container {
            width: 80%;
            margin: 0 auto;
            padding-bottom: 30px;
        }
        video {
            display: block;
            margin: 0 auto;
        }
        .progress-bar {
            width: 20%;
            height: 20px;
            background-color: #f0f0f0;
            margin-bottom: 10px;
            display: inline-block;
            vertical-align: middle;
            margin-left: 10px;
        }
        .progress {
            height: 100%;
            background-color: #4caf50;
        }
        .score {
            display: inline-block;
            vertical-align: middle;
            font-weight: bold;
        }
        .video-category {
            padding-bottom: 30px;
            margin-bottom: 20px;
        }
        button {
            background-color: #008CBA;
            border: none;
            color: white;
            padding: 8px 16px;
            text-align: center;
            text-decoration: none;
            display: inline-block;
            font-size: 14px;
            margin: 4px 2px;
            cursor: pointer;
            border-radius: 4px;
            box-shadow: 0 1px 2px rgba(0,0,0,0.2);
            transition: background-color 0.2s ease;
        }
        button:hover {
            background-color: #006494;
        }
        h1 {
            color: #070707;
            text-align: center;
            background-color: #cb6a16; /* Change the background color to your desired color */
            padding: 10px;
            margin-bottom: 20px;  
        }
        .clips-container {
            text-align: center;
        }

    </style>
    <script>
        function playClip(videoPlayer, start, end) {
            videoPlayer.currentTime = start;
            videoPlayer.play();
            videoPlayer.ontimeupdate = function () {
                if (videoPlayer.currentTime >= end) {
                    videoPlayer.pause();
                }
            };
        }
    </script>
</head>
<body>
    <div class="container">
        <h1>Video Classification</h1>
        {% for video in duration_data.data %}
            {% set video_id = video.video_id %}
            {% set video_path = video_mapping[video_id] %}
            <div class="video-category">
                <h2>{{ video.classes[0].name }}</h2>
                <p>Video file: {{video_path}}</p>
                <div>
                    <span class="score">Score: {{ '%.2f'|format(video.classes[0].score) }}</span>
                    <div class="progress-bar">
                        <div class="progress" style="width: {{ video.classes[0].score }}%;"></div>
                    </div>
                </div>
                <div>
                    <span class="score">Duration: {{ '%.2f%%'|format(video.classes[0].duration_ratio * 100) }}</span>
                    <div class="progress-bar">
                        <div class="progress" style="width: {{ video.classes[0].duration_ratio * 100 }}%;"></div>
                    </div>
                </div>
                <video id="video-{{ video_id }}" width="480" height="320" controls>
                    <source src="{{ url_for('serve_video', filename=video_path) }}" type="video/mp4">
                    Your browser does not support the video tag.
                </video>
                <div class="clips-container">
                    <h3>Sample Clips</h3>
                    {% for clip in top_clips[video_id] %}
                    <button onclick="playClip(document.getElementById('video-{{ video_id }}'), {{ clip.start }}, {{ clip.end }})">Clip: {{ clip.prompt }} | Start {{ '%.2f'|format(clip.start) }}s - End {{ '%.2f'|format(clip.end) }}s - Score {{ '%.2f'|format(clip.score) }}</button>
                    {% endfor %}
            </div>
            <br /><br />
        {% endfor %}
    </div>
</body>
</html>
</code></pre>

Flaskアプリの起動

素晴らしいですね!それでは、Jupyter Notebookの最後のセルを実行してFlaskアプリを起動しましょう:

<pre><code class="python">%run app.py
</code></pre>

すべてが想定通りに進んだことを確認する、以下のような出力が表示されるはずです 😊:

URLリンク http://127.0.0.1:5000 をクリックすると、以下のような画面が表示されます:

このチュートリアルで作成した完全なコードが含まれるJupyter Notebookはこちらからご確認いただけます - https://tinyurl.com/classifyNotebook

さらに高度な構築に挑戦してみませんか?

以下のようなアイデアをぜひ試してみてください:

  1. 3つの個別のプロンプトを選択する代わりに、ページネーションと遅延読み込みを実装して、すべてのクリップを表示します。これにより、分類されたクリップのより広い範囲を探索し、ビデオ分類結果のより包括的な視覚を得ることができます。

  2. ショートテール、ミディアムテール、ロングテールのプロンプトを実験し、しきい値を微調整して、Discordチャネルで他のマルチモーダル愛好家と分析結果を共有しましょう。

結び

これからの展開にもぜひご期待ください!まだ参加されていない方は、マルチモーダルAIに情熱を持つ同じ志を持った仲間とつながることができる、活気あるDiscordコミュニティにぜひご参加ください。

ビデオ分類とは、動画のコンテンツに基づいて、1つ以上の定義済みのカテゴリまたはラベルを自動的に割り当てるプロセスを指します。このタスクには、動画に存在するイベント、アクション、オブジェクト、またはその他の特徴を認識して理解するために、動画の視覚的情報(場合によっては音声情報も)を分析することが含まれます。ビデオ分類は、コンピュータビジョンにおける重要な研究領域であり、ビデオインデックス作成、コンテンツベースのビデオ検索、ビデオ推薦、ビデオ監視、人間の行動認識など、数多くの実用的なアプリケーションがあります。

かつて、ビデオ分類は定義済みのカテゴリやラベルに限定されており、イベント、アクション、オブジェクト、その他の特徴の特定に焦点を当てていました。モデルを再トレーニングしたり基準を更新したりすることなく、分類基準をカスタマイズすることは、遠い夢のように思われました。しかし、ここでTwelve Labs分類APIが登場し、トレーニング不要で、ほぼリアルタイムで、カスタム基準に基づいて動画を簡単かつ強力に分類できるようにすることで、その難題を解決します。まさにゲームチェンジャーと言えるでしょう!

Twelve Labs分類API - コンセプトの概要

Twelve Labs分類APIは、各動画内でクラスラベルが占める時間に基づいて、インデックス化された動画にラベルを付けるように設計されています。その時間が50%未満の場合、クラスラベルは適用されません。そのため、特に大きな動画をアップロードする場合は、クラスとそのプロンプトを慎重に設計することが重要です。APIサービスは任意の数のクラスに対応できるため、クラス内に必要な数だけプロンプトを追加できます。

たとえば、あなたの愛犬ブルーノと愛猫カーラがさまざまなイタズラをしている面白い動画のコレクションがあるとします。これらの動画をTwelve Labsのプラットフォームにアップロードし、毛むくじゃらの友達の愉快な冒険に合わせてカスタマイズした独自の分類基準を作成できます。

<pre><code class="json">"classes": [
      {
            "name": "Doge_Bruno",
            "prompts": [
                "playing with my dog",
                "my dog doing funny things",
                "dog playing with water"
            ]
        },
        {
            "name": "Kitty_Karla",
            "prompts": [
                "cat jumping",
                "cat playing with toys"
            ]
        }
  ]
  </code></pre>

たった1回のAPIコールで、作成した基準を使用してアップロードした動画を分類できます。プロンプトをいくつか忘れてしまったり、新しいクラスを導入したくなったりした場合は、JSONにクラスとプロンプトを追加するだけで簡単に対応できます。新しいモデルをトレーニングしたり、既存のモデルを再トレーニングしたりする必要はないため、プロセス全体が非常にスムーズです。

分類結果

クイック概要

前提条件:このチュートリアルをスムーズに進めるために、Twelve Labs APIスイートへの登録を済ませ、必要なパッケージをインストールしてください。基本に慣れるために、最初2番目のチュートリアルを読んでおくことをお勧めします 🤓。

動画のアップロード:Twelve Labsプラットフォームに動画を送信すると、動画が自動的にインデックス化され、カスタムの分類基準を追加してコンテンツをその場で管理できるようになります。うれしいことに、機械学習(ML)モデルをトレーニングする必要すらありません 😆😁😊。

動画の分類:いよいよ本番です!独自のカスタムクラスと、各クラス内のさまざまなプロンプトを作成します。基準を定義したら、構築したシステムをすぐに使って結果を取得できます。遅延なく、すぐに欲しい情報が手に入ります! 🍿✌️🥳

デモアプリの構築: Flaskベースのアプリを作成して、分類APIからの結果を活用し、コンピューターのローカルフォルダーに保存されている動画にアクセスします。そして、カスタムデザインされたスマートなHTMLページをレンダリングして、分類結果をスタイリッシュに表示します 🔍💻🎨。👨‍🎨

前提条件

最初のチュートリアルでは、シンプルな自然言語クエリを使用して動画内の特定の瞬間を見つける基本について説明しました。わかりやすくするために、プラットフォームには動画を1つだけアップロードし、インデックスの作成、インデックスの構成、タスクAPIの定義、動画インデックス作成タスクの基本的な監視、Flaskベースのデモアプリの作成手順などの重要な概念について説明しました。

2番目のチュートリアルではさらに一歩進め、複数の検索クエリを組み合わせて、より正確でターゲットを絞った検索を作成する方法を検討しました。複数の動画を非同期でアップロードし、複数のインデックスを作成し、動画インデックス作成タスクを監視してタスク完了の推定時間などの詳細を取得するためのコードを追加実装しました。また、複数の動画に対応し、HTMLテンプレートを使用して表示するようにFlaskアプリを構成しました。

この流れを引き継ぎ、今回のチュートリアルでは、Pythonに組み込まれているconcurrent.futuresライブラリを使用した同期動画アップロードについて説明します。動画のインデックス作成ステータスを監視し、それをCSVファイルに記録します。さらに、入力した分類基準と関連する分類APIレスポンスのフィールドをHTMLテンプレートに表示させて、結果を解釈しやすくします。

このチュートリアルや以前のチュートリアルを読んでいる中で、何か問題が発生した場合は、遠慮なくサポートまでご連絡ください。私たちはDiscordサーバーを通じて、快速急行よりも速いレスポンスタイムで迅速なサポートを提供していることを誇りに思っています 🚅🏎️⚡️。または、メールでのご連絡も可能です。Twelve Labsは現在オープンベータ版であるため、Twelve Labsアカウントを作成してAPIダッシュボードにアクセスし、APIキーを取得できます。無料クレジットを使用すれば、最大10時間の動画コンテンツを分類できます。

<pre><code class="python">%env API_KEY=<your_API_key>
%env API_URL=https://api.twelvelabs.io/v1.1

!pip install requests
!pip install flask

import os
import requests
import glob
from pprint import pprint


#Retrieve the URL of the API and the API key
API_URL = os.getenv("API_URL")
assert API_URL

API_KEY = os.getenv("API_KEY")
assert API_KEY
</code></pre>

動画のアップロード

インデックスを作成し、動画アップロード用に構成します:

<pre><code class="python"># Construct the URL of the `/indexes` endpoint
INDEXES_URL = f"{API_URL}/indexes"

# Set the header of the request
default_header = {
    "x-api-key": API_KEY
}

# Define a function to create an index with a given name
def create_index(index_name, index_options, engine):
    # Declare a dictionary named data
    data = {
        "engine_id": engine,
        "index_options": index_options,
        "index_name": index_name,
    }

    # Create an index
    response = requests.post(INDEXES_URL, headers=default_header, json=data)

    # Store the unique identifier of your index
    INDEX_ID = response.json().get('_id')

    # Check if the status code is 201 and print success
    if response.status_code == 201:
        print(f"Status code: {response.status_code} - The request was successful and a new index was created.")
    else:
        print(f"Status code: {response.status_code}")
    pprint(response.json())
    return INDEX_ID


# Create the indexes
index_id_content_classification = create_index(index_name = "insta+tiktok", index_options=["visual", "conversation", "text_in_video", "logo"], engine = "marengo2.5")

# Print the created index IDs
print(f"Created index IDs: {index_id_content_classification}")
</code></pre>
<pre><code class="bash">Status code: 201 - The request was successful and a new index was created.
{'_id': '64544b858b1dd6cde172af77'}
Created index IDs: 64544b858b1dd6cde172af77
</code></pre>

アップロード関数の記述

今回は、指定されたフォルダーからすべての動画を自動的に取得し、動画ファイルと同じ名前を割り当て、プラットフォームにアップロードするコードを作成しました。これは、Pythonライブラリを使用して同期的に実行されます。インデックスを作成したいすべての動画を1つのフォルダーに入れるだけで準備完了です。インデックス作成プロセス全体には、最も長い動画の長さの約40%の時間がかかります。後で同じインデックスにさらに動画を追加したい場合も簡単です。新しいフォルダーは必要ありません。既存のフォルダーに放り込むだけです。コードが機能して、プロセスを開始する前に、同じ名前のインデックス付き動画や保留中のインデックス作成タスクがないかチェックします。これにより、面倒な重複を避けることができます。非常に便利ですよね? 😄

<pre><code class="python">import os
import requests
from concurrent.futures import ThreadPoolExecutor

TASKS_URL = f"{API_URL}/tasks"
TASK_ID_LIST = []
video_folder = 'classify'  # folder containing the video files
INDEX_ID = '64544b858b1dd6cde172af77'

def upload_video(file_name):
    # Validate if a video already exists in the index
    task_list_response = requests.get(
        TASKS_URL,
        headers=default_header,
        params={"index_id": INDEX_ID, "filename": file_name},
    )
    if "data" in task_list_response.json():
        task_list = task_list_response.json()["data"]
        if len(task_list) > 0:
            if task_list[0]['status'] == 'ready': 
                print(f"Video '{file_name}' already exists in index {INDEX_ID}")
            else:
                print("task pending or validating")
            return

    # Proceed further to create a new task to index the current video if the video didn't exist in the index already
    print("Entering task creation code for the file: ", file_name)
    
    if file_name.endswith('.mp4'):  # Make sure the file is an MP4 video
        file_path = os.path.join(video_folder, file_name)  # Get the full path of the video file
        with open(file_path, "rb") as file_stream:
            data = {
                "index_id": INDEX_ID,
                "language": "en"
            }
            file_param = [
                ("video_file", (file_name, file_stream, "application/octet-stream")),] #The video will be indexed on the platform using the same name as the video file itself.
            response = requests.post(TASKS_URL, headers=default_header, data=data, files=file_param)
            TASK_ID = response.json().get("_id")
            TASK_ID_LIST.append(TASK_ID)
            # Check if the status code is 201 and print success
            if response.status_code == 201:
                print(f"Status code: {response.status_code} - The request was successful and a new resource was created.")
            else:
                print(f"Status code: {response.status_code}")
            print(f"File name: {file_name}")
            pprint(response.json())
            print("\n")

# Get list of video files
video_files = [f for f in os.listdir(video_folder) if f.endswith('.mp4')]

# Create a ThreadPoolExecutor
with ThreadPoolExecutor() as executor:
    # Use executor to run upload_video in parallel for all video files
    executor.map(upload_video, video_files)
</code></pre>

インデックス作成プロセスの監視

アップロード関数と同様に、同時に発生するすべてのタスクを追跡する監視関数を設計しました。同時にインデックス化されている各動画の推定残り時間とアップロードのパーセンテージを、整理されたCSVファイルに忠実に記録します。この気の利いた関数は、フォルダー内のすべての動画のインデックス作成が完了するまで実行され続けます。最後に、同期インデックス作成プロセスにかかった合計時間を秒単位でわかりやすく表示します。素晴らしい効率性だと思いませんか?

<pre><code class="python">import time
import csv
from concurrent.futures import ThreadPoolExecutor, as_completed

def monitor_upload_status(task_id):
    TASK_STATUS_URL = f"{API_URL}/tasks/{task_id}"
    while True:
        response = requests.get(TASK_STATUS_URL, headers=default_header)
        STATUS = response.json().get("status")
        if STATUS == "ready":
            return task_id, STATUS
        remain_seconds = response.json().get('process', {}).get('remain_seconds', None)
        upload_percentage = response.json().get('process', {}).get('upload_percentage', None)
        if remain_seconds is not None:
            print(f"Task ID: {task_id}, Remaining seconds: {remain_seconds}, Upload Percentage: {upload_percentage}")
        else:
             print(f"Task ID: {task_id}, Status: {STATUS}")
        time.sleep(10)

# Define starting time
start = time.time()
print("Starting to monitor...")

# Monitor the indexing process for all tasks
with ThreadPoolExecutor() as executor:
    futures = {executor.submit(monitor_upload_status, task_id) for task_id in TASK_ID_LIST}
    with open('upload_status.csv', 'w', newline='') as file:
        writer = csv.writer(file)
        writer.writerow(["Task ID", "Status"])
        for future in as_completed(futures):
            task_id, status = future.result()
            writer.writerow([task_id, status])

# Define ending time
end = time.time()
print("Monitoring finished")
print("Time elapsed (in seconds): ", end - start)
</code></pre>

出力:

<pre><code class="language-plaintext">Starting to monitor...
Monitoring finished
Time elapsed (in seconds):  253.00311
</code></pre>

インデックス内のすべての動画のリスト:

必要な動画がすべてインデックス化されていることを確認するために、インデックス内のすべての動画をリストして徹底的なダブルチェックを行いましょう。さらに、すべての動画IDとそれに対応する名前を含む便利なリストを作成します。このリストは、後で分類APIから返される動画クリップ(分類基準に一致するセグメント)の適切な動画名を取得する必要があるときに役立ちます。

なお、インデックス化された動画が11個あるため、ページ制限を20に変更しました。デフォルトでは、APIは1ページにつき10個の結果を返すため、制限を更新しないと、1つの結果がページ2に隠れてしまい、video_id_name_listの作成に使用するresponse_jsonに含まれなくなってしまいます。そのため、すべてを1ページにまとめておきましょう!

<pre><code class="python"># List all the videos in an index
INDEX_ID='64544b858b1dd6cde172af77'
default_header = {
    "x-api-key": API_KEY
}
# INDEX_ID='64502d238b1dd6cde172a9c5' #movies
# INDEX_ID= '64399bc25b65d57eaecafb35' #lex
INDEXES_VIDEOS_URL = f"{API_URL}/indexes/{INDEX_ID}/videos?page_limit=20"
response = requests.get(INDEXES_VIDEOS_URL, headers=default_header)

response_json = response.json()
pprint(response_json)

video_id_name_list = [{'video_id': video['_id'], 'video_name': video['metadata']['filename']} for video in response_json['data']]

pprint(video_id_name_list)
</code></pre>

出力:

<pre><code class="python">{'data': [{'_id': '64544bb486daab572f3494a0',
           'created_at': '2023-05-05T00:19:33Z',
           'metadata': {'duration': 507.5,
                        'engine_id': 'marengo2.5',
                        'filename': 'JetTila.mp4',
                        'fps': 30,
                        'height': 720,
                        'size': 44891944,
                        'width': 1280},
           'updated_at': '2023-05-05T00:20:04Z'},
          {'_id': '64544bad86daab572f34949f',
           'created_at': '2023-05-05T00:19:32Z',
           'metadata': {'duration': 516.682833,
                        'engine_id': 'marengo2.5',
                        'filename': 'Kylie.mp4',
                        'fps': 23.976023976023978,
                        'height': 720,
                        'size': 37594080,
                        'width': 1280},
           'updated_at': '2023-05-05T00:19:57Z'},
          {'_id': '64544b9286daab572f34949e',
           'created_at': '2023-05-05T00:19:27Z',
           'metadata': {'duration': 34.333333,
                        'engine_id': 'marengo2.5',
                        'filename': 'Oh_my.mp4',
                        'fps': 30,
                        'height': 1280,
                        'size': 10480126,
                        'width': 720},
           'updated_at': '2023-05-05T00:19:30Z'},
          .
					.
					.
					{'_id': '64544b8786daab572f349496',
           'created_at': '2023-05-05T00:19:18Z',
           'metadata': {'duration': 14.333333,
                        'engine_id': 'marengo2.5',
                        'filename': 'cats.mp4',
                        'fps': 30,
                        'height': 1280,
                        'size': 1304438,
                        'width': 720},
           'updated_at': '2023-05-05T00:19:19Z'}],
 'page_info': {'limit_per_page': 20,
               'page': 1,
               'total_duration': 1363.925599,
               'total_page': 1,
               'total_results': 11}}
[{'video_id': '64544bb486daab572f3494a0', 'video_name': 'JetTila.mp4'},
 {'video_id': '64544bad86daab572f34949f', 'video_name': 'Kylie.mp4'},
 {'video_id': '64544b9286daab572f34949e', 'video_name': 'Oh_my.mp4'},
 {'video_id': '64544b8e86daab572f34949c', 'video_name': 'Pitbull.mp4'},
 {'video_id': '64544b9286daab572f34949d', 'video_name': 'She.mp4'},
 {'video_id': '64544b8d86daab572f34949b', 'video_name': 'fun.mp4'},
 {'video_id': '64544b8986daab572f349497', 'video_name': 'Dance.mp4'},
 {'video_id': '64544b8986daab572f349498', 'video_name': 'Jennie.mp4'},
 {'video_id': '64544b8a86daab572f349499', 'video_name': 'McDonald.mp4'},
 {'video_id': '64544b8c86daab572f34949a', 'video_name': 'Orangutan.mp4'},
 {'video_id': '64544b8786daab572f349496', 'video_name': 'cats.mp4'}]
 </code></pre>

動画の分類

コードに入る前に、開発の背景にある理論を簡単に確認しておきましょう。コードのみに興味がある場合は、読み飛ばしていただいて構いません。分類に関して、以下のパラメーターを使用して動作を制御できます。

  • classes:プラットフォームが識別する必要があるエンティティまたはアクションの名前と定義を概説するオブジェクトの配列。各オブジェクトには以下のフィールドが含まれます:

  • name:このクラスに割り当てたい名前を表す文字列。

  • prompts:クラスに含まれる内容を説明する文字列の配列。プラットフォームは、動画を分類するためにここで提供する値に従います。

  • threshold(しきい値):リクエストで説明したプロンプトに沿った信頼度レベルに基づいて、結果を微調整するためにしきい値パラメーターを使用します。範囲は0〜100で、設定しない場合はデフォルト値の75が適用されます。このパラメーターを使用して最も関連性の高い結果のみを取得することで、返されるレスポンスを絞り込むことができます。

分類基準を設定し、Twelve Labsの分類APIを使用して分類リクエストを行いましょう。今回のデモではデフォルトのしきい値設定を使用します:

<pre><code class="python"># Construct the URL of the `/classify/bulk` endpoint
CLASSIFY_BULK_URL = f"{API_URL}/classify/bulk"

# Set the header of the request
headers = {
    "x-api-key": API_KEY
}

# Declare a dictionary named `data`
data =  {
  "conversation_option": "semantic",
  "options": ["visual", "conversation"],
  "index_id" : INDEX_ID,
  "include_clips": False,
  "classes": [
      {
            "name": "BeautyTok",
            "prompts": [
                "Makeup",
                "Skincare",
                "cosmetic products",
                "doing nails",
                "doing hair",
                "DanceTok",
                "dance tutorial",
                "dance competition",
                "dance challenge",
                "dance trend",
                "dancing with friends"
            ]
        },
        {
            "name": "CookTok",
            "prompts": [
                "cooking tutorial",
                "cooking utensils",
                "baking tutorials",
                "recipes",
                "restaurants",
                "food",
                "pasta"
            ]
        },
        {
            "name": "AnimalTok",
            "prompts": [
                "dog",
                "cat",
                "birds",
                "fish",
                "playing with pets",
                "pets doing funny things"
            ]
        },
        {
            "name": "ArtTok",
            "prompts": [
                "handicraft",
                "drawing",
                "graffiti",
                "sketching",
                "digital art",
                "coloring",
                "sketchbook",
                "artwork",
                "artists"
            ]
        }
  ]
}

# Make a classification request
response = requests.post(CLASSIFY_BULK_URL, headers=headers, json=data)
print (f'Status code: {response.status_code}')
pprint(response.json())
duration_data = response.json()
</code></pre>

クラスに含まれるプロンプトに基づいて、動画全体にクラスラベルが割り当てられます。適切な動画セグメント(プロンプトに関連するクリップ)をピンポイントで特定し、正確な動画ラベル付けを実現するには、関連する多数のプロンプトを提供することが重要です。クラスラベルに一致する動画の長さが動画の合計の長さの50%を超えた場合にのみ、そのクラスラベルが割り当てられます。この全体の長さは、プロンプトに一致する動画クリップを結合することによって確定されます。

実行した分類APIコールの結果は以下の通りです。「duration ratio」は動画全体の長さに対する動画セグメントの割合を表し、「score」はモデルの信頼度を示し、「name」はクラスラベルを指します。一致するすべての動画が信頼度スコアの降順で表示されます:

<pre><code class="python">Status code: 200
{'data': [{'classes': [{'duration_ratio': 1,
                        'name': 'AnimalTok',
                        'score': 95.35111111111111}],
           'video_id': '64544b8786daab572f349496'},
          {'classes': [{'duration_ratio': 1,
                        'name': 'AnimalTok',
                        'score': 95.14666666666668}],
           'video_id': '64544b8e86daab572f34949c'},
          .
			.
			.
          {'classes': [{'duration_ratio': 0.8175611166393244,
                        'name': 'ArtTok',
                        'score': 89.45777777777778}],
           'video_id': '64544b9286daab572f34949d'}],
 'page_info': {'limit_per_page': 10,
               'next_page_token': '',
               'page_expired_at': '2023-05-05T17:37:30Z',
               'prev_page_token': '',
               'total_results': 10}}
 </code></pre>

それでは、同じコードを書き直して分類APIをコールしてみましょう。しかし今回は1点だけ異なる仕様を実装します、include_clipsをTrueに設定します。これにより、クラス内で提供されたプロンプトに一致するすべての関連動画クリップと、それぞれのメタデータを取得します:

<pre><code class="python">
# Construct the URL of the `/classify/bulk` endpoint
CLASSIFY_BULK_URL = f"{API_URL}/classify/bulk"

# Set the header of the request
headers = {
    "x-api-key": API_KEY
}

# Declare a dictionary named `data`
data =  {
  "conversation_option": "semantic",
  "options": ["visual", "conversation"],
  "index_id" : INDEX_ID,
  "include_clips": True,
  "classes": [
      {
            "name": "BeautyTok",
            "prompts": [
                "Makeup",
                "Skincare",
                "cosmetic products",
                "doing nails",
                "doing hair",
                "DanceTok",
                "dance tutorial",
                "dance competition",
                "dance challenge",
                "dance trend",
                "dancing with friends"
            ]
        },
        {
            "name": "CookTok",
            "prompts": [
                "cooking tutorial",
                "cooking utensils",
                "baking tutorials",
                "recipes",
                "restaurants",
                "food",
                "pasta"
            ]
        },
        {
            "name": "AnimalTok",
            "prompts": [
                "dog",
                "cat",
                "birds",
                "fish",
                "playing with pets",
                "pets doing funny things"
            ]
        },
        {
            "name": "ArtTok",
            "prompts": [
                "handicraft",
                "drawing",
                "graffiti",
                "sketching",
                "digital art",
                "coloring",
                "sketchbook",
                "artwork",
                "artists"
            ]
        }
  ]
}

# Make a classification request
response = requests.post(CLASSIFY_BULK_URL, headers=headers, json=data)
print (f'Status code: {response.status_code}')
pprint(response.json())
duration_data = response.json()
</code></pre>

出力:

<pre><code class="python">{'data': [{'classes': [{'clips': [{'end': 14,
                                   'option': '',
                                   'prompt': 'cat',
                                   'score': 84.77,
                                   'start': 0},
                                  {'end': 14,
                                   'option': '',
                                   'prompt': 'pets doing funny things',
                                   'score': 83.56,
                                   'start': 8.34375},
                                  {'end': 14,
                                   'option': '',
                                   'prompt': 'playing with pets',
                                   'score': 68.11,
                                   'start': 8.34375},
                                  {'end': 8.34375,
                                   'option': '',
                                   'prompt': 'pets doing funny things',
                                   'score': 58.26,
                                   'start': 0}],
                        'duration_ratio': 1,
                        'name': 'AnimalTok',
                        'score': 95.35111111111111}],
			.
			.
			.	
           'video_id': '64544b8786daab572f349496'},
          {'classes': [{'clips': [{'end': 49.189,
                                   'option': '',
                                   'prompt': 'artists',
                                   'score': 78.14,
                                   'start': 0.55},
                                  {'end': 23.659,
                                   'option': '',
                                   'prompt': 'sketching',
                                   'score': 58.85,
                                   'start': 0.55}],
                        'duration_ratio': 0.8175611166393244,
                        'name': 'ArtTok',
                        'score': 89.45777777777778}],
			.
			.
			.
			'video_id': '64544b9286daab572f34949d'}],
 'page_info': {'limit_per_page': 10,
               'next_page_token': '',
               'page_expired_at': '2023-05-05T17:37:30Z',
               'prev_page_token': '',
               'total_results': 10}}
               </code></pre>

分かりやすくするために、出力をトリミングしています。出力に、開始タイムスタンプと終了タイムスタンプ、特定のクリップの信頼度スコア、およびそれに関連するプロンプトを含む、各動画のクリップデータが表示されるようになったことに注目してください。各プロンプトに関連付けられた分類オプション(例:visualとconversation。visualは音声と視覚の一致、conversationは対話の一致を表します)を統合するために、APIエンドポイントの改良を進めています。

それでは、JSONの結果と、前に作成したvideo_id_name_listをシリアル化(pickle)してファイルに保存しましょう:

<pre><code class="python">import pickle

with open('video_id_name_list.pickle', 'wb') as handle:
    pickle.dump(video_id_name_list, handle, protocol=pickle.HIGHEST_PROTOCOL)

with open('duration_data.pickle', 'wb') as handle:
    pickle.dump(duration_data, handle, protocol=pickle.HIGHEST_PROTOCOL)

with open('clips_data.pickle', 'wb') as handle:
    pickle.dump(clips_data, handle, protocol=pickle.HIGHEST_PROTOCOL)
    </code></pre>

デモアプリの構築

以前のチュートリアルと同様に、Webページをホストし、シリアル化されたデータを利用するFlaskベースのデモアプリを作成します。このデータをローカルドライブから取得した動画に適用することで、見栄えの良い分類結果Webページを作成します。これにより、ビデオ分類APIがアプリケーションをどのように強化し、印象的な結果をもたらすかを直接体験できます。

ディレクトリ構造は以下のようになります:

<pre><code class="markdown">my_flask_app/
│   app.py
│   sample_notebook.ipynb
└───templates/
│	│   index.html
└───classify/
	│   <your_video_1.mp4><your_video_2.mp4><your_video_3.mp4>
			.
			.
			.
</code></pre>

Flaskアプリのコード

このチュートリアルでは、ローカルディレクトリから動画ファイルを提供し、HTML5ビデオプレーヤーを使用して特定のセグメントを再生する方法を少し工夫しています。Flaskアプリケーションで使用されるserve_video関数は、Flaskアプリケーションスクリプトと同じディレクトリにあるclassify_tryディレクトリからビデオファイルを提供します。HTMLテンプレートの url_for('serve_video', filename=video_mapping[video.video_id]) 式は、提供されたビデオファイルのURLを生成します。

「include_clips」をTrueに設定したときの分類APIの出力からお気づきかもしれませんが、APIは多数のクリップとそのメタデータを返しました。シンプルにするため、またこれらのクリップを含む結果を示すために、get_top_clips関数を含めました。この関数は3つの固有のプロンプトを見つけ、それらに関連付けられたすべてのクリップのメタデータを返すため、分類結果をより包括的に見ることができます。

以下は、「app.py」 ファイルのコードです:

<pre><code class="python">from flask import Flask, render_template, send_from_directory
import pickle
import os
from collections import defaultdict

app = Flask(__name__)

# Replace the following dictionaries with your data
with open('video_id_name_list.pickle', 'rb') as handle:
    video_id_name_list = pickle.load(handle)

with open('duration_data.pickle', 'rb') as handle:
    duration_data = pickle.load(handle)

with open('clips_data.pickle', 'rb') as handle:
    clips_data = pickle.load(handle)

VIDEO_DIRECTORY = os.path.join(os.path.dirname(os.path.realpath(__file__)), "classify")

@app.route('/<path:filename>')
def serve_video(filename):
    print(VIDEO_DIRECTORY, filename)
    return send_from_directory(directory=VIDEO_DIRECTORY, path=filename)

def get_top_clips(clips_data, num_clips=3):
    top_clips = defaultdict(list)
    for video in clips_data['data']:
        video_id = video['video_id']
        unique_prompts = set()
        for clip_class in video['classes']:
            for clip in clip_class['clips']:
                if clip['prompt'] not in unique_prompts and len(unique_prompts) < num_clips:
                    top_clips[video_id].append(clip)
                    unique_prompts.add(clip['prompt'])
    return top_clips

@app.route('/')
def home():
    video_id_name_dict = {video['video_id']: video['video_name'] for video in video_id_name_list}
    top_clips = get_top_clips(clips_data)
    return render_template('index.html', video_mapping=video_id_name_dict, duration_data=duration_data, top_clips=top_clips)


if __name__ == '__main__':
    app.run(debug=True)
</code></pre>

HTMLテンプレート

以下はJinja2ベースのHTMLテンプレートのサンプルです。HTMLファイル内にコードを組み込み、準備して渡したシリアル化データのフィールドを使用して反復処理します。このテンプレートは、ローカルドライブから必要な動画を取得し、分類基準に応じて結果を表示します:

<pre><code class="language-html"><!doctype html>
<html lang="en">
<head>
    <link rel="shortcut icon" href="#" />
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Classification Output</title>
    <style>
        body {
            background-color: #f0e7cc;
            font-family: Arial, sans-serif;
        }
        .container {
            width: 80%;
            margin: 0 auto;
            padding-bottom: 30px;
        }
        video {
            display: block;
            margin: 0 auto;
        }
        .progress-bar {
            width: 20%;
            height: 20px;
            background-color: #f0f0f0;
            margin-bottom: 10px;
            display: inline-block;
            vertical-align: middle;
            margin-left: 10px;
        }
        .progress {
            height: 100%;
            background-color: #4caf50;
        }
        .score {
            display: inline-block;
            vertical-align: middle;
            font-weight: bold;
        }
        .video-category {
            padding-bottom: 30px;
            margin-bottom: 20px;
        }
        button {
            background-color: #008CBA;
            border: none;
            color: white;
            padding: 8px 16px;
            text-align: center;
            text-decoration: none;
            display: inline-block;
            font-size: 14px;
            margin: 4px 2px;
            cursor: pointer;
            border-radius: 4px;
            box-shadow: 0 1px 2px rgba(0,0,0,0.2);
            transition: background-color 0.2s ease;
        }
        button:hover {
            background-color: #006494;
        }
        h1 {
            color: #070707;
            text-align: center;
            background-color: #cb6a16; /* Change the background color to your desired color */
            padding: 10px;
            margin-bottom: 20px;  
        }
        .clips-container {
            text-align: center;
        }

    </style>
    <script>
        function playClip(videoPlayer, start, end) {
            videoPlayer.currentTime = start;
            videoPlayer.play();
            videoPlayer.ontimeupdate = function () {
                if (videoPlayer.currentTime >= end) {
                    videoPlayer.pause();
                }
            };
        }
    </script>
</head>
<body>
    <div class="container">
        <h1>Video Classification</h1>
        {% for video in duration_data.data %}
            {% set video_id = video.video_id %}
            {% set video_path = video_mapping[video_id] %}
            <div class="video-category">
                <h2>{{ video.classes[0].name }}</h2>
                <p>Video file: {{video_path}}</p>
                <div>
                    <span class="score">Score: {{ '%.2f'|format(video.classes[0].score) }}</span>
                    <div class="progress-bar">
                        <div class="progress" style="width: {{ video.classes[0].score }}%;"></div>
                    </div>
                </div>
                <div>
                    <span class="score">Duration: {{ '%.2f%%'|format(video.classes[0].duration_ratio * 100) }}</span>
                    <div class="progress-bar">
                        <div class="progress" style="width: {{ video.classes[0].duration_ratio * 100 }}%;"></div>
                    </div>
                </div>
                <video id="video-{{ video_id }}" width="480" height="320" controls>
                    <source src="{{ url_for('serve_video', filename=video_path) }}" type="video/mp4">
                    Your browser does not support the video tag.
                </video>
                <div class="clips-container">
                    <h3>Sample Clips</h3>
                    {% for clip in top_clips[video_id] %}
                    <button onclick="playClip(document.getElementById('video-{{ video_id }}'), {{ clip.start }}, {{ clip.end }})">Clip: {{ clip.prompt }} | Start {{ '%.2f'|format(clip.start) }}s - End {{ '%.2f'|format(clip.end) }}s - Score {{ '%.2f'|format(clip.score) }}</button>
                    {% endfor %}
            </div>
            <br /><br />
        {% endfor %}
    </div>
</body>
</html>
</code></pre>

Flaskアプリの起動

素晴らしいですね!それでは、Jupyter Notebookの最後のセルを実行してFlaskアプリを起動しましょう:

<pre><code class="python">%run app.py
</code></pre>

すべてが想定通りに進んだことを確認する、以下のような出力が表示されるはずです 😊:

URLリンク http://127.0.0.1:5000 をクリックすると、以下のような画面が表示されます:

このチュートリアルで作成した完全なコードが含まれるJupyter Notebookはこちらからご確認いただけます - https://tinyurl.com/classifyNotebook

さらに高度な構築に挑戦してみませんか?

以下のようなアイデアをぜひ試してみてください:

  1. 3つの個別のプロンプトを選択する代わりに、ページネーションと遅延読み込みを実装して、すべてのクリップを表示します。これにより、分類されたクリップのより広い範囲を探索し、ビデオ分類結果のより包括的な視覚を得ることができます。

  2. ショートテール、ミディアムテール、ロングテールのプロンプトを実験し、しきい値を微調整して、Discordチャネルで他のマルチモーダル愛好家と分析結果を共有しましょう。

結び

これからの展開にもぜひご期待ください!まだ参加されていない方は、マルチモーダルAIに情熱を持つ同じ志を持った仲間とつながることができる、活気あるDiscordコミュニティにぜひご参加ください。