チュートリアル
シェードファインダーアプリの構築:Twelve LabsのAPIを使用して動画内の特定の役割の色を特定する

ミラン・キム
このチュートリアルでは、Twelve Labsのimage-to-video(画像から動画)検索APIを使用して、テキストではなく画像でクエリを実行することにより、動画コンテンツ内の特定のカラーや商品を検出する「Shade Finder」アプリの構築手順を解説します。また、インデックス化された動画の閲覧と、タイムスタンプ付き検索結果の表示を行うための、JavaScriptおよびNodeで構成されたフロントエンドも備えています。
このチュートリアルでは、Twelve Labsのimage-to-video(画像から動画)検索APIを使用して、テキストではなく画像でクエリを実行することにより、動画コンテンツ内の特定のカラーや商品を検出する「Shade Finder」アプリの構築手順を解説します。また、インデックス化された動画の閲覧と、タイムスタンプ付き検索結果の表示を行うための、JavaScriptおよびNodeで構成されたフロントエンドも備えています。

この記事の内容
ニュースレターに登録する
ニュースレターに登録する
ビデオ理解に関する最新の技術進歩、チュートリアル、業界の動向をお届けします
ビデオ理解に関する最新の技術進歩、チュートリアル、業界の動向をお届けします
AIを活用してビデオを検索、分析、探索します。
2024/08/21
12分
記事へのリンクをコピー
動画内の特定の色のトーンをピンポイントで特定したい、例えば好きな色合いが特徴的な製品や特定の瞬間を見つけたいと思ったことはありませんか?最近、パーソナルカラー診断サービスを受け、私にはベリー系のトーンが一番似合うことが分かりました。
自分がアーカイブに保存しているYouTube動画のコレクションをくまなく探しながら、それらの正確な色合いの製品を簡単に特定できる方法があればいいのにと思いました。幸いなことに、Twelve Labsの画像から動画への検索テクノロジーの力を活用して、まさにそれを実現するアプリを作成することができました。
このチュートリアルでは、Twelve Labs APIを使用して、この「Shade Finder(シェードファインダー)」アプリをどのように構築したかを順を追って説明します。ベリー系の色味の完璧なリップスティックを探している方にも、動画内の特定の色を見つけることに興味があるだけの方にも、このガイドは最先端のAIを活用してそれを簡単に行うのに役立ちます。それでは、早速始めましょう!
📌 デモをチェックしてみてください!

前提条件
Twelve Labs プレイグラウンドにアクセスし、サインアップしてAPIキーを生成します。
次に、インデックスを作成し、このインデックスに動画をアップロードします。それが完了したら、動画検索を始める準備は完了です!
このアプリはJavaScriptとNodeで構築されました。
このアプリのすべてのファイルを含むリポジトリは、GitHubで入手できます。
目次
アプリの構造はシンプルで、分かりやすいものです。大まかに言うと、index.html、script.js、server.jsの3つの主要なコンポーネントで構成されています。
まず index.html の概要を簡単に説明したあと、サーバー側とクライアント側の両方のフローを掘り下げます。そこでは、動画の取得、単一の動画の取得、画像に基づく検索、そしてページトークンを使用した検索結果のページネーションの方法について説明します。

HTML
index.html ファイルはアプリの骨組みとして機能し、基本的な構造とレイアウトを提供します。server.js ファイルは、SDKを介してTwelve Labs APIへのすべてのAPI呼び出しを管理し、アプリが関連データを効率的に処理して返すようにします。script.js ファイルはクライアント側のロジックとして機能し、ユーザーの操作を処理し、サーバーにリクエストを送信し、検索操作を実行します。
以下は index.html のbody部分で、アプリのコアカポーネントを配置しています。
クエリ画像を表示するための画像カルーセル
クエリを開始するための検索ボタン
特定のインデックスからの動画を表示する動画リスト
検索ボタンがクリックされた後に結果を表示する検索結果セクション
<body> <h1 class="text-3xl text-center m-5 p-3"><i class="fa-solid fa-palette"></i> Shade Finder</h1> <div class="m-5 p-3"> <p class="text-center m-5" id="color-label"></p> <div class="flex justify-center gap-5"> <button id="prev"><i class="fa-solid fa-chevron-left"></i></button> <div class="size-40"><img id="carousel-image"></div> <button id="next"><i class="fa-solid fa-chevron-right"></i></button> </div> <div class="flex justify-center m-5 gap-2"> <button id="search" class="bg-lime-400 py-2.5 px-3">Search</button> </div> </div> <div id ="video-list-container" class="container max-w-5xl mx-auto py-4"> <div id ="video-list-loading" class="container max-w-5xl mx-auto py-4"> </div> <div id="video-list" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 justify-items-center"></div> <div id="video-list-pagination" class="flex justify-center m-5 gap-2"></div> </div> <div id="search-result-container" class="container w-5/6 mx-auto py-4 hidden"> <div id="search-result-list" class="grid grid-cols-1 md:grid-cols-4 justify-center"></div> </div> <script src="./script.js"></script> </body
サーバー
server.js は、Twelve Labs APIへのすべてのAPI呼び出しを管理するファイルです。これには4つのルートがあります。動画の取得/ページネーション、動画の取得、(画像から動画への)検索、そしてページトークンによる検索結果の取得です。
Twelve Labs APIへの4つのリクエスト
💡Twelve Labsは、アプリケーション内にプラットフォームを構築して活用できるようにするSDKを提供しています。このアプリでは、Javascript SDK (バージョン 0.2.5) が使用されました。
セットアップ
1 - Twelve Labs APIキーとインデックスIDを.envに保存する
backendフォルダ内に、キーがコメントアウトされた .env ファイル があります。コメントアウトを解除し、値を更新してください。
.env
TWELVE_LABS_API_KEY=<YOUR API KEY> TWELVE_LABS_INDEX_ID=<YOUR_INDEX_ID>
2 - Twelve Labs SDKをインストールしてインポートする
まず、twelvelabs-js パッケージをインストールします。
yarn add twelvelabs-js # or npm i twelvelabs-js
次に、必要なパッケージをアプリケーションにインポートします。以下に示すように、server.js(Node.jsで構築)にインポートしました。
const fs = require("fs"); const path = require("path"); const { TwelveLabs } = require("twelvelabs-js");
最後に、APIキーを使用してSDKクライアントを初期化します。
const client = new TwelveLabs({ apiKey: API_KEY });
以下の全体像をご覧ください。
"use strict"; const express = require("express"); const dotenv = require("dotenv"); const cors = require("cors"); const asyncHandler = require("express-async-handler"); const fs = require("fs"); const path = require("path"); const { TwelveLabs } = require("twelvelabs-js"); dotenv.config(); const app = express(); app.use(express.json()); app.use(cors()); app.use(express.static(path.join(__dirname, "../frontend/public"))); const PORT = 5001; const API_KEY = process.env.TWELVE_LABS_API_KEY; const INDEX_ID = process.env.TWELVE_LABS_INDEX_ID; const client = new TwelveLabs({ apiKey: API_KEY }); app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
リクエスト 1. 動画の取得/ページネーション
ページごとに動画を取得するには、client.index.video.listPagination を使用し、インデックスIDと目的のページを渡します。必要に応じて、1ページあたりに返される動画の数を制御するために pageLimit パラメータを含めることもできます。
💡 ヒント: API ドキュメントに記載されている(すべてのリクエスト用の)パラメータは、Javascript SDK内でキャメルケース(camelCase)に変換することで使用できます。
動画を受信した後、後で使用するために id と metadata を抽出し、pageInfo と合わせて返します。
💡 動画のページネーションの詳細については、ガイドをご覧ください。公式のサンプルコードも非常に役立ちます!
/** Get videos */ app.get( "/videos", asyncHandler(async (req, res, next) => { const { page_limit, page } = req.query; const videosResponse = await client.index.video.listPagination(INDEX_ID, { pageLimit: page_limit, page: page, }); const videos = videosResponse.data.map((video) => ({ id: video.id, metadata: video.metadata, })); res.json({ videos, page_info: videosResponse.pageInfo, }); }) );
動画レスポンス
videosResponse= VideoListWithPagination { ..., data: [ Video { _resource: [Video], _indexId: '...', id: '...', metadata: [Object], hls: undefined, source: undefined, indexedAt: '2024-06-27T05:11:29Z', createdAt: '2024-06-27T05:01:35Z', updatedAt: '2024-06-27T05:01:52Z' }, ... ], pageInfo: { page: 1, limitPerPage: 12, totalPage: 3, totalResults: 29, totalDuration: 19122 } }
リクエスト 2. 動画の取得
動画の取得/ページネーションと同様に、インデックスIDと動画IDを渡して client.index.video.retrieve を使用することで、単一の動画の詳細を取得できます。
動画データを受信した後、必要な情報(metadata、hls、source)のみを抽出して返します。具体的には、後ほどメタデータから動画タイトル、HLSからサムネイルのURL、ソースからURLを使用します。
/** Get a video of an index */ app.get( "/videos/:videoId", asyncHandler(async (req, res, next) => { const { videoId } = req.params; const videoResponse = await client.index.video.retrieve(INDEX_ID, videoId); res.json({ metadata: videoResponse.metadata, hls: videoResponse.hls, source: videoResponse.source, }); }) );
動画レスポンス
videoResponse= Video { ..., id: '...', metadata: { duration: 54, engine_ids: [ 'marengo2.6', 'pegasus1.1' ], filename: 'tirtir korean cushion review', fps: 30, height: 1280, size: 9601300, video_title: 'tirtir korean cushion review', width: 720 }, hls: { videoUrl: '... .m3u8', thumbnailUrls: ['... .jpg'], status: 'COMPLETE', updatedAt: '2024-05-22T02:49:49.074Z' }, source: { type: 'youtube', name: 'theoliviasaurusrex', url: 'https://www.youtube.com/watch?v=tOabvdtTa-U' }, indexedAt: '2024-05-22T03:03:53Z', createdAt: '2024-05-22T02:49:28Z', updatedAt: '2024-05-22T02:49:36Z' }
リクエスト 3. 検索 (画像から動画)
ここからが楽しいパートです!client.search.query メソッドを使用し、4つの必須パラメータ(indexId、queryMediaFile、queryMediaType、options)を渡すことで、画像から動画への検索を実行できます。
特に、queryMediaFileを正しく渡すには、いくつかの手順が必要です。
パスの構築: まず、画像ファイルへのフルパスを構築する必要があります。このアプリでは、すべての画像がすでに images フォルダに保存されています。そのため、現在のディレクトリ(__dirname)、このアプリにとっての相対パスである ../frontend/public/images、および画像のファイル名(imageSrc)を使用して構築を行います。
存在確認: パスを構築した後、画像ファイルがその場所に存在するかどうかを確認します。ファイルが見つからない場合は、404エラーレスポンスがクライアントに返されます。
読み取りストリームの作成: ファイルが存在する場合は、画像ファイルから読み取り可能なストリームが作成されます。このストリームは、その後 Twelve Labs API に効率的に送信されます。
このアプリでは、threshold、pageLimit、adjustConfidenceLevel などのオプションパラメータも含めました。パラメータの詳細な一覧は API ドキュメントでご確認いただけます。
動画を受信した後、後で使用するために data と pageInfo を抽出し、クライアントに返します。
💡 画像クエリ検索の詳細については、ガイドを必ず確認してください。公式のサンプルコードも非常に便利です!
/** Search videos based on an image query */ app.get( "/search", asyncHandler(async (req, res, next) => { const { imageSrc, threshold, pageLimit, adjustConfidenceLevel } = req.query; const imagePath = path.join( __dirname, "../frontend/public/images", imageSrc ); if (!fs.existsSync(imagePath)) { console.error("Image not found at path:", imagePath); return res.status(404).json({ error: "Image not found" }); } const searchResponse = await client.search.query({ indexId: INDEX_ID, queryMediaFile: fs.createReadStream(imagePath), queryMediaType: "image", options: ["visual"], threshold: threshold, pageLimit: pageLimit, adjustConfidenceLevel: adjustConfidenceLevel, }); res.json({ searchResults: searchResponse.data, pageInfo: searchResponse.pageInfo, }); }) );
検索レスポンス
searchResponse= SearchResult { ..., pool: { totalCount: 29, totalDuration: 19122, indexId: '...' }, data: [ { score: 84.45, start: 379.13333333341933, end: 381, metadata: [Array], videoId: '...', confidence: 'high', thumbnailUrl: '...' }, ... ], pageInfo: { limitPerPage: 12, totalResults: 20, pageExpiredAt: '2024-08-15T04:09:46Z' nextPageToken: '...' //This might not exist } }
リクエスト 4. ページトークンによる検索
ページトークンによる検索は非常にシンプルです。client.search.byPageToken を使用し、前のリクエスト(リクエスト 3)から取得した pageToken を渡します。レスポンスは、最初の検索リクエスト(リクエスト 3)から受信したものと同じ構造になります。
/** Get search results of a specific page */ app.get( "/search/:pageToken", asyncHandler(async (req, res, next) => { const { pageToken } = req.params; let searchByPageResponse = await client.search.byPageToken(`${pageToken}`); res.json({ searchResults: searchByPageResponse.data, pageInfo: searchByPageResponse.pageInfo, }); }) );
クライアント
必要なすべてのAPIエンドポイントを含めてサーバーの設定が完全に完了したので、ここからはクライアント側のコードに焦点を当てます。アプリケーションのこの部分は、サーバーにリクエストを送信し、受信したデータを処理する役割を担います。
サーバー側で確立されたフローに従って、まずアプリが最初にインデックスから動画を表示する仕組みを説明します。その後、動画の検索機能と検索結果のページネーション機能について説明します。
1 - インデックスの動画表示

showVideos 関数
ページがレンダリングされるときに最初に実行される関数の1つが showVideos です。
async function showVideos(page = 1) { videoList.innerHTML = ""; ... try { const { videosDetail, pageInfo } = await getVideoOfVideos(page); videoListLoading.removeChild(loadingSpinnerContainer); if (videosDetail) { videosDetail.forEach((video) => { const videoContainer = createVideoContainer(video); videoList.appendChild(videoContainer); }); videoListLoading.classList.remove("min-h-[300px]"); createPaginationButtons(pageInfo, page); } } catch (error) { console.error("Error fetching videos:", error); } }
DOM内の既存の動画リストをクリアします。
特定のページにあるすべての動画の詳細を取得する getVideoOfVideos を呼び出します。
動画の詳細をループ処理して、各動画のコンテナを作成し追加します。
最後に、pageInfo に基づいてページネーションのボタンを設定します。
getVideoOfVideos 関数
getVideoOfVideos 関数は、特定のページの動画を取得し、その後、各動画の詳細を取得する役割を持ちます。
async function getVideoOfVideos(page = 1) { const videosResponse = await getVideos(page); if (videosResponse.videos.length > 0) { const videosDetail = await Promise.all( videosResponse.videos.map((video) => getVideo(video.id)) ); return { videosDetail, pageInfo: videosResponse.page_info }; } }
getVideos は、指定されたページの動画を取得するためにサーバーにリクエストを送信します(「サーバー」セクションのリクエスト1の実装通り)。
動画が見つかった場合、関数は各動画をループして、詳細を取得するために getVideo を呼び出します(「サーバー」セクションのリクエスト2の実装通り)。
その後、その後のリクエストを最適化するために、シンプルなキャッシュ機構を使用して詳細がキャッシュされます。
動画コンテナの作成
showVideos が各動画の詳細を取得すると、動画のURL、サムネイルURL、動画のタイトルなどの詳細に基づいて動画コンテナを作成します。
ページネーションボタン
最後に、getVideos から取得した総ページ数に基づいてページネーションボタンが作成されます。各ボタンには、それぞれのページに対して showVideos を呼び出すイベントリスナーが設定されます。
function createPageButton(pageNumber, currentPage) { const pageButton = document.createElement("button"); pageButton.textContent = pageNumber; ... if (pageNumber === currentPage) { pageButton.classList.add("bg-slate-200", "font-medium"); pageButton.disabled = true; } else { pageButton.classList.add("bg-transparent"); pageButton.addEventListener("click", () => showVideos(pageNumber)); } return pageButton; }
2 - 画像による動画検索

ユーザーが「Search」ボタンをクリックすると、handleSearchButtonClick 関数が実行され、サーバーへの検索リクエストが行われます。どのように機能するのか、ステップバイステップで見ていきましょう。
async function handleSearchButtonClick() { toggleSearchButton(false); nextPageToken = null; searchResultContainer.innerHTML = ""; videoListContainer.classList.add("hidden"); searchResultContainer.classList.remove("hidden"); searchResultList.innerHTML = ""; const loadingSpinnerContainer = createLoadingSpinner(); searchResultContainer.appendChild(loadingSpinnerContainer); try { const { searchResults } = await searchByImage(); searchResultContainer.removeChild(loadingSpinnerContainer); if (searchResults.length > 0) { showSearchResults(searchResults); } else { displayNoResultsMessage(); } } catch (error) { console.error("Error fetching search results:", error); } finally { toggleSearchButton(true); } }
まず、検索処理中にボタンを無効化するために、検索ボタンの状態(トグル)を false に切り替えます。
次に、関数は nextPageToken を null にリセットし、searchResultContainer と searchResultList 内の既存のコンテンツをクリアします。また、videoListContainer を非表示にし、searchResultContainer が表示されるようにします。
その後、検索が進行中であることを示すために読み込みスピナーが作成され、表示されます。
サーバーへ検索リクエストを行う searchByImage を呼び出すことで、検索が実行されます。
検索完了後、読み込みスピナーは削除され、showSearchResults を使って検索結果が表示されるか、結果が見つからなかった場合はメッセージが表示されます。
最後に、検索ボタンの状態を true に戻して有効化し、ユーザーが望む場合に再度検索を実行できるようにします。
3 - ページTokenによる検索結果の表示
検索結果が複数ページにまたがる場合(すなわち、nextPageToken が存在する場合)、createShowMoreButton 関数が実行され、ユーザーに「Show More」ボタンが表示されます。このボタンにより、次のページの検索結果が取得および表示されます。その仕組みを順を追ってご説明します。

function createShowMoreButton() { removeExistingButton(); const showMoreButtonContainer = document.createElement("div"); showMoreButtonContainer.id = "show-more-button"; showMoreButtonContainer.classList.add("flex", "justify-center", "my-4"); const showMoreButton = document.createElement("button"); showMoreButton.innerHTML = '<i class="fa-solid fa-chevron-up"></i> Show More'; showMoreButton.addEventListener("click", async () => { const loadingSpinnerContainer = createLoadingSpinner(); searchResultContainer.appendChild(loadingSpinnerContainer); const nextPageResults = await getNextSearchResults(nextPageToken); searchResultContainer.removeChild(loadingSpinnerContainer); showSearchResults(nextPageResults.searchResults); nextPageToken = nextPageResults.pageInfo.nextPageToken || null; }); showMoreButtonContainer.appendChild(showMoreButton); searchResultContainer.appendChild(showMoreButtonContainer); }
まず、ボタンが重複するのを防ぐために、既存の「Show More」ボタンを削除します。
次に、表示用のコンテナと「Show More」ボタンを作成します。
クリック動作を処理するために、ボタンにイベントリスナーを追加します。クリックすると、読み込みスピナーの表示、次の検索結果の取得、スピナーの削除、新しい検索結果の表示、および nextPageToken の更新を行います。
最後に、「Show More」ボタンとそのコンテナを searchResultContainer に追加して、ユーザーに表示されるようにします。
結論
この記事が、Twelve Labsの新しい画像から動画への検索(image-to-video search)APIとその実用的な応用例についての洞察を得る助けとなれば幸いです。最後までお読みいただきありがとうございました。これらの進歩をご自身のプロジェクトでどのように活用されるかを楽しみにしています!
動画内の特定の色のトーンをピンポイントで特定したい、例えば好きな色合いが特徴的な製品や特定の瞬間を見つけたいと思ったことはありませんか?最近、パーソナルカラー診断サービスを受け、私にはベリー系のトーンが一番似合うことが分かりました。
自分がアーカイブに保存しているYouTube動画のコレクションをくまなく探しながら、それらの正確な色合いの製品を簡単に特定できる方法があればいいのにと思いました。幸いなことに、Twelve Labsの画像から動画への検索テクノロジーの力を活用して、まさにそれを実現するアプリを作成することができました。
このチュートリアルでは、Twelve Labs APIを使用して、この「Shade Finder(シェードファインダー)」アプリをどのように構築したかを順を追って説明します。ベリー系の色味の完璧なリップスティックを探している方にも、動画内の特定の色を見つけることに興味があるだけの方にも、このガイドは最先端のAIを活用してそれを簡単に行うのに役立ちます。それでは、早速始めましょう!
📌 デモをチェックしてみてください!

前提条件
Twelve Labs プレイグラウンドにアクセスし、サインアップしてAPIキーを生成します。
次に、インデックスを作成し、このインデックスに動画をアップロードします。それが完了したら、動画検索を始める準備は完了です!
このアプリはJavaScriptとNodeで構築されました。
このアプリのすべてのファイルを含むリポジトリは、GitHubで入手できます。
目次
アプリの構造はシンプルで、分かりやすいものです。大まかに言うと、index.html、script.js、server.jsの3つの主要なコンポーネントで構成されています。
まず index.html の概要を簡単に説明したあと、サーバー側とクライアント側の両方のフローを掘り下げます。そこでは、動画の取得、単一の動画の取得、画像に基づく検索、そしてページトークンを使用した検索結果のページネーションの方法について説明します。

HTML
index.html ファイルはアプリの骨組みとして機能し、基本的な構造とレイアウトを提供します。server.js ファイルは、SDKを介してTwelve Labs APIへのすべてのAPI呼び出しを管理し、アプリが関連データを効率的に処理して返すようにします。script.js ファイルはクライアント側のロジックとして機能し、ユーザーの操作を処理し、サーバーにリクエストを送信し、検索操作を実行します。
以下は index.html のbody部分で、アプリのコアカポーネントを配置しています。
クエリ画像を表示するための画像カルーセル
クエリを開始するための検索ボタン
特定のインデックスからの動画を表示する動画リスト
検索ボタンがクリックされた後に結果を表示する検索結果セクション
<body> <h1 class="text-3xl text-center m-5 p-3"><i class="fa-solid fa-palette"></i> Shade Finder</h1> <div class="m-5 p-3"> <p class="text-center m-5" id="color-label"></p> <div class="flex justify-center gap-5"> <button id="prev"><i class="fa-solid fa-chevron-left"></i></button> <div class="size-40"><img id="carousel-image"></div> <button id="next"><i class="fa-solid fa-chevron-right"></i></button> </div> <div class="flex justify-center m-5 gap-2"> <button id="search" class="bg-lime-400 py-2.5 px-3">Search</button> </div> </div> <div id ="video-list-container" class="container max-w-5xl mx-auto py-4"> <div id ="video-list-loading" class="container max-w-5xl mx-auto py-4"> </div> <div id="video-list" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 justify-items-center"></div> <div id="video-list-pagination" class="flex justify-center m-5 gap-2"></div> </div> <div id="search-result-container" class="container w-5/6 mx-auto py-4 hidden"> <div id="search-result-list" class="grid grid-cols-1 md:grid-cols-4 justify-center"></div> </div> <script src="./script.js"></script> </body
サーバー
server.js は、Twelve Labs APIへのすべてのAPI呼び出しを管理するファイルです。これには4つのルートがあります。動画の取得/ページネーション、動画の取得、(画像から動画への)検索、そしてページトークンによる検索結果の取得です。
Twelve Labs APIへの4つのリクエスト
💡Twelve Labsは、アプリケーション内にプラットフォームを構築して活用できるようにするSDKを提供しています。このアプリでは、Javascript SDK (バージョン 0.2.5) が使用されました。
セットアップ
1 - Twelve Labs APIキーとインデックスIDを.envに保存する
backendフォルダ内に、キーがコメントアウトされた .env ファイル があります。コメントアウトを解除し、値を更新してください。
.env
TWELVE_LABS_API_KEY=<YOUR API KEY> TWELVE_LABS_INDEX_ID=<YOUR_INDEX_ID>
2 - Twelve Labs SDKをインストールしてインポートする
まず、twelvelabs-js パッケージをインストールします。
yarn add twelvelabs-js # or npm i twelvelabs-js
次に、必要なパッケージをアプリケーションにインポートします。以下に示すように、server.js(Node.jsで構築)にインポートしました。
const fs = require("fs"); const path = require("path"); const { TwelveLabs } = require("twelvelabs-js");
最後に、APIキーを使用してSDKクライアントを初期化します。
const client = new TwelveLabs({ apiKey: API_KEY });
以下の全体像をご覧ください。
"use strict"; const express = require("express"); const dotenv = require("dotenv"); const cors = require("cors"); const asyncHandler = require("express-async-handler"); const fs = require("fs"); const path = require("path"); const { TwelveLabs } = require("twelvelabs-js"); dotenv.config(); const app = express(); app.use(express.json()); app.use(cors()); app.use(express.static(path.join(__dirname, "../frontend/public"))); const PORT = 5001; const API_KEY = process.env.TWELVE_LABS_API_KEY; const INDEX_ID = process.env.TWELVE_LABS_INDEX_ID; const client = new TwelveLabs({ apiKey: API_KEY }); app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
リクエスト 1. 動画の取得/ページネーション
ページごとに動画を取得するには、client.index.video.listPagination を使用し、インデックスIDと目的のページを渡します。必要に応じて、1ページあたりに返される動画の数を制御するために pageLimit パラメータを含めることもできます。
💡 ヒント: API ドキュメントに記載されている(すべてのリクエスト用の)パラメータは、Javascript SDK内でキャメルケース(camelCase)に変換することで使用できます。
動画を受信した後、後で使用するために id と metadata を抽出し、pageInfo と合わせて返します。
💡 動画のページネーションの詳細については、ガイドをご覧ください。公式のサンプルコードも非常に役立ちます!
/** Get videos */ app.get( "/videos", asyncHandler(async (req, res, next) => { const { page_limit, page } = req.query; const videosResponse = await client.index.video.listPagination(INDEX_ID, { pageLimit: page_limit, page: page, }); const videos = videosResponse.data.map((video) => ({ id: video.id, metadata: video.metadata, })); res.json({ videos, page_info: videosResponse.pageInfo, }); }) );
動画レスポンス
videosResponse= VideoListWithPagination { ..., data: [ Video { _resource: [Video], _indexId: '...', id: '...', metadata: [Object], hls: undefined, source: undefined, indexedAt: '2024-06-27T05:11:29Z', createdAt: '2024-06-27T05:01:35Z', updatedAt: '2024-06-27T05:01:52Z' }, ... ], pageInfo: { page: 1, limitPerPage: 12, totalPage: 3, totalResults: 29, totalDuration: 19122 } }
リクエスト 2. 動画の取得
動画の取得/ページネーションと同様に、インデックスIDと動画IDを渡して client.index.video.retrieve を使用することで、単一の動画の詳細を取得できます。
動画データを受信した後、必要な情報(metadata、hls、source)のみを抽出して返します。具体的には、後ほどメタデータから動画タイトル、HLSからサムネイルのURL、ソースからURLを使用します。
/** Get a video of an index */ app.get( "/videos/:videoId", asyncHandler(async (req, res, next) => { const { videoId } = req.params; const videoResponse = await client.index.video.retrieve(INDEX_ID, videoId); res.json({ metadata: videoResponse.metadata, hls: videoResponse.hls, source: videoResponse.source, }); }) );
動画レスポンス
videoResponse= Video { ..., id: '...', metadata: { duration: 54, engine_ids: [ 'marengo2.6', 'pegasus1.1' ], filename: 'tirtir korean cushion review', fps: 30, height: 1280, size: 9601300, video_title: 'tirtir korean cushion review', width: 720 }, hls: { videoUrl: '... .m3u8', thumbnailUrls: ['... .jpg'], status: 'COMPLETE', updatedAt: '2024-05-22T02:49:49.074Z' }, source: { type: 'youtube', name: 'theoliviasaurusrex', url: 'https://www.youtube.com/watch?v=tOabvdtTa-U' }, indexedAt: '2024-05-22T03:03:53Z', createdAt: '2024-05-22T02:49:28Z', updatedAt: '2024-05-22T02:49:36Z' }
リクエスト 3. 検索 (画像から動画)
ここからが楽しいパートです!client.search.query メソッドを使用し、4つの必須パラメータ(indexId、queryMediaFile、queryMediaType、options)を渡すことで、画像から動画への検索を実行できます。
特に、queryMediaFileを正しく渡すには、いくつかの手順が必要です。
パスの構築: まず、画像ファイルへのフルパスを構築する必要があります。このアプリでは、すべての画像がすでに images フォルダに保存されています。そのため、現在のディレクトリ(__dirname)、このアプリにとっての相対パスである ../frontend/public/images、および画像のファイル名(imageSrc)を使用して構築を行います。
存在確認: パスを構築した後、画像ファイルがその場所に存在するかどうかを確認します。ファイルが見つからない場合は、404エラーレスポンスがクライアントに返されます。
読み取りストリームの作成: ファイルが存在する場合は、画像ファイルから読み取り可能なストリームが作成されます。このストリームは、その後 Twelve Labs API に効率的に送信されます。
このアプリでは、threshold、pageLimit、adjustConfidenceLevel などのオプションパラメータも含めました。パラメータの詳細な一覧は API ドキュメントでご確認いただけます。
動画を受信した後、後で使用するために data と pageInfo を抽出し、クライアントに返します。
💡 画像クエリ検索の詳細については、ガイドを必ず確認してください。公式のサンプルコードも非常に便利です!
/** Search videos based on an image query */ app.get( "/search", asyncHandler(async (req, res, next) => { const { imageSrc, threshold, pageLimit, adjustConfidenceLevel } = req.query; const imagePath = path.join( __dirname, "../frontend/public/images", imageSrc ); if (!fs.existsSync(imagePath)) { console.error("Image not found at path:", imagePath); return res.status(404).json({ error: "Image not found" }); } const searchResponse = await client.search.query({ indexId: INDEX_ID, queryMediaFile: fs.createReadStream(imagePath), queryMediaType: "image", options: ["visual"], threshold: threshold, pageLimit: pageLimit, adjustConfidenceLevel: adjustConfidenceLevel, }); res.json({ searchResults: searchResponse.data, pageInfo: searchResponse.pageInfo, }); }) );
検索レスポンス
searchResponse= SearchResult { ..., pool: { totalCount: 29, totalDuration: 19122, indexId: '...' }, data: [ { score: 84.45, start: 379.13333333341933, end: 381, metadata: [Array], videoId: '...', confidence: 'high', thumbnailUrl: '...' }, ... ], pageInfo: { limitPerPage: 12, totalResults: 20, pageExpiredAt: '2024-08-15T04:09:46Z' nextPageToken: '...' //This might not exist } }
リクエスト 4. ページトークンによる検索
ページトークンによる検索は非常にシンプルです。client.search.byPageToken を使用し、前のリクエスト(リクエスト 3)から取得した pageToken を渡します。レスポンスは、最初の検索リクエスト(リクエスト 3)から受信したものと同じ構造になります。
/** Get search results of a specific page */ app.get( "/search/:pageToken", asyncHandler(async (req, res, next) => { const { pageToken } = req.params; let searchByPageResponse = await client.search.byPageToken(`${pageToken}`); res.json({ searchResults: searchByPageResponse.data, pageInfo: searchByPageResponse.pageInfo, }); }) );
クライアント
必要なすべてのAPIエンドポイントを含めてサーバーの設定が完全に完了したので、ここからはクライアント側のコードに焦点を当てます。アプリケーションのこの部分は、サーバーにリクエストを送信し、受信したデータを処理する役割を担います。
サーバー側で確立されたフローに従って、まずアプリが最初にインデックスから動画を表示する仕組みを説明します。その後、動画の検索機能と検索結果のページネーション機能について説明します。
1 - インデックスの動画表示

showVideos 関数
ページがレンダリングされるときに最初に実行される関数の1つが showVideos です。
async function showVideos(page = 1) { videoList.innerHTML = ""; ... try { const { videosDetail, pageInfo } = await getVideoOfVideos(page); videoListLoading.removeChild(loadingSpinnerContainer); if (videosDetail) { videosDetail.forEach((video) => { const videoContainer = createVideoContainer(video); videoList.appendChild(videoContainer); }); videoListLoading.classList.remove("min-h-[300px]"); createPaginationButtons(pageInfo, page); } } catch (error) { console.error("Error fetching videos:", error); } }
DOM内の既存の動画リストをクリアします。
特定のページにあるすべての動画の詳細を取得する getVideoOfVideos を呼び出します。
動画の詳細をループ処理して、各動画のコンテナを作成し追加します。
最後に、pageInfo に基づいてページネーションのボタンを設定します。
getVideoOfVideos 関数
getVideoOfVideos 関数は、特定のページの動画を取得し、その後、各動画の詳細を取得する役割を持ちます。
async function getVideoOfVideos(page = 1) { const videosResponse = await getVideos(page); if (videosResponse.videos.length > 0) { const videosDetail = await Promise.all( videosResponse.videos.map((video) => getVideo(video.id)) ); return { videosDetail, pageInfo: videosResponse.page_info }; } }
getVideos は、指定されたページの動画を取得するためにサーバーにリクエストを送信します(「サーバー」セクションのリクエスト1の実装通り)。
動画が見つかった場合、関数は各動画をループして、詳細を取得するために getVideo を呼び出します(「サーバー」セクションのリクエスト2の実装通り)。
その後、その後のリクエストを最適化するために、シンプルなキャッシュ機構を使用して詳細がキャッシュされます。
動画コンテナの作成
showVideos が各動画の詳細を取得すると、動画のURL、サムネイルURL、動画のタイトルなどの詳細に基づいて動画コンテナを作成します。
ページネーションボタン
最後に、getVideos から取得した総ページ数に基づいてページネーションボタンが作成されます。各ボタンには、それぞれのページに対して showVideos を呼び出すイベントリスナーが設定されます。
function createPageButton(pageNumber, currentPage) { const pageButton = document.createElement("button"); pageButton.textContent = pageNumber; ... if (pageNumber === currentPage) { pageButton.classList.add("bg-slate-200", "font-medium"); pageButton.disabled = true; } else { pageButton.classList.add("bg-transparent"); pageButton.addEventListener("click", () => showVideos(pageNumber)); } return pageButton; }
2 - 画像による動画検索

ユーザーが「Search」ボタンをクリックすると、handleSearchButtonClick 関数が実行され、サーバーへの検索リクエストが行われます。どのように機能するのか、ステップバイステップで見ていきましょう。
async function handleSearchButtonClick() { toggleSearchButton(false); nextPageToken = null; searchResultContainer.innerHTML = ""; videoListContainer.classList.add("hidden"); searchResultContainer.classList.remove("hidden"); searchResultList.innerHTML = ""; const loadingSpinnerContainer = createLoadingSpinner(); searchResultContainer.appendChild(loadingSpinnerContainer); try { const { searchResults } = await searchByImage(); searchResultContainer.removeChild(loadingSpinnerContainer); if (searchResults.length > 0) { showSearchResults(searchResults); } else { displayNoResultsMessage(); } } catch (error) { console.error("Error fetching search results:", error); } finally { toggleSearchButton(true); } }
まず、検索処理中にボタンを無効化するために、検索ボタンの状態(トグル)を false に切り替えます。
次に、関数は nextPageToken を null にリセットし、searchResultContainer と searchResultList 内の既存のコンテンツをクリアします。また、videoListContainer を非表示にし、searchResultContainer が表示されるようにします。
その後、検索が進行中であることを示すために読み込みスピナーが作成され、表示されます。
サーバーへ検索リクエストを行う searchByImage を呼び出すことで、検索が実行されます。
検索完了後、読み込みスピナーは削除され、showSearchResults を使って検索結果が表示されるか、結果が見つからなかった場合はメッセージが表示されます。
最後に、検索ボタンの状態を true に戻して有効化し、ユーザーが望む場合に再度検索を実行できるようにします。
3 - ページTokenによる検索結果の表示
検索結果が複数ページにまたがる場合(すなわち、nextPageToken が存在する場合)、createShowMoreButton 関数が実行され、ユーザーに「Show More」ボタンが表示されます。このボタンにより、次のページの検索結果が取得および表示されます。その仕組みを順を追ってご説明します。

function createShowMoreButton() { removeExistingButton(); const showMoreButtonContainer = document.createElement("div"); showMoreButtonContainer.id = "show-more-button"; showMoreButtonContainer.classList.add("flex", "justify-center", "my-4"); const showMoreButton = document.createElement("button"); showMoreButton.innerHTML = '<i class="fa-solid fa-chevron-up"></i> Show More'; showMoreButton.addEventListener("click", async () => { const loadingSpinnerContainer = createLoadingSpinner(); searchResultContainer.appendChild(loadingSpinnerContainer); const nextPageResults = await getNextSearchResults(nextPageToken); searchResultContainer.removeChild(loadingSpinnerContainer); showSearchResults(nextPageResults.searchResults); nextPageToken = nextPageResults.pageInfo.nextPageToken || null; }); showMoreButtonContainer.appendChild(showMoreButton); searchResultContainer.appendChild(showMoreButtonContainer); }
まず、ボタンが重複するのを防ぐために、既存の「Show More」ボタンを削除します。
次に、表示用のコンテナと「Show More」ボタンを作成します。
クリック動作を処理するために、ボタンにイベントリスナーを追加します。クリックすると、読み込みスピナーの表示、次の検索結果の取得、スピナーの削除、新しい検索結果の表示、および nextPageToken の更新を行います。
最後に、「Show More」ボタンとそのコンテナを searchResultContainer に追加して、ユーザーに表示されるようにします。
結論
この記事が、Twelve Labsの新しい画像から動画への検索(image-to-video search)APIとその実用的な応用例についての洞察を得る助けとなれば幸いです。最後までお読みいただきありがとうございました。これらの進歩をご自身のプロジェクトでどのように活用されるかを楽しみにしています!
動画内の特定の色のトーンをピンポイントで特定したい、例えば好きな色合いが特徴的な製品や特定の瞬間を見つけたいと思ったことはありませんか?最近、パーソナルカラー診断サービスを受け、私にはベリー系のトーンが一番似合うことが分かりました。
自分がアーカイブに保存しているYouTube動画のコレクションをくまなく探しながら、それらの正確な色合いの製品を簡単に特定できる方法があればいいのにと思いました。幸いなことに、Twelve Labsの画像から動画への検索テクノロジーの力を活用して、まさにそれを実現するアプリを作成することができました。
このチュートリアルでは、Twelve Labs APIを使用して、この「Shade Finder(シェードファインダー)」アプリをどのように構築したかを順を追って説明します。ベリー系の色味の完璧なリップスティックを探している方にも、動画内の特定の色を見つけることに興味があるだけの方にも、このガイドは最先端のAIを活用してそれを簡単に行うのに役立ちます。それでは、早速始めましょう!
📌 デモをチェックしてみてください!

前提条件
Twelve Labs プレイグラウンドにアクセスし、サインアップしてAPIキーを生成します。
次に、インデックスを作成し、このインデックスに動画をアップロードします。それが完了したら、動画検索を始める準備は完了です!
このアプリはJavaScriptとNodeで構築されました。
このアプリのすべてのファイルを含むリポジトリは、GitHubで入手できます。
目次
アプリの構造はシンプルで、分かりやすいものです。大まかに言うと、index.html、script.js、server.jsの3つの主要なコンポーネントで構成されています。
まず index.html の概要を簡単に説明したあと、サーバー側とクライアント側の両方のフローを掘り下げます。そこでは、動画の取得、単一の動画の取得、画像に基づく検索、そしてページトークンを使用した検索結果のページネーションの方法について説明します。

HTML
index.html ファイルはアプリの骨組みとして機能し、基本的な構造とレイアウトを提供します。server.js ファイルは、SDKを介してTwelve Labs APIへのすべてのAPI呼び出しを管理し、アプリが関連データを効率的に処理して返すようにします。script.js ファイルはクライアント側のロジックとして機能し、ユーザーの操作を処理し、サーバーにリクエストを送信し、検索操作を実行します。
以下は index.html のbody部分で、アプリのコアカポーネントを配置しています。
クエリ画像を表示するための画像カルーセル
クエリを開始するための検索ボタン
特定のインデックスからの動画を表示する動画リスト
検索ボタンがクリックされた後に結果を表示する検索結果セクション
<body> <h1 class="text-3xl text-center m-5 p-3"><i class="fa-solid fa-palette"></i> Shade Finder</h1> <div class="m-5 p-3"> <p class="text-center m-5" id="color-label"></p> <div class="flex justify-center gap-5"> <button id="prev"><i class="fa-solid fa-chevron-left"></i></button> <div class="size-40"><img id="carousel-image"></div> <button id="next"><i class="fa-solid fa-chevron-right"></i></button> </div> <div class="flex justify-center m-5 gap-2"> <button id="search" class="bg-lime-400 py-2.5 px-3">Search</button> </div> </div> <div id ="video-list-container" class="container max-w-5xl mx-auto py-4"> <div id ="video-list-loading" class="container max-w-5xl mx-auto py-4"> </div> <div id="video-list" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 justify-items-center"></div> <div id="video-list-pagination" class="flex justify-center m-5 gap-2"></div> </div> <div id="search-result-container" class="container w-5/6 mx-auto py-4 hidden"> <div id="search-result-list" class="grid grid-cols-1 md:grid-cols-4 justify-center"></div> </div> <script src="./script.js"></script> </body
サーバー
server.js は、Twelve Labs APIへのすべてのAPI呼び出しを管理するファイルです。これには4つのルートがあります。動画の取得/ページネーション、動画の取得、(画像から動画への)検索、そしてページトークンによる検索結果の取得です。
Twelve Labs APIへの4つのリクエスト
💡Twelve Labsは、アプリケーション内にプラットフォームを構築して活用できるようにするSDKを提供しています。このアプリでは、Javascript SDK (バージョン 0.2.5) が使用されました。
セットアップ
1 - Twelve Labs APIキーとインデックスIDを.envに保存する
backendフォルダ内に、キーがコメントアウトされた .env ファイル があります。コメントアウトを解除し、値を更新してください。
.env
TWELVE_LABS_API_KEY=<YOUR API KEY> TWELVE_LABS_INDEX_ID=<YOUR_INDEX_ID>
2 - Twelve Labs SDKをインストールしてインポートする
まず、twelvelabs-js パッケージをインストールします。
yarn add twelvelabs-js # or npm i twelvelabs-js
次に、必要なパッケージをアプリケーションにインポートします。以下に示すように、server.js(Node.jsで構築)にインポートしました。
const fs = require("fs"); const path = require("path"); const { TwelveLabs } = require("twelvelabs-js");
最後に、APIキーを使用してSDKクライアントを初期化します。
const client = new TwelveLabs({ apiKey: API_KEY });
以下の全体像をご覧ください。
"use strict"; const express = require("express"); const dotenv = require("dotenv"); const cors = require("cors"); const asyncHandler = require("express-async-handler"); const fs = require("fs"); const path = require("path"); const { TwelveLabs } = require("twelvelabs-js"); dotenv.config(); const app = express(); app.use(express.json()); app.use(cors()); app.use(express.static(path.join(__dirname, "../frontend/public"))); const PORT = 5001; const API_KEY = process.env.TWELVE_LABS_API_KEY; const INDEX_ID = process.env.TWELVE_LABS_INDEX_ID; const client = new TwelveLabs({ apiKey: API_KEY }); app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
リクエスト 1. 動画の取得/ページネーション
ページごとに動画を取得するには、client.index.video.listPagination を使用し、インデックスIDと目的のページを渡します。必要に応じて、1ページあたりに返される動画の数を制御するために pageLimit パラメータを含めることもできます。
💡 ヒント: API ドキュメントに記載されている(すべてのリクエスト用の)パラメータは、Javascript SDK内でキャメルケース(camelCase)に変換することで使用できます。
動画を受信した後、後で使用するために id と metadata を抽出し、pageInfo と合わせて返します。
💡 動画のページネーションの詳細については、ガイドをご覧ください。公式のサンプルコードも非常に役立ちます!
/** Get videos */ app.get( "/videos", asyncHandler(async (req, res, next) => { const { page_limit, page } = req.query; const videosResponse = await client.index.video.listPagination(INDEX_ID, { pageLimit: page_limit, page: page, }); const videos = videosResponse.data.map((video) => ({ id: video.id, metadata: video.metadata, })); res.json({ videos, page_info: videosResponse.pageInfo, }); }) );
動画レスポンス
videosResponse= VideoListWithPagination { ..., data: [ Video { _resource: [Video], _indexId: '...', id: '...', metadata: [Object], hls: undefined, source: undefined, indexedAt: '2024-06-27T05:11:29Z', createdAt: '2024-06-27T05:01:35Z', updatedAt: '2024-06-27T05:01:52Z' }, ... ], pageInfo: { page: 1, limitPerPage: 12, totalPage: 3, totalResults: 29, totalDuration: 19122 } }
リクエスト 2. 動画の取得
動画の取得/ページネーションと同様に、インデックスIDと動画IDを渡して client.index.video.retrieve を使用することで、単一の動画の詳細を取得できます。
動画データを受信した後、必要な情報(metadata、hls、source)のみを抽出して返します。具体的には、後ほどメタデータから動画タイトル、HLSからサムネイルのURL、ソースからURLを使用します。
/** Get a video of an index */ app.get( "/videos/:videoId", asyncHandler(async (req, res, next) => { const { videoId } = req.params; const videoResponse = await client.index.video.retrieve(INDEX_ID, videoId); res.json({ metadata: videoResponse.metadata, hls: videoResponse.hls, source: videoResponse.source, }); }) );
動画レスポンス
videoResponse= Video { ..., id: '...', metadata: { duration: 54, engine_ids: [ 'marengo2.6', 'pegasus1.1' ], filename: 'tirtir korean cushion review', fps: 30, height: 1280, size: 9601300, video_title: 'tirtir korean cushion review', width: 720 }, hls: { videoUrl: '... .m3u8', thumbnailUrls: ['... .jpg'], status: 'COMPLETE', updatedAt: '2024-05-22T02:49:49.074Z' }, source: { type: 'youtube', name: 'theoliviasaurusrex', url: 'https://www.youtube.com/watch?v=tOabvdtTa-U' }, indexedAt: '2024-05-22T03:03:53Z', createdAt: '2024-05-22T02:49:28Z', updatedAt: '2024-05-22T02:49:36Z' }
リクエスト 3. 検索 (画像から動画)
ここからが楽しいパートです!client.search.query メソッドを使用し、4つの必須パラメータ(indexId、queryMediaFile、queryMediaType、options)を渡すことで、画像から動画への検索を実行できます。
特に、queryMediaFileを正しく渡すには、いくつかの手順が必要です。
パスの構築: まず、画像ファイルへのフルパスを構築する必要があります。このアプリでは、すべての画像がすでに images フォルダに保存されています。そのため、現在のディレクトリ(__dirname)、このアプリにとっての相対パスである ../frontend/public/images、および画像のファイル名(imageSrc)を使用して構築を行います。
存在確認: パスを構築した後、画像ファイルがその場所に存在するかどうかを確認します。ファイルが見つからない場合は、404エラーレスポンスがクライアントに返されます。
読み取りストリームの作成: ファイルが存在する場合は、画像ファイルから読み取り可能なストリームが作成されます。このストリームは、その後 Twelve Labs API に効率的に送信されます。
このアプリでは、threshold、pageLimit、adjustConfidenceLevel などのオプションパラメータも含めました。パラメータの詳細な一覧は API ドキュメントでご確認いただけます。
動画を受信した後、後で使用するために data と pageInfo を抽出し、クライアントに返します。
💡 画像クエリ検索の詳細については、ガイドを必ず確認してください。公式のサンプルコードも非常に便利です!
/** Search videos based on an image query */ app.get( "/search", asyncHandler(async (req, res, next) => { const { imageSrc, threshold, pageLimit, adjustConfidenceLevel } = req.query; const imagePath = path.join( __dirname, "../frontend/public/images", imageSrc ); if (!fs.existsSync(imagePath)) { console.error("Image not found at path:", imagePath); return res.status(404).json({ error: "Image not found" }); } const searchResponse = await client.search.query({ indexId: INDEX_ID, queryMediaFile: fs.createReadStream(imagePath), queryMediaType: "image", options: ["visual"], threshold: threshold, pageLimit: pageLimit, adjustConfidenceLevel: adjustConfidenceLevel, }); res.json({ searchResults: searchResponse.data, pageInfo: searchResponse.pageInfo, }); }) );
検索レスポンス
searchResponse= SearchResult { ..., pool: { totalCount: 29, totalDuration: 19122, indexId: '...' }, data: [ { score: 84.45, start: 379.13333333341933, end: 381, metadata: [Array], videoId: '...', confidence: 'high', thumbnailUrl: '...' }, ... ], pageInfo: { limitPerPage: 12, totalResults: 20, pageExpiredAt: '2024-08-15T04:09:46Z' nextPageToken: '...' //This might not exist } }
リクエスト 4. ページトークンによる検索
ページトークンによる検索は非常にシンプルです。client.search.byPageToken を使用し、前のリクエスト(リクエスト 3)から取得した pageToken を渡します。レスポンスは、最初の検索リクエスト(リクエスト 3)から受信したものと同じ構造になります。
/** Get search results of a specific page */ app.get( "/search/:pageToken", asyncHandler(async (req, res, next) => { const { pageToken } = req.params; let searchByPageResponse = await client.search.byPageToken(`${pageToken}`); res.json({ searchResults: searchByPageResponse.data, pageInfo: searchByPageResponse.pageInfo, }); }) );
クライアント
必要なすべてのAPIエンドポイントを含めてサーバーの設定が完全に完了したので、ここからはクライアント側のコードに焦点を当てます。アプリケーションのこの部分は、サーバーにリクエストを送信し、受信したデータを処理する役割を担います。
サーバー側で確立されたフローに従って、まずアプリが最初にインデックスから動画を表示する仕組みを説明します。その後、動画の検索機能と検索結果のページネーション機能について説明します。
1 - インデックスの動画表示

showVideos 関数
ページがレンダリングされるときに最初に実行される関数の1つが showVideos です。
async function showVideos(page = 1) { videoList.innerHTML = ""; ... try { const { videosDetail, pageInfo } = await getVideoOfVideos(page); videoListLoading.removeChild(loadingSpinnerContainer); if (videosDetail) { videosDetail.forEach((video) => { const videoContainer = createVideoContainer(video); videoList.appendChild(videoContainer); }); videoListLoading.classList.remove("min-h-[300px]"); createPaginationButtons(pageInfo, page); } } catch (error) { console.error("Error fetching videos:", error); } }
DOM内の既存の動画リストをクリアします。
特定のページにあるすべての動画の詳細を取得する getVideoOfVideos を呼び出します。
動画の詳細をループ処理して、各動画のコンテナを作成し追加します。
最後に、pageInfo に基づいてページネーションのボタンを設定します。
getVideoOfVideos 関数
getVideoOfVideos 関数は、特定のページの動画を取得し、その後、各動画の詳細を取得する役割を持ちます。
async function getVideoOfVideos(page = 1) { const videosResponse = await getVideos(page); if (videosResponse.videos.length > 0) { const videosDetail = await Promise.all( videosResponse.videos.map((video) => getVideo(video.id)) ); return { videosDetail, pageInfo: videosResponse.page_info }; } }
getVideos は、指定されたページの動画を取得するためにサーバーにリクエストを送信します(「サーバー」セクションのリクエスト1の実装通り)。
動画が見つかった場合、関数は各動画をループして、詳細を取得するために getVideo を呼び出します(「サーバー」セクションのリクエスト2の実装通り)。
その後、その後のリクエストを最適化するために、シンプルなキャッシュ機構を使用して詳細がキャッシュされます。
動画コンテナの作成
showVideos が各動画の詳細を取得すると、動画のURL、サムネイルURL、動画のタイトルなどの詳細に基づいて動画コンテナを作成します。
ページネーションボタン
最後に、getVideos から取得した総ページ数に基づいてページネーションボタンが作成されます。各ボタンには、それぞれのページに対して showVideos を呼び出すイベントリスナーが設定されます。
function createPageButton(pageNumber, currentPage) { const pageButton = document.createElement("button"); pageButton.textContent = pageNumber; ... if (pageNumber === currentPage) { pageButton.classList.add("bg-slate-200", "font-medium"); pageButton.disabled = true; } else { pageButton.classList.add("bg-transparent"); pageButton.addEventListener("click", () => showVideos(pageNumber)); } return pageButton; }
2 - 画像による動画検索

ユーザーが「Search」ボタンをクリックすると、handleSearchButtonClick 関数が実行され、サーバーへの検索リクエストが行われます。どのように機能するのか、ステップバイステップで見ていきましょう。
async function handleSearchButtonClick() { toggleSearchButton(false); nextPageToken = null; searchResultContainer.innerHTML = ""; videoListContainer.classList.add("hidden"); searchResultContainer.classList.remove("hidden"); searchResultList.innerHTML = ""; const loadingSpinnerContainer = createLoadingSpinner(); searchResultContainer.appendChild(loadingSpinnerContainer); try { const { searchResults } = await searchByImage(); searchResultContainer.removeChild(loadingSpinnerContainer); if (searchResults.length > 0) { showSearchResults(searchResults); } else { displayNoResultsMessage(); } } catch (error) { console.error("Error fetching search results:", error); } finally { toggleSearchButton(true); } }
まず、検索処理中にボタンを無効化するために、検索ボタンの状態(トグル)を false に切り替えます。
次に、関数は nextPageToken を null にリセットし、searchResultContainer と searchResultList 内の既存のコンテンツをクリアします。また、videoListContainer を非表示にし、searchResultContainer が表示されるようにします。
その後、検索が進行中であることを示すために読み込みスピナーが作成され、表示されます。
サーバーへ検索リクエストを行う searchByImage を呼び出すことで、検索が実行されます。
検索完了後、読み込みスピナーは削除され、showSearchResults を使って検索結果が表示されるか、結果が見つからなかった場合はメッセージが表示されます。
最後に、検索ボタンの状態を true に戻して有効化し、ユーザーが望む場合に再度検索を実行できるようにします。
3 - ページTokenによる検索結果の表示
検索結果が複数ページにまたがる場合(すなわち、nextPageToken が存在する場合)、createShowMoreButton 関数が実行され、ユーザーに「Show More」ボタンが表示されます。このボタンにより、次のページの検索結果が取得および表示されます。その仕組みを順を追ってご説明します。

function createShowMoreButton() { removeExistingButton(); const showMoreButtonContainer = document.createElement("div"); showMoreButtonContainer.id = "show-more-button"; showMoreButtonContainer.classList.add("flex", "justify-center", "my-4"); const showMoreButton = document.createElement("button"); showMoreButton.innerHTML = '<i class="fa-solid fa-chevron-up"></i> Show More'; showMoreButton.addEventListener("click", async () => { const loadingSpinnerContainer = createLoadingSpinner(); searchResultContainer.appendChild(loadingSpinnerContainer); const nextPageResults = await getNextSearchResults(nextPageToken); searchResultContainer.removeChild(loadingSpinnerContainer); showSearchResults(nextPageResults.searchResults); nextPageToken = nextPageResults.pageInfo.nextPageToken || null; }); showMoreButtonContainer.appendChild(showMoreButton); searchResultContainer.appendChild(showMoreButtonContainer); }
まず、ボタンが重複するのを防ぐために、既存の「Show More」ボタンを削除します。
次に、表示用のコンテナと「Show More」ボタンを作成します。
クリック動作を処理するために、ボタンにイベントリスナーを追加します。クリックすると、読み込みスピナーの表示、次の検索結果の取得、スピナーの削除、新しい検索結果の表示、および nextPageToken の更新を行います。
最後に、「Show More」ボタンとそのコンテナを searchResultContainer に追加して、ユーザーに表示されるようにします。
結論
この記事が、Twelve Labsの新しい画像から動画への検索(image-to-video search)APIとその実用的な応用例についての洞察を得る助けとなれば幸いです。最後までお読みいただきありがとうございました。これらの進歩をご自身のプロジェクトでどのように活用されるかを楽しみにしています!




