
チュートリアル
Twelve Labsを使用したBrand Integration Assistant(ブランド統合アシスタント)とAd Break Finder(広告枠検出)アプリの構築

ミラン・キム
このチュートリアルでは、Twelve LabsのAnalyze APIおよびEmbed APIとPineconeを組み合わせて、広告メタデータタグの自動生成、マルチモーダル類似性検索による文脈に沿ったコンテンツ動画の検出、そしてAIが生成したチャプター区切りでのミッドロール広告挿入のシミュレーションを行う「ブランド統合アシスタントおよび広告ブレイクファインダーアプリ」の構築手順を解説します。
このチュートリアルでは、Twelve LabsのAnalyze APIおよびEmbed APIとPineconeを組み合わせて、広告メタデータタグの自動生成、マルチモーダル類似性検索による文脈に沿ったコンテンツ動画の検出、そしてAIが生成したチャプター区切りでのミッドロール広告挿入のシミュレーションを行う「ブランド統合アシスタントおよび広告ブレイクファインダーアプリ」の構築手順を解説します。

この記事の内容
No headings found on page
ニュースレターに登録する
ニュースレターに登録する
ビデオ理解に関する最新の技術進歩、チュートリアル、業界の動向をお届けします
ビデオ理解に関する最新の技術進歩、チュートリアル、業界の動向をお届けします
AIを活用してビデオを検索、分析、探索します。
2025/07/14
12分
記事へのリンクをコピー
はじめに
視聴者は、見ているコンテンツと一致しない、無関係な広告に圧倒されることがよくあります。この不一致は不満を招き、広告が押し付けがましく感じられたり、タイミングが悪いと感じられたりする原因になります。
ブランド統合アシスタント & 広告挿入点ファインダー アプリは、コンテキストに関連する広告レコメンデーションを提供することでこれを解決し、エンゲージメントの適切な瞬間に、適切な視聴者へ適切なメッセージが届くようにします。
このチュートリアルでは、アプリのコア機能全体における動作方法を学習します:
タグの自動生成: アップロードされた各広告が分析され、トピックカテゴリ、感情、ブランド、ターゲット層(性別・年齢)、場所といった豊かなメタデータが生成され、スマートなフィルタリング、検索、コンテキスト構築が可能になります。
コンテキストに整合したコンテンツの検索: AIを活用した類似性検索を使用して、ビデオとテキストの両方の埋め込み(embeddings)に基づき、広告と意味的に整合しているコンテンツビデオを検索します。
広告挿入点の推奨とシミュレーション: コンテンツをチャプターに自動分割し、ミッドロール広告の挿入をシミュレートすることで、シームレスで没入感のある広告体験を実現します。
前提条件
Twelve Labs プレイグラウンドにサインアップしてAPIキーを生成し、広告用とコンテンツビデオ用にインデックスをそれぞれ2つ作成します。
Pineconeのアカウントを設定し、ビデオ埋め込みを保存するためのインデックスを作成します。
Dimensionsを1024に、MetricをCosineに設定してください
関連するGitHubリポジトリでアプリケーションのソースコードを確認します。
よりスムーズなセットアップと開発体験のために、JavaScript、TypeScript、Next.jsの知識があると役立ちます。
デモ
デモアプリケーションでご自身で試してみるか、以下のクイックデモビデオをご覧になり、実際にどのように機能するか確認してください。 https://www.loom.com/share/233cc8cb66ae44218e3cff69afb772d7
デモ全体のウェビナー録画は以下からご覧いただけます:

アプリの仕組み
アプリの内部には、広告ライブラリ(Ads Library)とコンテキスト整合性分析(Contextual Alignment Analysis)の2つのメインメニューがあります。
広告ライブラリ - ブランドマーケター向けに、自動生成されたタグ付きの広告ビデオを整理して表示します。トピックカテゴリ、感情、ブランド、性別、年齢、場所で広告をフィルタリングしたり、Twelve Labs Search APIを使用してショートテールまたはロングテールのキーワードで検索したりできます。このチュートリアルでは、広告ライブラリ内の自動タグ生成機能に焦点を当てます。
コンテキスト整合性分析 - このセクションでは、各広告に対してコンテキストが最も関連性の高いコンテンツビデオを検索できます。Twelve Labs EMBEDおよびGETビデオAPI(タグおよびビデオ埋め込み用)と、類似性ベースのフィルタリングを行うPineconeによって、親和性の高いコンテンツを表面化させます。
その後、ユーザーはコンテンツビデオを選択し、広告挿入のための自動チャプター生成を行い、チャプターの切り替わりで広告の再生をシミュレートできます。
以下のチュートリアルでは、コンテンツマッチングと広告シミュレーションの両方の機能について詳しく説明します。

アプリの3つの主要機能とその仕組み
主要機能 1. 自動タグ生成(「広告ライブラリ」内)
広告ライブラリでは、インデックスに登録されたビデオのコレクションをブラウズし、自動生成されたタグを表示できます。これらのタグは、ビデオをトピック、感情、ブランド、ユーザー層などに分類するのに役立ち、これらはすべて Twelve Labs の Analyze API を使用して抽出されます。
ステップ 1 - 各ビデオのタグを生成する
ビデオがロードされ、メタデータが不完全または失われていると判断された場合、システムは generateMetadata を呼び出して、Twelve Labs の Analyze API を使ってタグを取得します。
⭐️Twelve Labs' Analyze API の詳細についてはこちらを参照してください。
🔁 使用されている場所
この呼び出しは page.tsx の processVideoMetadataSingle 内にあり、以下のようになっています:
ads-library/page.ts (lines 279-290)
if (!video.user_metadata || Object.keys(video.user_metadata).length === 0 || !video.user_metadata.topic_category && !video.user_metadata.emotions && !video.user_metadata.brands && !video.user_metadata.locations)) { setVideosInProcessing(prev => [...prev, videoId]); const hashtagText = await generateMetadata(videoId); if (hashtagText) { const metadata = parseHashtags(hashtagText);
generateMetadata 関数は、サーバー側のAPI呼び出しをトリガーして Twelve Labs エンジンからAI生成されたタグを要求するカスタムフックです。
これにより、api/analyze/route.ts 内のバックエンドハンドラーがトリガーされ、Twelve Labs の Analyze API 用に構造化された具体的なプロンプトが作成されます。このプロンプトにより、返されるデータが適切に分類され、一貫してフォーマットされるため、簡単にタグに変換してフィルターメニューに表示できます。バックエンドルートの主要な部分は以下のとおりです:
api/analyze/route.ts (lines 1 - 85)
import { NextResponse } from 'next/server'; const API_KEY = process.env.TWELVELABS_API_KEY; const TWELVELABS_API_BASE_URL = process.env.TWELVELABS_API_BASE_URL; export const maxDuration = 60; export async function GET(req: Request) { const { searchParams } = new URL(req.url); const videoId = searchParams.get("videoId"); const prompt = `You are a marketing assistant specialized in generating hashtags for video content. Based on the input video metadata, generate a list of hashtags labeled by category. **Output Format:** Each line must be in the format: [Category]: [Hashtag] (e.g., sector: #beauty) **Allowed Values:** Gender: Male, Female Age: 18-25, 25-34, 35-44, 45-54, 55+ Topic: Beauty, Fashion, Tech, Travel, CPG, Food & Bev, Retail, Other Emotions: sorrow, happiness, laughter, anger, empathy, fear, love, trust, sadness, belonging, guilt, compassion, pride **Instructions:** 1. Use only the values provided in Allowed Values. 2. Do not invent new values except for Brands and Location. Only use values from the Allowed Values. 3. Output must contain at least one hashtag for each of the following categories: - Gender - Age - Topic - Emotions - Location - Brands 4. Do not output any explanations or category names—only return the final hashtag list. **Output Example:** Gender: female Age: 25-34 Topic: beauty Emotions: happiness Location: Los Angeles Brands: Fenty Beauty --- ` … const url = `${TWELVELABS_API_BASE_URL}/analyze`; const options = { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": API_KEY, }, body: JSON.stringify({ prompt: prompt, video_id: videoId, stream: false }) }; try { const response = await fetch(url, options); …
ステップ 2 - 各ビデオに PUT リクエストを送信して生成されたタグを保存する
/api/analyze ルートを使用してタグを生成したら、次のステップでそれらをインデックスされたライブラリのビデオオブジェクトに保存します。これは、Twelve Labs インデックス内のビデオのメタデータを更新する PUT API コールを通じて行われます。
⭐️ Twelve Labs のUpdate Video Information APIの詳細についてはこちらを参照してください。
この処理は updateVideoMetadata フックによって行われ、最終的に api/videos/metadata/route.ts のバックエンドルートを呼び出します。
❗️カスタムメタデータを保存するには、各ビデオを更新する際にキーに user_metadata を使用していることを確認してください。
api/videos/metadata/route.ts (lines 1-68)
import { NextRequest, NextResponse } from 'next/server'; … export async function PUT(request: NextRequest) { try { // Parse request body const body: MetadataUpdateRequest = await request.json(); const { videoId, indexId, metadata } = body; … // Prepare API request const url = `${TWELVELABS_API_BASE_URL}/indexes/${indexId}/videos/${videoId}`; const requestBody = { user_metadata: { source: metadata.source || '', sector: metadata.sector || '', emotions: metadata.emotions || '', brands: metadata.brands || '', locations: metadata.locations || '', demographics: metadata.demographics || '' } }; const options = { method: 'PUT', headers: { 'Content-Type': 'application/json', 'x-api-key': API_KEY, }, body: JSON.stringify(requestBody) }; // Call Twelve Labs API const response = await fetch(url, options); …
🔁 使用されている場所
この呼び出しは page.tsx の processVideoMetadataSingle 内にあり、以下のようになっています:
ads-library/page.tsx (lines 289-292)
if (hashtagText) { const metadata = parseHashtags(hashtagText); await updateVideoMetadata(videoId, adsIndexId, metadata);
📌 user_metadata には何が含まれているか?
保存される user_metadata オブジェクトは、次のような重要フィールドを含んでいます:
{
"gender": "female",
"age": "25-34",
"topic": "beauty",
"emotions": "happiness",
"location": "Los Angeles",
"brands": "Fenty Beauty"
この一貫したフォーマットにより、カテゴリ別フィルター UI、検索、およびダッシュボードでの視覚的グルーピングが可能になります。これらのカスタムメタデータは Twelve Labs から取得されるビデオに埋め込まれているため、GET リクエストを使用して各ビデオを取得し、必要に応じてメタデータを表示できます。
⭐️Twelve Labs' Retrieve Video Information API の詳細についてはこちらを参照してください。
主要機能 2. 類似ビデオの検索(「コンテキスト整合性分析」内)
コンテキスト整合性分析機能は、ビデオとテキストの埋め込み(embeddings)を比較することにより、選択された広告に最も関連のあるコンテンツビデオを見つけるのに役立ちます。これらの埋め込みは以下の通りです:
Twelve Labs によって生成される
類似性検索のために Pinecone を介して保存およびクエリされる
これを可能にするために、以下を確認する必要があります:
すべてのコンテンツビデオに埋め込みが存在すること
選択された広告ビデオに埋め込みが存在すること
すべての埋め込みが同じ Pinecone インデックスに保存されていること
❗️Twelve Labs を通じてビデオをインデックス登録すると、埋め込みが自動的に生成され、Retrieve Video Information API コールで取得できます。
ステップ 1 - コンテンツビデオの埋め込みを処理する
類似性検索を実行する前に、すべてのコンテンツビデオの埋め込みを Pinecone に保存する必要があります。これは、クライアント側の関数 processContentVideoEmbeddings() によって処理されます。
💡 内部フロー

🔧 コア関数
checkVectorExists は、ビデオの埋め込みベクトルがすでに Pinecone に存在するかどうかを確認します。内部的にバックエンドルートを呼び出します。
api/vectors/exists (lines 15-27)
// Fetch vectors using metadata filter instead of direct ID const queryResponse = await index.query({ vector: new Array(1024).fill(0), filter: { tl_video_id: videoId }, topK: 1, includeMetadata: true }); return NextResponse.json({ exists: queryResponse.matches.length > 0 });
埋め込みが存在しない場合は、getAndStoreEmbeddings が以下を行います:
Twelve Labs(/api/videos/[videoId]?embed=true)から埋め込みを取得します
/api/vectors/store を介して Pinecone に保存します
api/videos/[videoId] (lines 77-95)
// Base URL let url = `${TWELVELABS_API_BASE_URL}/indexes/${indexId}/videos/${videoId}`; // Always include embedding query parameters if requested if (requestEmbeddings) { // Include only supported embedding options url += `?embedding_option=visual-text&embedding_option=audio`; } const options = { method: "GET", headers: { "x-api-key": `${API_KEY}`, "Accept": "application/json" }, }; try { const response = await fetch(url, options);
api/vectors/store (lines 126-173)
// Create vectors from embedding segments const vectors = embedding.video_embedding.segments.map((segment: Segment, index: number) => { // Create a meaningful and unique vector ID const vectorId = `${vectorIdBase}_segment${index + 1}`; const vector = { id: vectorId, values: segment.float, metadata: { video_file: actualFileName, video_title: videoTitle, video_segment: index + 1, start_time: segment.start_offset_sec, end_time: segment.end_offset_sec, scope: segment.embedding_scope, tl_video_id: videoId, tl_index_id: indexId, category } }; return vector; }); try { const index = getPineconeIndex(); // Upload vectors in batches const batchSize = 100; const totalBatches = Math.ceil(vectors.length / batchSize); console.log(`🚀 FILENAME DEBUG - Starting vector upload with ${totalBatches} batches...`); for (let i = 0; i < vectors.length; i += batchSize) { const batch = vectors.slice(i, i + batchSize); const batchNumber = Math.floor(i / batchSize) + 1; try { // Test Pinecone connection before upserting try { await index.describeIndexStats(); } catch (statsError) { console.error(`❌ Pinecone connection test failed:`, statsError); throw new Error(`Failed to connect to Pinecone: ${statsError instanceof Error ? statsError.message : 'Unknown error'}`); } // Perform the actual upsert await index.upsert(batch);
ステップ 2 - 選択された広告ビデオの埋め込みを処理する
ユーザーが広告を選択すると、アプリはその埋め込みが準備できているかどうかを自動的にチェックします。このロジックは、選択された広告を監視する useEffect() フック内で実行されます:
contextual-analysis/page.tsx (lines 296-318)
// Automatically check ONLY the ad video embedding when a video is selected useEffect(() => { if (selectedVideoId && !isLoadingEmbeddings) { const cachedStatus = queryClient.getQueryData(['embeddingStatus', selectedVideoId]) as { checked: boolean, ready: boolean } | undefined; if (!cachedStatus?.checked) { setIsLoadingEmbeddings(true); ensureEmbeddings().then(success => { queryClient.setQueryData(['embeddingStatus', selectedVideoId], { checked: true, ready: success }); setEmbeddingsReady(success); setIsLoadingEmbeddings(false); }); } else { setEmbeddingsReady(cachedStatus.ready); } } }, [selectedVideoId, isLoadingEmbeddings, queryClient]);
🔧 コア関数
ensureEmbeddings は checkAndEnsureEmbeddings() を呼び出して以下を行います:
checkVectorExists を介して広告の埋め込みが存在するか確認します
欠落している場合、getAndStoreEmbeddings を使用して生成および保存します
オプションで、同じ呼び出しの中でコンテンツビデオすべてを処理します
❗️checkVectorExists() と getAndStoreEmbeddings() の内部動作についてはステップ 1 ですでに説明しているため、ここでは繰り返さずにそれらを参照します。
ステップ 3 - Pinecone での類似性検索 + TL 検索
すべてのビデオ埋め込み(広告 + コンテンツ)が配置されると、[Run Contextual Analysis(コンテキスト分析の実行)] ボタンをクリックして、以下の2つのタイプの類似性検索を並行して実行します:
テキストからビデオへの検索: 選択された広告のテキストタグ(専門分野や感情など)を使用して、意味的に関連のあるコンテンツビデオを検索します。
ビデオからビデオへの検索: 選択された広告のフレームレベルのビデオ埋め込みを使用して、視覚的/コンテキスト的に類似したコンテンツクリップを検索します。
両方の結果はマージされてスコアリングされ、両方の検索で見つかった一致項目が優先されます。
テキストからビデオへの検索
contextual-analysis/page.tsx (lines 351-384)
const handleContextualAnalysis = async () => { … try { textResults = await textToVideoEmbeddingSearch(selectedVideoId, adsIndexId, contentIndexId); … try { videoResults = await videoToVideoEmbeddingSearch(selectedVideoId, adsIndexId, contentIndexId); …
textToVideoEmbeddingSearch は、選択された広告から専門分野や感情のタグ、およびビデオのタイトルを抽出します。
それらをテキストプロンプトとして api/embeddingSearch/textToVideo ルートに送信します。
Twelve Labs はテキスト埋め込みを生成し、これを使用して Pinecone に意味的に類似したコンテンツビデオをクエリします。
api/embeddingSearch/textToVideo (lines 20-45)
const { data: embedData } = await axios.post(url, formData, { … // extract embedding vector from text_embedding object const textEmbedding = embedData.text_embedding.segments[0].float; … // Get index and search const searchResults = await index.query({ vector: textEmbedding, filter: { // video_type: 'ad', tl_index_id: indexId, scope: 'clip' }, topK: 10, includeMetadata: true, });
ビデオからビデオへの検索
videoToVideoEmbeddingSearch は、選択された広告のフレームレベルのセグメント(ベクトル値)を見つけます。
各セグメントについて、Pinecone 内のコンテンツインデックスに対して類似性クエリを実行します。
各結果は、ビデオ埋め込みにおけるクリップレベルの一致を反映します。
api/embeddingSearch/videoToVideo (lines 22-50)
// First, get the original video's clip embedding const originalClipQuery = await index.query({ filter: { tl_video_id: videoId, scope: 'clip' }, topK: 100, includeMetadata: true, includeValues: true, vector: new Array(1024).fill(0) }); // If we found matching clips, search for similar ads for each match const similarResults = []; if (originalClipQuery.matches.length > 0) { for (const originalClip of originalClipQuery.matches) { const vectorValues = originalClip.values || new Array(1024).fill(0); const queryResult = await index.query({ vector: vectorValues, filter: { tl_index_id: indexId, scope: 'clip' }, topK: 5, includeMetadata: true, }); similarResults.push(queryResult); } }
結果のマージ
両方の検索結果はビデオ ID によってマージされます。ビデオが両方の検索に含まれている場合、そのスコアは 2倍にブーストされます。
contextual-analysis/page.tsx (lines 412-428)
if (combinedResultsMap.has(videoId)) { // This is a match found in both searches - update it const existingResult = combinedResultsMap.get(videoId); // Apply a significant boost for results found in both searches (50% boost) const boostMultiplier = 2; // Combine the scores: use the max of both scores and apply the boost const maxScore = Math.max(existingResult.textScore, result.score); const boostedScore = maxScore * boostMultiplier; combinedResultsMap.set(videoId, { ...existingResult, videoScore: result.score, finalScore: boostedScore, // Boosted score for appearing in both searches source: "BOTH" });
主要機能 3. チャプターを「GENERATE」して広告挿入(ミッドロール)を実装する(「コンテキスト整合性分析」内)
この機能は、選択したコンテンツを意味のあるチャプターに分割し、選択したチャプターの最後に適切な広告を挿入することで、現実のミッドロール広告の挿入をシミュレートし、コンテキストに沿ったビデオレコメンデーション体験を強化します。
ステップ 1 - 選択したコンテンツのチャプターを自動生成する
ユーザーが VideoModal を開くと、アプリは自動的に generateChapters API を呼び出して、選択したコンテンツビデオをセグメント化します。
// Fetch chapters data const { data: chaptersData, isLoading: isChaptersLoading } = useQuery({ queryKey: ["chapters", videoId], queryFn: () => generateChapters(videoId), enabled: isOpen && !!videoId, });
各チャプターには以下が含まれます:
end: チャプターの終了時間(広告キューのポイントとして使用されます)
chapter_title: 生成された短いタイトル
chapter_summary: そのシーンと、それがなぜ広告挿入点として戦略的に適切であるかの説明(1文)
これらのチャプターは チャプタータイムラインバー上 に視覚化され、各ドットが end ポイントを示します。ドットをクリックすると、そのチャプターが終了する直前に広告を挿入するシミュレーションが行われます。
ユーザー体験(UX)の動作:
ドットはチャプター終了マーカーとして表示されます。
ドットをクリックすると、そのキューポイントで 「Show Ad(広告の表示)」 のオーバーレイが表示されます。
広告はスキップ可能で、実際のミッドロール広告のように再生されます。
サーバーロジック:Twelve Labs を介したチャプター生成
チャプターは、カスタムプロンプトとともに Twelve Labs の summarize エンドポイントを使用して生成されます。
api/generateChapters (lines 19-30)
const url = `${TWELVELABS_API_BASE_URL}/summarize`; const options = { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": `${API_KEY}`, }, body: JSON.stringify({type: "chapter", video_id: videoId, prompt: "Chapterize this video into 3 chapters. For every chapter, describe why it is a strategically appropriate point for placing an advertisement. Do not mention what type of advertisement would be suitable, as the ad content has already been determined. "}) }; try { const response = await fetch(url, options);
クライアント側のヘルパー:generateChapters
React Query でチャプターデータを取得するために使用されます:
export const generateChapters = async (videoId: string): Promise<ChaptersData> => { try { const response = await fetch(`/api/generateChapters?videoId=${videoId}`);
ステップ 2 - 選択したチャプターの区切りで広告を挿入および再生する
ユーザーがチャプターマーカーをクリックすると次のようになります:
コンテンツビデオはチャプターが終了する 3 秒前までシークします。
再生がチャプターの終了時間に達すると、アプリは広告再生シーケンスに移行します。
広告が終了すると、元のコンテンツがチャプターの区切りの直後から再開されます。
チャプターマーカークリックロジック
チャプターマーカーをクリックすると、プレーヤーはそのチャプターが終了する直前にシークし、ミッドロール広告を再生する準備をします。
VideoModal.tsx (lines 102-126)
// Chapter click handler const handleChapterClick = (index: number) => { if (playbackSequence === 'ad') { return; } if (!adVideoDetail?.hls?.video_url) { console.warn("No ad selected. Please select an ad in the contextual analysis page."); return; } if (!chaptersData) return; const chapter = chaptersData.chapters[index]; setSelectedChapter(index); setHasPlayedAd(false); setPlaybackSequence('video'); setShowChapterInfo(true); if (playerRef.current) { // Start 3 seconds before the chapter end time const startTime = Math.max(0, chapter.end - 3); playerRef.current.seekTo(startTime, 'seconds'); } };
進行状況の監視 – チャプター終了時に広告をトリガー
コンテンツの再生中、アプリは現在の再生時間がチャプターのエンドポイントに達したかどうかを確認し、条件が満たされている場合は広告の再生に切り替えます。
// Track video progress const handleProgress = (state: { playedSeconds: number }) => { if (selectedChapter === null || !chaptersData || !adVideoDetail) { return; } const chapter = chaptersData.chapters[selectedChapter]; const timeDiff = state.playedSeconds - chapter.end; const isLastChapter = selectedChapter === chaptersData.chapters.length - 1; if playbackSequence === 'video' && !hasPlayedAd && ((isLastChapter && Math.abs(timeDiff) < 0.5) || (!isLastChapter && timeDiff >= 0)) ) { setPlaybackSequence('ad'); setHasPlayedAd(true); } };
広告再生&コンテンツ再開
広告が終わると、アプリはチャプターが終了した場所から自動的にコンテンツの再生を再開します。
VideoModal.tsx (lines 128-136)
// Ad ended handler const handleAdEnded = () => { if (selectedChapter === null || !chaptersData) return; const chapter = chaptersData.chapters[selectedChapter]; setPlaybackSequence('video'); setReturnToTime(chapter.end); setIsPlaying(true); };
// Ad ended handler const handleAdEnded = () => { if (selectedChapter === null || !chaptersData) return; const chapter = chaptersData.chapters[selectedChapter]; setPlaybackSequence('video'); setReturnToTime(chapter.end); setIsPlaying(true); };
これにより、自然な区切りで脈絡(コンテキスト)に関連した広告を表示するのに最適な、スマートな広告挿入が可能な没入型でチャプター対応の視聴体験が創出されます。
まとめ
このチュートリアルでは、埋め込みベクトルの生成と保存から、類似性検索の実行、そして最後にチャプターによるセグメント化を使用したミッドロール広告の挿入シミュレーションまで、コンテキスト分析の流れ全体を追ってきました。Twelve Labs のマルチモーダル埋め込み(multimodal embeddings)と Pinecone のベクトルフィルタリングを組み合わせることで、スマートでコンテンツを認識した広告体験を提供できます。この基盤は、リアルタイムの広告ターゲティング、A/Bテスト、大規模パーソナライズ広告の配信へとさらに拡張できます。
はじめに
視聴者は、見ているコンテンツと一致しない、無関係な広告に圧倒されることがよくあります。この不一致は不満を招き、広告が押し付けがましく感じられたり、タイミングが悪いと感じられたりする原因になります。
ブランド統合アシスタント & 広告挿入点ファインダー アプリは、コンテキストに関連する広告レコメンデーションを提供することでこれを解決し、エンゲージメントの適切な瞬間に、適切な視聴者へ適切なメッセージが届くようにします。
このチュートリアルでは、アプリのコア機能全体における動作方法を学習します:
タグの自動生成: アップロードされた各広告が分析され、トピックカテゴリ、感情、ブランド、ターゲット層(性別・年齢)、場所といった豊かなメタデータが生成され、スマートなフィルタリング、検索、コンテキスト構築が可能になります。
コンテキストに整合したコンテンツの検索: AIを活用した類似性検索を使用して、ビデオとテキストの両方の埋め込み(embeddings)に基づき、広告と意味的に整合しているコンテンツビデオを検索します。
広告挿入点の推奨とシミュレーション: コンテンツをチャプターに自動分割し、ミッドロール広告の挿入をシミュレートすることで、シームレスで没入感のある広告体験を実現します。
前提条件
Twelve Labs プレイグラウンドにサインアップしてAPIキーを生成し、広告用とコンテンツビデオ用にインデックスをそれぞれ2つ作成します。
Pineconeのアカウントを設定し、ビデオ埋め込みを保存するためのインデックスを作成します。
Dimensionsを1024に、MetricをCosineに設定してください
関連するGitHubリポジトリでアプリケーションのソースコードを確認します。
よりスムーズなセットアップと開発体験のために、JavaScript、TypeScript、Next.jsの知識があると役立ちます。
デモ
デモアプリケーションでご自身で試してみるか、以下のクイックデモビデオをご覧になり、実際にどのように機能するか確認してください。 https://www.loom.com/share/233cc8cb66ae44218e3cff69afb772d7
デモ全体のウェビナー録画は以下からご覧いただけます:

アプリの仕組み
アプリの内部には、広告ライブラリ(Ads Library)とコンテキスト整合性分析(Contextual Alignment Analysis)の2つのメインメニューがあります。
広告ライブラリ - ブランドマーケター向けに、自動生成されたタグ付きの広告ビデオを整理して表示します。トピックカテゴリ、感情、ブランド、性別、年齢、場所で広告をフィルタリングしたり、Twelve Labs Search APIを使用してショートテールまたはロングテールのキーワードで検索したりできます。このチュートリアルでは、広告ライブラリ内の自動タグ生成機能に焦点を当てます。
コンテキスト整合性分析 - このセクションでは、各広告に対してコンテキストが最も関連性の高いコンテンツビデオを検索できます。Twelve Labs EMBEDおよびGETビデオAPI(タグおよびビデオ埋め込み用)と、類似性ベースのフィルタリングを行うPineconeによって、親和性の高いコンテンツを表面化させます。
その後、ユーザーはコンテンツビデオを選択し、広告挿入のための自動チャプター生成を行い、チャプターの切り替わりで広告の再生をシミュレートできます。
以下のチュートリアルでは、コンテンツマッチングと広告シミュレーションの両方の機能について詳しく説明します。

アプリの3つの主要機能とその仕組み
主要機能 1. 自動タグ生成(「広告ライブラリ」内)
広告ライブラリでは、インデックスに登録されたビデオのコレクションをブラウズし、自動生成されたタグを表示できます。これらのタグは、ビデオをトピック、感情、ブランド、ユーザー層などに分類するのに役立ち、これらはすべて Twelve Labs の Analyze API を使用して抽出されます。
ステップ 1 - 各ビデオのタグを生成する
ビデオがロードされ、メタデータが不完全または失われていると判断された場合、システムは generateMetadata を呼び出して、Twelve Labs の Analyze API を使ってタグを取得します。
⭐️Twelve Labs' Analyze API の詳細についてはこちらを参照してください。
🔁 使用されている場所
この呼び出しは page.tsx の processVideoMetadataSingle 内にあり、以下のようになっています:
ads-library/page.ts (lines 279-290)
if (!video.user_metadata || Object.keys(video.user_metadata).length === 0 || !video.user_metadata.topic_category && !video.user_metadata.emotions && !video.user_metadata.brands && !video.user_metadata.locations)) { setVideosInProcessing(prev => [...prev, videoId]); const hashtagText = await generateMetadata(videoId); if (hashtagText) { const metadata = parseHashtags(hashtagText);
generateMetadata 関数は、サーバー側のAPI呼び出しをトリガーして Twelve Labs エンジンからAI生成されたタグを要求するカスタムフックです。
これにより、api/analyze/route.ts 内のバックエンドハンドラーがトリガーされ、Twelve Labs の Analyze API 用に構造化された具体的なプロンプトが作成されます。このプロンプトにより、返されるデータが適切に分類され、一貫してフォーマットされるため、簡単にタグに変換してフィルターメニューに表示できます。バックエンドルートの主要な部分は以下のとおりです:
api/analyze/route.ts (lines 1 - 85)
import { NextResponse } from 'next/server'; const API_KEY = process.env.TWELVELABS_API_KEY; const TWELVELABS_API_BASE_URL = process.env.TWELVELABS_API_BASE_URL; export const maxDuration = 60; export async function GET(req: Request) { const { searchParams } = new URL(req.url); const videoId = searchParams.get("videoId"); const prompt = `You are a marketing assistant specialized in generating hashtags for video content. Based on the input video metadata, generate a list of hashtags labeled by category. **Output Format:** Each line must be in the format: [Category]: [Hashtag] (e.g., sector: #beauty) **Allowed Values:** Gender: Male, Female Age: 18-25, 25-34, 35-44, 45-54, 55+ Topic: Beauty, Fashion, Tech, Travel, CPG, Food & Bev, Retail, Other Emotions: sorrow, happiness, laughter, anger, empathy, fear, love, trust, sadness, belonging, guilt, compassion, pride **Instructions:** 1. Use only the values provided in Allowed Values. 2. Do not invent new values except for Brands and Location. Only use values from the Allowed Values. 3. Output must contain at least one hashtag for each of the following categories: - Gender - Age - Topic - Emotions - Location - Brands 4. Do not output any explanations or category names—only return the final hashtag list. **Output Example:** Gender: female Age: 25-34 Topic: beauty Emotions: happiness Location: Los Angeles Brands: Fenty Beauty --- ` … const url = `${TWELVELABS_API_BASE_URL}/analyze`; const options = { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": API_KEY, }, body: JSON.stringify({ prompt: prompt, video_id: videoId, stream: false }) }; try { const response = await fetch(url, options); …
ステップ 2 - 各ビデオに PUT リクエストを送信して生成されたタグを保存する
/api/analyze ルートを使用してタグを生成したら、次のステップでそれらをインデックスされたライブラリのビデオオブジェクトに保存します。これは、Twelve Labs インデックス内のビデオのメタデータを更新する PUT API コールを通じて行われます。
⭐️ Twelve Labs のUpdate Video Information APIの詳細についてはこちらを参照してください。
この処理は updateVideoMetadata フックによって行われ、最終的に api/videos/metadata/route.ts のバックエンドルートを呼び出します。
❗️カスタムメタデータを保存するには、各ビデオを更新する際にキーに user_metadata を使用していることを確認してください。
api/videos/metadata/route.ts (lines 1-68)
import { NextRequest, NextResponse } from 'next/server'; … export async function PUT(request: NextRequest) { try { // Parse request body const body: MetadataUpdateRequest = await request.json(); const { videoId, indexId, metadata } = body; … // Prepare API request const url = `${TWELVELABS_API_BASE_URL}/indexes/${indexId}/videos/${videoId}`; const requestBody = { user_metadata: { source: metadata.source || '', sector: metadata.sector || '', emotions: metadata.emotions || '', brands: metadata.brands || '', locations: metadata.locations || '', demographics: metadata.demographics || '' } }; const options = { method: 'PUT', headers: { 'Content-Type': 'application/json', 'x-api-key': API_KEY, }, body: JSON.stringify(requestBody) }; // Call Twelve Labs API const response = await fetch(url, options); …
🔁 使用されている場所
この呼び出しは page.tsx の processVideoMetadataSingle 内にあり、以下のようになっています:
ads-library/page.tsx (lines 289-292)
if (hashtagText) { const metadata = parseHashtags(hashtagText); await updateVideoMetadata(videoId, adsIndexId, metadata);
📌 user_metadata には何が含まれているか?
保存される user_metadata オブジェクトは、次のような重要フィールドを含んでいます:
{
"gender": "female",
"age": "25-34",
"topic": "beauty",
"emotions": "happiness",
"location": "Los Angeles",
"brands": "Fenty Beauty"
この一貫したフォーマットにより、カテゴリ別フィルター UI、検索、およびダッシュボードでの視覚的グルーピングが可能になります。これらのカスタムメタデータは Twelve Labs から取得されるビデオに埋め込まれているため、GET リクエストを使用して各ビデオを取得し、必要に応じてメタデータを表示できます。
⭐️Twelve Labs' Retrieve Video Information API の詳細についてはこちらを参照してください。
主要機能 2. 類似ビデオの検索(「コンテキスト整合性分析」内)
コンテキスト整合性分析機能は、ビデオとテキストの埋め込み(embeddings)を比較することにより、選択された広告に最も関連のあるコンテンツビデオを見つけるのに役立ちます。これらの埋め込みは以下の通りです:
Twelve Labs によって生成される
類似性検索のために Pinecone を介して保存およびクエリされる
これを可能にするために、以下を確認する必要があります:
すべてのコンテンツビデオに埋め込みが存在すること
選択された広告ビデオに埋め込みが存在すること
すべての埋め込みが同じ Pinecone インデックスに保存されていること
❗️Twelve Labs を通じてビデオをインデックス登録すると、埋め込みが自動的に生成され、Retrieve Video Information API コールで取得できます。
ステップ 1 - コンテンツビデオの埋め込みを処理する
類似性検索を実行する前に、すべてのコンテンツビデオの埋め込みを Pinecone に保存する必要があります。これは、クライアント側の関数 processContentVideoEmbeddings() によって処理されます。
💡 内部フロー

🔧 コア関数
checkVectorExists は、ビデオの埋め込みベクトルがすでに Pinecone に存在するかどうかを確認します。内部的にバックエンドルートを呼び出します。
api/vectors/exists (lines 15-27)
// Fetch vectors using metadata filter instead of direct ID const queryResponse = await index.query({ vector: new Array(1024).fill(0), filter: { tl_video_id: videoId }, topK: 1, includeMetadata: true }); return NextResponse.json({ exists: queryResponse.matches.length > 0 });
埋め込みが存在しない場合は、getAndStoreEmbeddings が以下を行います:
Twelve Labs(/api/videos/[videoId]?embed=true)から埋め込みを取得します
/api/vectors/store を介して Pinecone に保存します
api/videos/[videoId] (lines 77-95)
// Base URL let url = `${TWELVELABS_API_BASE_URL}/indexes/${indexId}/videos/${videoId}`; // Always include embedding query parameters if requested if (requestEmbeddings) { // Include only supported embedding options url += `?embedding_option=visual-text&embedding_option=audio`; } const options = { method: "GET", headers: { "x-api-key": `${API_KEY}`, "Accept": "application/json" }, }; try { const response = await fetch(url, options);
api/vectors/store (lines 126-173)
// Create vectors from embedding segments const vectors = embedding.video_embedding.segments.map((segment: Segment, index: number) => { // Create a meaningful and unique vector ID const vectorId = `${vectorIdBase}_segment${index + 1}`; const vector = { id: vectorId, values: segment.float, metadata: { video_file: actualFileName, video_title: videoTitle, video_segment: index + 1, start_time: segment.start_offset_sec, end_time: segment.end_offset_sec, scope: segment.embedding_scope, tl_video_id: videoId, tl_index_id: indexId, category } }; return vector; }); try { const index = getPineconeIndex(); // Upload vectors in batches const batchSize = 100; const totalBatches = Math.ceil(vectors.length / batchSize); console.log(`🚀 FILENAME DEBUG - Starting vector upload with ${totalBatches} batches...`); for (let i = 0; i < vectors.length; i += batchSize) { const batch = vectors.slice(i, i + batchSize); const batchNumber = Math.floor(i / batchSize) + 1; try { // Test Pinecone connection before upserting try { await index.describeIndexStats(); } catch (statsError) { console.error(`❌ Pinecone connection test failed:`, statsError); throw new Error(`Failed to connect to Pinecone: ${statsError instanceof Error ? statsError.message : 'Unknown error'}`); } // Perform the actual upsert await index.upsert(batch);
ステップ 2 - 選択された広告ビデオの埋め込みを処理する
ユーザーが広告を選択すると、アプリはその埋め込みが準備できているかどうかを自動的にチェックします。このロジックは、選択された広告を監視する useEffect() フック内で実行されます:
contextual-analysis/page.tsx (lines 296-318)
// Automatically check ONLY the ad video embedding when a video is selected useEffect(() => { if (selectedVideoId && !isLoadingEmbeddings) { const cachedStatus = queryClient.getQueryData(['embeddingStatus', selectedVideoId]) as { checked: boolean, ready: boolean } | undefined; if (!cachedStatus?.checked) { setIsLoadingEmbeddings(true); ensureEmbeddings().then(success => { queryClient.setQueryData(['embeddingStatus', selectedVideoId], { checked: true, ready: success }); setEmbeddingsReady(success); setIsLoadingEmbeddings(false); }); } else { setEmbeddingsReady(cachedStatus.ready); } } }, [selectedVideoId, isLoadingEmbeddings, queryClient]);
🔧 コア関数
ensureEmbeddings は checkAndEnsureEmbeddings() を呼び出して以下を行います:
checkVectorExists を介して広告の埋め込みが存在するか確認します
欠落している場合、getAndStoreEmbeddings を使用して生成および保存します
オプションで、同じ呼び出しの中でコンテンツビデオすべてを処理します
❗️checkVectorExists() と getAndStoreEmbeddings() の内部動作についてはステップ 1 ですでに説明しているため、ここでは繰り返さずにそれらを参照します。
ステップ 3 - Pinecone での類似性検索 + TL 検索
すべてのビデオ埋め込み(広告 + コンテンツ)が配置されると、[Run Contextual Analysis(コンテキスト分析の実行)] ボタンをクリックして、以下の2つのタイプの類似性検索を並行して実行します:
テキストからビデオへの検索: 選択された広告のテキストタグ(専門分野や感情など)を使用して、意味的に関連のあるコンテンツビデオを検索します。
ビデオからビデオへの検索: 選択された広告のフレームレベルのビデオ埋め込みを使用して、視覚的/コンテキスト的に類似したコンテンツクリップを検索します。
両方の結果はマージされてスコアリングされ、両方の検索で見つかった一致項目が優先されます。
テキストからビデオへの検索
contextual-analysis/page.tsx (lines 351-384)
const handleContextualAnalysis = async () => { … try { textResults = await textToVideoEmbeddingSearch(selectedVideoId, adsIndexId, contentIndexId); … try { videoResults = await videoToVideoEmbeddingSearch(selectedVideoId, adsIndexId, contentIndexId); …
textToVideoEmbeddingSearch は、選択された広告から専門分野や感情のタグ、およびビデオのタイトルを抽出します。
それらをテキストプロンプトとして api/embeddingSearch/textToVideo ルートに送信します。
Twelve Labs はテキスト埋め込みを生成し、これを使用して Pinecone に意味的に類似したコンテンツビデオをクエリします。
api/embeddingSearch/textToVideo (lines 20-45)
const { data: embedData } = await axios.post(url, formData, { … // extract embedding vector from text_embedding object const textEmbedding = embedData.text_embedding.segments[0].float; … // Get index and search const searchResults = await index.query({ vector: textEmbedding, filter: { // video_type: 'ad', tl_index_id: indexId, scope: 'clip' }, topK: 10, includeMetadata: true, });
ビデオからビデオへの検索
videoToVideoEmbeddingSearch は、選択された広告のフレームレベルのセグメント(ベクトル値)を見つけます。
各セグメントについて、Pinecone 内のコンテンツインデックスに対して類似性クエリを実行します。
各結果は、ビデオ埋め込みにおけるクリップレベルの一致を反映します。
api/embeddingSearch/videoToVideo (lines 22-50)
// First, get the original video's clip embedding const originalClipQuery = await index.query({ filter: { tl_video_id: videoId, scope: 'clip' }, topK: 100, includeMetadata: true, includeValues: true, vector: new Array(1024).fill(0) }); // If we found matching clips, search for similar ads for each match const similarResults = []; if (originalClipQuery.matches.length > 0) { for (const originalClip of originalClipQuery.matches) { const vectorValues = originalClip.values || new Array(1024).fill(0); const queryResult = await index.query({ vector: vectorValues, filter: { tl_index_id: indexId, scope: 'clip' }, topK: 5, includeMetadata: true, }); similarResults.push(queryResult); } }
結果のマージ
両方の検索結果はビデオ ID によってマージされます。ビデオが両方の検索に含まれている場合、そのスコアは 2倍にブーストされます。
contextual-analysis/page.tsx (lines 412-428)
if (combinedResultsMap.has(videoId)) { // This is a match found in both searches - update it const existingResult = combinedResultsMap.get(videoId); // Apply a significant boost for results found in both searches (50% boost) const boostMultiplier = 2; // Combine the scores: use the max of both scores and apply the boost const maxScore = Math.max(existingResult.textScore, result.score); const boostedScore = maxScore * boostMultiplier; combinedResultsMap.set(videoId, { ...existingResult, videoScore: result.score, finalScore: boostedScore, // Boosted score for appearing in both searches source: "BOTH" });
主要機能 3. チャプターを「GENERATE」して広告挿入(ミッドロール)を実装する(「コンテキスト整合性分析」内)
この機能は、選択したコンテンツを意味のあるチャプターに分割し、選択したチャプターの最後に適切な広告を挿入することで、現実のミッドロール広告の挿入をシミュレートし、コンテキストに沿ったビデオレコメンデーション体験を強化します。
ステップ 1 - 選択したコンテンツのチャプターを自動生成する
ユーザーが VideoModal を開くと、アプリは自動的に generateChapters API を呼び出して、選択したコンテンツビデオをセグメント化します。
// Fetch chapters data const { data: chaptersData, isLoading: isChaptersLoading } = useQuery({ queryKey: ["chapters", videoId], queryFn: () => generateChapters(videoId), enabled: isOpen && !!videoId, });
各チャプターには以下が含まれます:
end: チャプターの終了時間(広告キューのポイントとして使用されます)
chapter_title: 生成された短いタイトル
chapter_summary: そのシーンと、それがなぜ広告挿入点として戦略的に適切であるかの説明(1文)
これらのチャプターは チャプタータイムラインバー上 に視覚化され、各ドットが end ポイントを示します。ドットをクリックすると、そのチャプターが終了する直前に広告を挿入するシミュレーションが行われます。
ユーザー体験(UX)の動作:
ドットはチャプター終了マーカーとして表示されます。
ドットをクリックすると、そのキューポイントで 「Show Ad(広告の表示)」 のオーバーレイが表示されます。
広告はスキップ可能で、実際のミッドロール広告のように再生されます。
サーバーロジック:Twelve Labs を介したチャプター生成
チャプターは、カスタムプロンプトとともに Twelve Labs の summarize エンドポイントを使用して生成されます。
api/generateChapters (lines 19-30)
const url = `${TWELVELABS_API_BASE_URL}/summarize`; const options = { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": `${API_KEY}`, }, body: JSON.stringify({type: "chapter", video_id: videoId, prompt: "Chapterize this video into 3 chapters. For every chapter, describe why it is a strategically appropriate point for placing an advertisement. Do not mention what type of advertisement would be suitable, as the ad content has already been determined. "}) }; try { const response = await fetch(url, options);
クライアント側のヘルパー:generateChapters
React Query でチャプターデータを取得するために使用されます:
export const generateChapters = async (videoId: string): Promise<ChaptersData> => { try { const response = await fetch(`/api/generateChapters?videoId=${videoId}`);
ステップ 2 - 選択したチャプターの区切りで広告を挿入および再生する
ユーザーがチャプターマーカーをクリックすると次のようになります:
コンテンツビデオはチャプターが終了する 3 秒前までシークします。
再生がチャプターの終了時間に達すると、アプリは広告再生シーケンスに移行します。
広告が終了すると、元のコンテンツがチャプターの区切りの直後から再開されます。
チャプターマーカークリックロジック
チャプターマーカーをクリックすると、プレーヤーはそのチャプターが終了する直前にシークし、ミッドロール広告を再生する準備をします。
VideoModal.tsx (lines 102-126)
// Chapter click handler const handleChapterClick = (index: number) => { if (playbackSequence === 'ad') { return; } if (!adVideoDetail?.hls?.video_url) { console.warn("No ad selected. Please select an ad in the contextual analysis page."); return; } if (!chaptersData) return; const chapter = chaptersData.chapters[index]; setSelectedChapter(index); setHasPlayedAd(false); setPlaybackSequence('video'); setShowChapterInfo(true); if (playerRef.current) { // Start 3 seconds before the chapter end time const startTime = Math.max(0, chapter.end - 3); playerRef.current.seekTo(startTime, 'seconds'); } };
進行状況の監視 – チャプター終了時に広告をトリガー
コンテンツの再生中、アプリは現在の再生時間がチャプターのエンドポイントに達したかどうかを確認し、条件が満たされている場合は広告の再生に切り替えます。
// Track video progress const handleProgress = (state: { playedSeconds: number }) => { if (selectedChapter === null || !chaptersData || !adVideoDetail) { return; } const chapter = chaptersData.chapters[selectedChapter]; const timeDiff = state.playedSeconds - chapter.end; const isLastChapter = selectedChapter === chaptersData.chapters.length - 1; if playbackSequence === 'video' && !hasPlayedAd && ((isLastChapter && Math.abs(timeDiff) < 0.5) || (!isLastChapter && timeDiff >= 0)) ) { setPlaybackSequence('ad'); setHasPlayedAd(true); } };
広告再生&コンテンツ再開
広告が終わると、アプリはチャプターが終了した場所から自動的にコンテンツの再生を再開します。
VideoModal.tsx (lines 128-136)
// Ad ended handler const handleAdEnded = () => { if (selectedChapter === null || !chaptersData) return; const chapter = chaptersData.chapters[selectedChapter]; setPlaybackSequence('video'); setReturnToTime(chapter.end); setIsPlaying(true); };
// Ad ended handler const handleAdEnded = () => { if (selectedChapter === null || !chaptersData) return; const chapter = chaptersData.chapters[selectedChapter]; setPlaybackSequence('video'); setReturnToTime(chapter.end); setIsPlaying(true); };
これにより、自然な区切りで脈絡(コンテキスト)に関連した広告を表示するのに最適な、スマートな広告挿入が可能な没入型でチャプター対応の視聴体験が創出されます。
まとめ
このチュートリアルでは、埋め込みベクトルの生成と保存から、類似性検索の実行、そして最後にチャプターによるセグメント化を使用したミッドロール広告の挿入シミュレーションまで、コンテキスト分析の流れ全体を追ってきました。Twelve Labs のマルチモーダル埋め込み(multimodal embeddings)と Pinecone のベクトルフィルタリングを組み合わせることで、スマートでコンテンツを認識した広告体験を提供できます。この基盤は、リアルタイムの広告ターゲティング、A/Bテスト、大規模パーソナライズ広告の配信へとさらに拡張できます。
はじめに
視聴者は、見ているコンテンツと一致しない、無関係な広告に圧倒されることがよくあります。この不一致は不満を招き、広告が押し付けがましく感じられたり、タイミングが悪いと感じられたりする原因になります。
ブランド統合アシスタント & 広告挿入点ファインダー アプリは、コンテキストに関連する広告レコメンデーションを提供することでこれを解決し、エンゲージメントの適切な瞬間に、適切な視聴者へ適切なメッセージが届くようにします。
このチュートリアルでは、アプリのコア機能全体における動作方法を学習します:
タグの自動生成: アップロードされた各広告が分析され、トピックカテゴリ、感情、ブランド、ターゲット層(性別・年齢)、場所といった豊かなメタデータが生成され、スマートなフィルタリング、検索、コンテキスト構築が可能になります。
コンテキストに整合したコンテンツの検索: AIを活用した類似性検索を使用して、ビデオとテキストの両方の埋め込み(embeddings)に基づき、広告と意味的に整合しているコンテンツビデオを検索します。
広告挿入点の推奨とシミュレーション: コンテンツをチャプターに自動分割し、ミッドロール広告の挿入をシミュレートすることで、シームレスで没入感のある広告体験を実現します。
前提条件
Twelve Labs プレイグラウンドにサインアップしてAPIキーを生成し、広告用とコンテンツビデオ用にインデックスをそれぞれ2つ作成します。
Pineconeのアカウントを設定し、ビデオ埋め込みを保存するためのインデックスを作成します。
Dimensionsを1024に、MetricをCosineに設定してください
関連するGitHubリポジトリでアプリケーションのソースコードを確認します。
よりスムーズなセットアップと開発体験のために、JavaScript、TypeScript、Next.jsの知識があると役立ちます。
デモ
デモアプリケーションでご自身で試してみるか、以下のクイックデモビデオをご覧になり、実際にどのように機能するか確認してください。 https://www.loom.com/share/233cc8cb66ae44218e3cff69afb772d7
デモ全体のウェビナー録画は以下からご覧いただけます:

アプリの仕組み
アプリの内部には、広告ライブラリ(Ads Library)とコンテキスト整合性分析(Contextual Alignment Analysis)の2つのメインメニューがあります。
広告ライブラリ - ブランドマーケター向けに、自動生成されたタグ付きの広告ビデオを整理して表示します。トピックカテゴリ、感情、ブランド、性別、年齢、場所で広告をフィルタリングしたり、Twelve Labs Search APIを使用してショートテールまたはロングテールのキーワードで検索したりできます。このチュートリアルでは、広告ライブラリ内の自動タグ生成機能に焦点を当てます。
コンテキスト整合性分析 - このセクションでは、各広告に対してコンテキストが最も関連性の高いコンテンツビデオを検索できます。Twelve Labs EMBEDおよびGETビデオAPI(タグおよびビデオ埋め込み用)と、類似性ベースのフィルタリングを行うPineconeによって、親和性の高いコンテンツを表面化させます。
その後、ユーザーはコンテンツビデオを選択し、広告挿入のための自動チャプター生成を行い、チャプターの切り替わりで広告の再生をシミュレートできます。
以下のチュートリアルでは、コンテンツマッチングと広告シミュレーションの両方の機能について詳しく説明します。

アプリの3つの主要機能とその仕組み
主要機能 1. 自動タグ生成(「広告ライブラリ」内)
広告ライブラリでは、インデックスに登録されたビデオのコレクションをブラウズし、自動生成されたタグを表示できます。これらのタグは、ビデオをトピック、感情、ブランド、ユーザー層などに分類するのに役立ち、これらはすべて Twelve Labs の Analyze API を使用して抽出されます。
ステップ 1 - 各ビデオのタグを生成する
ビデオがロードされ、メタデータが不完全または失われていると判断された場合、システムは generateMetadata を呼び出して、Twelve Labs の Analyze API を使ってタグを取得します。
⭐️Twelve Labs' Analyze API の詳細についてはこちらを参照してください。
🔁 使用されている場所
この呼び出しは page.tsx の processVideoMetadataSingle 内にあり、以下のようになっています:
ads-library/page.ts (lines 279-290)
if (!video.user_metadata || Object.keys(video.user_metadata).length === 0 || !video.user_metadata.topic_category && !video.user_metadata.emotions && !video.user_metadata.brands && !video.user_metadata.locations)) { setVideosInProcessing(prev => [...prev, videoId]); const hashtagText = await generateMetadata(videoId); if (hashtagText) { const metadata = parseHashtags(hashtagText);
generateMetadata 関数は、サーバー側のAPI呼び出しをトリガーして Twelve Labs エンジンからAI生成されたタグを要求するカスタムフックです。
これにより、api/analyze/route.ts 内のバックエンドハンドラーがトリガーされ、Twelve Labs の Analyze API 用に構造化された具体的なプロンプトが作成されます。このプロンプトにより、返されるデータが適切に分類され、一貫してフォーマットされるため、簡単にタグに変換してフィルターメニューに表示できます。バックエンドルートの主要な部分は以下のとおりです:
api/analyze/route.ts (lines 1 - 85)
import { NextResponse } from 'next/server'; const API_KEY = process.env.TWELVELABS_API_KEY; const TWELVELABS_API_BASE_URL = process.env.TWELVELABS_API_BASE_URL; export const maxDuration = 60; export async function GET(req: Request) { const { searchParams } = new URL(req.url); const videoId = searchParams.get("videoId"); const prompt = `You are a marketing assistant specialized in generating hashtags for video content. Based on the input video metadata, generate a list of hashtags labeled by category. **Output Format:** Each line must be in the format: [Category]: [Hashtag] (e.g., sector: #beauty) **Allowed Values:** Gender: Male, Female Age: 18-25, 25-34, 35-44, 45-54, 55+ Topic: Beauty, Fashion, Tech, Travel, CPG, Food & Bev, Retail, Other Emotions: sorrow, happiness, laughter, anger, empathy, fear, love, trust, sadness, belonging, guilt, compassion, pride **Instructions:** 1. Use only the values provided in Allowed Values. 2. Do not invent new values except for Brands and Location. Only use values from the Allowed Values. 3. Output must contain at least one hashtag for each of the following categories: - Gender - Age - Topic - Emotions - Location - Brands 4. Do not output any explanations or category names—only return the final hashtag list. **Output Example:** Gender: female Age: 25-34 Topic: beauty Emotions: happiness Location: Los Angeles Brands: Fenty Beauty --- ` … const url = `${TWELVELABS_API_BASE_URL}/analyze`; const options = { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": API_KEY, }, body: JSON.stringify({ prompt: prompt, video_id: videoId, stream: false }) }; try { const response = await fetch(url, options); …
ステップ 2 - 各ビデオに PUT リクエストを送信して生成されたタグを保存する
/api/analyze ルートを使用してタグを生成したら、次のステップでそれらをインデックスされたライブラリのビデオオブジェクトに保存します。これは、Twelve Labs インデックス内のビデオのメタデータを更新する PUT API コールを通じて行われます。
⭐️ Twelve Labs のUpdate Video Information APIの詳細についてはこちらを参照してください。
この処理は updateVideoMetadata フックによって行われ、最終的に api/videos/metadata/route.ts のバックエンドルートを呼び出します。
❗️カスタムメタデータを保存するには、各ビデオを更新する際にキーに user_metadata を使用していることを確認してください。
api/videos/metadata/route.ts (lines 1-68)
import { NextRequest, NextResponse } from 'next/server'; … export async function PUT(request: NextRequest) { try { // Parse request body const body: MetadataUpdateRequest = await request.json(); const { videoId, indexId, metadata } = body; … // Prepare API request const url = `${TWELVELABS_API_BASE_URL}/indexes/${indexId}/videos/${videoId}`; const requestBody = { user_metadata: { source: metadata.source || '', sector: metadata.sector || '', emotions: metadata.emotions || '', brands: metadata.brands || '', locations: metadata.locations || '', demographics: metadata.demographics || '' } }; const options = { method: 'PUT', headers: { 'Content-Type': 'application/json', 'x-api-key': API_KEY, }, body: JSON.stringify(requestBody) }; // Call Twelve Labs API const response = await fetch(url, options); …
🔁 使用されている場所
この呼び出しは page.tsx の processVideoMetadataSingle 内にあり、以下のようになっています:
ads-library/page.tsx (lines 289-292)
if (hashtagText) { const metadata = parseHashtags(hashtagText); await updateVideoMetadata(videoId, adsIndexId, metadata);
📌 user_metadata には何が含まれているか?
保存される user_metadata オブジェクトは、次のような重要フィールドを含んでいます:
{
"gender": "female",
"age": "25-34",
"topic": "beauty",
"emotions": "happiness",
"location": "Los Angeles",
"brands": "Fenty Beauty"
この一貫したフォーマットにより、カテゴリ別フィルター UI、検索、およびダッシュボードでの視覚的グルーピングが可能になります。これらのカスタムメタデータは Twelve Labs から取得されるビデオに埋め込まれているため、GET リクエストを使用して各ビデオを取得し、必要に応じてメタデータを表示できます。
⭐️Twelve Labs' Retrieve Video Information API の詳細についてはこちらを参照してください。
主要機能 2. 類似ビデオの検索(「コンテキスト整合性分析」内)
コンテキスト整合性分析機能は、ビデオとテキストの埋め込み(embeddings)を比較することにより、選択された広告に最も関連のあるコンテンツビデオを見つけるのに役立ちます。これらの埋め込みは以下の通りです:
Twelve Labs によって生成される
類似性検索のために Pinecone を介して保存およびクエリされる
これを可能にするために、以下を確認する必要があります:
すべてのコンテンツビデオに埋め込みが存在すること
選択された広告ビデオに埋め込みが存在すること
すべての埋め込みが同じ Pinecone インデックスに保存されていること
❗️Twelve Labs を通じてビデオをインデックス登録すると、埋め込みが自動的に生成され、Retrieve Video Information API コールで取得できます。
ステップ 1 - コンテンツビデオの埋め込みを処理する
類似性検索を実行する前に、すべてのコンテンツビデオの埋め込みを Pinecone に保存する必要があります。これは、クライアント側の関数 processContentVideoEmbeddings() によって処理されます。
💡 内部フロー

🔧 コア関数
checkVectorExists は、ビデオの埋め込みベクトルがすでに Pinecone に存在するかどうかを確認します。内部的にバックエンドルートを呼び出します。
api/vectors/exists (lines 15-27)
// Fetch vectors using metadata filter instead of direct ID const queryResponse = await index.query({ vector: new Array(1024).fill(0), filter: { tl_video_id: videoId }, topK: 1, includeMetadata: true }); return NextResponse.json({ exists: queryResponse.matches.length > 0 });
埋め込みが存在しない場合は、getAndStoreEmbeddings が以下を行います:
Twelve Labs(/api/videos/[videoId]?embed=true)から埋め込みを取得します
/api/vectors/store を介して Pinecone に保存します
api/videos/[videoId] (lines 77-95)
// Base URL let url = `${TWELVELABS_API_BASE_URL}/indexes/${indexId}/videos/${videoId}`; // Always include embedding query parameters if requested if (requestEmbeddings) { // Include only supported embedding options url += `?embedding_option=visual-text&embedding_option=audio`; } const options = { method: "GET", headers: { "x-api-key": `${API_KEY}`, "Accept": "application/json" }, }; try { const response = await fetch(url, options);
api/vectors/store (lines 126-173)
// Create vectors from embedding segments const vectors = embedding.video_embedding.segments.map((segment: Segment, index: number) => { // Create a meaningful and unique vector ID const vectorId = `${vectorIdBase}_segment${index + 1}`; const vector = { id: vectorId, values: segment.float, metadata: { video_file: actualFileName, video_title: videoTitle, video_segment: index + 1, start_time: segment.start_offset_sec, end_time: segment.end_offset_sec, scope: segment.embedding_scope, tl_video_id: videoId, tl_index_id: indexId, category } }; return vector; }); try { const index = getPineconeIndex(); // Upload vectors in batches const batchSize = 100; const totalBatches = Math.ceil(vectors.length / batchSize); console.log(`🚀 FILENAME DEBUG - Starting vector upload with ${totalBatches} batches...`); for (let i = 0; i < vectors.length; i += batchSize) { const batch = vectors.slice(i, i + batchSize); const batchNumber = Math.floor(i / batchSize) + 1; try { // Test Pinecone connection before upserting try { await index.describeIndexStats(); } catch (statsError) { console.error(`❌ Pinecone connection test failed:`, statsError); throw new Error(`Failed to connect to Pinecone: ${statsError instanceof Error ? statsError.message : 'Unknown error'}`); } // Perform the actual upsert await index.upsert(batch);
ステップ 2 - 選択された広告ビデオの埋め込みを処理する
ユーザーが広告を選択すると、アプリはその埋め込みが準備できているかどうかを自動的にチェックします。このロジックは、選択された広告を監視する useEffect() フック内で実行されます:
contextual-analysis/page.tsx (lines 296-318)
// Automatically check ONLY the ad video embedding when a video is selected useEffect(() => { if (selectedVideoId && !isLoadingEmbeddings) { const cachedStatus = queryClient.getQueryData(['embeddingStatus', selectedVideoId]) as { checked: boolean, ready: boolean } | undefined; if (!cachedStatus?.checked) { setIsLoadingEmbeddings(true); ensureEmbeddings().then(success => { queryClient.setQueryData(['embeddingStatus', selectedVideoId], { checked: true, ready: success }); setEmbeddingsReady(success); setIsLoadingEmbeddings(false); }); } else { setEmbeddingsReady(cachedStatus.ready); } } }, [selectedVideoId, isLoadingEmbeddings, queryClient]);
🔧 コア関数
ensureEmbeddings は checkAndEnsureEmbeddings() を呼び出して以下を行います:
checkVectorExists を介して広告の埋め込みが存在するか確認します
欠落している場合、getAndStoreEmbeddings を使用して生成および保存します
オプションで、同じ呼び出しの中でコンテンツビデオすべてを処理します
❗️checkVectorExists() と getAndStoreEmbeddings() の内部動作についてはステップ 1 ですでに説明しているため、ここでは繰り返さずにそれらを参照します。
ステップ 3 - Pinecone での類似性検索 + TL 検索
すべてのビデオ埋め込み(広告 + コンテンツ)が配置されると、[Run Contextual Analysis(コンテキスト分析の実行)] ボタンをクリックして、以下の2つのタイプの類似性検索を並行して実行します:
テキストからビデオへの検索: 選択された広告のテキストタグ(専門分野や感情など)を使用して、意味的に関連のあるコンテンツビデオを検索します。
ビデオからビデオへの検索: 選択された広告のフレームレベルのビデオ埋め込みを使用して、視覚的/コンテキスト的に類似したコンテンツクリップを検索します。
両方の結果はマージされてスコアリングされ、両方の検索で見つかった一致項目が優先されます。
テキストからビデオへの検索
contextual-analysis/page.tsx (lines 351-384)
const handleContextualAnalysis = async () => { … try { textResults = await textToVideoEmbeddingSearch(selectedVideoId, adsIndexId, contentIndexId); … try { videoResults = await videoToVideoEmbeddingSearch(selectedVideoId, adsIndexId, contentIndexId); …
textToVideoEmbeddingSearch は、選択された広告から専門分野や感情のタグ、およびビデオのタイトルを抽出します。
それらをテキストプロンプトとして api/embeddingSearch/textToVideo ルートに送信します。
Twelve Labs はテキスト埋め込みを生成し、これを使用して Pinecone に意味的に類似したコンテンツビデオをクエリします。
api/embeddingSearch/textToVideo (lines 20-45)
const { data: embedData } = await axios.post(url, formData, { … // extract embedding vector from text_embedding object const textEmbedding = embedData.text_embedding.segments[0].float; … // Get index and search const searchResults = await index.query({ vector: textEmbedding, filter: { // video_type: 'ad', tl_index_id: indexId, scope: 'clip' }, topK: 10, includeMetadata: true, });
ビデオからビデオへの検索
videoToVideoEmbeddingSearch は、選択された広告のフレームレベルのセグメント(ベクトル値)を見つけます。
各セグメントについて、Pinecone 内のコンテンツインデックスに対して類似性クエリを実行します。
各結果は、ビデオ埋め込みにおけるクリップレベルの一致を反映します。
api/embeddingSearch/videoToVideo (lines 22-50)
// First, get the original video's clip embedding const originalClipQuery = await index.query({ filter: { tl_video_id: videoId, scope: 'clip' }, topK: 100, includeMetadata: true, includeValues: true, vector: new Array(1024).fill(0) }); // If we found matching clips, search for similar ads for each match const similarResults = []; if (originalClipQuery.matches.length > 0) { for (const originalClip of originalClipQuery.matches) { const vectorValues = originalClip.values || new Array(1024).fill(0); const queryResult = await index.query({ vector: vectorValues, filter: { tl_index_id: indexId, scope: 'clip' }, topK: 5, includeMetadata: true, }); similarResults.push(queryResult); } }
結果のマージ
両方の検索結果はビデオ ID によってマージされます。ビデオが両方の検索に含まれている場合、そのスコアは 2倍にブーストされます。
contextual-analysis/page.tsx (lines 412-428)
if (combinedResultsMap.has(videoId)) { // This is a match found in both searches - update it const existingResult = combinedResultsMap.get(videoId); // Apply a significant boost for results found in both searches (50% boost) const boostMultiplier = 2; // Combine the scores: use the max of both scores and apply the boost const maxScore = Math.max(existingResult.textScore, result.score); const boostedScore = maxScore * boostMultiplier; combinedResultsMap.set(videoId, { ...existingResult, videoScore: result.score, finalScore: boostedScore, // Boosted score for appearing in both searches source: "BOTH" });
主要機能 3. チャプターを「GENERATE」して広告挿入(ミッドロール)を実装する(「コンテキスト整合性分析」内)
この機能は、選択したコンテンツを意味のあるチャプターに分割し、選択したチャプターの最後に適切な広告を挿入することで、現実のミッドロール広告の挿入をシミュレートし、コンテキストに沿ったビデオレコメンデーション体験を強化します。
ステップ 1 - 選択したコンテンツのチャプターを自動生成する
ユーザーが VideoModal を開くと、アプリは自動的に generateChapters API を呼び出して、選択したコンテンツビデオをセグメント化します。
// Fetch chapters data const { data: chaptersData, isLoading: isChaptersLoading } = useQuery({ queryKey: ["chapters", videoId], queryFn: () => generateChapters(videoId), enabled: isOpen && !!videoId, });
各チャプターには以下が含まれます:
end: チャプターの終了時間(広告キューのポイントとして使用されます)
chapter_title: 生成された短いタイトル
chapter_summary: そのシーンと、それがなぜ広告挿入点として戦略的に適切であるかの説明(1文)
これらのチャプターは チャプタータイムラインバー上 に視覚化され、各ドットが end ポイントを示します。ドットをクリックすると、そのチャプターが終了する直前に広告を挿入するシミュレーションが行われます。
ユーザー体験(UX)の動作:
ドットはチャプター終了マーカーとして表示されます。
ドットをクリックすると、そのキューポイントで 「Show Ad(広告の表示)」 のオーバーレイが表示されます。
広告はスキップ可能で、実際のミッドロール広告のように再生されます。
サーバーロジック:Twelve Labs を介したチャプター生成
チャプターは、カスタムプロンプトとともに Twelve Labs の summarize エンドポイントを使用して生成されます。
api/generateChapters (lines 19-30)
const url = `${TWELVELABS_API_BASE_URL}/summarize`; const options = { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": `${API_KEY}`, }, body: JSON.stringify({type: "chapter", video_id: videoId, prompt: "Chapterize this video into 3 chapters. For every chapter, describe why it is a strategically appropriate point for placing an advertisement. Do not mention what type of advertisement would be suitable, as the ad content has already been determined. "}) }; try { const response = await fetch(url, options);
クライアント側のヘルパー:generateChapters
React Query でチャプターデータを取得するために使用されます:
export const generateChapters = async (videoId: string): Promise<ChaptersData> => { try { const response = await fetch(`/api/generateChapters?videoId=${videoId}`);
ステップ 2 - 選択したチャプターの区切りで広告を挿入および再生する
ユーザーがチャプターマーカーをクリックすると次のようになります:
コンテンツビデオはチャプターが終了する 3 秒前までシークします。
再生がチャプターの終了時間に達すると、アプリは広告再生シーケンスに移行します。
広告が終了すると、元のコンテンツがチャプターの区切りの直後から再開されます。
チャプターマーカークリックロジック
チャプターマーカーをクリックすると、プレーヤーはそのチャプターが終了する直前にシークし、ミッドロール広告を再生する準備をします。
VideoModal.tsx (lines 102-126)
// Chapter click handler const handleChapterClick = (index: number) => { if (playbackSequence === 'ad') { return; } if (!adVideoDetail?.hls?.video_url) { console.warn("No ad selected. Please select an ad in the contextual analysis page."); return; } if (!chaptersData) return; const chapter = chaptersData.chapters[index]; setSelectedChapter(index); setHasPlayedAd(false); setPlaybackSequence('video'); setShowChapterInfo(true); if (playerRef.current) { // Start 3 seconds before the chapter end time const startTime = Math.max(0, chapter.end - 3); playerRef.current.seekTo(startTime, 'seconds'); } };
進行状況の監視 – チャプター終了時に広告をトリガー
コンテンツの再生中、アプリは現在の再生時間がチャプターのエンドポイントに達したかどうかを確認し、条件が満たされている場合は広告の再生に切り替えます。
// Track video progress const handleProgress = (state: { playedSeconds: number }) => { if (selectedChapter === null || !chaptersData || !adVideoDetail) { return; } const chapter = chaptersData.chapters[selectedChapter]; const timeDiff = state.playedSeconds - chapter.end; const isLastChapter = selectedChapter === chaptersData.chapters.length - 1; if playbackSequence === 'video' && !hasPlayedAd && ((isLastChapter && Math.abs(timeDiff) < 0.5) || (!isLastChapter && timeDiff >= 0)) ) { setPlaybackSequence('ad'); setHasPlayedAd(true); } };
広告再生&コンテンツ再開
広告が終わると、アプリはチャプターが終了した場所から自動的にコンテンツの再生を再開します。
VideoModal.tsx (lines 128-136)
// Ad ended handler const handleAdEnded = () => { if (selectedChapter === null || !chaptersData) return; const chapter = chaptersData.chapters[selectedChapter]; setPlaybackSequence('video'); setReturnToTime(chapter.end); setIsPlaying(true); };
// Ad ended handler const handleAdEnded = () => { if (selectedChapter === null || !chaptersData) return; const chapter = chaptersData.chapters[selectedChapter]; setPlaybackSequence('video'); setReturnToTime(chapter.end); setIsPlaying(true); };
これにより、自然な区切りで脈絡(コンテキスト)に関連した広告を表示するのに最適な、スマートな広告挿入が可能な没入型でチャプター対応の視聴体験が創出されます。
まとめ
このチュートリアルでは、埋め込みベクトルの生成と保存から、類似性検索の実行、そして最後にチャプターによるセグメント化を使用したミッドロール広告の挿入シミュレーションまで、コンテキスト分析の流れ全体を追ってきました。Twelve Labs のマルチモーダル埋め込み(multimodal embeddings)と Pinecone のベクトルフィルタリングを組み合わせることで、スマートでコンテンツを認識した広告体験を提供できます。この基盤は、リアルタイムの広告ターゲティング、A/Bテスト、大規模パーソナライズ広告の配信へとさらに拡張できます。




