チュートリアル
TwelveLabs API を使用したショッパブルビデオアプリケーションの構築

ミラン・キム
このチュートリアルでは、Twelve Labs Analyze APIを使用して配信ビデオ内の商品を自動検出し、タイムスタンプ付きのコンテキストに沿った説明を生成し、再生を一切妨げることなくインタラクティブな購入リンクを表示する、Shoppable Video(動画内ショッピング)アプリの構築手順を詳しく解説します。
このチュートリアルでは、Twelve Labs Analyze APIを使用して配信ビデオ内の商品を自動検出し、タイムスタンプ付きのコンテキストに沿った説明を生成し、再生を一切妨げることなくインタラクティブな購入リンクを表示する、Shoppable Video(動画内ショッピング)アプリの構築手順を詳しく解説します。

この記事の内容
ニュースレターに登録する
ニュースレターに登録する
ビデオ理解に関する最新の技術進歩、チュートリアル、業界の動向をお届けします
ビデオ理解に関する最新の技術進歩、チュートリアル、業界の動向をお届けします
AIを活用してビデオを検索、分析、探索します。
2025/09/02
10分
記事へのリンクをコピー
番組を見ていて、「これどこで買えるんだろう?」と思ったことはありませんか?これまで、動画配信プラットフォームは、手動による商品タグ付け、静的なオーバーレイ、そして購入の瞬間にスムーズさを欠く決済フローといった、不便な解決策に悩まされてきました。
⭐️デモはこちらからご確認ください!

そこで登場するのがショッパブルビデオ(Shoppable Video)です。Twelve Labs APIを使用して構築されたこのデモアプリは、あらゆる動画をインタラクティブなショッピング体験に変える方法を示しています。シーン内の商品を自動的に検出し、文脈に合わせた説明を生成し、視聴者が再生を一時停止することなく決済できるようにします。TikTok Shopのような雰囲気を、長尺の配信コンテンツ向けに最適化したものとお考えください!
前提条件
Twelve Labs Playgroundに登録し、APIキーを生成します
インデックスを作成し、アプリに適用したい動画を少なくとも1つアップロードします
GitHubからプロジェクトをクローンし、環境を設定します(READMEの「Quick Start」に従ってください)
アプリのデモ
仕組み

このアプリは、以下のシンプルなフローに従って動作します:
動画の取得(GET videos API): アプリはデフォルトのTwelve Labsインデックスから動画リストを取得し、デフォルトで最新の動画を選択します。
動画詳細の取得(GET video API): 選択された動画について、アプリはHLS再生URLや既存の商品メタデータを含む完全な詳細を取得します。
動画の分析(ANALYZE API): 商品メタデータが存在しない場合、アプリはTwelve LabsのAnalyze APIをカスタムプロンプトで呼び出し、商品を検出して文脈に沿った説明を生成します。
メタデータの保存(PUT video API): 分析結果はPUTリクエストを介して動画に保存されるため、後で再分析することなく取得できます。
UIでのレンダリング: アプリは、商品が画面に表示されている間だけ動画の上に商品マーカーを表示し、サイドバーに詳細情報を表示して、Amazonで直接購入できるリンクを提供します。
コード解説
1 - 動画の取得 – GET videos API
アプリはまず、デフォルトのTwelve Labsインデックスから動画のリストを取得します。これにより、UIに利用可能な動画が表示され、デフォルトで最新の動画を選択できるようになります。
1-1. クライアント:動画リストの取得
page.tsxにおいて、loadVideos関数はインデックスIDを指定して/api/videosを呼び出します。最大50件の動画を取得し、自動的に最新のものを選びます。
// TwelveLabsインデックスから動画を読み込む const loadVideos = useCallback(async () => { const defaultIndexId = process.env.NEXT_PUBLIC_DEFAULT_INDEX_ID; if (!defaultIndexId) { console.error('Default index ID not configured'); return; } setIsLoadingVideos(true); try { const response = await fetch(`/api/videos?index_id=${defaultIndexId}&limit=50`); if (!response.ok) { throw new Error(`Failed to fetch videos: ${response.statusText}`); } const data = await response.json(); setVideos(data.data || []); // デフォルトで最新の動画を選択(APIは新しい順に返すためリストの最初) if (data.data && data.data.length > 0) { setSelectedVideoId(data.data[0]._id); }
1-2. サーバー: /api/videos
これは、Twelve Labsの GET /v1.3/indexes/{indexId}/videosにプロキシするシンプルなNext.jsのAPIルートです。実装の全容はGitHubリポジトリでご確認ください。
⭐️Twelve Labsの GET videos API の詳細については、こちらをご確認ください。
2 - 動画詳細の取得 – GET video API
動画が選択されると、アプリはHLS再生URLや既存の商品メタデータを含む動画の詳細情報を取得します。
2-1. クライアント:動画詳細の取得
動画が選択されると、loadVideoDetailが呼び出されます。インデックスIDを指定して/api/videos/[videoId]ルートにアクセスし、HLS再生URLを含む動画詳細を取得して、商品メタデータがすでに存在するかどうかを確認します。
// 動画が選択されたときに動画の詳細を読み込む const loadVideoDetail = useCallback(async (videoId: string) => { const defaultIndexId = process.env.NEXT_PUBLIC_DEFAULT_INDEX_ID; if (!defaultIndexId || !videoId) { return; } setIsLoadingVideoDetail(true); // ... try { const response = await fetch(`/api/videos/${videoId}?indexId=${defaultIndexId}`); if (!response.ok) { throw new Error(`Failed to fetch video detail: ${response.statusText}`); } const data = await response.json(); setVideoDetail(data); // 動画URLを設定する前に、カスタムメタデータが存在するか確認し、必要に応じて生成する await checkAndGenerateMetadata(videoId, defaultIndexId, data); // HLSデータが利用可能な場合、動画のURLを設定する(メタデータ確認の後) if (data.hls?.video_url) { setVideoUrl(data.hls.video_url); } // ...
2-2. サーバー: /api/videos/[videoId]
このルートは、Twelve Labsの
GET /v1.3/indexes/{indexId}/videos/{videoId}にプロキシします。実装の全容はGitHubリポジトリをご確認ください。
⭐️Twelve Labsの GET video API の詳細については、こちらをご確認ください。
3 - 動画の分析 – ANALYZE API
アプリは、選択された動画に対して利用可能な商品メタデータが存在しない場合のみ、Analyze APIを呼び出します。
3-1. クライアント:分析のトリガー (page.tsx)
`checkAndGenerateMetadata` 内で、クライアントはまず `videoData.user_metadata` を確認します。利用可能なものがなければ、/api/analyze?videoId=... を呼び出し、商品のJSON配列をパースします。
// 必要に応じてメタデータを確認・生成する const checkAndGenerateMetadata = useCallback(async (videoId: string, indexId: string, videoData: VideoDetail, forceReanalyze = false) => { // 1) 既存のメタデータが存在する場合はそれを使用する(forceReanalyzeでない限り) if (!forceReanalyze && videoData.user_metadata && Object.keys(videoData.user_metadata).length > 0) { if (videoData.user_metadata.products) { let existingProducts; try { // メタデータがJSON文字列として保存されている場合はパースする if (typeof videoData.user_metadata.products === 'string') { existingProducts = JSON.parse(videoData.user_metadata.products); } else { existingProducts = videoData.user_metadata.products; } setProducts(existingProducts); // ... setIsAnalyzingVideo(false); // 既存のメタデータを使用する場合は分析を停止する return; } // 2) 存在しない場合は、今すぐ分析を実行する setIsAnalyzingVideo(true); try { const analyzeResponse = await fetch(`/api/analyze?videoId=${videoId}${forceReanalyze ? '&forceReanalyze=true' : ''}`); // ... const analyzeData = await analyzeResponse.json();
3-2. サーバー: /api/analyze
/api/analyzeルートは、Twelve LabsのAnalyze APIを呼び出します。
⭐️Twelve Labsの Analyze API の詳細については、こちらをご確認ください。
このAPIでは、モデルが構造化された機械可読な結果を返すように、慎重に設計されたプロンプトが必要となります。
📌プロンプト設計
APIレスポンスが完全かつ簡単にパースできるように、プロンプトは以下のように工夫されています:
timeline(タイムライン):
[秒単位の開始時間, 終了時間]。画面にマーカーを表示するタイミングを制御します。brand(ブランド)/ product_name(商品名):商品ラベルおよびAmazonの検索クエリに使用されます。
location(位置):1920×1080のフレームにおける割合として
[x%, y%, 幅%, 高さ%]を指定。解像度に依存しない座標となります。price(価格):視認可能、または推測可能な場合に、購入の判断材料として追加します。
description(説明):文脈に沿った短い概要(ナレーション、字幕、シーン)。
また、以下のルールを適用しています:
出力は有効なJSON配列のみとする(マークダウンやプロースは含めない)。
同じシーンに複数の商品が表示される場合は、別々にリスト化する。
実際のプロンプトテキストを含むサーバーコードについては、以下をご覧ください。
export async function GET(req: Request) { const { searchParams } = new URL(req.url); const videoId = searchParams.get("videoId"); const forceReanalyze = searchParams.get("forceReanalyze") === "true"; const prompt = ` 動画に映っているすべての商品を、以下の詳細情報とともにリストアップしてください: - timeline: [秒単位の開始時間, 終了時間] - brand: ブランド名 - product_name: 正式な商品名 - location: [x%, y%, 幅%, 高さ%] — 16:9のアスペクト比の動画プレーヤーに対する相対的なパーセンテージ値 - price: 表示または言及されている場合の価格。動画から直接価格情報を見つけられない場合は、外部検索または自身の情報を使用してください - description: 商品について話されている内容、または表示されている内容を説明してください ⚠️ 同一シーンに複数の商品が表示される場合は、それぞれの位置座標を個別に分けてリストアップしてください。 レスポンスは有効なJSON配列形式(マークダウン等なし)のみで返してください: [ { "timeline": [start, end], "brand": "brand_name", "product_name": "product_name", "location": [x%, y%, width%, height%], "price": "price_info", "description": "product_description" } ] ` // ... const url = `${TWELVELABS_API_BASE_URL}/analyze`; const requestBody = { prompt: prompt, video_id: videoId, stream: false }; const options = { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": API_KEY, }, body: JSON.stringify(requestBody) }; try { const response = await fetch(url, options); // ... // data.dataだけでなく、完全なデータオブジェクトを返す return NextResponse.json(data, { status: 200 }); } // ... }
期待される出力結果の例(responseText):
{
"Id":"5cef3150-dc0a-42e1-8ae5-ba8aab536206",
"data":"[\n {\n \
"timeline\": [7, 8],\n \
"brand\": \"Google\",\n \
"product_name\": \"Chromecast\",\n \
"location\": [50, 50, 100, 100],\n \
"price\": \"$35\",\n \
"description\": \"お気に入りのコンテンツを、テレビの大画面で。\"\n }\n]",
"finish_reason":"stop","usage":{"output_tokens"
* 注意: dataは文字列化されたJSON配列であるため、クライアント側でJSON.parseする必要があります。
4 - メタデータの保存 – PUT video API
分析レスポンスがパースされると、その結果が動画に保存されます。これにより、次回以降のリクエスト時に再分析を行うことなく、商品を直接読み込むことができます。
4-1. クライアント:商品メタデータの保存
クライアントは /api/videos/saveMetadata に対し、Twelve Labs APIにプロキシするPUTリクエストを送信します。
// 生成されたメタデータを保存する const saveRequestBody = { videoId: videoId, indexId: indexId, metadata: { products: products, analyzed_at: new Date().toISOString(), reanalyzed: forceReanalyze } }; const saveResponse = await fetch('/api/videos/saveMetadata', { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(saveRequestBody) });
4-2. サーバー: /api/videos/saveMetadata
サーバーのルート(/api/videos/saveMetadata)は、Twelve Labs APIのPUT /v1.3/indexes/{indexId}/videos/{videoId}を呼び出すシンプルなラッパーです。実装の全容はGitHubリポジトリから確認できます。
⭐️Twelve Labsの PUT video API の詳細については、こちらをご確認ください。
5 - UIでのレンダリング
商品メタデータが保存され取得できる状態になると、アプリは商品マーカーと詳細を動的にレンダリングします。
5-1. 動画プレーヤー:商品マーカー

ProductVideoPlayer.tsxでは、現在の再生時点(タイムライン)で商品が表示されている場合のみ、動画の上にマーカーが描画されます。
ProductVideoPlayer.tsx(199行目〜225行目)
{/* 商品マーカーオーバーレイ */} <div className="absolute inset-0 pointer-events-none"> {visibleProducts.map(product => { const position = calculateMarkerPosition(product); return ( <button key={product.product_name} className="product-marker absolute rounded-full bg-black bg-opacity-70 flex items-center justify-center cursor-pointer pointer-events-auto animate-pulse-slow transition-all duration-300 ease-in-out z-10" style={{ left: position.left, top: position.top, width: '48px', height: '48px', transform: 'translate(-50%, -50%)' }} onClick={(e) => { e.stopPropagation(); console.log('Product marker clicked:', product.product_name); onProductSelect(product); }} aria-label={`${product.product_name} の詳細を表示`} > <ShoppingBag className="text-white" style={{ fontSize: '24px' }} /> </button> ); })} </div>
5-2. サイドバー:商品の詳細
ProductDetailSidebar.tsxでは、検出された各商品がその説明と「Amazonで探す」ボタンとともにリスト表示されます。
ProductDetailSidebar.tsx(279行目〜311行目)
onClick={() => { if (shouldEnableShopButton) { // ... // 検索クエリを作成 let searchQuery; if (shouldFilterBrand) { // ブランドがフィルターされている場合は、商品名のみを使用 searchQuery = product.product_name; } else { // ブランドが有効な場合は、ブランド+商品名を使用 searchQuery = `${product.brand} ${product.product_name}`; } const q = encodeURIComponent(searchQuery); window.open(`https://www.amazon.com/s?k=${q}`, '_blank'); } }}
👉 これらのコンポーネントが組み合わさることで、シームレスな「映像からそのまま購入できる(shop-the-look)」体験が実現します:
マーカーは、その商品が表示される特定のシーン(タイムライン)の間のみ、画面上に現れます。
サイドバーには、商品名、説明、ブランド、そして購入用のリンクが表示されます。
最後に
このチュートリアルでは、Twelve Labs APIを活用してインタラクティブに買い物ができる「ショッパブルビデオ」のデモを構築しました。動画の取得、カスタムプロンプトによる分析、メタデータの保存、そして商品詳細をUIにレンダリングする一連の流れを解説しました。
このシンプルな設計は、AIを活用した高度な動画理解が、あらゆる動画をどのようにインタラクティブなショッピング体験に変えられるかを示しています。ぜひGitHubリポジトリにアクセスして、ご自身のプロジェクトへの活用やカスタマイズにお役立てください!
番組を見ていて、「これどこで買えるんだろう?」と思ったことはありませんか?これまで、動画配信プラットフォームは、手動による商品タグ付け、静的なオーバーレイ、そして購入の瞬間にスムーズさを欠く決済フローといった、不便な解決策に悩まされてきました。
⭐️デモはこちらからご確認ください!

そこで登場するのがショッパブルビデオ(Shoppable Video)です。Twelve Labs APIを使用して構築されたこのデモアプリは、あらゆる動画をインタラクティブなショッピング体験に変える方法を示しています。シーン内の商品を自動的に検出し、文脈に合わせた説明を生成し、視聴者が再生を一時停止することなく決済できるようにします。TikTok Shopのような雰囲気を、長尺の配信コンテンツ向けに最適化したものとお考えください!
前提条件
Twelve Labs Playgroundに登録し、APIキーを生成します
インデックスを作成し、アプリに適用したい動画を少なくとも1つアップロードします
GitHubからプロジェクトをクローンし、環境を設定します(READMEの「Quick Start」に従ってください)
アプリのデモ
仕組み

このアプリは、以下のシンプルなフローに従って動作します:
動画の取得(GET videos API): アプリはデフォルトのTwelve Labsインデックスから動画リストを取得し、デフォルトで最新の動画を選択します。
動画詳細の取得(GET video API): 選択された動画について、アプリはHLS再生URLや既存の商品メタデータを含む完全な詳細を取得します。
動画の分析(ANALYZE API): 商品メタデータが存在しない場合、アプリはTwelve LabsのAnalyze APIをカスタムプロンプトで呼び出し、商品を検出して文脈に沿った説明を生成します。
メタデータの保存(PUT video API): 分析結果はPUTリクエストを介して動画に保存されるため、後で再分析することなく取得できます。
UIでのレンダリング: アプリは、商品が画面に表示されている間だけ動画の上に商品マーカーを表示し、サイドバーに詳細情報を表示して、Amazonで直接購入できるリンクを提供します。
コード解説
1 - 動画の取得 – GET videos API
アプリはまず、デフォルトのTwelve Labsインデックスから動画のリストを取得します。これにより、UIに利用可能な動画が表示され、デフォルトで最新の動画を選択できるようになります。
1-1. クライアント:動画リストの取得
page.tsxにおいて、loadVideos関数はインデックスIDを指定して/api/videosを呼び出します。最大50件の動画を取得し、自動的に最新のものを選びます。
// TwelveLabsインデックスから動画を読み込む const loadVideos = useCallback(async () => { const defaultIndexId = process.env.NEXT_PUBLIC_DEFAULT_INDEX_ID; if (!defaultIndexId) { console.error('Default index ID not configured'); return; } setIsLoadingVideos(true); try { const response = await fetch(`/api/videos?index_id=${defaultIndexId}&limit=50`); if (!response.ok) { throw new Error(`Failed to fetch videos: ${response.statusText}`); } const data = await response.json(); setVideos(data.data || []); // デフォルトで最新の動画を選択(APIは新しい順に返すためリストの最初) if (data.data && data.data.length > 0) { setSelectedVideoId(data.data[0]._id); }
1-2. サーバー: /api/videos
これは、Twelve Labsの GET /v1.3/indexes/{indexId}/videosにプロキシするシンプルなNext.jsのAPIルートです。実装の全容はGitHubリポジトリでご確認ください。
⭐️Twelve Labsの GET videos API の詳細については、こちらをご確認ください。
2 - 動画詳細の取得 – GET video API
動画が選択されると、アプリはHLS再生URLや既存の商品メタデータを含む動画の詳細情報を取得します。
2-1. クライアント:動画詳細の取得
動画が選択されると、loadVideoDetailが呼び出されます。インデックスIDを指定して/api/videos/[videoId]ルートにアクセスし、HLS再生URLを含む動画詳細を取得して、商品メタデータがすでに存在するかどうかを確認します。
// 動画が選択されたときに動画の詳細を読み込む const loadVideoDetail = useCallback(async (videoId: string) => { const defaultIndexId = process.env.NEXT_PUBLIC_DEFAULT_INDEX_ID; if (!defaultIndexId || !videoId) { return; } setIsLoadingVideoDetail(true); // ... try { const response = await fetch(`/api/videos/${videoId}?indexId=${defaultIndexId}`); if (!response.ok) { throw new Error(`Failed to fetch video detail: ${response.statusText}`); } const data = await response.json(); setVideoDetail(data); // 動画URLを設定する前に、カスタムメタデータが存在するか確認し、必要に応じて生成する await checkAndGenerateMetadata(videoId, defaultIndexId, data); // HLSデータが利用可能な場合、動画のURLを設定する(メタデータ確認の後) if (data.hls?.video_url) { setVideoUrl(data.hls.video_url); } // ...
2-2. サーバー: /api/videos/[videoId]
このルートは、Twelve Labsの
GET /v1.3/indexes/{indexId}/videos/{videoId}にプロキシします。実装の全容はGitHubリポジトリをご確認ください。
⭐️Twelve Labsの GET video API の詳細については、こちらをご確認ください。
3 - 動画の分析 – ANALYZE API
アプリは、選択された動画に対して利用可能な商品メタデータが存在しない場合のみ、Analyze APIを呼び出します。
3-1. クライアント:分析のトリガー (page.tsx)
`checkAndGenerateMetadata` 内で、クライアントはまず `videoData.user_metadata` を確認します。利用可能なものがなければ、/api/analyze?videoId=... を呼び出し、商品のJSON配列をパースします。
// 必要に応じてメタデータを確認・生成する const checkAndGenerateMetadata = useCallback(async (videoId: string, indexId: string, videoData: VideoDetail, forceReanalyze = false) => { // 1) 既存のメタデータが存在する場合はそれを使用する(forceReanalyzeでない限り) if (!forceReanalyze && videoData.user_metadata && Object.keys(videoData.user_metadata).length > 0) { if (videoData.user_metadata.products) { let existingProducts; try { // メタデータがJSON文字列として保存されている場合はパースする if (typeof videoData.user_metadata.products === 'string') { existingProducts = JSON.parse(videoData.user_metadata.products); } else { existingProducts = videoData.user_metadata.products; } setProducts(existingProducts); // ... setIsAnalyzingVideo(false); // 既存のメタデータを使用する場合は分析を停止する return; } // 2) 存在しない場合は、今すぐ分析を実行する setIsAnalyzingVideo(true); try { const analyzeResponse = await fetch(`/api/analyze?videoId=${videoId}${forceReanalyze ? '&forceReanalyze=true' : ''}`); // ... const analyzeData = await analyzeResponse.json();
3-2. サーバー: /api/analyze
/api/analyzeルートは、Twelve LabsのAnalyze APIを呼び出します。
⭐️Twelve Labsの Analyze API の詳細については、こちらをご確認ください。
このAPIでは、モデルが構造化された機械可読な結果を返すように、慎重に設計されたプロンプトが必要となります。
📌プロンプト設計
APIレスポンスが完全かつ簡単にパースできるように、プロンプトは以下のように工夫されています:
timeline(タイムライン):
[秒単位の開始時間, 終了時間]。画面にマーカーを表示するタイミングを制御します。brand(ブランド)/ product_name(商品名):商品ラベルおよびAmazonの検索クエリに使用されます。
location(位置):1920×1080のフレームにおける割合として
[x%, y%, 幅%, 高さ%]を指定。解像度に依存しない座標となります。price(価格):視認可能、または推測可能な場合に、購入の判断材料として追加します。
description(説明):文脈に沿った短い概要(ナレーション、字幕、シーン)。
また、以下のルールを適用しています:
出力は有効なJSON配列のみとする(マークダウンやプロースは含めない)。
同じシーンに複数の商品が表示される場合は、別々にリスト化する。
実際のプロンプトテキストを含むサーバーコードについては、以下をご覧ください。
export async function GET(req: Request) { const { searchParams } = new URL(req.url); const videoId = searchParams.get("videoId"); const forceReanalyze = searchParams.get("forceReanalyze") === "true"; const prompt = ` 動画に映っているすべての商品を、以下の詳細情報とともにリストアップしてください: - timeline: [秒単位の開始時間, 終了時間] - brand: ブランド名 - product_name: 正式な商品名 - location: [x%, y%, 幅%, 高さ%] — 16:9のアスペクト比の動画プレーヤーに対する相対的なパーセンテージ値 - price: 表示または言及されている場合の価格。動画から直接価格情報を見つけられない場合は、外部検索または自身の情報を使用してください - description: 商品について話されている内容、または表示されている内容を説明してください ⚠️ 同一シーンに複数の商品が表示される場合は、それぞれの位置座標を個別に分けてリストアップしてください。 レスポンスは有効なJSON配列形式(マークダウン等なし)のみで返してください: [ { "timeline": [start, end], "brand": "brand_name", "product_name": "product_name", "location": [x%, y%, width%, height%], "price": "price_info", "description": "product_description" } ] ` // ... const url = `${TWELVELABS_API_BASE_URL}/analyze`; const requestBody = { prompt: prompt, video_id: videoId, stream: false }; const options = { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": API_KEY, }, body: JSON.stringify(requestBody) }; try { const response = await fetch(url, options); // ... // data.dataだけでなく、完全なデータオブジェクトを返す return NextResponse.json(data, { status: 200 }); } // ... }
期待される出力結果の例(responseText):
{
"Id":"5cef3150-dc0a-42e1-8ae5-ba8aab536206",
"data":"[\n {\n \
"timeline\": [7, 8],\n \
"brand\": \"Google\",\n \
"product_name\": \"Chromecast\",\n \
"location\": [50, 50, 100, 100],\n \
"price\": \"$35\",\n \
"description\": \"お気に入りのコンテンツを、テレビの大画面で。\"\n }\n]",
"finish_reason":"stop","usage":{"output_tokens"
* 注意: dataは文字列化されたJSON配列であるため、クライアント側でJSON.parseする必要があります。
4 - メタデータの保存 – PUT video API
分析レスポンスがパースされると、その結果が動画に保存されます。これにより、次回以降のリクエスト時に再分析を行うことなく、商品を直接読み込むことができます。
4-1. クライアント:商品メタデータの保存
クライアントは /api/videos/saveMetadata に対し、Twelve Labs APIにプロキシするPUTリクエストを送信します。
// 生成されたメタデータを保存する const saveRequestBody = { videoId: videoId, indexId: indexId, metadata: { products: products, analyzed_at: new Date().toISOString(), reanalyzed: forceReanalyze } }; const saveResponse = await fetch('/api/videos/saveMetadata', { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(saveRequestBody) });
4-2. サーバー: /api/videos/saveMetadata
サーバーのルート(/api/videos/saveMetadata)は、Twelve Labs APIのPUT /v1.3/indexes/{indexId}/videos/{videoId}を呼び出すシンプルなラッパーです。実装の全容はGitHubリポジトリから確認できます。
⭐️Twelve Labsの PUT video API の詳細については、こちらをご確認ください。
5 - UIでのレンダリング
商品メタデータが保存され取得できる状態になると、アプリは商品マーカーと詳細を動的にレンダリングします。
5-1. 動画プレーヤー:商品マーカー

ProductVideoPlayer.tsxでは、現在の再生時点(タイムライン)で商品が表示されている場合のみ、動画の上にマーカーが描画されます。
ProductVideoPlayer.tsx(199行目〜225行目)
{/* 商品マーカーオーバーレイ */} <div className="absolute inset-0 pointer-events-none"> {visibleProducts.map(product => { const position = calculateMarkerPosition(product); return ( <button key={product.product_name} className="product-marker absolute rounded-full bg-black bg-opacity-70 flex items-center justify-center cursor-pointer pointer-events-auto animate-pulse-slow transition-all duration-300 ease-in-out z-10" style={{ left: position.left, top: position.top, width: '48px', height: '48px', transform: 'translate(-50%, -50%)' }} onClick={(e) => { e.stopPropagation(); console.log('Product marker clicked:', product.product_name); onProductSelect(product); }} aria-label={`${product.product_name} の詳細を表示`} > <ShoppingBag className="text-white" style={{ fontSize: '24px' }} /> </button> ); })} </div>
5-2. サイドバー:商品の詳細
ProductDetailSidebar.tsxでは、検出された各商品がその説明と「Amazonで探す」ボタンとともにリスト表示されます。
ProductDetailSidebar.tsx(279行目〜311行目)
onClick={() => { if (shouldEnableShopButton) { // ... // 検索クエリを作成 let searchQuery; if (shouldFilterBrand) { // ブランドがフィルターされている場合は、商品名のみを使用 searchQuery = product.product_name; } else { // ブランドが有効な場合は、ブランド+商品名を使用 searchQuery = `${product.brand} ${product.product_name}`; } const q = encodeURIComponent(searchQuery); window.open(`https://www.amazon.com/s?k=${q}`, '_blank'); } }}
👉 これらのコンポーネントが組み合わさることで、シームレスな「映像からそのまま購入できる(shop-the-look)」体験が実現します:
マーカーは、その商品が表示される特定のシーン(タイムライン)の間のみ、画面上に現れます。
サイドバーには、商品名、説明、ブランド、そして購入用のリンクが表示されます。
最後に
このチュートリアルでは、Twelve Labs APIを活用してインタラクティブに買い物ができる「ショッパブルビデオ」のデモを構築しました。動画の取得、カスタムプロンプトによる分析、メタデータの保存、そして商品詳細をUIにレンダリングする一連の流れを解説しました。
このシンプルな設計は、AIを活用した高度な動画理解が、あらゆる動画をどのようにインタラクティブなショッピング体験に変えられるかを示しています。ぜひGitHubリポジトリにアクセスして、ご自身のプロジェクトへの活用やカスタマイズにお役立てください!
番組を見ていて、「これどこで買えるんだろう?」と思ったことはありませんか?これまで、動画配信プラットフォームは、手動による商品タグ付け、静的なオーバーレイ、そして購入の瞬間にスムーズさを欠く決済フローといった、不便な解決策に悩まされてきました。
⭐️デモはこちらからご確認ください!

そこで登場するのがショッパブルビデオ(Shoppable Video)です。Twelve Labs APIを使用して構築されたこのデモアプリは、あらゆる動画をインタラクティブなショッピング体験に変える方法を示しています。シーン内の商品を自動的に検出し、文脈に合わせた説明を生成し、視聴者が再生を一時停止することなく決済できるようにします。TikTok Shopのような雰囲気を、長尺の配信コンテンツ向けに最適化したものとお考えください!
前提条件
Twelve Labs Playgroundに登録し、APIキーを生成します
インデックスを作成し、アプリに適用したい動画を少なくとも1つアップロードします
GitHubからプロジェクトをクローンし、環境を設定します(READMEの「Quick Start」に従ってください)
アプリのデモ
仕組み

このアプリは、以下のシンプルなフローに従って動作します:
動画の取得(GET videos API): アプリはデフォルトのTwelve Labsインデックスから動画リストを取得し、デフォルトで最新の動画を選択します。
動画詳細の取得(GET video API): 選択された動画について、アプリはHLS再生URLや既存の商品メタデータを含む完全な詳細を取得します。
動画の分析(ANALYZE API): 商品メタデータが存在しない場合、アプリはTwelve LabsのAnalyze APIをカスタムプロンプトで呼び出し、商品を検出して文脈に沿った説明を生成します。
メタデータの保存(PUT video API): 分析結果はPUTリクエストを介して動画に保存されるため、後で再分析することなく取得できます。
UIでのレンダリング: アプリは、商品が画面に表示されている間だけ動画の上に商品マーカーを表示し、サイドバーに詳細情報を表示して、Amazonで直接購入できるリンクを提供します。
コード解説
1 - 動画の取得 – GET videos API
アプリはまず、デフォルトのTwelve Labsインデックスから動画のリストを取得します。これにより、UIに利用可能な動画が表示され、デフォルトで最新の動画を選択できるようになります。
1-1. クライアント:動画リストの取得
page.tsxにおいて、loadVideos関数はインデックスIDを指定して/api/videosを呼び出します。最大50件の動画を取得し、自動的に最新のものを選びます。
// TwelveLabsインデックスから動画を読み込む const loadVideos = useCallback(async () => { const defaultIndexId = process.env.NEXT_PUBLIC_DEFAULT_INDEX_ID; if (!defaultIndexId) { console.error('Default index ID not configured'); return; } setIsLoadingVideos(true); try { const response = await fetch(`/api/videos?index_id=${defaultIndexId}&limit=50`); if (!response.ok) { throw new Error(`Failed to fetch videos: ${response.statusText}`); } const data = await response.json(); setVideos(data.data || []); // デフォルトで最新の動画を選択(APIは新しい順に返すためリストの最初) if (data.data && data.data.length > 0) { setSelectedVideoId(data.data[0]._id); }
1-2. サーバー: /api/videos
これは、Twelve Labsの GET /v1.3/indexes/{indexId}/videosにプロキシするシンプルなNext.jsのAPIルートです。実装の全容はGitHubリポジトリでご確認ください。
⭐️Twelve Labsの GET videos API の詳細については、こちらをご確認ください。
2 - 動画詳細の取得 – GET video API
動画が選択されると、アプリはHLS再生URLや既存の商品メタデータを含む動画の詳細情報を取得します。
2-1. クライアント:動画詳細の取得
動画が選択されると、loadVideoDetailが呼び出されます。インデックスIDを指定して/api/videos/[videoId]ルートにアクセスし、HLS再生URLを含む動画詳細を取得して、商品メタデータがすでに存在するかどうかを確認します。
// 動画が選択されたときに動画の詳細を読み込む const loadVideoDetail = useCallback(async (videoId: string) => { const defaultIndexId = process.env.NEXT_PUBLIC_DEFAULT_INDEX_ID; if (!defaultIndexId || !videoId) { return; } setIsLoadingVideoDetail(true); // ... try { const response = await fetch(`/api/videos/${videoId}?indexId=${defaultIndexId}`); if (!response.ok) { throw new Error(`Failed to fetch video detail: ${response.statusText}`); } const data = await response.json(); setVideoDetail(data); // 動画URLを設定する前に、カスタムメタデータが存在するか確認し、必要に応じて生成する await checkAndGenerateMetadata(videoId, defaultIndexId, data); // HLSデータが利用可能な場合、動画のURLを設定する(メタデータ確認の後) if (data.hls?.video_url) { setVideoUrl(data.hls.video_url); } // ...
2-2. サーバー: /api/videos/[videoId]
このルートは、Twelve Labsの
GET /v1.3/indexes/{indexId}/videos/{videoId}にプロキシします。実装の全容はGitHubリポジトリをご確認ください。
⭐️Twelve Labsの GET video API の詳細については、こちらをご確認ください。
3 - 動画の分析 – ANALYZE API
アプリは、選択された動画に対して利用可能な商品メタデータが存在しない場合のみ、Analyze APIを呼び出します。
3-1. クライアント:分析のトリガー (page.tsx)
`checkAndGenerateMetadata` 内で、クライアントはまず `videoData.user_metadata` を確認します。利用可能なものがなければ、/api/analyze?videoId=... を呼び出し、商品のJSON配列をパースします。
// 必要に応じてメタデータを確認・生成する const checkAndGenerateMetadata = useCallback(async (videoId: string, indexId: string, videoData: VideoDetail, forceReanalyze = false) => { // 1) 既存のメタデータが存在する場合はそれを使用する(forceReanalyzeでない限り) if (!forceReanalyze && videoData.user_metadata && Object.keys(videoData.user_metadata).length > 0) { if (videoData.user_metadata.products) { let existingProducts; try { // メタデータがJSON文字列として保存されている場合はパースする if (typeof videoData.user_metadata.products === 'string') { existingProducts = JSON.parse(videoData.user_metadata.products); } else { existingProducts = videoData.user_metadata.products; } setProducts(existingProducts); // ... setIsAnalyzingVideo(false); // 既存のメタデータを使用する場合は分析を停止する return; } // 2) 存在しない場合は、今すぐ分析を実行する setIsAnalyzingVideo(true); try { const analyzeResponse = await fetch(`/api/analyze?videoId=${videoId}${forceReanalyze ? '&forceReanalyze=true' : ''}`); // ... const analyzeData = await analyzeResponse.json();
3-2. サーバー: /api/analyze
/api/analyzeルートは、Twelve LabsのAnalyze APIを呼び出します。
⭐️Twelve Labsの Analyze API の詳細については、こちらをご確認ください。
このAPIでは、モデルが構造化された機械可読な結果を返すように、慎重に設計されたプロンプトが必要となります。
📌プロンプト設計
APIレスポンスが完全かつ簡単にパースできるように、プロンプトは以下のように工夫されています:
timeline(タイムライン):
[秒単位の開始時間, 終了時間]。画面にマーカーを表示するタイミングを制御します。brand(ブランド)/ product_name(商品名):商品ラベルおよびAmazonの検索クエリに使用されます。
location(位置):1920×1080のフレームにおける割合として
[x%, y%, 幅%, 高さ%]を指定。解像度に依存しない座標となります。price(価格):視認可能、または推測可能な場合に、購入の判断材料として追加します。
description(説明):文脈に沿った短い概要(ナレーション、字幕、シーン)。
また、以下のルールを適用しています:
出力は有効なJSON配列のみとする(マークダウンやプロースは含めない)。
同じシーンに複数の商品が表示される場合は、別々にリスト化する。
実際のプロンプトテキストを含むサーバーコードについては、以下をご覧ください。
export async function GET(req: Request) { const { searchParams } = new URL(req.url); const videoId = searchParams.get("videoId"); const forceReanalyze = searchParams.get("forceReanalyze") === "true"; const prompt = ` 動画に映っているすべての商品を、以下の詳細情報とともにリストアップしてください: - timeline: [秒単位の開始時間, 終了時間] - brand: ブランド名 - product_name: 正式な商品名 - location: [x%, y%, 幅%, 高さ%] — 16:9のアスペクト比の動画プレーヤーに対する相対的なパーセンテージ値 - price: 表示または言及されている場合の価格。動画から直接価格情報を見つけられない場合は、外部検索または自身の情報を使用してください - description: 商品について話されている内容、または表示されている内容を説明してください ⚠️ 同一シーンに複数の商品が表示される場合は、それぞれの位置座標を個別に分けてリストアップしてください。 レスポンスは有効なJSON配列形式(マークダウン等なし)のみで返してください: [ { "timeline": [start, end], "brand": "brand_name", "product_name": "product_name", "location": [x%, y%, width%, height%], "price": "price_info", "description": "product_description" } ] ` // ... const url = `${TWELVELABS_API_BASE_URL}/analyze`; const requestBody = { prompt: prompt, video_id: videoId, stream: false }; const options = { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": API_KEY, }, body: JSON.stringify(requestBody) }; try { const response = await fetch(url, options); // ... // data.dataだけでなく、完全なデータオブジェクトを返す return NextResponse.json(data, { status: 200 }); } // ... }
期待される出力結果の例(responseText):
{
"Id":"5cef3150-dc0a-42e1-8ae5-ba8aab536206",
"data":"[\n {\n \
"timeline\": [7, 8],\n \
"brand\": \"Google\",\n \
"product_name\": \"Chromecast\",\n \
"location\": [50, 50, 100, 100],\n \
"price\": \"$35\",\n \
"description\": \"お気に入りのコンテンツを、テレビの大画面で。\"\n }\n]",
"finish_reason":"stop","usage":{"output_tokens"
* 注意: dataは文字列化されたJSON配列であるため、クライアント側でJSON.parseする必要があります。
4 - メタデータの保存 – PUT video API
分析レスポンスがパースされると、その結果が動画に保存されます。これにより、次回以降のリクエスト時に再分析を行うことなく、商品を直接読み込むことができます。
4-1. クライアント:商品メタデータの保存
クライアントは /api/videos/saveMetadata に対し、Twelve Labs APIにプロキシするPUTリクエストを送信します。
// 生成されたメタデータを保存する const saveRequestBody = { videoId: videoId, indexId: indexId, metadata: { products: products, analyzed_at: new Date().toISOString(), reanalyzed: forceReanalyze } }; const saveResponse = await fetch('/api/videos/saveMetadata', { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(saveRequestBody) });
4-2. サーバー: /api/videos/saveMetadata
サーバーのルート(/api/videos/saveMetadata)は、Twelve Labs APIのPUT /v1.3/indexes/{indexId}/videos/{videoId}を呼び出すシンプルなラッパーです。実装の全容はGitHubリポジトリから確認できます。
⭐️Twelve Labsの PUT video API の詳細については、こちらをご確認ください。
5 - UIでのレンダリング
商品メタデータが保存され取得できる状態になると、アプリは商品マーカーと詳細を動的にレンダリングします。
5-1. 動画プレーヤー:商品マーカー

ProductVideoPlayer.tsxでは、現在の再生時点(タイムライン)で商品が表示されている場合のみ、動画の上にマーカーが描画されます。
ProductVideoPlayer.tsx(199行目〜225行目)
{/* 商品マーカーオーバーレイ */} <div className="absolute inset-0 pointer-events-none"> {visibleProducts.map(product => { const position = calculateMarkerPosition(product); return ( <button key={product.product_name} className="product-marker absolute rounded-full bg-black bg-opacity-70 flex items-center justify-center cursor-pointer pointer-events-auto animate-pulse-slow transition-all duration-300 ease-in-out z-10" style={{ left: position.left, top: position.top, width: '48px', height: '48px', transform: 'translate(-50%, -50%)' }} onClick={(e) => { e.stopPropagation(); console.log('Product marker clicked:', product.product_name); onProductSelect(product); }} aria-label={`${product.product_name} の詳細を表示`} > <ShoppingBag className="text-white" style={{ fontSize: '24px' }} /> </button> ); })} </div>
5-2. サイドバー:商品の詳細
ProductDetailSidebar.tsxでは、検出された各商品がその説明と「Amazonで探す」ボタンとともにリスト表示されます。
ProductDetailSidebar.tsx(279行目〜311行目)
onClick={() => { if (shouldEnableShopButton) { // ... // 検索クエリを作成 let searchQuery; if (shouldFilterBrand) { // ブランドがフィルターされている場合は、商品名のみを使用 searchQuery = product.product_name; } else { // ブランドが有効な場合は、ブランド+商品名を使用 searchQuery = `${product.brand} ${product.product_name}`; } const q = encodeURIComponent(searchQuery); window.open(`https://www.amazon.com/s?k=${q}`, '_blank'); } }}
👉 これらのコンポーネントが組み合わさることで、シームレスな「映像からそのまま購入できる(shop-the-look)」体験が実現します:
マーカーは、その商品が表示される特定のシーン(タイムライン)の間のみ、画面上に現れます。
サイドバーには、商品名、説明、ブランド、そして購入用のリンクが表示されます。
最後に
このチュートリアルでは、Twelve Labs APIを活用してインタラクティブに買い物ができる「ショッパブルビデオ」のデモを構築しました。動画の取得、カスタムプロンプトによる分析、メタデータの保存、そして商品詳細をUIにレンダリングする一連の流れを解説しました。
このシンプルな設計は、AIを活用した高度な動画理解が、あらゆる動画をどのようにインタラクティブなショッピング体験に変えられるかを示しています。ぜひGitHubリポジトリにアクセスして、ご自身のプロジェクトへの活用やカスタマイズにお役立てください!




