チュートリアル

Twelve Labs APIを使用して、最適なインフルエンサーパートナーを特定する方法とは?

ミラン・キム

このチュートリアルでは、Twelve Labs Search APIを使用して、動画のタイトルや説明にブランド名が含まれていなくても、動画内で特定のブランドや製品に言及しているYouTubeインフルエンサーを見つけ出し、その結果をチャンネルごとに整理して表示する「Who Talked About Us(誰が私たちについて話したか)」アプリの構築手順を解説します。

このチュートリアルでは、Twelve Labs Search APIを使用して、動画のタイトルや説明にブランド名が含まれていなくても、動画内で特定のブランドや製品に言及しているYouTubeインフルエンサーを見つけ出し、その結果をチャンネルごとに整理して表示する「Who Talked About Us(誰が私たちについて話したか)」アプリの構築手順を解説します。

この記事の内容

No headings found on page

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

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

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

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

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

2024/01/16

16 分

記事へのリンクをコピー

イントロダクション

美容業界のマーケティングプロフェッショナルとしてインフルエンサーシップに長年関わってきた中で、ブランドにとって理想的なYouTubeやTikTokのインフルエンサーを選定する上での貴重な教訓を学びました。(そうです、私はマーケティングプロフェッショナルからソフトウェアエンジニアに転身したのです 😉)最も成功するコラボレーションは、すでにあなたの製品やブランドを心から愛用しているインフルエンサーと、オーガニックに発生する傾向があります。たとえば、あなたがブランド「A」を宣伝している場合、事前のコンタクトがなくても「A」について言及しているインフルエンサーを見つけることができるかもしれません。こちらからアプローチした際、彼らは通常、一緒に取り組むことに対して非常に好意的です。

しかし、これらのインフルエンサーを見つけ出すのは少々難しい場合があります。特に、動画のタイトルや説明欄にあなたのブランド名が明示的に記載されていない場合はなおさらです。これは私自身にとっても悩みの種でした。たとえば、あるYouTuberが「冬のマストハブアイテムTop 10」のような動画の中で、ブランド名を挙げずにあなたの製品を紹介していた場合、YouTubeでの通常のキーワード検索では見つけることができません。

そこで、Twelve Labs APIを活用した「Who Talked About Us(誰が私たちについて話したか)」の出番です。通常のYouTubeやTikTokの検索とは異なり、このAPIは高度な文脈依存の動画検索を可能にします。タイトルや説明欄に頼る代わりに、APIは動画から動き、オブジェクト、人物、音、画面上のテキスト、そして音声など、さまざまな要素を抽出します。キーワードや、「MACのゴールドハイライターを使用」といった具体的な説明を入力するだけで、あなたのブランドや製品について言及している動画やチャンネルを、その言及が飛び出す正確な瞬間とともに発見することができます。 

これにより、アプローチしたいインフルエンサーのリストを、彼らが言及した製品の詳細や文脈とともに作成できます。これにより、これら潜在的なインフルエンサーに関する貴重なインサイトが得られ、より効果的にアプローチし、有意義なつながりを築くことが可能になります。 

それでは、Twelve Labs APIのパワーを活用したアプリを作成するためのステップバイステップの道のりを始めましょう!

前提条件

  • Twelve Labs APIの世界に飛び込む前に、まずはサインアップしてAPIキーを生成する必要があります。Twelve Labs Playgroundにアクセスしてサインアップし、APIキーを生成してください。サインアップすると、最大10時間の動画コンテンツをインデックス登録できる無料クレジットが提供されます!

  • このアプリに必要なすべてのファイルが含まれているリポジトリは、Githubで公開されています。

  • このアプリは、JavaScript、Node、React、およびReact Queryを使用して構築されています。これらの技術に馴染みがあると理解しやすいですが、そうでなくても心配はいりません。この記事から得られる最も重要な成果は、Twelve Labs APIについて学び、このアプリがそれをどのように活用しているかを理解することです。

1 - コンポーネントのデザイン

このアプリはReactを使用して構築されており、Reactの本質は再利用可能なコンポーネントに分解することにあります。そのため、まずはコンポーネントのデザインから開始し、もちろん、何度も修正を重ねました。



大局的に見ると、このアプリはExistingIndexForm、IndexForm、そしてVideoIndexで構成されています。IndexFormは、ユーザーが新しいインデックスを作成できるシンプルなフォームです。ExistingIndexFormは、ユーザーが以前このアプリで作成したインデックスのIDを送信できるもう一つのシンプルなフォームです。VideoIndexは、インデックスの詳細を取得する呼び出しが行われ、VideoComponentsが存在する場所です。

VideoComponentsは、動画に関連するすべてのコンポーネントで構成されています。UploadYoutubeVideoコンポーネントは、動画のダウンロード/インデックス登録を可能にし、各動画タスクのステータスを表示します。VideoListは、インデックスの動画をPageNavとともにレンダリングし、各ページに12個ずつフェッチして表示します。SearchFormは、ユーザーの入力(検索クエリ)を受け取り、検索API呼び出しに渡す動画検索を処理するコンポーネントです。SearchResultsは検索結果と動画のAPIを呼び出し、インフルエンサーごとに整理します。その後、SearchResultが各検索結果をユーザーフレンドリーな方法で表示します。

2 - サーバーとAPIフックの構築

Server.jsとapiHooks.jsは、Twelve Labs APIやytdl-coreなどの他のライブラリからのすべてのAPI呼び出しを管理するファイルです。Server.jsは、Twelve Labs APIを呼び出すためのすべてのエンドポイントや、その他のAPI呼び出しが存在する場所です。apiHooks.jsは、状態、キャッシュ、およびデータフェッチを管理するためのカスタムReact Queryフックのセットです。サーバーの、ひいてはアプリ全体の主要機能であるTwelve Labs APIを使用した動画検索について、その使用方法を詳しく見ていきましょう。

Twelve Labs APIを使用する4つのステップ

最初のステップは、動画用のインデックスを作成することです。次に、このインデックスに動画をアップロードします。続いて、動画のメタデータを更新して、各動画にYouTubeチャンネルとURLを追加します(このステップはこのアプリ固有のもので、一般的にはオプションです)。最後に、動画検索を行う準備が整います。このアプリでは、すべてのAPI呼び出し(Twelve Labsおよびその他)と動画アップロードに関連する関数をserver.jsファイルに整理しました。

セットアップ

ルートディレクトリに.envファイルを作成し、必要に応じて値を更新します。以下をコピー&ペーストして値をカスタマイズするだけでも可能です。

.env

REACT_APP_API_URL=https://api.twelvelabs.io/v1.1
REACT_APP_API_KEY=<YOUR API KEY>
REACT_APP_SERVER_URL=<YOUR SERVER URL>
REACT_APP_PORT_NUMBER=<YOUR PORT NUMBER>



  • REACT_APP_API_URL: このアプリはv1.1をサポートしています

  • REACT_APP_API_KEY: 前のステップで生成したAPIキーを保存します

  • REACT_APP_SERVER_URL: 「http://localhost」のようになる可能性があります

  • REACT_APP_PORT_NUMBER: 使用したいポート番号を設定します(例: 4001)

必要なファイル内で環境変数の値にアクセスするには、process.envを利用できます。たとえば、server.jsファイルでは、process.env.REACT_APP_APP_URLを使用してAPI_URLにアクセスし、保存できます。次の例は、これを実現する方法を示しています。

server.js (15行目 - 19行目)

/** 定数を定義し、TL APIエンドポイントを設定する */
const TWELVE_LABS_API_KEY = process.env.REACT_APP_API_KEY;
const API_BASE_URL = process.env.REACT_APP_API_URL;
const TWELVE_LABS_API = axios.create({ baseURL: API_BASE_URL });
const PORT_NUMBER = process.env.REACT_APP_PORT_NUMBER;

ステップ 1. インデックスの作成

インデックスとは、動画をアップロード、インデックス登録、検索できる動画ライブラリのようなものです。日付、テーマ、またはYouTubeチャンネルごとに独自のインデックスを作成できます。インデックスを作成するには、メソッドを「POST」、エンドポイントを「https://api.twelvelabs.io/v1.1/indexes」に設定し、ヘッダーを追加して、engine_id、index_options、index_nameなどの必要なデータを提供するだけです。index_optionsについては、4つのオプションのサブセットを選択できます。このアプリでは、4つのオプションすべてが含まれています。

💡インデックス作成の詳細については、APIリファレンスを確認してください

server.js (98行目 - 119行目)

/** インデックスを作成する */
app.post("/indexes", async (request, response, next) => {
 const headers = {
   "Content-Type": "application/json",
   "x-api-key": TWELVE_LABS_API_KEY,
 };


 const data = {
   engine_id: "marengo2.5",
   index_options: ["visual", "conversation", "text_in_video", "logo"],
   index_name: request.body.indexName,
 };


 try {
   const apiResponse = await TWELVE_LABS_API.post("/indexes", data, {
     headers,
   });
   response.json(apiResponse.data);
 } catch (error) {
   console.error("サーバー側のエラー:", error);
   response.json({ error });
 }
});

ステップ 2. YouTubeのURLを使って動画をアップロードする

このアプリは、チャンネルIDプレイリストID、または以下のようなURLオブジェクトの配列を持つJSONファイルを介した、YouTube動画の一括アップロードをサポートしています。

example.json

[
     { "url": "VIDEO URL" },
     { "url": "VIDEO URL" }
     ...
    ]

💡YouTubeのURLによる動画のアップロードは、Twelve Labs API v1.2で利用可能になりました。このアプリはv1.1を使用しているため、YouTubeから動画をダウンロードできるライブラリであるytdl-coreを利用して、手動で実装しています。

プロセスとしては、提供されたYouTube URLから動画をダウンロードし、Twelve Labs APIに送信して動画をインデックス登録します。これは、server.jsのエンドポイント「/download」に実装されています。まず、リクエストのボディから動画データとインデックス情報を抽出します。次に、動画をチャンクに分けてダウンロードし、安全なファイル名にするためにタイトルをサニタイズして、インデックス登録用に送信します。すべての動画がダウンロードされ、インデックス登録された後、サーバーはタスクIDとインデックスIDを返します。各ステップを分解して詳しく見ていきましょう。

1 - リクエストから情報を抽出する

最初のステップは、リクエストのボディから動画データとインデックス情報を抽出することです。動画の総数、処理された動画の数、および動画のダウンロードとインデックス登録のチャンクサイズ(このアプリでは5に設定)を追跡するための変数を設定します。また、動画インデックス登録プロセスからのレスポンスを格納するための配列を初期化します。

server.js (397行目 - 409行目)

/** 分析用の動画をダウンロードし、インデックス登録して、タスクIDとインデックスIDを返す */
app.post(
 "/download",
 bodyParser.urlencoded(),
 async (request, response, next) => {
   try {
     // ステップ 1: リクエストから動画データとインデックス情報を抽出する
     const jsonVideos = request.body.videoData;
     const totalVideos = jsonVideos.length;
     let processedVideosCount = 0;
     const chunk_size = 5;
     let videoIndexingResponses = [];
     console.log("動画をダウンロード中...");
2 - 動画をチャンクに分けてダウンロードする

次のステップは、動画をチャンクに分けてダウンロードすることです。各チャンクについて、動画データを反復処理し、ytdl-coreライブラリを使用して提供されたURLから動画をダウンロードします。安全なファイル名を作成するために、動画のタイトルはサニタイズされます。動画のダウンロードが完了すると、その進捗が記録されます。

💡ここでは、ダウンロードした動画を「videos」フォルダに保存するためのvideoPathを設定しています。

server.js (411行目 - 442行目)

// ステップ 2: 動画をチャンクに分けてダウンロードする
     for (let i = 0; i < totalVideos; i += chunk_size) {
       const videoChunk = jsonVideos.slice(i, i + chunk_size);
       const chunkDownloadedVideos = [];


       // 現在のチャンク内の各動画をダウンロードする。
       await Promise.all(
         videoChunk.map(async (videoData) => {
           try {
             // ダウンロードした動画の安全なファイル名を生成する
             const safeName = sanitize(videoData.title);
             const videoPath = `videos/${safeName}.mp4`;


             // 提供されたURLから動画をダウンロードする
             const stream = ytdl(videoData.url, {
               filter: "videoandaudio",
               format: ".mp4",
             });
             await streamPipeline(stream, fs.createWriteStream(videoPath));


             console.log(`${videoPath} -- ダウンロード完了`);


             chunkDownloadedVideos.push({
               videoPath: videoPath,
               videoData: videoData,
             });
           } catch (error) {
             console.log(`${videoData.title} のダウンロード中にエラーが発生しました`);
             console.error(error);
           }
         })
       );
3 - インデックス登録用に動画を送信する

動画のチャンクをダウンロードした後、これらのダウンロードした動画をインデックス登録用に送信します。すべてのインデックス登録タスクが完了するのを待ち、インデックス登録送信の進捗を記録します。

server.js (444行目 - 481行目)

// ステップ 3: ダウンロードした動画をインデックス登録用に送信する
       console.log(
         `インデックス登録用に動画を送信中 | チャンク ${
           Math.floor(i / chunk_size) + 1
         }`
       );


       const chunkVideoIndexingResponses = await Promise.all(
         chunkDownloadedVideos.map(async (chunkDownloadedVideo) => {
           console.log(
             `インデックス登録用に ${chunkDownloadedVideo.videoPath} を送信中...`
           );
           const indexingResponse = await indexVideo(
             chunkDownloadedVideo.videoPath,
             request.body.index_id
           );


           // indexingResponse に videoData を追加する
           indexingResponse.videoData = chunkDownloadedVideo.videoData;


           return indexingResponse;
         })
       ).catch(next);
       
       console.log("チャンクのインデックス送信が完了しました | タスクID:");


       processedVideosCount += videoChunk.length;


       console.log(
         `${totalVideos} 本中 ${processedVideosCount} 本の動画を処理しました`
       );


       videoIndexingResponses = videoIndexingResponses.concat(
         chunkVideoIndexingResponses
       );


       await new Promise((resolve) => setTimeout(resolve, 1000));
     }

また、indexVideo関数も簡単に見ておきましょう。動画のインデックス登録自体は非常にシンプルです。他のAPI呼び出しと同様に、プロセスにはTwelve Labs APIへのPOSTリクエストでのインデックス登録タスクの開始が含まれます。このリクエストには、特定のパラメータ(ターゲットインデックスを指定するindex_id、ビデオファイル(この場合はビデオパスからストリームされたビデオデータ)、および言語設定(英語の場合は'en'))が含まれます。

💡動画のインデックス登録(動画インデックス登録タスクの作成)の詳細については、APIリファレンスを確認してください

server.js (330行目 - 349行目)

/** ダウンロードした動画を受け取り、インデックス登録プロセスを開始する */
const indexVideo = async (videoPath, indexId) => {
 const headers = {
   headers: {
     accept: "application/json",
     "Content-Type": "multipart/form-data",
     "x-api-key": TWELVE_LABS_API_KEY,
   },
 };


 let params = {
   index_id: indexId,
   video_file: fs.createReadStream(videoPath),
   language: "en",
 };


 const response = await TWELVE_LABS_API.post("/tasks", params, headers);


 return await response.data;
};

この呼び出しからのresponse.dataは、以下のようなタスクIDを返します。以前のコードスニペットで見たように、各タスクIDはindexingResponseに保存されます。

response.data

{
	"_id": "6527732e23c1347ffbe3a802"
}

ただし、response.dataの他に、後で各動画のメタデータに追加するためにビデオデータが必要になるため、indexingResponseにvideoDataを追加しています。したがって、各最終indexingResponseは以下のようになり、videoIndexingResponsesに結合されます。

videoIndexingResponses

[
  {
    _id: '6527732e23c1347ffbe3a802',
    videoData: {
      url: 'https://www.youtube.com/watch?v=WuWnt2NHJxk&list=PLI8sddTC_LM0SG6JMYmrbrHZy7j5zUjMA&index=4&pp=iAQB',
      title: 'Everyday Makeup Using ONLY 3 Products !!',
      authorName: 'Smitha Deepak',
      thumbnails: [Array]
    }
  },
  
]
4 - タスクIDを返す

すべての動画がダウンロードされ、インデックス登録用に送信されると、タスクIDのオブジェクト(上記の配列)とインデックスIDをクライアントに返します。

server.js (483行目 - 498行目)

// ステップ 4: インデックスタスクのタスクIDとインデックスIDを返す
     console.log(
       "すべての動画のインデックス送信が完了しました。タスクID:"
     );


     console.log(videoIndexingResponses);


     response.json({
       taskIds: videoIndexingResponses,
       indexId: request.body.index_id,
     });
   } catch (error) {
     next(error);
   }
 }
);

ステップ 3. 動画のメタデータを更新する

一般的に、このステップはオプションですが、このアプリでは動画をYouTubeのURLで表示し、チャンネル/インフルエンサーごとに分類するために必要です。

アプリでの動画の表示方法

ご覧の通り、アプリは各動画の薄緑色のカプセル型バッジにチャンネル名を表示します。また、動画はTwelve LabsサーバーからのビデオURLではなく、YouTube URLを使用してReact Playerを介して読み込まれます。動画のデフォルトメタデータにはチャンネル名とYouTube URLが含まれないため、この情報を各動画のメタデータに追加する必要があります。 

以下は、動画のデフォルトメタデータの例です。

"metadata": {
		"duration": 188.6,
		"filename": "3 MINUTE MAKEUP THAT WILL LAST ALL DAY.mp4",
		"fps": 25,
		"height": 720,
		"size": 40566200,
		"video_title": "3 MINUTE MAKEUP THAT WILL LAST ALL DAY.mp4",
		"width": 1280,
		}

任意のキーと値のペアを追加したり、既存のメタデータを更新したりできます。このアプリでは、メタデータに3つの追加データを追加しています:1) 著者(チャンネル名)、2) YouTube URL、および 3) whoTalkedAboutUsのブール値(動画がアプリを介してアップロードされたかどうかを示すマーク)。 

この作業はserver.jsファイルで行われ、そこには動画メタデータの更新を処理するエンドポイントがあります。ここでは、追加または更新するデータを含むPUTリクエストが行われます。

💡動画情報の更新の詳細については、APIリファレンスを確認してください

server.js (290行目 - 310行目)

/** 動画のメタデータを更新する */
app.put("/update/:indexId/:videoId", async (request, response, next) => {
 const indexId = request.params.indexId;
 const videoId = request.params.videoId;
 const data = request.body;
 const headers = {
   "Content-Type": "application/json",
   "x-api-key": TWELVE_LABS_API_KEY,
 };


 try {
   const apiResponse = await TWELVE_LABS_API.put(
     `/indexes/${indexId}/videos/${videoId}`,
     data,
     { headers }
   );
   response.json(apiResponse.data);
 } catch (error) {
   return next(error);
 }
});

次に、これがUploadYouTubeVideo.jsファイルでどのように実装されているかを見てみましょう。

UploadYouTubeVideo.js (190行目 - 223行目)

async function updateMetadata() {
   const updatePromises = completeTasks.map(async (completeTask) => {
     const matchingVid = taskVideos.find(
       (taskVid) =>
         `${sanitize(taskVid.title)}.mp4` === completeTask.metadata.filename
     );
     if (matchingVid) {
       const authorName = matchingVid.author.name;
       const youtubeUrl = matchingVid.video_url || matchingVid.shortUrl;
       const data = {
         metadata: {
           author: authorName,
           youtubeUrl: youtubeUrl,
           whoTalkedAboutUs: true,
         },
       };
       try {
         await fetch(
           `${UPDATE_VIDEO_URL}/${currIndex}/${completeTask.video_id}`,
           {
             method: "PUT",
             headers: {
               "Content-Type": "application/json",
             },
             body: JSON.stringify(data),
           }
         );
       } catch (error) {
         console.error(error);
       }
     }
   });
   await Promise.all(updatePromises);
 }

updateMetadata関数は、すべてのタスク動画から一致する完了済みのタスク動画を見つけます。一致するごとに、著者名とYouTube URLを抽出し、カスタムメタデータを構築します。すべての動画に対して、whoTalkedAboutUsキーの値はtrueに設定されます。その後、サーバーにフェッチしてこれらの変更を適用します。

💡 提供するデータは「metadata」キーを持つオブジェクトの形式である必要があり、ここでキーと値のペアを追加または変更して、ビデオデータをパーソナライズすることができます。

このプロセスの後、動画メタデータには、新しく追加された「author」、「youtubeUrl」、および「whoTalkedAboutUs」が誇らしげに表示されます!全体として、メタデータの更新は、動画コレクションに自分だけの印を付けるための素晴らしい方法です。

"metadata": {
		"author": "Justine Feather", //追加されました!
		"duration": 188.6,
		"filename": "3 MINUTE MAKEUP THAT WILL LAST ALL DAY.mp4",
		"fps": 25,
		"height": 720,
		"size": 40566200,
		"video_title": "3 MINUTE MAKEUP THAT WILL LAST ALL DAY.mp4",
		"whoTalkedAboutUs": true, //追加されました!
		"width": 1280,
		"youtubeUrl": "https://www.youtube.com/watch?v=aYQWSNAL4D8" //追加されました!
		}

ステップ 4. 動画を検索する

ついに、お待ちかねの動画検索の瞬間です!インデックス内のインデックス登録済み動画から検索を行うことができます。

このアプリでは、検索結果を表示する際にページネーションが実装されています。したがって、検索結果の取得と表示は、POSTリクエストを行って最初の検索結果を取得することと、次のページのトークンを使用してGETリクエストを行って以降の検索結果ページをフェッチすることの2つの部分で構成されます。

検索POSTリクエストを行う

最初の検索結果を取得するには、server.jsファイルにある検索エンドポイントにPOSTリクエストを行う必要があります。リクエストからindexIdとqueryを取得し、TwelveLabs API用に「/search」へのPOSTリクエストを行います。

「search_options」には、「visual」、「conversation」、「text_in_video」、「logo」の4つすべてを含めました。また、「threshold」、「sort_option」、「group_by」といった複数のオプションも適用しています。これらのオプションすべてを使用して、信頼度が「中」および「高」の結果をフィルタリングし、動画ごとにグループ化し、クリップ数で並べ替えています。

💡検索リクエストの送信に関する詳細については、APIリファレンスを確認してください

server.js (217行目 - 243行目)

/** 与えられたクエリで動画を検索する */
app.post("/search", async (request, response, next) => {
 const headers = {
   accept: "application/json",
   "Content-Type": "application/json",
   "x-api-key": TWELVE_LABS_API_KEY,
 };


 const data = {
   index_id: request.body.indexId,
   search_options: ["visual", "conversation", "text_in_video", "logo"],
   query: request.body.query,
   group_by: "video",
   sort_option: "clip_count",
   threshold: "medium",
   page_limit: 2,
 };


 try {
   const apiResponse = await TWELVE_LABS_API.post("/search", data, {
     headers,
   });
   response.json(apiResponse.data);
 } catch (error) {
   return next(error);
 }
});

「/search」エンドポイントが設定されたら、useGetVideosOfSearchResultsが機能します。これは、React Queryフックを使用して「ビデオ検索」と「ビデオ取得」の機能を同時に動かすReact Queryフックです。これはSearchResults.jsにインポートされて実行され、クエリレスポンスからネクストページトークン、検索結果、および検索結果動画が抽出されます。

SearchResults.js (25行目 - 33行目)

/** 初期の検索結果と対応する動画を取得する */
 const {
   initialSearchData: {
     page_info: { next_page_token: initialNextPageToken } = {},
   } = {},
   initialSearchResults,
   initialSearchResultVideos,
   refetch,
 } = useGetVideosOfSearchResults(currIndex, finalSearchQuery);

useGetVideosOfSearchResults関数は、apiHooks.jsで定義されています。上述の通り、useQueriesフックを使用して複数のクエリを同時に実行します。最初の検索結果を取得し、次に各々の初期検索結果と一致する動画の情報を取得します。

apiHooks.js (152行目 - 177行目)

export function useGetVideosOfSearchResults(indexId, query) {
 const {
   data: initialSearchData,
   refetch,
   isLoading,
 } = useSearchVideo(indexId, query);
 const initialSearchResults = initialSearchData.data || [];


 const resultVideos = useQueries({
   queries: initialSearchResults.map((searchResult) => ({
     queryKey: [keys.SEARCH, indexId, searchResult.id],
     queryFn: () =>
       apiConfig.TWELVE_LABS_API.get(
         `${apiConfig.INDEXES_URL}/${indexId}/videos/${searchResult.id}`
       ).then((res) => res.data),
   })),
 });
 const initialSearchResultVideos = resultVideos.map(({ data }) => data);
 return {
   initialSearchData,
   initialSearchResults,
   initialSearchResultVideos,
   refetch,
   isLoading,
 };
}

ビデオ検索の結果を取得するために、まずuseSearchVideoフックが呼び出されているのがわかります。

apiHooks.js (93行目 - 102行目)

export function useSearchVideo(indexId, query) {
 return useQuery({
   queryKey: [keys.SEARCH, indexId, query],
   queryFn: () =>
     apiConfig.TWELVE_LABS_API.post(apiConfig.SEARCH_URL, {
       indexId,
       query,
     }).then((res) => res.data),
 });
}

APIフックの呼び出しの結果として取得するデータ(initialSearchResults と initialSearchResultVideos)は、以下のようになります。

initialSearchResults

[
	
 {clips: 
[
  {score: 76.1, start: 21.15625, end: 37.15625, metadata: Array(1),  
   video_id: '65651c021cbcab2e74d86eb3', confidence: “medium”,   
   modules: [{...}]},
   {},
    
], 
 id: '65651c021cbcab2e74d86eb3'}, 
 {clips: Array(6), id:... },
  
]

各initialSearchResultは、検索結果を含む「clips」と、該当する検索結果のビデオIDである「id」で構成されています。 

initialSearchResultVideos

[
	{
	     “created_at”: "2023-12-07T18:32:01Z"
     “hls”: {...},
           “indexed_at”: "2023-12-07T18:34:06Z",
           “metadata”: {...}, 
           “Updated_at”: "2023-12-07T18:35:01Z",
           “_id”: "65720fa11cbcab2e74d87aab"
	}, 
      
]

initialSearchResultVideosは、対応する検索結果のメタデータを含むビデオオブジェクトで構成されています。 

これで、最初の検索結果とそれに対応する動画が取得できました。ただし、次のページ用のトークン(next_page_token)がある場合は、さらに検索結果が存在することを意味します。このため、すべての検索結果と動画を一元化して保存するために、追加の状態(combinedSearchResults と combinedSearchResultVideos)を設定しています。特にこのアプリでは結果をインフルエンサー(YouTubeチャンネル)ごとに整理して表示するため、まずデータを結合し、整理して、ユーザーに提示するためにこれらの状態が必要不可欠です。 

それでは、次のページの検索結果をフェッチする方法について見ていきましょう。

検索GETリクエストを行う

初期の検索データから次のページのトークン(next_page_token)を取得したことを覚えていますか?そのトークンを使用して追加の検索結果を取得するGETリクエストを行うことができます。検索結果にnext_page_tokenが含まれている限り、すべての検索結果を漏れなく収集するためにリクエストを送り続ける必要があります。

SearchResults.js (50行目 - 53行目)

const nextPageResultsData = await fetchNextPageSearchResults(
       queryClient,
       nextPageToken
     );

fetchNextPageSearchResults は、Twelve Labs APIの「/search」エンドポイントにGETリクエストを呼び出してデータを取得します。ユーザーがボタンをクリックしたときにのみ条件付きでデータをフェッチするため、ここではfetchQueryを使用しました。

apiHooks.js (179行目 - 196行目)

export async function fetchNextPageSearchResults(queryClient, nextPageToken) {
 try {
   const response = await queryClient.fetchQuery({
     queryKey: [keys.SEARCH, nextPageToken],
     queryFn: async () => {
       const response = await apiConfig.TWELVE_LABS_API.get(
         `${apiConfig.SEARCH_URL}/${nextPageToken}`
       );
       const data = response.data;
       return data;
     },
   });
   return response;
 } catch (error) {
   console.error("検索結果の次のページの取得中にエラーが発生しました:", error);
   throw error;
 }
}

検索結果に基づき、最初の検索結果で行った作業と同様に、fetchNextpageSearchResultVideosを使用して対応する映像データを取得します。最後に、それらはcombinedSearchResultsとcombinedSearchResultVideosに追加されます。

combinedSearchResultsとcombinedSearchResultVideosが変化するたびに、結果が再整理されます(関連コードはこちら)。整理された検索結果は、その後SearchResultコンポーネントを通じてレンダリングされます。

💡 検索結果のページネーションコントロールのさらなる詳細については、APIガイドラインを確認してください!

これで、必要なすべてのデータにアクセスするためのサーバーとAPIフックが完成しました。次に、データを取得、操作、そしてユーザーに表示するためのコンポーネントを構築できます。次のステップでは、これらすべてのコンポーネントがどのように組み合わさって、強力なインフルエンサー検索アプリケーションを作成するかを見ていきます。お楽しみに!

3 - コンテナコンポーネントの構築

コンポーネント構築のプロセスにおいては、コンテナコンポーネント(Container Components)から作り始めるのが簡単です。プレゼンテーションコンポーネント(Presentation Components)は、コンテナコンポーネントからのAPIレスポンスや状態(State)に依存することが多いためです。 

💡 コンテナコンポーネントおよびプレゼンテーションコンポーネントという用語に馴染みがないかもしれませんが、これらはReactの文脈でよく使われる用語です。コンテナコンポーネント(スマートコンポーネントとも呼ばれます)は、データのフェッチや状態変化などのアプリのロジックやデータフローを処理します。プレゼンテーションコンポーネント(ダムコンポーネントとも呼ばれます)は、propsを介してコンテナコンポーネントから機能を受け取り、ユーザーインターフェースの描画やデータの表示に集中します。

3.1 - VideoComponent.js

VideoComponentsはアプリの中心的な存在であり、動画のアップロード、動画検索、インデックス内の動画とYouTubeチャンネル名の表示など、さまざまな機能を提供します。ユーザーは簡単に新しい動画をアップロードし、検索を実行し、ページ分けされた動画ライブラリビューにアクセスできます。また、必要に応じてインデックスを削除することもできます。どのように機能するのか見てみましょう。

videoComponents.js (21行目 - 67行目)

/** 動画とのやり取りを含むコンポーネント
*
*  VideoIndex -> VideoComponents -> { UploadYouTubeVideo, VideoList, PageNav,
*  SearchForm, SearchResults }
*
*/


export function VideoComponents({
 currIndex,
 vidPage,
 setVidPage,
 taskVideos,
 setTaskVideos,
}) {
 const [searchQuery, setSearchQuery] = useState("");
 const [finalSearchQuery, setFinalSearchQuery] = useState("");
 const [isSubmitting, setIsSubmitting] = useState(false);
 const { setIndexId } = useContext(setIndexIdContext);


 const queryClient = useQueryClient();


 const {
   data: videosData,
   refetch: refetchVideos,
   isPreviousData,
 } = useGetVideos(currIndex, vidPage, VID_PAGE_LIMIT);
 
 const videos = videosData?.data;


 const { data: authors, refetch: refetchAuthors } =
   useGetAllAuthors(currIndex);


 function reset() {
   setSearchQuery("");
   setFinalSearchQuery("");
 }


 useEffect(() => {
   queryClient.invalidateQueries({
     queryKey: [keys.VIDEOS, currIndex, vidPage],
   });
 }, [taskVideos, currIndex, vidPage]);


 useEffect(() => {
   queryClient.invalidateQueries({
     queryKey: [keys.AUTHORS, currIndex],
   });
 }, [videos, currIndex]);

VideoComponentsにおいて、動画や著者の情報を取得するためのクエリが作成されていることがわかります。これには、データを最新に保つために各クエリを無効化(Invalidate)することも含まれます。状態(State)であるsearchQuery、finalSearchQuery、isSubmittingは、UploadYouTubeVideoやSearchResultsなどの子コンポーネントと共有されるため、ここで設定されます。

UploadYouTubeVideo のレンダリング

デフォルトとして、各VideoIndexは、YouTubeプレイリスト、YouTubeチャンネル、またはビデオURLから成るJSONファイルを介して、一括アップロードを可能にするUploadYouTubeVideoを表示します。UploadYouTubeVideoの詳細は後ほどカバーします。

videoComponents.js (69行目 - 82行目)

return (
	<>
		<div className="videoUploadForm">
			<div className="display-6 m-4">新しい動画のアップロード>
			<UploadYoutubeVideo
     		currIndex={currIndex}
     		taskVideos={taskVideos}
     		setTaskVideos={setTaskVideos}
     		refetchVideos={refetchVideos}
     		isSubmitting={isSubmitting}
     		setIsSubmitting={setIsSubmitting}
     		reset={reset}
			&sol;>
		<

SearchForm と VideoList のレンダリング

インデックス内に既に動画がある場合は、UploadYoutubeVideoコンポーネントに加えて、動画検索フォームも表示される必要があります。finalSearchQueryがない場合(動画検索がまだ実行されていないことを示します)、PageNavコンポーネントを利用して、動画リストのみが1ページあたり12個ずつ表示されます。VideoListの詳細はこの後すぐ紹介します。

videoComponents.js (97行目 - 162行目)

export function VideoComponents({ ... }) {
  return (
    ...
    {videos && videos.length > 0 && (
      <div>
        <div className="videoSearchForm">
          <div className="title">動画を検索<&sol;div>
          {/* <div className="m-auto p-3"> */}
          <SearchForm
            setSearchQuery={setSearchQuery}
            searchQuery={searchQuery}
            setFinalSearchQuery={setFinalSearchQuery}
          />
          {/* <&sol;div> */}
        <&sol;div>

        {!finalSearchQuery && (
          <div>
            <div className="channelPills">
              <ErrorBoundary
                FallbackComponent={({ error }) => (
                  <ErrorFallback error={error} />
                )}
                onReset={() => refetchAuthors()}
                resetKeys={[keys.AUTHORS, currIndex]}
              >
                <div className="subtitle">
                  インデックス内のすべてのインフルエンサー ({authors?.length || 0}){" "}
                <&sol;div>
                {authors.map((author) => (
                  <div key={author} className="channelPill">
                    <Suspense fallback={<LoadingSpinner />}>
                      {author}
                    <&sol;Suspense>
                  <&sol;div>
                ))}
              <&sol;ErrorBoundary>
            <&sol;div>

            <Container fluid className="mb-2">
              <Row>
                <ErrorBoundary
                  FallbackComponent={({ error }) => (
                    <ErrorFallback error={error} />
                  )}
                  onReset={() => refetchVideos()}
                  resetKeys={[keys.VIDEOS, currIndex, vidPage]}
                >
                  {videos && (
                    <Suspense fallback={<LoadingSpinner />}>
                      <VideoList
                        videos={videos}
                        refetchVideos={refetchVideos}
                      />
                    <&sol;Suspense>
                  )}
                  <Container fluid className="d-flex justify-content-center">
                    <PageNav
                      page={vidPage}
                      setPage={setVidPage}
                      data={videosData}
                      isPreviousData={isPreviousData}
                    />
                  <&sol;Container>
                <&sol;ErrorBoundary>
              <&sol;Row>
            <&sol;Container>
          <

検索フォームと動画リスト

SearchResults のレンダリング

もし finalSearchQuery が存在する場合(動画検索が行われた際)、動画リストの代わりに検索結果がレンダリングされます。

VideoComponents.js (164行目 - 186行目)

{finalSearchQuery AND (
           <div>
             <Container fluid className="m-3">
               <Row>
                 <SearchResults
                   currIndex={currIndex}
                   allAuthors={authors}
                   finalSearchQuery={finalSearchQuery}
                 &sol;>
               <&sol;Row>
             <&sol;Container>
             <div className="resetButtonWrapper">
               <button className="resetButton" onClick={reset}>
                 {backIcon AND (
                   <img src={backIcon} alt="Icon" className="icon" >
                 )}
                 &nbsp;すべての動画に戻る
               <&sol;button>
             <&sol;div>
           <&sol;div>
         )}
       <

ご興味があれば、どのように検索結果をYouTubeチャンネルごとにグループ化するかの詳細をご覧ください。検索結果は、それらに含まれていないチャンネルも示すのがこのアプリの特徴的な部分でもあります!

検索結果

3.2 - UploadYoutubeVideo.js

UploadYoutubeVideoは、VideoComponents.jsの一部としてレンダリングされる主要なコンテナコンポーネントであり、YouTube動画をアップロードしてインデックス登録する役割を担います。また、ユーザーがアップロードリクエストを送信した直後にタスク動画表示を行ったり、各タスクの進行状況を示したりします。 

そのため、選択したJSONファイル、YouTubeチャンネル / プレイリストID、インデックス登録のステータスを含むタスクIDを保存するための重要ないくつかの状態を管理します。さらに、ファイル選択、コンポーネントの状態のリセット、またはビデオデータを取得・インデックス登録するためのAPIリクエストの送信などの関数を提供します。コア関数の1つである、indexYouTubeVideos関数について詳細を見ていきましょう。

UploadYoutubeVideo.js (155行目 - 188行目)

const indexYouTubeVideos = async () => {
   setIsSubmitting(true);
   updateMainMessage(
     "動画のアップロード中はページをリフレッシュしないでください。アップロード中も検索を行うことはできます!"
   );


   const videoData = taskVideos.map((taskVideo) => {
     return {
       url: taskVideo.video_url || taskVideo.url,
       title: taskVideo.title,
       authorName: taskVideo.author.name,
       thumbnails: taskVideo.thumbnails,
     };
   });


   const requestData = {
     videoData: videoData,
     index_id: currIndex,
   };

   const data = {
     method: "POST",
     headers: {
       Accept: "application/json",
       "Content-Type": "application/json",
     },
     body: JSON.stringify(requestData),
   };


   const response = await fetch(DOWNLOAD_URL.toString(), data);
   const json = await response.json();
   setIndexId(json.indexId);
   setTaskIds(json.taskIds);
 };

indexYouTubeVideosは、YouTube動画のインデックス作成プロセスを調整します。最初に、ユーザーにページをリフレッシュしないよう指示するメッセージを表示して開始します。そして目標となるタスク動画のURL、タイトル、著者名、サムネイルをマッピングしたオブジェクトとしてビデオデータを用意します。

次に、このビデオデータとインデックスIDを含むリクエストペイロードを構築します。ダウンロードURL(ステップ 2. YouTubeのURLを使って動画をアップロードするで確認済みです)に対してPOSTリクエストを送信し、JSONレスポンスを待ちます。レスポンスには、タスクIDとインデックスIDが含まれています。これに従って、taskIdsとindexIdの状態が更新されます。

UploadYouTubeVideoには、多くの状態や関数を持つ他のサブコンポーネントがあります。ここではその詳細すべてをカバーしませんが、ぜひ深く掘り下げて探求してみてください。質問等があればお気軽にどうぞ!

4 - プレゼンテーションコンポーネントの構築

これで難しい部分は完了し、最後にプレゼンテーションコンポーネントの構築をして仕上げです。プレゼンテーションコンポーネントは、VideoComponentsから渡されたタスク動画や動画に基づき、動画プレイヤーをシンプルにレンダリングするものです。このアプリでは、VideoListとTaskVideoをプレゼンテーションコンポーネントと呼ぶのが良いでしょう。以下、VideoListを例として見てみます。

VideoList.js (16行目 - 51行目)

function VideoList({ videos, refetchVideos }) {
 	const numVideos = videos.length;
 
  return videos.map((video, index) => (
   <ErrorBoundary
     FallbackComponent={ErrorFallback}
     onReset={() => refetchVideos()}
     resetKeys={[keys.VIDEOS]}
     key={video._id + "-" + index}
   >
     <Suspense>
       <Col
         sm={12}
         md={numVideos > 1 ? 6 : 12}
         lg={numVideos > 1 ? 4 : 12}
         xl={numVideos > 1 ? 3 : 12}
         className="mb-5 mt-3"
       >
         {" "}
         <ReactPlayer
           url={video.metadata.youtubeUrl}
           controls
           width="100%"
           height="250px"
         &sol;>
         <div className="channelAndVideoName">
           <div className="channelPillSmall">{video.metadata.author}<&sol;div>
           <div className="filename-text">
             {video.metadata.filename.replace(".mp4", "")}
           <&sol;div>
         <&sol;div>
       <&sol;Col>
     <&sol;Suspense>
   <

これは、動画をマッピングしてReactPlayerを介してそのプレイヤーを描画し、各動画の著者(インフルエンサー)とファイル名を表示します。実際の様子は、上の「SearchFormとVideoListのレンダリング」の画像セクションで確かめることができます。

まとめ

この記事が、Twelve Labsの動画検索APIと特定の状況でのその具体的な使用方法についての役立つインサイトをご提供できていれば幸いです。これは数ある利用ケースの1つに過ぎず、皆様のチームに合ったソリューションを構築する自由があります。開発を楽しんでください!

次のステップ

イントロダクション

美容業界のマーケティングプロフェッショナルとしてインフルエンサーシップに長年関わってきた中で、ブランドにとって理想的なYouTubeやTikTokのインフルエンサーを選定する上での貴重な教訓を学びました。(そうです、私はマーケティングプロフェッショナルからソフトウェアエンジニアに転身したのです 😉)最も成功するコラボレーションは、すでにあなたの製品やブランドを心から愛用しているインフルエンサーと、オーガニックに発生する傾向があります。たとえば、あなたがブランド「A」を宣伝している場合、事前のコンタクトがなくても「A」について言及しているインフルエンサーを見つけることができるかもしれません。こちらからアプローチした際、彼らは通常、一緒に取り組むことに対して非常に好意的です。

しかし、これらのインフルエンサーを見つけ出すのは少々難しい場合があります。特に、動画のタイトルや説明欄にあなたのブランド名が明示的に記載されていない場合はなおさらです。これは私自身にとっても悩みの種でした。たとえば、あるYouTuberが「冬のマストハブアイテムTop 10」のような動画の中で、ブランド名を挙げずにあなたの製品を紹介していた場合、YouTubeでの通常のキーワード検索では見つけることができません。

そこで、Twelve Labs APIを活用した「Who Talked About Us(誰が私たちについて話したか)」の出番です。通常のYouTubeやTikTokの検索とは異なり、このAPIは高度な文脈依存の動画検索を可能にします。タイトルや説明欄に頼る代わりに、APIは動画から動き、オブジェクト、人物、音、画面上のテキスト、そして音声など、さまざまな要素を抽出します。キーワードや、「MACのゴールドハイライターを使用」といった具体的な説明を入力するだけで、あなたのブランドや製品について言及している動画やチャンネルを、その言及が飛び出す正確な瞬間とともに発見することができます。 

これにより、アプローチしたいインフルエンサーのリストを、彼らが言及した製品の詳細や文脈とともに作成できます。これにより、これら潜在的なインフルエンサーに関する貴重なインサイトが得られ、より効果的にアプローチし、有意義なつながりを築くことが可能になります。 

それでは、Twelve Labs APIのパワーを活用したアプリを作成するためのステップバイステップの道のりを始めましょう!

前提条件

  • Twelve Labs APIの世界に飛び込む前に、まずはサインアップしてAPIキーを生成する必要があります。Twelve Labs Playgroundにアクセスしてサインアップし、APIキーを生成してください。サインアップすると、最大10時間の動画コンテンツをインデックス登録できる無料クレジットが提供されます!

  • このアプリに必要なすべてのファイルが含まれているリポジトリは、Githubで公開されています。

  • このアプリは、JavaScript、Node、React、およびReact Queryを使用して構築されています。これらの技術に馴染みがあると理解しやすいですが、そうでなくても心配はいりません。この記事から得られる最も重要な成果は、Twelve Labs APIについて学び、このアプリがそれをどのように活用しているかを理解することです。

1 - コンポーネントのデザイン

このアプリはReactを使用して構築されており、Reactの本質は再利用可能なコンポーネントに分解することにあります。そのため、まずはコンポーネントのデザインから開始し、もちろん、何度も修正を重ねました。



大局的に見ると、このアプリはExistingIndexForm、IndexForm、そしてVideoIndexで構成されています。IndexFormは、ユーザーが新しいインデックスを作成できるシンプルなフォームです。ExistingIndexFormは、ユーザーが以前このアプリで作成したインデックスのIDを送信できるもう一つのシンプルなフォームです。VideoIndexは、インデックスの詳細を取得する呼び出しが行われ、VideoComponentsが存在する場所です。

VideoComponentsは、動画に関連するすべてのコンポーネントで構成されています。UploadYoutubeVideoコンポーネントは、動画のダウンロード/インデックス登録を可能にし、各動画タスクのステータスを表示します。VideoListは、インデックスの動画をPageNavとともにレンダリングし、各ページに12個ずつフェッチして表示します。SearchFormは、ユーザーの入力(検索クエリ)を受け取り、検索API呼び出しに渡す動画検索を処理するコンポーネントです。SearchResultsは検索結果と動画のAPIを呼び出し、インフルエンサーごとに整理します。その後、SearchResultが各検索結果をユーザーフレンドリーな方法で表示します。

2 - サーバーとAPIフックの構築

Server.jsとapiHooks.jsは、Twelve Labs APIやytdl-coreなどの他のライブラリからのすべてのAPI呼び出しを管理するファイルです。Server.jsは、Twelve Labs APIを呼び出すためのすべてのエンドポイントや、その他のAPI呼び出しが存在する場所です。apiHooks.jsは、状態、キャッシュ、およびデータフェッチを管理するためのカスタムReact Queryフックのセットです。サーバーの、ひいてはアプリ全体の主要機能であるTwelve Labs APIを使用した動画検索について、その使用方法を詳しく見ていきましょう。

Twelve Labs APIを使用する4つのステップ

最初のステップは、動画用のインデックスを作成することです。次に、このインデックスに動画をアップロードします。続いて、動画のメタデータを更新して、各動画にYouTubeチャンネルとURLを追加します(このステップはこのアプリ固有のもので、一般的にはオプションです)。最後に、動画検索を行う準備が整います。このアプリでは、すべてのAPI呼び出し(Twelve Labsおよびその他)と動画アップロードに関連する関数をserver.jsファイルに整理しました。

セットアップ

ルートディレクトリに.envファイルを作成し、必要に応じて値を更新します。以下をコピー&ペーストして値をカスタマイズするだけでも可能です。

.env

REACT_APP_API_URL=https://api.twelvelabs.io/v1.1
REACT_APP_API_KEY=<YOUR API KEY>
REACT_APP_SERVER_URL=<YOUR SERVER URL>
REACT_APP_PORT_NUMBER=<YOUR PORT NUMBER>



  • REACT_APP_API_URL: このアプリはv1.1をサポートしています

  • REACT_APP_API_KEY: 前のステップで生成したAPIキーを保存します

  • REACT_APP_SERVER_URL: 「http://localhost」のようになる可能性があります

  • REACT_APP_PORT_NUMBER: 使用したいポート番号を設定します(例: 4001)

必要なファイル内で環境変数の値にアクセスするには、process.envを利用できます。たとえば、server.jsファイルでは、process.env.REACT_APP_APP_URLを使用してAPI_URLにアクセスし、保存できます。次の例は、これを実現する方法を示しています。

server.js (15行目 - 19行目)

/** 定数を定義し、TL APIエンドポイントを設定する */
const TWELVE_LABS_API_KEY = process.env.REACT_APP_API_KEY;
const API_BASE_URL = process.env.REACT_APP_API_URL;
const TWELVE_LABS_API = axios.create({ baseURL: API_BASE_URL });
const PORT_NUMBER = process.env.REACT_APP_PORT_NUMBER;

ステップ 1. インデックスの作成

インデックスとは、動画をアップロード、インデックス登録、検索できる動画ライブラリのようなものです。日付、テーマ、またはYouTubeチャンネルごとに独自のインデックスを作成できます。インデックスを作成するには、メソッドを「POST」、エンドポイントを「https://api.twelvelabs.io/v1.1/indexes」に設定し、ヘッダーを追加して、engine_id、index_options、index_nameなどの必要なデータを提供するだけです。index_optionsについては、4つのオプションのサブセットを選択できます。このアプリでは、4つのオプションすべてが含まれています。

💡インデックス作成の詳細については、APIリファレンスを確認してください

server.js (98行目 - 119行目)

/** インデックスを作成する */
app.post("/indexes", async (request, response, next) => {
 const headers = {
   "Content-Type": "application/json",
   "x-api-key": TWELVE_LABS_API_KEY,
 };


 const data = {
   engine_id: "marengo2.5",
   index_options: ["visual", "conversation", "text_in_video", "logo"],
   index_name: request.body.indexName,
 };


 try {
   const apiResponse = await TWELVE_LABS_API.post("/indexes", data, {
     headers,
   });
   response.json(apiResponse.data);
 } catch (error) {
   console.error("サーバー側のエラー:", error);
   response.json({ error });
 }
});

ステップ 2. YouTubeのURLを使って動画をアップロードする

このアプリは、チャンネルIDプレイリストID、または以下のようなURLオブジェクトの配列を持つJSONファイルを介した、YouTube動画の一括アップロードをサポートしています。

example.json

[
     { "url": "VIDEO URL" },
     { "url": "VIDEO URL" }
     ...
    ]

💡YouTubeのURLによる動画のアップロードは、Twelve Labs API v1.2で利用可能になりました。このアプリはv1.1を使用しているため、YouTubeから動画をダウンロードできるライブラリであるytdl-coreを利用して、手動で実装しています。

プロセスとしては、提供されたYouTube URLから動画をダウンロードし、Twelve Labs APIに送信して動画をインデックス登録します。これは、server.jsのエンドポイント「/download」に実装されています。まず、リクエストのボディから動画データとインデックス情報を抽出します。次に、動画をチャンクに分けてダウンロードし、安全なファイル名にするためにタイトルをサニタイズして、インデックス登録用に送信します。すべての動画がダウンロードされ、インデックス登録された後、サーバーはタスクIDとインデックスIDを返します。各ステップを分解して詳しく見ていきましょう。

1 - リクエストから情報を抽出する

最初のステップは、リクエストのボディから動画データとインデックス情報を抽出することです。動画の総数、処理された動画の数、および動画のダウンロードとインデックス登録のチャンクサイズ(このアプリでは5に設定)を追跡するための変数を設定します。また、動画インデックス登録プロセスからのレスポンスを格納するための配列を初期化します。

server.js (397行目 - 409行目)

/** 分析用の動画をダウンロードし、インデックス登録して、タスクIDとインデックスIDを返す */
app.post(
 "/download",
 bodyParser.urlencoded(),
 async (request, response, next) => {
   try {
     // ステップ 1: リクエストから動画データとインデックス情報を抽出する
     const jsonVideos = request.body.videoData;
     const totalVideos = jsonVideos.length;
     let processedVideosCount = 0;
     const chunk_size = 5;
     let videoIndexingResponses = [];
     console.log("動画をダウンロード中...");
2 - 動画をチャンクに分けてダウンロードする

次のステップは、動画をチャンクに分けてダウンロードすることです。各チャンクについて、動画データを反復処理し、ytdl-coreライブラリを使用して提供されたURLから動画をダウンロードします。安全なファイル名を作成するために、動画のタイトルはサニタイズされます。動画のダウンロードが完了すると、その進捗が記録されます。

💡ここでは、ダウンロードした動画を「videos」フォルダに保存するためのvideoPathを設定しています。

server.js (411行目 - 442行目)

// ステップ 2: 動画をチャンクに分けてダウンロードする
     for (let i = 0; i < totalVideos; i += chunk_size) {
       const videoChunk = jsonVideos.slice(i, i + chunk_size);
       const chunkDownloadedVideos = [];


       // 現在のチャンク内の各動画をダウンロードする。
       await Promise.all(
         videoChunk.map(async (videoData) => {
           try {
             // ダウンロードした動画の安全なファイル名を生成する
             const safeName = sanitize(videoData.title);
             const videoPath = `videos/${safeName}.mp4`;


             // 提供されたURLから動画をダウンロードする
             const stream = ytdl(videoData.url, {
               filter: "videoandaudio",
               format: ".mp4",
             });
             await streamPipeline(stream, fs.createWriteStream(videoPath));


             console.log(`${videoPath} -- ダウンロード完了`);


             chunkDownloadedVideos.push({
               videoPath: videoPath,
               videoData: videoData,
             });
           } catch (error) {
             console.log(`${videoData.title} のダウンロード中にエラーが発生しました`);
             console.error(error);
           }
         })
       );
3 - インデックス登録用に動画を送信する

動画のチャンクをダウンロードした後、これらのダウンロードした動画をインデックス登録用に送信します。すべてのインデックス登録タスクが完了するのを待ち、インデックス登録送信の進捗を記録します。

server.js (444行目 - 481行目)

// ステップ 3: ダウンロードした動画をインデックス登録用に送信する
       console.log(
         `インデックス登録用に動画を送信中 | チャンク ${
           Math.floor(i / chunk_size) + 1
         }`
       );


       const chunkVideoIndexingResponses = await Promise.all(
         chunkDownloadedVideos.map(async (chunkDownloadedVideo) => {
           console.log(
             `インデックス登録用に ${chunkDownloadedVideo.videoPath} を送信中...`
           );
           const indexingResponse = await indexVideo(
             chunkDownloadedVideo.videoPath,
             request.body.index_id
           );


           // indexingResponse に videoData を追加する
           indexingResponse.videoData = chunkDownloadedVideo.videoData;


           return indexingResponse;
         })
       ).catch(next);
       
       console.log("チャンクのインデックス送信が完了しました | タスクID:");


       processedVideosCount += videoChunk.length;


       console.log(
         `${totalVideos} 本中 ${processedVideosCount} 本の動画を処理しました`
       );


       videoIndexingResponses = videoIndexingResponses.concat(
         chunkVideoIndexingResponses
       );


       await new Promise((resolve) => setTimeout(resolve, 1000));
     }

また、indexVideo関数も簡単に見ておきましょう。動画のインデックス登録自体は非常にシンプルです。他のAPI呼び出しと同様に、プロセスにはTwelve Labs APIへのPOSTリクエストでのインデックス登録タスクの開始が含まれます。このリクエストには、特定のパラメータ(ターゲットインデックスを指定するindex_id、ビデオファイル(この場合はビデオパスからストリームされたビデオデータ)、および言語設定(英語の場合は'en'))が含まれます。

💡動画のインデックス登録(動画インデックス登録タスクの作成)の詳細については、APIリファレンスを確認してください

server.js (330行目 - 349行目)

/** ダウンロードした動画を受け取り、インデックス登録プロセスを開始する */
const indexVideo = async (videoPath, indexId) => {
 const headers = {
   headers: {
     accept: "application/json",
     "Content-Type": "multipart/form-data",
     "x-api-key": TWELVE_LABS_API_KEY,
   },
 };


 let params = {
   index_id: indexId,
   video_file: fs.createReadStream(videoPath),
   language: "en",
 };


 const response = await TWELVE_LABS_API.post("/tasks", params, headers);


 return await response.data;
};

この呼び出しからのresponse.dataは、以下のようなタスクIDを返します。以前のコードスニペットで見たように、各タスクIDはindexingResponseに保存されます。

response.data

{
	"_id": "6527732e23c1347ffbe3a802"
}

ただし、response.dataの他に、後で各動画のメタデータに追加するためにビデオデータが必要になるため、indexingResponseにvideoDataを追加しています。したがって、各最終indexingResponseは以下のようになり、videoIndexingResponsesに結合されます。

videoIndexingResponses

[
  {
    _id: '6527732e23c1347ffbe3a802',
    videoData: {
      url: 'https://www.youtube.com/watch?v=WuWnt2NHJxk&list=PLI8sddTC_LM0SG6JMYmrbrHZy7j5zUjMA&index=4&pp=iAQB',
      title: 'Everyday Makeup Using ONLY 3 Products !!',
      authorName: 'Smitha Deepak',
      thumbnails: [Array]
    }
  },
  
]
4 - タスクIDを返す

すべての動画がダウンロードされ、インデックス登録用に送信されると、タスクIDのオブジェクト(上記の配列)とインデックスIDをクライアントに返します。

server.js (483行目 - 498行目)

// ステップ 4: インデックスタスクのタスクIDとインデックスIDを返す
     console.log(
       "すべての動画のインデックス送信が完了しました。タスクID:"
     );


     console.log(videoIndexingResponses);


     response.json({
       taskIds: videoIndexingResponses,
       indexId: request.body.index_id,
     });
   } catch (error) {
     next(error);
   }
 }
);

ステップ 3. 動画のメタデータを更新する

一般的に、このステップはオプションですが、このアプリでは動画をYouTubeのURLで表示し、チャンネル/インフルエンサーごとに分類するために必要です。

アプリでの動画の表示方法

ご覧の通り、アプリは各動画の薄緑色のカプセル型バッジにチャンネル名を表示します。また、動画はTwelve LabsサーバーからのビデオURLではなく、YouTube URLを使用してReact Playerを介して読み込まれます。動画のデフォルトメタデータにはチャンネル名とYouTube URLが含まれないため、この情報を各動画のメタデータに追加する必要があります。 

以下は、動画のデフォルトメタデータの例です。

"metadata": {
		"duration": 188.6,
		"filename": "3 MINUTE MAKEUP THAT WILL LAST ALL DAY.mp4",
		"fps": 25,
		"height": 720,
		"size": 40566200,
		"video_title": "3 MINUTE MAKEUP THAT WILL LAST ALL DAY.mp4",
		"width": 1280,
		}

任意のキーと値のペアを追加したり、既存のメタデータを更新したりできます。このアプリでは、メタデータに3つの追加データを追加しています:1) 著者(チャンネル名)、2) YouTube URL、および 3) whoTalkedAboutUsのブール値(動画がアプリを介してアップロードされたかどうかを示すマーク)。 

この作業はserver.jsファイルで行われ、そこには動画メタデータの更新を処理するエンドポイントがあります。ここでは、追加または更新するデータを含むPUTリクエストが行われます。

💡動画情報の更新の詳細については、APIリファレンスを確認してください

server.js (290行目 - 310行目)

/** 動画のメタデータを更新する */
app.put("/update/:indexId/:videoId", async (request, response, next) => {
 const indexId = request.params.indexId;
 const videoId = request.params.videoId;
 const data = request.body;
 const headers = {
   "Content-Type": "application/json",
   "x-api-key": TWELVE_LABS_API_KEY,
 };


 try {
   const apiResponse = await TWELVE_LABS_API.put(
     `/indexes/${indexId}/videos/${videoId}`,
     data,
     { headers }
   );
   response.json(apiResponse.data);
 } catch (error) {
   return next(error);
 }
});

次に、これがUploadYouTubeVideo.jsファイルでどのように実装されているかを見てみましょう。

UploadYouTubeVideo.js (190行目 - 223行目)

async function updateMetadata() {
   const updatePromises = completeTasks.map(async (completeTask) => {
     const matchingVid = taskVideos.find(
       (taskVid) =>
         `${sanitize(taskVid.title)}.mp4` === completeTask.metadata.filename
     );
     if (matchingVid) {
       const authorName = matchingVid.author.name;
       const youtubeUrl = matchingVid.video_url || matchingVid.shortUrl;
       const data = {
         metadata: {
           author: authorName,
           youtubeUrl: youtubeUrl,
           whoTalkedAboutUs: true,
         },
       };
       try {
         await fetch(
           `${UPDATE_VIDEO_URL}/${currIndex}/${completeTask.video_id}`,
           {
             method: "PUT",
             headers: {
               "Content-Type": "application/json",
             },
             body: JSON.stringify(data),
           }
         );
       } catch (error) {
         console.error(error);
       }
     }
   });
   await Promise.all(updatePromises);
 }

updateMetadata関数は、すべてのタスク動画から一致する完了済みのタスク動画を見つけます。一致するごとに、著者名とYouTube URLを抽出し、カスタムメタデータを構築します。すべての動画に対して、whoTalkedAboutUsキーの値はtrueに設定されます。その後、サーバーにフェッチしてこれらの変更を適用します。

💡 提供するデータは「metadata」キーを持つオブジェクトの形式である必要があり、ここでキーと値のペアを追加または変更して、ビデオデータをパーソナライズすることができます。

このプロセスの後、動画メタデータには、新しく追加された「author」、「youtubeUrl」、および「whoTalkedAboutUs」が誇らしげに表示されます!全体として、メタデータの更新は、動画コレクションに自分だけの印を付けるための素晴らしい方法です。

"metadata": {
		"author": "Justine Feather", //追加されました!
		"duration": 188.6,
		"filename": "3 MINUTE MAKEUP THAT WILL LAST ALL DAY.mp4",
		"fps": 25,
		"height": 720,
		"size": 40566200,
		"video_title": "3 MINUTE MAKEUP THAT WILL LAST ALL DAY.mp4",
		"whoTalkedAboutUs": true, //追加されました!
		"width": 1280,
		"youtubeUrl": "https://www.youtube.com/watch?v=aYQWSNAL4D8" //追加されました!
		}

ステップ 4. 動画を検索する

ついに、お待ちかねの動画検索の瞬間です!インデックス内のインデックス登録済み動画から検索を行うことができます。

このアプリでは、検索結果を表示する際にページネーションが実装されています。したがって、検索結果の取得と表示は、POSTリクエストを行って最初の検索結果を取得することと、次のページのトークンを使用してGETリクエストを行って以降の検索結果ページをフェッチすることの2つの部分で構成されます。

検索POSTリクエストを行う

最初の検索結果を取得するには、server.jsファイルにある検索エンドポイントにPOSTリクエストを行う必要があります。リクエストからindexIdとqueryを取得し、TwelveLabs API用に「/search」へのPOSTリクエストを行います。

「search_options」には、「visual」、「conversation」、「text_in_video」、「logo」の4つすべてを含めました。また、「threshold」、「sort_option」、「group_by」といった複数のオプションも適用しています。これらのオプションすべてを使用して、信頼度が「中」および「高」の結果をフィルタリングし、動画ごとにグループ化し、クリップ数で並べ替えています。

💡検索リクエストの送信に関する詳細については、APIリファレンスを確認してください

server.js (217行目 - 243行目)

/** 与えられたクエリで動画を検索する */
app.post("/search", async (request, response, next) => {
 const headers = {
   accept: "application/json",
   "Content-Type": "application/json",
   "x-api-key": TWELVE_LABS_API_KEY,
 };


 const data = {
   index_id: request.body.indexId,
   search_options: ["visual", "conversation", "text_in_video", "logo"],
   query: request.body.query,
   group_by: "video",
   sort_option: "clip_count",
   threshold: "medium",
   page_limit: 2,
 };


 try {
   const apiResponse = await TWELVE_LABS_API.post("/search", data, {
     headers,
   });
   response.json(apiResponse.data);
 } catch (error) {
   return next(error);
 }
});

「/search」エンドポイントが設定されたら、useGetVideosOfSearchResultsが機能します。これは、React Queryフックを使用して「ビデオ検索」と「ビデオ取得」の機能を同時に動かすReact Queryフックです。これはSearchResults.jsにインポートされて実行され、クエリレスポンスからネクストページトークン、検索結果、および検索結果動画が抽出されます。

SearchResults.js (25行目 - 33行目)

/** 初期の検索結果と対応する動画を取得する */
 const {
   initialSearchData: {
     page_info: { next_page_token: initialNextPageToken } = {},
   } = {},
   initialSearchResults,
   initialSearchResultVideos,
   refetch,
 } = useGetVideosOfSearchResults(currIndex, finalSearchQuery);

useGetVideosOfSearchResults関数は、apiHooks.jsで定義されています。上述の通り、useQueriesフックを使用して複数のクエリを同時に実行します。最初の検索結果を取得し、次に各々の初期検索結果と一致する動画の情報を取得します。

apiHooks.js (152行目 - 177行目)

export function useGetVideosOfSearchResults(indexId, query) {
 const {
   data: initialSearchData,
   refetch,
   isLoading,
 } = useSearchVideo(indexId, query);
 const initialSearchResults = initialSearchData.data || [];


 const resultVideos = useQueries({
   queries: initialSearchResults.map((searchResult) => ({
     queryKey: [keys.SEARCH, indexId, searchResult.id],
     queryFn: () =>
       apiConfig.TWELVE_LABS_API.get(
         `${apiConfig.INDEXES_URL}/${indexId}/videos/${searchResult.id}`
       ).then((res) => res.data),
   })),
 });
 const initialSearchResultVideos = resultVideos.map(({ data }) => data);
 return {
   initialSearchData,
   initialSearchResults,
   initialSearchResultVideos,
   refetch,
   isLoading,
 };
}

ビデオ検索の結果を取得するために、まずuseSearchVideoフックが呼び出されているのがわかります。

apiHooks.js (93行目 - 102行目)

export function useSearchVideo(indexId, query) {
 return useQuery({
   queryKey: [keys.SEARCH, indexId, query],
   queryFn: () =>
     apiConfig.TWELVE_LABS_API.post(apiConfig.SEARCH_URL, {
       indexId,
       query,
     }).then((res) => res.data),
 });
}

APIフックの呼び出しの結果として取得するデータ(initialSearchResults と initialSearchResultVideos)は、以下のようになります。

initialSearchResults

[
	
 {clips: 
[
  {score: 76.1, start: 21.15625, end: 37.15625, metadata: Array(1),  
   video_id: '65651c021cbcab2e74d86eb3', confidence: “medium”,   
   modules: [{...}]},
   {},
    
], 
 id: '65651c021cbcab2e74d86eb3'}, 
 {clips: Array(6), id:... },
  
]

各initialSearchResultは、検索結果を含む「clips」と、該当する検索結果のビデオIDである「id」で構成されています。 

initialSearchResultVideos

[
	{
	     “created_at”: "2023-12-07T18:32:01Z"
     “hls”: {...},
           “indexed_at”: "2023-12-07T18:34:06Z",
           “metadata”: {...}, 
           “Updated_at”: "2023-12-07T18:35:01Z",
           “_id”: "65720fa11cbcab2e74d87aab"
	}, 
      
]

initialSearchResultVideosは、対応する検索結果のメタデータを含むビデオオブジェクトで構成されています。 

これで、最初の検索結果とそれに対応する動画が取得できました。ただし、次のページ用のトークン(next_page_token)がある場合は、さらに検索結果が存在することを意味します。このため、すべての検索結果と動画を一元化して保存するために、追加の状態(combinedSearchResults と combinedSearchResultVideos)を設定しています。特にこのアプリでは結果をインフルエンサー(YouTubeチャンネル)ごとに整理して表示するため、まずデータを結合し、整理して、ユーザーに提示するためにこれらの状態が必要不可欠です。 

それでは、次のページの検索結果をフェッチする方法について見ていきましょう。

検索GETリクエストを行う

初期の検索データから次のページのトークン(next_page_token)を取得したことを覚えていますか?そのトークンを使用して追加の検索結果を取得するGETリクエストを行うことができます。検索結果にnext_page_tokenが含まれている限り、すべての検索結果を漏れなく収集するためにリクエストを送り続ける必要があります。

SearchResults.js (50行目 - 53行目)

const nextPageResultsData = await fetchNextPageSearchResults(
       queryClient,
       nextPageToken
     );

fetchNextPageSearchResults は、Twelve Labs APIの「/search」エンドポイントにGETリクエストを呼び出してデータを取得します。ユーザーがボタンをクリックしたときにのみ条件付きでデータをフェッチするため、ここではfetchQueryを使用しました。

apiHooks.js (179行目 - 196行目)

export async function fetchNextPageSearchResults(queryClient, nextPageToken) {
 try {
   const response = await queryClient.fetchQuery({
     queryKey: [keys.SEARCH, nextPageToken],
     queryFn: async () => {
       const response = await apiConfig.TWELVE_LABS_API.get(
         `${apiConfig.SEARCH_URL}/${nextPageToken}`
       );
       const data = response.data;
       return data;
     },
   });
   return response;
 } catch (error) {
   console.error("検索結果の次のページの取得中にエラーが発生しました:", error);
   throw error;
 }
}

検索結果に基づき、最初の検索結果で行った作業と同様に、fetchNextpageSearchResultVideosを使用して対応する映像データを取得します。最後に、それらはcombinedSearchResultsとcombinedSearchResultVideosに追加されます。

combinedSearchResultsとcombinedSearchResultVideosが変化するたびに、結果が再整理されます(関連コードはこちら)。整理された検索結果は、その後SearchResultコンポーネントを通じてレンダリングされます。

💡 検索結果のページネーションコントロールのさらなる詳細については、APIガイドラインを確認してください!

これで、必要なすべてのデータにアクセスするためのサーバーとAPIフックが完成しました。次に、データを取得、操作、そしてユーザーに表示するためのコンポーネントを構築できます。次のステップでは、これらすべてのコンポーネントがどのように組み合わさって、強力なインフルエンサー検索アプリケーションを作成するかを見ていきます。お楽しみに!

3 - コンテナコンポーネントの構築

コンポーネント構築のプロセスにおいては、コンテナコンポーネント(Container Components)から作り始めるのが簡単です。プレゼンテーションコンポーネント(Presentation Components)は、コンテナコンポーネントからのAPIレスポンスや状態(State)に依存することが多いためです。 

💡 コンテナコンポーネントおよびプレゼンテーションコンポーネントという用語に馴染みがないかもしれませんが、これらはReactの文脈でよく使われる用語です。コンテナコンポーネント(スマートコンポーネントとも呼ばれます)は、データのフェッチや状態変化などのアプリのロジックやデータフローを処理します。プレゼンテーションコンポーネント(ダムコンポーネントとも呼ばれます)は、propsを介してコンテナコンポーネントから機能を受け取り、ユーザーインターフェースの描画やデータの表示に集中します。

3.1 - VideoComponent.js

VideoComponentsはアプリの中心的な存在であり、動画のアップロード、動画検索、インデックス内の動画とYouTubeチャンネル名の表示など、さまざまな機能を提供します。ユーザーは簡単に新しい動画をアップロードし、検索を実行し、ページ分けされた動画ライブラリビューにアクセスできます。また、必要に応じてインデックスを削除することもできます。どのように機能するのか見てみましょう。

videoComponents.js (21行目 - 67行目)

/** 動画とのやり取りを含むコンポーネント
*
*  VideoIndex -> VideoComponents -> { UploadYouTubeVideo, VideoList, PageNav,
*  SearchForm, SearchResults }
*
*/


export function VideoComponents({
 currIndex,
 vidPage,
 setVidPage,
 taskVideos,
 setTaskVideos,
}) {
 const [searchQuery, setSearchQuery] = useState("");
 const [finalSearchQuery, setFinalSearchQuery] = useState("");
 const [isSubmitting, setIsSubmitting] = useState(false);
 const { setIndexId } = useContext(setIndexIdContext);


 const queryClient = useQueryClient();


 const {
   data: videosData,
   refetch: refetchVideos,
   isPreviousData,
 } = useGetVideos(currIndex, vidPage, VID_PAGE_LIMIT);
 
 const videos = videosData?.data;


 const { data: authors, refetch: refetchAuthors } =
   useGetAllAuthors(currIndex);


 function reset() {
   setSearchQuery("");
   setFinalSearchQuery("");
 }


 useEffect(() => {
   queryClient.invalidateQueries({
     queryKey: [keys.VIDEOS, currIndex, vidPage],
   });
 }, [taskVideos, currIndex, vidPage]);


 useEffect(() => {
   queryClient.invalidateQueries({
     queryKey: [keys.AUTHORS, currIndex],
   });
 }, [videos, currIndex]);

VideoComponentsにおいて、動画や著者の情報を取得するためのクエリが作成されていることがわかります。これには、データを最新に保つために各クエリを無効化(Invalidate)することも含まれます。状態(State)であるsearchQuery、finalSearchQuery、isSubmittingは、UploadYouTubeVideoやSearchResultsなどの子コンポーネントと共有されるため、ここで設定されます。

UploadYouTubeVideo のレンダリング

デフォルトとして、各VideoIndexは、YouTubeプレイリスト、YouTubeチャンネル、またはビデオURLから成るJSONファイルを介して、一括アップロードを可能にするUploadYouTubeVideoを表示します。UploadYouTubeVideoの詳細は後ほどカバーします。

videoComponents.js (69行目 - 82行目)

return (
	<>
		<div className="videoUploadForm">
			<div className="display-6 m-4">新しい動画のアップロード>
			<UploadYoutubeVideo
     		currIndex={currIndex}
     		taskVideos={taskVideos}
     		setTaskVideos={setTaskVideos}
     		refetchVideos={refetchVideos}
     		isSubmitting={isSubmitting}
     		setIsSubmitting={setIsSubmitting}
     		reset={reset}
			&sol;>
		<

SearchForm と VideoList のレンダリング

インデックス内に既に動画がある場合は、UploadYoutubeVideoコンポーネントに加えて、動画検索フォームも表示される必要があります。finalSearchQueryがない場合(動画検索がまだ実行されていないことを示します)、PageNavコンポーネントを利用して、動画リストのみが1ページあたり12個ずつ表示されます。VideoListの詳細はこの後すぐ紹介します。

videoComponents.js (97行目 - 162行目)

export function VideoComponents({ ... }) {
  return (
    ...
    {videos && videos.length > 0 && (
      <div>
        <div className="videoSearchForm">
          <div className="title">動画を検索<&sol;div>
          {/* <div className="m-auto p-3"> */}
          <SearchForm
            setSearchQuery={setSearchQuery}
            searchQuery={searchQuery}
            setFinalSearchQuery={setFinalSearchQuery}
          />
          {/* <&sol;div> */}
        <&sol;div>

        {!finalSearchQuery && (
          <div>
            <div className="channelPills">
              <ErrorBoundary
                FallbackComponent={({ error }) => (
                  <ErrorFallback error={error} />
                )}
                onReset={() => refetchAuthors()}
                resetKeys={[keys.AUTHORS, currIndex]}
              >
                <div className="subtitle">
                  インデックス内のすべてのインフルエンサー ({authors?.length || 0}){" "}
                <&sol;div>
                {authors.map((author) => (
                  <div key={author} className="channelPill">
                    <Suspense fallback={<LoadingSpinner />}>
                      {author}
                    <&sol;Suspense>
                  <&sol;div>
                ))}
              <&sol;ErrorBoundary>
            <&sol;div>

            <Container fluid className="mb-2">
              <Row>
                <ErrorBoundary
                  FallbackComponent={({ error }) => (
                    <ErrorFallback error={error} />
                  )}
                  onReset={() => refetchVideos()}
                  resetKeys={[keys.VIDEOS, currIndex, vidPage]}
                >
                  {videos && (
                    <Suspense fallback={<LoadingSpinner />}>
                      <VideoList
                        videos={videos}
                        refetchVideos={refetchVideos}
                      />
                    <&sol;Suspense>
                  )}
                  <Container fluid className="d-flex justify-content-center">
                    <PageNav
                      page={vidPage}
                      setPage={setVidPage}
                      data={videosData}
                      isPreviousData={isPreviousData}
                    />
                  <&sol;Container>
                <&sol;ErrorBoundary>
              <&sol;Row>
            <&sol;Container>
          <

検索フォームと動画リスト

SearchResults のレンダリング

もし finalSearchQuery が存在する場合(動画検索が行われた際)、動画リストの代わりに検索結果がレンダリングされます。

VideoComponents.js (164行目 - 186行目)

{finalSearchQuery AND (
           <div>
             <Container fluid className="m-3">
               <Row>
                 <SearchResults
                   currIndex={currIndex}
                   allAuthors={authors}
                   finalSearchQuery={finalSearchQuery}
                 &sol;>
               <&sol;Row>
             <&sol;Container>
             <div className="resetButtonWrapper">
               <button className="resetButton" onClick={reset}>
                 {backIcon AND (
                   <img src={backIcon} alt="Icon" className="icon" >
                 )}
                 &nbsp;すべての動画に戻る
               <&sol;button>
             <&sol;div>
           <&sol;div>
         )}
       <

ご興味があれば、どのように検索結果をYouTubeチャンネルごとにグループ化するかの詳細をご覧ください。検索結果は、それらに含まれていないチャンネルも示すのがこのアプリの特徴的な部分でもあります!

検索結果

3.2 - UploadYoutubeVideo.js

UploadYoutubeVideoは、VideoComponents.jsの一部としてレンダリングされる主要なコンテナコンポーネントであり、YouTube動画をアップロードしてインデックス登録する役割を担います。また、ユーザーがアップロードリクエストを送信した直後にタスク動画表示を行ったり、各タスクの進行状況を示したりします。 

そのため、選択したJSONファイル、YouTubeチャンネル / プレイリストID、インデックス登録のステータスを含むタスクIDを保存するための重要ないくつかの状態を管理します。さらに、ファイル選択、コンポーネントの状態のリセット、またはビデオデータを取得・インデックス登録するためのAPIリクエストの送信などの関数を提供します。コア関数の1つである、indexYouTubeVideos関数について詳細を見ていきましょう。

UploadYoutubeVideo.js (155行目 - 188行目)

const indexYouTubeVideos = async () => {
   setIsSubmitting(true);
   updateMainMessage(
     "動画のアップロード中はページをリフレッシュしないでください。アップロード中も検索を行うことはできます!"
   );


   const videoData = taskVideos.map((taskVideo) => {
     return {
       url: taskVideo.video_url || taskVideo.url,
       title: taskVideo.title,
       authorName: taskVideo.author.name,
       thumbnails: taskVideo.thumbnails,
     };
   });


   const requestData = {
     videoData: videoData,
     index_id: currIndex,
   };

   const data = {
     method: "POST",
     headers: {
       Accept: "application/json",
       "Content-Type": "application/json",
     },
     body: JSON.stringify(requestData),
   };


   const response = await fetch(DOWNLOAD_URL.toString(), data);
   const json = await response.json();
   setIndexId(json.indexId);
   setTaskIds(json.taskIds);
 };

indexYouTubeVideosは、YouTube動画のインデックス作成プロセスを調整します。最初に、ユーザーにページをリフレッシュしないよう指示するメッセージを表示して開始します。そして目標となるタスク動画のURL、タイトル、著者名、サムネイルをマッピングしたオブジェクトとしてビデオデータを用意します。

次に、このビデオデータとインデックスIDを含むリクエストペイロードを構築します。ダウンロードURL(ステップ 2. YouTubeのURLを使って動画をアップロードするで確認済みです)に対してPOSTリクエストを送信し、JSONレスポンスを待ちます。レスポンスには、タスクIDとインデックスIDが含まれています。これに従って、taskIdsとindexIdの状態が更新されます。

UploadYouTubeVideoには、多くの状態や関数を持つ他のサブコンポーネントがあります。ここではその詳細すべてをカバーしませんが、ぜひ深く掘り下げて探求してみてください。質問等があればお気軽にどうぞ!

4 - プレゼンテーションコンポーネントの構築

これで難しい部分は完了し、最後にプレゼンテーションコンポーネントの構築をして仕上げです。プレゼンテーションコンポーネントは、VideoComponentsから渡されたタスク動画や動画に基づき、動画プレイヤーをシンプルにレンダリングするものです。このアプリでは、VideoListとTaskVideoをプレゼンテーションコンポーネントと呼ぶのが良いでしょう。以下、VideoListを例として見てみます。

VideoList.js (16行目 - 51行目)

function VideoList({ videos, refetchVideos }) {
 	const numVideos = videos.length;
 
  return videos.map((video, index) => (
   <ErrorBoundary
     FallbackComponent={ErrorFallback}
     onReset={() => refetchVideos()}
     resetKeys={[keys.VIDEOS]}
     key={video._id + "-" + index}
   >
     <Suspense>
       <Col
         sm={12}
         md={numVideos > 1 ? 6 : 12}
         lg={numVideos > 1 ? 4 : 12}
         xl={numVideos > 1 ? 3 : 12}
         className="mb-5 mt-3"
       >
         {" "}
         <ReactPlayer
           url={video.metadata.youtubeUrl}
           controls
           width="100%"
           height="250px"
         &sol;>
         <div className="channelAndVideoName">
           <div className="channelPillSmall">{video.metadata.author}<&sol;div>
           <div className="filename-text">
             {video.metadata.filename.replace(".mp4", "")}
           <&sol;div>
         <&sol;div>
       <&sol;Col>
     <&sol;Suspense>
   <

これは、動画をマッピングしてReactPlayerを介してそのプレイヤーを描画し、各動画の著者(インフルエンサー)とファイル名を表示します。実際の様子は、上の「SearchFormとVideoListのレンダリング」の画像セクションで確かめることができます。

まとめ

この記事が、Twelve Labsの動画検索APIと特定の状況でのその具体的な使用方法についての役立つインサイトをご提供できていれば幸いです。これは数ある利用ケースの1つに過ぎず、皆様のチームに合ったソリューションを構築する自由があります。開発を楽しんでください!

次のステップ

イントロダクション

美容業界のマーケティングプロフェッショナルとしてインフルエンサーシップに長年関わってきた中で、ブランドにとって理想的なYouTubeやTikTokのインフルエンサーを選定する上での貴重な教訓を学びました。(そうです、私はマーケティングプロフェッショナルからソフトウェアエンジニアに転身したのです 😉)最も成功するコラボレーションは、すでにあなたの製品やブランドを心から愛用しているインフルエンサーと、オーガニックに発生する傾向があります。たとえば、あなたがブランド「A」を宣伝している場合、事前のコンタクトがなくても「A」について言及しているインフルエンサーを見つけることができるかもしれません。こちらからアプローチした際、彼らは通常、一緒に取り組むことに対して非常に好意的です。

しかし、これらのインフルエンサーを見つけ出すのは少々難しい場合があります。特に、動画のタイトルや説明欄にあなたのブランド名が明示的に記載されていない場合はなおさらです。これは私自身にとっても悩みの種でした。たとえば、あるYouTuberが「冬のマストハブアイテムTop 10」のような動画の中で、ブランド名を挙げずにあなたの製品を紹介していた場合、YouTubeでの通常のキーワード検索では見つけることができません。

そこで、Twelve Labs APIを活用した「Who Talked About Us(誰が私たちについて話したか)」の出番です。通常のYouTubeやTikTokの検索とは異なり、このAPIは高度な文脈依存の動画検索を可能にします。タイトルや説明欄に頼る代わりに、APIは動画から動き、オブジェクト、人物、音、画面上のテキスト、そして音声など、さまざまな要素を抽出します。キーワードや、「MACのゴールドハイライターを使用」といった具体的な説明を入力するだけで、あなたのブランドや製品について言及している動画やチャンネルを、その言及が飛び出す正確な瞬間とともに発見することができます。 

これにより、アプローチしたいインフルエンサーのリストを、彼らが言及した製品の詳細や文脈とともに作成できます。これにより、これら潜在的なインフルエンサーに関する貴重なインサイトが得られ、より効果的にアプローチし、有意義なつながりを築くことが可能になります。 

それでは、Twelve Labs APIのパワーを活用したアプリを作成するためのステップバイステップの道のりを始めましょう!

前提条件

  • Twelve Labs APIの世界に飛び込む前に、まずはサインアップしてAPIキーを生成する必要があります。Twelve Labs Playgroundにアクセスしてサインアップし、APIキーを生成してください。サインアップすると、最大10時間の動画コンテンツをインデックス登録できる無料クレジットが提供されます!

  • このアプリに必要なすべてのファイルが含まれているリポジトリは、Githubで公開されています。

  • このアプリは、JavaScript、Node、React、およびReact Queryを使用して構築されています。これらの技術に馴染みがあると理解しやすいですが、そうでなくても心配はいりません。この記事から得られる最も重要な成果は、Twelve Labs APIについて学び、このアプリがそれをどのように活用しているかを理解することです。

1 - コンポーネントのデザイン

このアプリはReactを使用して構築されており、Reactの本質は再利用可能なコンポーネントに分解することにあります。そのため、まずはコンポーネントのデザインから開始し、もちろん、何度も修正を重ねました。



大局的に見ると、このアプリはExistingIndexForm、IndexForm、そしてVideoIndexで構成されています。IndexFormは、ユーザーが新しいインデックスを作成できるシンプルなフォームです。ExistingIndexFormは、ユーザーが以前このアプリで作成したインデックスのIDを送信できるもう一つのシンプルなフォームです。VideoIndexは、インデックスの詳細を取得する呼び出しが行われ、VideoComponentsが存在する場所です。

VideoComponentsは、動画に関連するすべてのコンポーネントで構成されています。UploadYoutubeVideoコンポーネントは、動画のダウンロード/インデックス登録を可能にし、各動画タスクのステータスを表示します。VideoListは、インデックスの動画をPageNavとともにレンダリングし、各ページに12個ずつフェッチして表示します。SearchFormは、ユーザーの入力(検索クエリ)を受け取り、検索API呼び出しに渡す動画検索を処理するコンポーネントです。SearchResultsは検索結果と動画のAPIを呼び出し、インフルエンサーごとに整理します。その後、SearchResultが各検索結果をユーザーフレンドリーな方法で表示します。

2 - サーバーとAPIフックの構築

Server.jsとapiHooks.jsは、Twelve Labs APIやytdl-coreなどの他のライブラリからのすべてのAPI呼び出しを管理するファイルです。Server.jsは、Twelve Labs APIを呼び出すためのすべてのエンドポイントや、その他のAPI呼び出しが存在する場所です。apiHooks.jsは、状態、キャッシュ、およびデータフェッチを管理するためのカスタムReact Queryフックのセットです。サーバーの、ひいてはアプリ全体の主要機能であるTwelve Labs APIを使用した動画検索について、その使用方法を詳しく見ていきましょう。

Twelve Labs APIを使用する4つのステップ

最初のステップは、動画用のインデックスを作成することです。次に、このインデックスに動画をアップロードします。続いて、動画のメタデータを更新して、各動画にYouTubeチャンネルとURLを追加します(このステップはこのアプリ固有のもので、一般的にはオプションです)。最後に、動画検索を行う準備が整います。このアプリでは、すべてのAPI呼び出し(Twelve Labsおよびその他)と動画アップロードに関連する関数をserver.jsファイルに整理しました。

セットアップ

ルートディレクトリに.envファイルを作成し、必要に応じて値を更新します。以下をコピー&ペーストして値をカスタマイズするだけでも可能です。

.env

REACT_APP_API_URL=https://api.twelvelabs.io/v1.1
REACT_APP_API_KEY=<YOUR API KEY>
REACT_APP_SERVER_URL=<YOUR SERVER URL>
REACT_APP_PORT_NUMBER=<YOUR PORT NUMBER>



  • REACT_APP_API_URL: このアプリはv1.1をサポートしています

  • REACT_APP_API_KEY: 前のステップで生成したAPIキーを保存します

  • REACT_APP_SERVER_URL: 「http://localhost」のようになる可能性があります

  • REACT_APP_PORT_NUMBER: 使用したいポート番号を設定します(例: 4001)

必要なファイル内で環境変数の値にアクセスするには、process.envを利用できます。たとえば、server.jsファイルでは、process.env.REACT_APP_APP_URLを使用してAPI_URLにアクセスし、保存できます。次の例は、これを実現する方法を示しています。

server.js (15行目 - 19行目)

/** 定数を定義し、TL APIエンドポイントを設定する */
const TWELVE_LABS_API_KEY = process.env.REACT_APP_API_KEY;
const API_BASE_URL = process.env.REACT_APP_API_URL;
const TWELVE_LABS_API = axios.create({ baseURL: API_BASE_URL });
const PORT_NUMBER = process.env.REACT_APP_PORT_NUMBER;

ステップ 1. インデックスの作成

インデックスとは、動画をアップロード、インデックス登録、検索できる動画ライブラリのようなものです。日付、テーマ、またはYouTubeチャンネルごとに独自のインデックスを作成できます。インデックスを作成するには、メソッドを「POST」、エンドポイントを「https://api.twelvelabs.io/v1.1/indexes」に設定し、ヘッダーを追加して、engine_id、index_options、index_nameなどの必要なデータを提供するだけです。index_optionsについては、4つのオプションのサブセットを選択できます。このアプリでは、4つのオプションすべてが含まれています。

💡インデックス作成の詳細については、APIリファレンスを確認してください

server.js (98行目 - 119行目)

/** インデックスを作成する */
app.post("/indexes", async (request, response, next) => {
 const headers = {
   "Content-Type": "application/json",
   "x-api-key": TWELVE_LABS_API_KEY,
 };


 const data = {
   engine_id: "marengo2.5",
   index_options: ["visual", "conversation", "text_in_video", "logo"],
   index_name: request.body.indexName,
 };


 try {
   const apiResponse = await TWELVE_LABS_API.post("/indexes", data, {
     headers,
   });
   response.json(apiResponse.data);
 } catch (error) {
   console.error("サーバー側のエラー:", error);
   response.json({ error });
 }
});

ステップ 2. YouTubeのURLを使って動画をアップロードする

このアプリは、チャンネルIDプレイリストID、または以下のようなURLオブジェクトの配列を持つJSONファイルを介した、YouTube動画の一括アップロードをサポートしています。

example.json

[
     { "url": "VIDEO URL" },
     { "url": "VIDEO URL" }
     ...
    ]

💡YouTubeのURLによる動画のアップロードは、Twelve Labs API v1.2で利用可能になりました。このアプリはv1.1を使用しているため、YouTubeから動画をダウンロードできるライブラリであるytdl-coreを利用して、手動で実装しています。

プロセスとしては、提供されたYouTube URLから動画をダウンロードし、Twelve Labs APIに送信して動画をインデックス登録します。これは、server.jsのエンドポイント「/download」に実装されています。まず、リクエストのボディから動画データとインデックス情報を抽出します。次に、動画をチャンクに分けてダウンロードし、安全なファイル名にするためにタイトルをサニタイズして、インデックス登録用に送信します。すべての動画がダウンロードされ、インデックス登録された後、サーバーはタスクIDとインデックスIDを返します。各ステップを分解して詳しく見ていきましょう。

1 - リクエストから情報を抽出する

最初のステップは、リクエストのボディから動画データとインデックス情報を抽出することです。動画の総数、処理された動画の数、および動画のダウンロードとインデックス登録のチャンクサイズ(このアプリでは5に設定)を追跡するための変数を設定します。また、動画インデックス登録プロセスからのレスポンスを格納するための配列を初期化します。

server.js (397行目 - 409行目)

/** 分析用の動画をダウンロードし、インデックス登録して、タスクIDとインデックスIDを返す */
app.post(
 "/download",
 bodyParser.urlencoded(),
 async (request, response, next) => {
   try {
     // ステップ 1: リクエストから動画データとインデックス情報を抽出する
     const jsonVideos = request.body.videoData;
     const totalVideos = jsonVideos.length;
     let processedVideosCount = 0;
     const chunk_size = 5;
     let videoIndexingResponses = [];
     console.log("動画をダウンロード中...");
2 - 動画をチャンクに分けてダウンロードする

次のステップは、動画をチャンクに分けてダウンロードすることです。各チャンクについて、動画データを反復処理し、ytdl-coreライブラリを使用して提供されたURLから動画をダウンロードします。安全なファイル名を作成するために、動画のタイトルはサニタイズされます。動画のダウンロードが完了すると、その進捗が記録されます。

💡ここでは、ダウンロードした動画を「videos」フォルダに保存するためのvideoPathを設定しています。

server.js (411行目 - 442行目)

// ステップ 2: 動画をチャンクに分けてダウンロードする
     for (let i = 0; i < totalVideos; i += chunk_size) {
       const videoChunk = jsonVideos.slice(i, i + chunk_size);
       const chunkDownloadedVideos = [];


       // 現在のチャンク内の各動画をダウンロードする。
       await Promise.all(
         videoChunk.map(async (videoData) => {
           try {
             // ダウンロードした動画の安全なファイル名を生成する
             const safeName = sanitize(videoData.title);
             const videoPath = `videos/${safeName}.mp4`;


             // 提供されたURLから動画をダウンロードする
             const stream = ytdl(videoData.url, {
               filter: "videoandaudio",
               format: ".mp4",
             });
             await streamPipeline(stream, fs.createWriteStream(videoPath));


             console.log(`${videoPath} -- ダウンロード完了`);


             chunkDownloadedVideos.push({
               videoPath: videoPath,
               videoData: videoData,
             });
           } catch (error) {
             console.log(`${videoData.title} のダウンロード中にエラーが発生しました`);
             console.error(error);
           }
         })
       );
3 - インデックス登録用に動画を送信する

動画のチャンクをダウンロードした後、これらのダウンロードした動画をインデックス登録用に送信します。すべてのインデックス登録タスクが完了するのを待ち、インデックス登録送信の進捗を記録します。

server.js (444行目 - 481行目)

// ステップ 3: ダウンロードした動画をインデックス登録用に送信する
       console.log(
         `インデックス登録用に動画を送信中 | チャンク ${
           Math.floor(i / chunk_size) + 1
         }`
       );


       const chunkVideoIndexingResponses = await Promise.all(
         chunkDownloadedVideos.map(async (chunkDownloadedVideo) => {
           console.log(
             `インデックス登録用に ${chunkDownloadedVideo.videoPath} を送信中...`
           );
           const indexingResponse = await indexVideo(
             chunkDownloadedVideo.videoPath,
             request.body.index_id
           );


           // indexingResponse に videoData を追加する
           indexingResponse.videoData = chunkDownloadedVideo.videoData;


           return indexingResponse;
         })
       ).catch(next);
       
       console.log("チャンクのインデックス送信が完了しました | タスクID:");


       processedVideosCount += videoChunk.length;


       console.log(
         `${totalVideos} 本中 ${processedVideosCount} 本の動画を処理しました`
       );


       videoIndexingResponses = videoIndexingResponses.concat(
         chunkVideoIndexingResponses
       );


       await new Promise((resolve) => setTimeout(resolve, 1000));
     }

また、indexVideo関数も簡単に見ておきましょう。動画のインデックス登録自体は非常にシンプルです。他のAPI呼び出しと同様に、プロセスにはTwelve Labs APIへのPOSTリクエストでのインデックス登録タスクの開始が含まれます。このリクエストには、特定のパラメータ(ターゲットインデックスを指定するindex_id、ビデオファイル(この場合はビデオパスからストリームされたビデオデータ)、および言語設定(英語の場合は'en'))が含まれます。

💡動画のインデックス登録(動画インデックス登録タスクの作成)の詳細については、APIリファレンスを確認してください

server.js (330行目 - 349行目)

/** ダウンロードした動画を受け取り、インデックス登録プロセスを開始する */
const indexVideo = async (videoPath, indexId) => {
 const headers = {
   headers: {
     accept: "application/json",
     "Content-Type": "multipart/form-data",
     "x-api-key": TWELVE_LABS_API_KEY,
   },
 };


 let params = {
   index_id: indexId,
   video_file: fs.createReadStream(videoPath),
   language: "en",
 };


 const response = await TWELVE_LABS_API.post("/tasks", params, headers);


 return await response.data;
};

この呼び出しからのresponse.dataは、以下のようなタスクIDを返します。以前のコードスニペットで見たように、各タスクIDはindexingResponseに保存されます。

response.data

{
	"_id": "6527732e23c1347ffbe3a802"
}

ただし、response.dataの他に、後で各動画のメタデータに追加するためにビデオデータが必要になるため、indexingResponseにvideoDataを追加しています。したがって、各最終indexingResponseは以下のようになり、videoIndexingResponsesに結合されます。

videoIndexingResponses

[
  {
    _id: '6527732e23c1347ffbe3a802',
    videoData: {
      url: 'https://www.youtube.com/watch?v=WuWnt2NHJxk&list=PLI8sddTC_LM0SG6JMYmrbrHZy7j5zUjMA&index=4&pp=iAQB',
      title: 'Everyday Makeup Using ONLY 3 Products !!',
      authorName: 'Smitha Deepak',
      thumbnails: [Array]
    }
  },
  
]
4 - タスクIDを返す

すべての動画がダウンロードされ、インデックス登録用に送信されると、タスクIDのオブジェクト(上記の配列)とインデックスIDをクライアントに返します。

server.js (483行目 - 498行目)

// ステップ 4: インデックスタスクのタスクIDとインデックスIDを返す
     console.log(
       "すべての動画のインデックス送信が完了しました。タスクID:"
     );


     console.log(videoIndexingResponses);


     response.json({
       taskIds: videoIndexingResponses,
       indexId: request.body.index_id,
     });
   } catch (error) {
     next(error);
   }
 }
);

ステップ 3. 動画のメタデータを更新する

一般的に、このステップはオプションですが、このアプリでは動画をYouTubeのURLで表示し、チャンネル/インフルエンサーごとに分類するために必要です。

アプリでの動画の表示方法

ご覧の通り、アプリは各動画の薄緑色のカプセル型バッジにチャンネル名を表示します。また、動画はTwelve LabsサーバーからのビデオURLではなく、YouTube URLを使用してReact Playerを介して読み込まれます。動画のデフォルトメタデータにはチャンネル名とYouTube URLが含まれないため、この情報を各動画のメタデータに追加する必要があります。 

以下は、動画のデフォルトメタデータの例です。

"metadata": {
		"duration": 188.6,
		"filename": "3 MINUTE MAKEUP THAT WILL LAST ALL DAY.mp4",
		"fps": 25,
		"height": 720,
		"size": 40566200,
		"video_title": "3 MINUTE MAKEUP THAT WILL LAST ALL DAY.mp4",
		"width": 1280,
		}

任意のキーと値のペアを追加したり、既存のメタデータを更新したりできます。このアプリでは、メタデータに3つの追加データを追加しています:1) 著者(チャンネル名)、2) YouTube URL、および 3) whoTalkedAboutUsのブール値(動画がアプリを介してアップロードされたかどうかを示すマーク)。 

この作業はserver.jsファイルで行われ、そこには動画メタデータの更新を処理するエンドポイントがあります。ここでは、追加または更新するデータを含むPUTリクエストが行われます。

💡動画情報の更新の詳細については、APIリファレンスを確認してください

server.js (290行目 - 310行目)

/** 動画のメタデータを更新する */
app.put("/update/:indexId/:videoId", async (request, response, next) => {
 const indexId = request.params.indexId;
 const videoId = request.params.videoId;
 const data = request.body;
 const headers = {
   "Content-Type": "application/json",
   "x-api-key": TWELVE_LABS_API_KEY,
 };


 try {
   const apiResponse = await TWELVE_LABS_API.put(
     `/indexes/${indexId}/videos/${videoId}`,
     data,
     { headers }
   );
   response.json(apiResponse.data);
 } catch (error) {
   return next(error);
 }
});

次に、これがUploadYouTubeVideo.jsファイルでどのように実装されているかを見てみましょう。

UploadYouTubeVideo.js (190行目 - 223行目)

async function updateMetadata() {
   const updatePromises = completeTasks.map(async (completeTask) => {
     const matchingVid = taskVideos.find(
       (taskVid) =>
         `${sanitize(taskVid.title)}.mp4` === completeTask.metadata.filename
     );
     if (matchingVid) {
       const authorName = matchingVid.author.name;
       const youtubeUrl = matchingVid.video_url || matchingVid.shortUrl;
       const data = {
         metadata: {
           author: authorName,
           youtubeUrl: youtubeUrl,
           whoTalkedAboutUs: true,
         },
       };
       try {
         await fetch(
           `${UPDATE_VIDEO_URL}/${currIndex}/${completeTask.video_id}`,
           {
             method: "PUT",
             headers: {
               "Content-Type": "application/json",
             },
             body: JSON.stringify(data),
           }
         );
       } catch (error) {
         console.error(error);
       }
     }
   });
   await Promise.all(updatePromises);
 }

updateMetadata関数は、すべてのタスク動画から一致する完了済みのタスク動画を見つけます。一致するごとに、著者名とYouTube URLを抽出し、カスタムメタデータを構築します。すべての動画に対して、whoTalkedAboutUsキーの値はtrueに設定されます。その後、サーバーにフェッチしてこれらの変更を適用します。

💡 提供するデータは「metadata」キーを持つオブジェクトの形式である必要があり、ここでキーと値のペアを追加または変更して、ビデオデータをパーソナライズすることができます。

このプロセスの後、動画メタデータには、新しく追加された「author」、「youtubeUrl」、および「whoTalkedAboutUs」が誇らしげに表示されます!全体として、メタデータの更新は、動画コレクションに自分だけの印を付けるための素晴らしい方法です。

"metadata": {
		"author": "Justine Feather", //追加されました!
		"duration": 188.6,
		"filename": "3 MINUTE MAKEUP THAT WILL LAST ALL DAY.mp4",
		"fps": 25,
		"height": 720,
		"size": 40566200,
		"video_title": "3 MINUTE MAKEUP THAT WILL LAST ALL DAY.mp4",
		"whoTalkedAboutUs": true, //追加されました!
		"width": 1280,
		"youtubeUrl": "https://www.youtube.com/watch?v=aYQWSNAL4D8" //追加されました!
		}

ステップ 4. 動画を検索する

ついに、お待ちかねの動画検索の瞬間です!インデックス内のインデックス登録済み動画から検索を行うことができます。

このアプリでは、検索結果を表示する際にページネーションが実装されています。したがって、検索結果の取得と表示は、POSTリクエストを行って最初の検索結果を取得することと、次のページのトークンを使用してGETリクエストを行って以降の検索結果ページをフェッチすることの2つの部分で構成されます。

検索POSTリクエストを行う

最初の検索結果を取得するには、server.jsファイルにある検索エンドポイントにPOSTリクエストを行う必要があります。リクエストからindexIdとqueryを取得し、TwelveLabs API用に「/search」へのPOSTリクエストを行います。

「search_options」には、「visual」、「conversation」、「text_in_video」、「logo」の4つすべてを含めました。また、「threshold」、「sort_option」、「group_by」といった複数のオプションも適用しています。これらのオプションすべてを使用して、信頼度が「中」および「高」の結果をフィルタリングし、動画ごとにグループ化し、クリップ数で並べ替えています。

💡検索リクエストの送信に関する詳細については、APIリファレンスを確認してください

server.js (217行目 - 243行目)

/** 与えられたクエリで動画を検索する */
app.post("/search", async (request, response, next) => {
 const headers = {
   accept: "application/json",
   "Content-Type": "application/json",
   "x-api-key": TWELVE_LABS_API_KEY,
 };


 const data = {
   index_id: request.body.indexId,
   search_options: ["visual", "conversation", "text_in_video", "logo"],
   query: request.body.query,
   group_by: "video",
   sort_option: "clip_count",
   threshold: "medium",
   page_limit: 2,
 };


 try {
   const apiResponse = await TWELVE_LABS_API.post("/search", data, {
     headers,
   });
   response.json(apiResponse.data);
 } catch (error) {
   return next(error);
 }
});

「/search」エンドポイントが設定されたら、useGetVideosOfSearchResultsが機能します。これは、React Queryフックを使用して「ビデオ検索」と「ビデオ取得」の機能を同時に動かすReact Queryフックです。これはSearchResults.jsにインポートされて実行され、クエリレスポンスからネクストページトークン、検索結果、および検索結果動画が抽出されます。

SearchResults.js (25行目 - 33行目)

/** 初期の検索結果と対応する動画を取得する */
 const {
   initialSearchData: {
     page_info: { next_page_token: initialNextPageToken } = {},
   } = {},
   initialSearchResults,
   initialSearchResultVideos,
   refetch,
 } = useGetVideosOfSearchResults(currIndex, finalSearchQuery);

useGetVideosOfSearchResults関数は、apiHooks.jsで定義されています。上述の通り、useQueriesフックを使用して複数のクエリを同時に実行します。最初の検索結果を取得し、次に各々の初期検索結果と一致する動画の情報を取得します。

apiHooks.js (152行目 - 177行目)

export function useGetVideosOfSearchResults(indexId, query) {
 const {
   data: initialSearchData,
   refetch,
   isLoading,
 } = useSearchVideo(indexId, query);
 const initialSearchResults = initialSearchData.data || [];


 const resultVideos = useQueries({
   queries: initialSearchResults.map((searchResult) => ({
     queryKey: [keys.SEARCH, indexId, searchResult.id],
     queryFn: () =>
       apiConfig.TWELVE_LABS_API.get(
         `${apiConfig.INDEXES_URL}/${indexId}/videos/${searchResult.id}`
       ).then((res) => res.data),
   })),
 });
 const initialSearchResultVideos = resultVideos.map(({ data }) => data);
 return {
   initialSearchData,
   initialSearchResults,
   initialSearchResultVideos,
   refetch,
   isLoading,
 };
}

ビデオ検索の結果を取得するために、まずuseSearchVideoフックが呼び出されているのがわかります。

apiHooks.js (93行目 - 102行目)

export function useSearchVideo(indexId, query) {
 return useQuery({
   queryKey: [keys.SEARCH, indexId, query],
   queryFn: () =>
     apiConfig.TWELVE_LABS_API.post(apiConfig.SEARCH_URL, {
       indexId,
       query,
     }).then((res) => res.data),
 });
}

APIフックの呼び出しの結果として取得するデータ(initialSearchResults と initialSearchResultVideos)は、以下のようになります。

initialSearchResults

[
	
 {clips: 
[
  {score: 76.1, start: 21.15625, end: 37.15625, metadata: Array(1),  
   video_id: '65651c021cbcab2e74d86eb3', confidence: “medium”,   
   modules: [{...}]},
   {},
    
], 
 id: '65651c021cbcab2e74d86eb3'}, 
 {clips: Array(6), id:... },
  
]

各initialSearchResultは、検索結果を含む「clips」と、該当する検索結果のビデオIDである「id」で構成されています。 

initialSearchResultVideos

[
	{
	     “created_at”: "2023-12-07T18:32:01Z"
     “hls”: {...},
           “indexed_at”: "2023-12-07T18:34:06Z",
           “metadata”: {...}, 
           “Updated_at”: "2023-12-07T18:35:01Z",
           “_id”: "65720fa11cbcab2e74d87aab"
	}, 
      
]

initialSearchResultVideosは、対応する検索結果のメタデータを含むビデオオブジェクトで構成されています。 

これで、最初の検索結果とそれに対応する動画が取得できました。ただし、次のページ用のトークン(next_page_token)がある場合は、さらに検索結果が存在することを意味します。このため、すべての検索結果と動画を一元化して保存するために、追加の状態(combinedSearchResults と combinedSearchResultVideos)を設定しています。特にこのアプリでは結果をインフルエンサー(YouTubeチャンネル)ごとに整理して表示するため、まずデータを結合し、整理して、ユーザーに提示するためにこれらの状態が必要不可欠です。 

それでは、次のページの検索結果をフェッチする方法について見ていきましょう。

検索GETリクエストを行う

初期の検索データから次のページのトークン(next_page_token)を取得したことを覚えていますか?そのトークンを使用して追加の検索結果を取得するGETリクエストを行うことができます。検索結果にnext_page_tokenが含まれている限り、すべての検索結果を漏れなく収集するためにリクエストを送り続ける必要があります。

SearchResults.js (50行目 - 53行目)

const nextPageResultsData = await fetchNextPageSearchResults(
       queryClient,
       nextPageToken
     );

fetchNextPageSearchResults は、Twelve Labs APIの「/search」エンドポイントにGETリクエストを呼び出してデータを取得します。ユーザーがボタンをクリックしたときにのみ条件付きでデータをフェッチするため、ここではfetchQueryを使用しました。

apiHooks.js (179行目 - 196行目)

export async function fetchNextPageSearchResults(queryClient, nextPageToken) {
 try {
   const response = await queryClient.fetchQuery({
     queryKey: [keys.SEARCH, nextPageToken],
     queryFn: async () => {
       const response = await apiConfig.TWELVE_LABS_API.get(
         `${apiConfig.SEARCH_URL}/${nextPageToken}`
       );
       const data = response.data;
       return data;
     },
   });
   return response;
 } catch (error) {
   console.error("検索結果の次のページの取得中にエラーが発生しました:", error);
   throw error;
 }
}

検索結果に基づき、最初の検索結果で行った作業と同様に、fetchNextpageSearchResultVideosを使用して対応する映像データを取得します。最後に、それらはcombinedSearchResultsとcombinedSearchResultVideosに追加されます。

combinedSearchResultsとcombinedSearchResultVideosが変化するたびに、結果が再整理されます(関連コードはこちら)。整理された検索結果は、その後SearchResultコンポーネントを通じてレンダリングされます。

💡 検索結果のページネーションコントロールのさらなる詳細については、APIガイドラインを確認してください!

これで、必要なすべてのデータにアクセスするためのサーバーとAPIフックが完成しました。次に、データを取得、操作、そしてユーザーに表示するためのコンポーネントを構築できます。次のステップでは、これらすべてのコンポーネントがどのように組み合わさって、強力なインフルエンサー検索アプリケーションを作成するかを見ていきます。お楽しみに!

3 - コンテナコンポーネントの構築

コンポーネント構築のプロセスにおいては、コンテナコンポーネント(Container Components)から作り始めるのが簡単です。プレゼンテーションコンポーネント(Presentation Components)は、コンテナコンポーネントからのAPIレスポンスや状態(State)に依存することが多いためです。 

💡 コンテナコンポーネントおよびプレゼンテーションコンポーネントという用語に馴染みがないかもしれませんが、これらはReactの文脈でよく使われる用語です。コンテナコンポーネント(スマートコンポーネントとも呼ばれます)は、データのフェッチや状態変化などのアプリのロジックやデータフローを処理します。プレゼンテーションコンポーネント(ダムコンポーネントとも呼ばれます)は、propsを介してコンテナコンポーネントから機能を受け取り、ユーザーインターフェースの描画やデータの表示に集中します。

3.1 - VideoComponent.js

VideoComponentsはアプリの中心的な存在であり、動画のアップロード、動画検索、インデックス内の動画とYouTubeチャンネル名の表示など、さまざまな機能を提供します。ユーザーは簡単に新しい動画をアップロードし、検索を実行し、ページ分けされた動画ライブラリビューにアクセスできます。また、必要に応じてインデックスを削除することもできます。どのように機能するのか見てみましょう。

videoComponents.js (21行目 - 67行目)

/** 動画とのやり取りを含むコンポーネント
*
*  VideoIndex -> VideoComponents -> { UploadYouTubeVideo, VideoList, PageNav,
*  SearchForm, SearchResults }
*
*/


export function VideoComponents({
 currIndex,
 vidPage,
 setVidPage,
 taskVideos,
 setTaskVideos,
}) {
 const [searchQuery, setSearchQuery] = useState("");
 const [finalSearchQuery, setFinalSearchQuery] = useState("");
 const [isSubmitting, setIsSubmitting] = useState(false);
 const { setIndexId } = useContext(setIndexIdContext);


 const queryClient = useQueryClient();


 const {
   data: videosData,
   refetch: refetchVideos,
   isPreviousData,
 } = useGetVideos(currIndex, vidPage, VID_PAGE_LIMIT);
 
 const videos = videosData?.data;


 const { data: authors, refetch: refetchAuthors } =
   useGetAllAuthors(currIndex);


 function reset() {
   setSearchQuery("");
   setFinalSearchQuery("");
 }


 useEffect(() => {
   queryClient.invalidateQueries({
     queryKey: [keys.VIDEOS, currIndex, vidPage],
   });
 }, [taskVideos, currIndex, vidPage]);


 useEffect(() => {
   queryClient.invalidateQueries({
     queryKey: [keys.AUTHORS, currIndex],
   });
 }, [videos, currIndex]);

VideoComponentsにおいて、動画や著者の情報を取得するためのクエリが作成されていることがわかります。これには、データを最新に保つために各クエリを無効化(Invalidate)することも含まれます。状態(State)であるsearchQuery、finalSearchQuery、isSubmittingは、UploadYouTubeVideoやSearchResultsなどの子コンポーネントと共有されるため、ここで設定されます。

UploadYouTubeVideo のレンダリング

デフォルトとして、各VideoIndexは、YouTubeプレイリスト、YouTubeチャンネル、またはビデオURLから成るJSONファイルを介して、一括アップロードを可能にするUploadYouTubeVideoを表示します。UploadYouTubeVideoの詳細は後ほどカバーします。

videoComponents.js (69行目 - 82行目)

return (
	<>
		<div className="videoUploadForm">
			<div className="display-6 m-4">新しい動画のアップロード>
			<UploadYoutubeVideo
     		currIndex={currIndex}
     		taskVideos={taskVideos}
     		setTaskVideos={setTaskVideos}
     		refetchVideos={refetchVideos}
     		isSubmitting={isSubmitting}
     		setIsSubmitting={setIsSubmitting}
     		reset={reset}
			&sol;>
		<

SearchForm と VideoList のレンダリング

インデックス内に既に動画がある場合は、UploadYoutubeVideoコンポーネントに加えて、動画検索フォームも表示される必要があります。finalSearchQueryがない場合(動画検索がまだ実行されていないことを示します)、PageNavコンポーネントを利用して、動画リストのみが1ページあたり12個ずつ表示されます。VideoListの詳細はこの後すぐ紹介します。

videoComponents.js (97行目 - 162行目)

export function VideoComponents({ ... }) {
  return (
    ...
    {videos && videos.length > 0 && (
      <div>
        <div className="videoSearchForm">
          <div className="title">動画を検索<&sol;div>
          {/* <div className="m-auto p-3"> */}
          <SearchForm
            setSearchQuery={setSearchQuery}
            searchQuery={searchQuery}
            setFinalSearchQuery={setFinalSearchQuery}
          />
          {/* <&sol;div> */}
        <&sol;div>

        {!finalSearchQuery && (
          <div>
            <div className="channelPills">
              <ErrorBoundary
                FallbackComponent={({ error }) => (
                  <ErrorFallback error={error} />
                )}
                onReset={() => refetchAuthors()}
                resetKeys={[keys.AUTHORS, currIndex]}
              >
                <div className="subtitle">
                  インデックス内のすべてのインフルエンサー ({authors?.length || 0}){" "}
                <&sol;div>
                {authors.map((author) => (
                  <div key={author} className="channelPill">
                    <Suspense fallback={<LoadingSpinner />}>
                      {author}
                    <&sol;Suspense>
                  <&sol;div>
                ))}
              <&sol;ErrorBoundary>
            <&sol;div>

            <Container fluid className="mb-2">
              <Row>
                <ErrorBoundary
                  FallbackComponent={({ error }) => (
                    <ErrorFallback error={error} />
                  )}
                  onReset={() => refetchVideos()}
                  resetKeys={[keys.VIDEOS, currIndex, vidPage]}
                >
                  {videos && (
                    <Suspense fallback={<LoadingSpinner />}>
                      <VideoList
                        videos={videos}
                        refetchVideos={refetchVideos}
                      />
                    <&sol;Suspense>
                  )}
                  <Container fluid className="d-flex justify-content-center">
                    <PageNav
                      page={vidPage}
                      setPage={setVidPage}
                      data={videosData}
                      isPreviousData={isPreviousData}
                    />
                  <&sol;Container>
                <&sol;ErrorBoundary>
              <&sol;Row>
            <&sol;Container>
          <

検索フォームと動画リスト

SearchResults のレンダリング

もし finalSearchQuery が存在する場合(動画検索が行われた際)、動画リストの代わりに検索結果がレンダリングされます。

VideoComponents.js (164行目 - 186行目)

{finalSearchQuery AND (
           <div>
             <Container fluid className="m-3">
               <Row>
                 <SearchResults
                   currIndex={currIndex}
                   allAuthors={authors}
                   finalSearchQuery={finalSearchQuery}
                 &sol;>
               <&sol;Row>
             <&sol;Container>
             <div className="resetButtonWrapper">
               <button className="resetButton" onClick={reset}>
                 {backIcon AND (
                   <img src={backIcon} alt="Icon" className="icon" >
                 )}
                 &nbsp;すべての動画に戻る
               <&sol;button>
             <&sol;div>
           <&sol;div>
         )}
       <

ご興味があれば、どのように検索結果をYouTubeチャンネルごとにグループ化するかの詳細をご覧ください。検索結果は、それらに含まれていないチャンネルも示すのがこのアプリの特徴的な部分でもあります!

検索結果

3.2 - UploadYoutubeVideo.js

UploadYoutubeVideoは、VideoComponents.jsの一部としてレンダリングされる主要なコンテナコンポーネントであり、YouTube動画をアップロードしてインデックス登録する役割を担います。また、ユーザーがアップロードリクエストを送信した直後にタスク動画表示を行ったり、各タスクの進行状況を示したりします。 

そのため、選択したJSONファイル、YouTubeチャンネル / プレイリストID、インデックス登録のステータスを含むタスクIDを保存するための重要ないくつかの状態を管理します。さらに、ファイル選択、コンポーネントの状態のリセット、またはビデオデータを取得・インデックス登録するためのAPIリクエストの送信などの関数を提供します。コア関数の1つである、indexYouTubeVideos関数について詳細を見ていきましょう。

UploadYoutubeVideo.js (155行目 - 188行目)

const indexYouTubeVideos = async () => {
   setIsSubmitting(true);
   updateMainMessage(
     "動画のアップロード中はページをリフレッシュしないでください。アップロード中も検索を行うことはできます!"
   );


   const videoData = taskVideos.map((taskVideo) => {
     return {
       url: taskVideo.video_url || taskVideo.url,
       title: taskVideo.title,
       authorName: taskVideo.author.name,
       thumbnails: taskVideo.thumbnails,
     };
   });


   const requestData = {
     videoData: videoData,
     index_id: currIndex,
   };

   const data = {
     method: "POST",
     headers: {
       Accept: "application/json",
       "Content-Type": "application/json",
     },
     body: JSON.stringify(requestData),
   };


   const response = await fetch(DOWNLOAD_URL.toString(), data);
   const json = await response.json();
   setIndexId(json.indexId);
   setTaskIds(json.taskIds);
 };

indexYouTubeVideosは、YouTube動画のインデックス作成プロセスを調整します。最初に、ユーザーにページをリフレッシュしないよう指示するメッセージを表示して開始します。そして目標となるタスク動画のURL、タイトル、著者名、サムネイルをマッピングしたオブジェクトとしてビデオデータを用意します。

次に、このビデオデータとインデックスIDを含むリクエストペイロードを構築します。ダウンロードURL(ステップ 2. YouTubeのURLを使って動画をアップロードするで確認済みです)に対してPOSTリクエストを送信し、JSONレスポンスを待ちます。レスポンスには、タスクIDとインデックスIDが含まれています。これに従って、taskIdsとindexIdの状態が更新されます。

UploadYouTubeVideoには、多くの状態や関数を持つ他のサブコンポーネントがあります。ここではその詳細すべてをカバーしませんが、ぜひ深く掘り下げて探求してみてください。質問等があればお気軽にどうぞ!

4 - プレゼンテーションコンポーネントの構築

これで難しい部分は完了し、最後にプレゼンテーションコンポーネントの構築をして仕上げです。プレゼンテーションコンポーネントは、VideoComponentsから渡されたタスク動画や動画に基づき、動画プレイヤーをシンプルにレンダリングするものです。このアプリでは、VideoListとTaskVideoをプレゼンテーションコンポーネントと呼ぶのが良いでしょう。以下、VideoListを例として見てみます。

VideoList.js (16行目 - 51行目)

function VideoList({ videos, refetchVideos }) {
 	const numVideos = videos.length;
 
  return videos.map((video, index) => (
   <ErrorBoundary
     FallbackComponent={ErrorFallback}
     onReset={() => refetchVideos()}
     resetKeys={[keys.VIDEOS]}
     key={video._id + "-" + index}
   >
     <Suspense>
       <Col
         sm={12}
         md={numVideos > 1 ? 6 : 12}
         lg={numVideos > 1 ? 4 : 12}
         xl={numVideos > 1 ? 3 : 12}
         className="mb-5 mt-3"
       >
         {" "}
         <ReactPlayer
           url={video.metadata.youtubeUrl}
           controls
           width="100%"
           height="250px"
         &sol;>
         <div className="channelAndVideoName">
           <div className="channelPillSmall">{video.metadata.author}<&sol;div>
           <div className="filename-text">
             {video.metadata.filename.replace(".mp4", "")}
           <&sol;div>
         <&sol;div>
       <&sol;Col>
     <&sol;Suspense>
   <

これは、動画をマッピングしてReactPlayerを介してそのプレイヤーを描画し、各動画の著者(インフルエンサー)とファイル名を表示します。実際の様子は、上の「SearchFormとVideoListのレンダリング」の画像セクションで確かめることができます。

まとめ

この記事が、Twelve Labsの動画検索APIと特定の状況でのその具体的な使用方法についての役立つインサイトをご提供できていれば幸いです。これは数ある利用ケースの1つに過ぎず、皆様のチームに合ったソリューションを構築する自由があります。開発を楽しんでください!

次のステップ