チュートリアル

Twelve Labsを使用した多言語ビデオ文字起こしアプリの構築

リシケシュ・ヤダフ

このチュートリアルでは、「マルチリンガル・ビデオ・トランスクライバー」の構築プロセスを順を追って解説します。このツールは、Twelve Labs APIを使用することで、手動での文字起こしや翻訳作業を行うことなく、任意の言語でタイムスタンプ付きの文字起こしを作成し、初心者のレベル、中級者のレベル、上級者のレベルに合わせた出力の複雑さの調整を可能にします。

このチュートリアルでは、「マルチリンガル・ビデオ・トランスクライバー」の構築プロセスを順を追って解説します。このツールは、Twelve Labs APIを使用することで、手動での文字起こしや翻訳作業を行うことなく、任意の言語でタイムスタンプ付きの文字起こしを作成し、初心者のレベル、中級者のレベル、上級者のレベルに合わせた出力の複雑さの調整を可能にします。

この記事の内容

No headings found on page

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

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

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

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

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

2024/11/19

10分

記事へのリンクをコピー

異なる言語の動画コンテンツを理解するのに苦労していませんか?あるいは、世界中のオーディエンスに向けてコンテンツをアクセシブルにすることに難しさを感じていませんか? 🌍

このチュートリアルでは、「マルチリンガル動画文字起こしアプリケーション(MultiLingual Video Transcriber Application)」を紹介し、その開発プロセスについて解説します。このアプリケーションは、Twelve LabsのAIモデルを活用して動画を理解し、複数の言語でシームレスな文字起こしを提供します。

このプログラムのユニークな点は、ユーザーが選択した習得レベル(初級、中級、上級)に合わせて文字起こしの内容を調整できることです。ユーザーは選択したレベルに最適化された文字起こしや翻訳テキストを受け取ることができます。さらに、このアプリケーションは正確なタイムスタンプを提供するため、ユーザーは発話された言葉と文字起こしテキストの同期を追うことができます。この機能により、コンテンツのナビゲーションや理解が容易になります。このアプリケーションの仕組みと、TwelveLabs Python SDKを使用して同様のソリューションを構築する方法について探っていきましょう。

アプリケーションのデモは、こちらから体験できます:動画マルチリンガル文字起こしデモ(Video Multilingual Transcriber)

コードにアクセスしてアプリを直接試してみたい場合は、こちらのReplit テンプレートをご利用ください。

前提条件

  • Twelve Labs Playgroundにサインアップして、APIキーを生成します。

  • このアプリケーションのリポジトリは Video Multilingual Transcriber で確認できます。

  • Flask、HTML、CSS、JavaScriptに関する基本的な知識があることを前提としています。

アプリケーションの仕組み

このセクションでは、マルチリンガル動画文字起こしアプリケーションを開発するための処理の流れを説明します。このプロセスは、動画を取得し、ユーザーの好みに応じて様々な言語と習得レベルの文字起こしを生成します。単なる単純な文字起こしにとどまらず、より包括的なソリューションを提供します。

システム構成は、フロントエンド層、バックエンド層、ストレージ層、そしてTwelveLabsサービスの4つの主要コンポーネントで構築されています。仕組みは次の通りです:ユーザーが(外国語の可能性がある)動画をアップロードし、希望する翻訳言語と習得レベル(初級、中級、上級)を選択します。

__wf_reserved_inherit

動画アップロード後に「送信(submit)」ボタンをクリックすると、インデックスID(Index ID)が生成され、今後の使用に備えてセッション状態で保存されます。次に、システムは動画をアップロードしてタスクID(Task ID)を作成し、インデックス処理が完了するとビデオID(Video ID)が取得されます。インデックス処理のエンベディングエンジンにはMarengo 2.6が使用され、文字起こしの生成には、Generate APIを介してPegasus 1.1エンジン(ジェネレーティブエンジン)が使用されます。

ユーザーの利便性を高めるため、アプリケーションは文字起こしとともにタイムスタンプを生成します。この機能により、文字起こしテキストと動画再生の間でインタラクティブな同期が可能になります。

準備手順

  1. Twelve Labs PlaygroundからAPIキーを取得し、環境変数を準備します。

  2. Githubからプロジェクトをクローンするか、Replit テンプレートを使用します。

  3. メインファイルと同じ階層に、APIキーを設定した.envファイルを作成します。

API_KEY=your_api_key_here

これらの手順が完了したら、いよいよアプリケーションの開発を開始しましょう!

VidScribe - 動画マルチリンガル文字起こしアプリのウォークスルー

このチュートリアルでは、最小限のフロントエンドを持つFlaskアプリケーションを構築します。ディレクトリ構造は以下の通りです:

.
├── app.py
├── requirements.txt
├── static
├── style.css
└── main.js
├── templates
└── index.html
└── uploads

Flaskアプリケーションの作成

準備手順が完了したので、Flaskアプリケーションを実装します。これにより、動画をアップロードしてユーザーの好みに応じた様々な言語の文字起こしを生成するためのシンプルな方法が構築できます。

仮想環境を準備して作成するために必要な依存関係は、こちらで確認できます:requirements.txt

Pythonの仮想環境を作成し、次のコマンドを実行してアプリケーションの環境をセットアップします:

pip install -r

1 - メインアプリケーションの構築

このセクションでは、重要なロジックと指示フローが含まれるメインアプリケーションのユーティリティ機能に焦点を当てます。メインアプリケーションをいくつかのセクションに分解して説明します:

  • インデックスの作成

  • 結果の生成

  • アップロード用コンポーネント

1.1 - インデックスの作成 

ここでは、Twelve Labs SDKを使用してインデックスを構成する方法について説明します。このFlaskアプリケーションは、ユーザーが動画をアップロードして様々な目的のために処理できるようにします。本番環境で確実に動作するように、安全なファイル名の処理システムとセッション管理を採用しています。

# 必要なモジュールのインポート

from flask import Flask, render_template, request, jsonify, send_from_directory, session
from werkzeug.utils import secure_filename
import os
import uuid
from twelvelabs import TwelveLabs
from twelvelabs.models.task import Task
from dotenv import load_dotenv

load_dotenv()

# 環境変数からTwelve LabsのAPIキーを読み込む
API_KEY = os.getenv("API_KEY")

app = Flask(__name__)
app.secret_key = os.urandom(24)  

UPLOAD_FOLDER = 'uploads'
ALLOWED_EXTENSIONS = {'mp4', 'avi', 'mov'}
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024


# Twelve Labs SDK クライアントの初期化
client = TwelveLabs(api_key=API_KEY)

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/')
def index():
    return render_template('index.html')

# ステータス確認用のユーティリティ関数
def on_task_update(task: Task):
    print(f"Status={task.status}")


def process_video(filepath, language, difficulty):
    try:
        if 'index_id' not in session:

	     # インデックス名の設定
            index_name = f"Translate{uuid.uuid4().hex[:8]}"

	     # エンジンの定義
            engines = [
                {
                    "name": "pegasus1.1",
                    "options": ["visual", "conversation"]
                },
                {
                    "name": "marengo2.6",
                    "options": ["visual", "conversation", "text_in_video", "logo"]
                }
            ]

	     # 設定を適用したインデックスの作成
            index = client.index.create(
                name=index_name,
                engines=engines
            )

            # インデックスIDをセッションに格納し、後の処理で再利用できるようにする
            session['index_id'] = index.id
            print(f"Created new index with ID: {index.id}")
        else:
            print(f"Using existing index with ID: {session['index_id']}")

	
	 # タスクの作成
        task = client.task.create(index_id=session['index_id'], file=filepath)
        task.wait_for_done(sleep_interval=5, callback=on_task_update)
        
        if task.status != "ready":
            raise RuntimeError(f"Indexing failed with status {task.status}")
        
        print(f"The unique identifier of your video is {task.video_id}.")

セッション状態を管理して、インデックスIDの作成を堅牢に処理します。UUIDによってインデックス名にランダムな番号が付加され、セッションごとに一意のインデックス名が作成されます。この新しいインデックスIDは、後で参照できるようにセッションに保存されます。

インデックス構成では、2つのエンジンを定義しています。動画のインデックス作成にはMarengo 2.6(エンベディングエンジン)を使用し、インデックスされた動画にアクセスして自由形式のプロンプトなどに基づいてコンテンツを生成するためにPegasus 1.1(ジェネレーティブエンジン)を使用します。

タスクを作成するには、インデックスIDと動画ファイルのパスを指定します。タスクの処理ステータスは `task.status` を使って追跡できます。インデックス作成が完了すると、次のステップで生成されたビデオIDが使用されます。

1.2  - 結果の生成

このセクションでは、ユーザーが選択した難易度レベルと希望する言語設定に基づいて、インデックス化された動画からテキストを生成する部分を扱います。私たちのプロンプトエンジニアリングシステムは、正確なタイムスタンプと翻訳を維持しながら、ユーザーの理解レベルに合わせて動画文字起こしの複雑さと詳細度を調整します。

# 難易度レベルに応じたアップデートされたプロンプト
        difficulty_prompts = {
            "beginner": "Provide a simplified and easy-to-understand",
            "intermediate": "Provide a moderately detailed",
            "advanced": "Provide a comprehensive and detailed"
        }
        
	 # ユーザーフォームから取得した難易度レベルを格納
        base_prompt = difficulty_prompts.get(difficulty, difficulty_prompts["intermediate"])
	 # 生成のためのオープンエンドプロンプト
        prompt = f"Provide the Only Transcript in the Translated {language.capitalize()} Language, {base_prompt} level with the timestamp duration (in the format of ss : ss) of the Indexed Video Content."
        
        res = client.generate.text(video_id=task.video_id, prompt=prompt, temperature=0.25)
        print(res)
        return {
            'status': 'ready',
            'message': 'File processed successfully',
            'transcript': res.data,
            'video_path': f'/uploads/{os.path.basename(filepath)}'
        }
        
    except Exception as e:
        print(f"Error processing video: {str(e)}")
        return {'status': 'error', 'message': str(e)}

辞書(dictionary)を用いたアプローチで難易度レベルを適切なプロンプトテンプレートにマッピングし、デフォルトのフォールバックとして中級レベルを設定します。一貫性があり信頼性の高い出力生成を実現するために、システムのTemperature値を0.25と低く維持しています。レスポンスには、タイムスタンプ付きの処理済み文字起こしデータが含まれます。

1.3  - アップロード用コンポーネント

このセクションでは、Flaskアプリケーションで安全なファイルアップロードのエンドポイントを生成および管理し、ユーザーからのファイル送信を処理する方法について解説します。このコンポーネントは、ファイルのアップロードプロセスを管理し、ファイルタイプを検証し、言語や難易度の設定を処理します。さらに、ワークフロー全体の適切なエラーハンドリングを維持しながら、アップロードされた動画の安全な保存を保証します。

# ファイルアップロードを処理するルート - POSTリクエストのみを許可
@app.route('/upload', methods=['POST'])
def upload_file():

    # リクエストにファイルが含まれているか確認
    if 'file' not in request.files:
        return jsonify({'status': 'error', 'message': 'No file part'}), 400
    
    # リクエストからファイルを取得
    file = request.files['file']
    # フォームデータから抽出した言語設定、未指定の場合はデフォルトでドイツ語を設定
    language = request.form.get('language', 'german')
    difficulty = request.form.get('difficulty', 'intermediate')
    
    # ファイルが実際に選択されているか検証
    if file.filename == '':
        return jsonify({'status': 'error', 'message': 'No selected file'}), 400

    # 許可されたファイル拡張子であるか確認し、処理を進行
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(filepath)
        
	 # 設定された言語と難易度で動画処理を実行
        result = process_video(filepath, language, difficulty)

        # 処理結果をJSON応答として送信
        return jsonify(result)
    # 許可されていないファイルタイプの場合はエラーを返す
    return jsonify({'status': 'error', 'message': 'File type not allowed'}), 400

# アップロードされたファイルを提供する配信ルート
@app.route('/uploads/<filename>')
def uploaded_file(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)

動画ファイルと関連するメタデータを処理するためのアップロード先エンドポイントを提供しています。このアップロードルートでは、安全なファイル名処理としてWerkzeugの secure_filename ユーティリティを使用し、適切なHTTPステータスコードを返す堅牢なエラーチェックを実装しています。言語や難易度の設定が指定されない場合、システムはそれぞれドイツ語と中級(intermediate)に設定します。また、ファイルを配信するルートがあることで、指定のディレクトリを通じてアップロード動画への安全なアクセスが可能になり、アクセス制限とファイルシステムのセキュリティが保護されます。

2 - JavaScriptによるコンポーネント処理

このセクションでは、app.jsファイルに含まれる主要な処理ユーティリティについて解説します。このJavaScriptファイルは、ファイルのアップロード、フォーム送信、タイムスタンプ付きの文字起こしデータと動画の同期再生、エラーハンドリング、そしてその他の画面のステータス管理を処理します。main.jsを以下の2つのセクションに分解して説明します:

  • ファイルのアップロードと検証

  • 文字起こしデータのパース処理

2.1 - ファイルのアップロードと検証

このセクションでは、クライアントサイドでの包括的な検証および送信処理の構成について紹介します。アップロード処理中に動画ファイルを検証し、選択ファイルの削除に対応し、フォームを非同期通信で送信する仕組みが組み込まれています。

 // アップロードされたファイルの検証
     function validateFile(file) {
        const validTypes = ['video/mp4', 'video/avi', 'video/quicktime'];
        const maxSize = 100 * 1024 * 1024; // 100MB

        if (!validTypes.includes(file.type)) {
            updateStatus('有効な動画ファイル(MP4, AVI, もしくは MOV)を選択してください', 'error');
            return false;
        }

        if (file.size > maxSize) {
            updateStatus('ファイルサイズは100MB未満にしてください', 'error');
            return false;
        }

        return true;
    }

    // 選択されたファイルの削除処理を行うユーティリティ関数
    function handleFileRemove(e) {
        e.preventDefault();
        fileInput.value = '';
        selectedFile.classList.add('hidden');
        uploadPrompt.textContent = '動画を選ぶか、ここにドラッグしてください';
        updateStatus('', '');
    }

    // フォーム送信の処理
    async function handleFormSubmit(e) {
        e.preventDefault();
        const formData = new FormData(e.target);
        
        if (!fileInput.files || !fileInput.files[0]) {
            updateStatus('まず始めにファイルを選択してください', 'error');
            return;
        }

        const loadingOverlay = document.getElementById('loading-overlay');
        loadingOverlay.classList.remove('hidden');
        updateStatus('ファイルをアップロードして処理中...', 'loading');
        // 過去の結果表示を切り替える
        hideResult();

        try {
         // フォームとともにPOSTリクエストを作成してエンドポイントへ送信
            const response = await fetch('/upload', {
                method: 'POST',
                body: formData
            });

	     // JSONレスポンスのパース
            const data = await response.json();
            console.log('Server response:', data);

            // アップロードと処理に成功したか確認
            if (response.ok && data.status === 'ready') {
                // 状態表示を完了に切り替えて取得データを反映表示
                updateStatus('処理が完了しました!', 'success');
                showResult();
                displayTranscript(data.transcript);
                displayVideo(data.video_path);
            } else {
                // 処理に失敗した場合はエラーをスローする
                throw new Error(data.message || 'データ処理中にエラーが発生しました');
            }
        } catch (error) {
            // エラーを検知してログを出力し表示
            console.error('Error:', error);
            updateStatus(`Error: ${error.message}`, 'error');
        } finally {
            // 処理が完了したら成否に関わらずローディング画面を非表示にする
            loadingOverlay.classList.add('hidden');
        }
    }

validateFile 関数により正しい形式の動画フォーマットと許容されるサイズ以外の不正なファイルが除外され、handleFileRemove が呼び出されると選択されていたファイルの削除と共にステータス表示がリセットされます。また、handleFormSubmit が非同期処理としての送信を仲介し、適切なエラー制御やロード時の進行状況を処理します。

この処理には、状況に合わせた表示を画面上に反映するために、ローディングオーバーレイの有効化・完了通知メッセージの切り替え・エラー発生時の通知表示といった細やかなステータス制御が含まれています。アップロードの各種段階で、常に一貫した使いやすさを提供します。

2.2 - オープンエンドプロンプトを処理する文字起こしパースの処理

このセクションでは、頑健な文字起こしパーサー(解析処理)のJavascript記述に焦点を当てます。このプログラムは、生成AIの自由なテキスト回答において検出される、様々な形式のタイムスタンプ情報や構文構成を網羅し、各構成の時間ベース位置に整合させてテキストを適切に配列します。

この処理により原型のテキストを構造化配列データに編集することで動画プレーヤーと文字起こしテキストのタイムラグの同期表現を行い、文字起こしの際に想定される例外部分や形式のブレに対する汎用的な適応を備えます。

 // Parses a transcript string into structured data with timestamps and text
    function parseTranscript(transcript) {
        console.log('Raw transcript:', transcript);
        // Initialize Map to store unique entries (prevents duplicates)
        const entries = new Map();
        
        if (!transcript) {
            console.error('Empty transcript received');
            return [];
        }
    
        // Extract data from JSON response and handle escapes
        let transcriptText = transcript;
        try {
            if (typeof transcript === 'string' && (transcript.includes('"id":') || transcript.includes("'id':"))) {
                const dataMatch = transcript.match(/['"]data['"]\s*:\s*['"]([^]+?)['"]\s*$/);
                if (dataMatch && dataMatch[1]) {
                    transcriptText = dataMatch[1]
                        .replace(/\\n/g, '\n')
                        .replace(/\\'/g, "'")
                        .replace(/\\"/g, '"')
                        .replace(/\\\\/g, '\\');
                }
            }
        } catch (e) {
            console.error('Error parsing JSON response:', e);
        }
    
        // Different timestamp patterns
        const patterns = [
            // HH:MM - HH:MM : "text"
            /(\d{2}):(\d{2})\s*-\s*(\d{2}):(\d{2})\s*:\s*["']([^"']+)["']/g,
            // HH:MM - HH:MM: text
            /(\d{2}):(\d{2})\s*-\s*(\d{2}):(\d{2})\s*:\s*([^"\n]+)/g,
            // Simple format with quotes
            /(\d{2}):(\d{2})\s*-\s*(\d{2}):(\d{2})\s*["']([^"']+)["']/g
        ];
    
        // Try each pattern against the transcript text for parsing
        for (const pattern of patterns) {
            let match;
            while ((match = pattern.exec(transcriptText)) !== null) {
                try {
                    const [_, startMin, startSec, endMin, endSec, text] = match;
                    // Convert timestamps to seconds
                    const startTime = parseInt(startMin) * 60 + parseInt(startSec);
                    const endTime = parseInt(endMin) * 60 + parseInt(endSec);
    
                    // Skip invalid timestamps
                    if (isNaN(startTime) || isNaN(endTime)) continue;
                    // Clean up theo transcript text, if appears
                    const cleanText = text
                        .replace(/^["'\s]+|["'\s]+$/g, '')
                        .replace(/\\n/g, ' ')
                        .replace(/\*\*/g, '')
                        .replace(/\\'/g, "'")
                        .replace(/\\"/g, '"')
                        .replace(/\\\\/g, '\\')
                        .replace(/\s+/g, ' ')
                        .trim();
    
                    if (cleanText && !cleanText.includes('Note:')) {
                        const key = `${startTime}-${cleanText.substring(0, 50)}`;
                        entries.set(key, {
                            start: startTime,
                            end: endTime,
                            text: cleanText
                        });
                    }
                } catch (e) {
                    console.error('Error processing match:', e);
                    continue;
                }
            }
        }
    
        // Convert Map to Array and sort by start time
        const sortedEntries = Array.from(entries.values())
            .sort((a, b) => a.start - b.start);
        // Log processed result
        console.log('Parsed entries:', sortedEntries);
        return sortedEntries;
    }

パースを機能させるために、各種パターンに対応する正規表現を使用し、時間指定を秒表記へと変換し、タイム表示に伴う不要ノイズを取り除くよう文字列処理を行います。Map型オブジェクトを用いて重複登録を抑止しつつ時系列を適正順に構成します。また、任意のタイミングで稼働追跡ができるようにデバッグを意識した実用的なログ記述が散りばめられています。

この最終生成物を配列としてまとめることで、動画再生プレーヤーや字幕描写処理等にそのまま適用でき、正確なタイミングで画面文字起こしを表示させることができます。

上で説明されたJavaScriptファイルのコード全体は、こちらのapp.jsファイルからご確認いただけます。

デモアプリケーションの実行

初めに、対象とする翻訳変換先の言語および希望する難易度習得レベルを画面から選択し、動画をアップロードします。アップロードが完了すると、自動的にインデックス化処理が開始されます。

__wf_reserved_inherit

インデックス作成と準備タスクが完了したら、動画IDを介してユーザーの選択設定に合わせて文字起こし文を生成します。下図は、Twelve Labs SDKを用いた生成結果を表示するデモ画面です。

__wf_reserved_inherit

Twelve Labsをさらに深く試してみたい場合は、教育用途、コンテンツ配信制作、その他の分野で動画認識を活用した独自ソリューションのアイデア生成などを進めてみてください。

このチュートリアルを応用するさらなるアイデア

マルチリンガル文字起こしアプリの基本構成を理解することで、多種多様なアイデアの着想やビジネス実用に向けた発展、新たな動画処理機能の開発などが可能になります。以下に、ユーザーの動画利用体験を豊かにする実用的な活用案を紹介します:

🌍 グローバルコンテンツクリエイター: 瞬時に多言語翻訳をまとめて自動文字起こしとして生成し、即時動画の現地ローカライズを進めオーディエンス層をグローバルに広げます。

🎓 国際遠隔教育・レッスンプログラム: ウェブ講義等の動画から自動で他国言語の字幕を作成し、生徒の語学学習レベル(初〜上級)に合わせて理解補助を行い、リソースの制限を軽減します。

💼 多国籍ビジネスにおけるミーティング効率化: 多国間のリモートビデオ面談を一斉にローカル言語へと書き出し、スムーズな多国間商談の要約や確認を可能にします。

まとめ

Twelve Labsを使用した、マルチリンガル動画自動文字起こしアプリケーションの開発と仕組みについてのチュートリアルに最後までお付き合いいただき、ありがとうございました。動画を高度に理解して処理する機能と、今回のアプリ構成フローを組み合わせることで、皆様の新しいアプリ開発の発想に繋がれば幸いです。機能追加や開発上の課題に対する皆様からの積極的なご意見をお待ちしています。

その他のドキュメント関連リソース

動画理解とテキスト生成処理に使用している各エンジンの仕様詳細は、こちらの資料をご覧ください:Marengo 2.6 (エンベディングエンジン) および Pegasus 1.1 (ジェネレーティブエンジン)。また、Twelve Labsを活用するためのリソースや動画データ解析の学習のために、以下の便利なコンテンツをご覧ください:

  • Discord コミュニティ: 開発仲間が多く籍を置く公式Discordにぜひ登録してください。不明点に関するQ&Aや開発アイデアの進捗共有などが活発に行われています。

  • サンプルアプリケーション群: すぐに試して理解を深められるアイデア別の導入デモなどを、次の開発のきっかけにお役立てください。

  • チュートリアル閲覧: Twelve Labsが有する多様なマルチメディア理解機能をさらに深く引き出すための方法を、詳細な手順書とともに追求できます。

ぜひこれらの公式リソースをフルに活用いただき、Twelve Labsの高度な映像理解技術によるまったく新しい革新的ビデオアプリケーションの創出に挑戦してください。

異なる言語の動画コンテンツを理解するのに苦労していませんか?あるいは、世界中のオーディエンスに向けてコンテンツをアクセシブルにすることに難しさを感じていませんか? 🌍

このチュートリアルでは、「マルチリンガル動画文字起こしアプリケーション(MultiLingual Video Transcriber Application)」を紹介し、その開発プロセスについて解説します。このアプリケーションは、Twelve LabsのAIモデルを活用して動画を理解し、複数の言語でシームレスな文字起こしを提供します。

このプログラムのユニークな点は、ユーザーが選択した習得レベル(初級、中級、上級)に合わせて文字起こしの内容を調整できることです。ユーザーは選択したレベルに最適化された文字起こしや翻訳テキストを受け取ることができます。さらに、このアプリケーションは正確なタイムスタンプを提供するため、ユーザーは発話された言葉と文字起こしテキストの同期を追うことができます。この機能により、コンテンツのナビゲーションや理解が容易になります。このアプリケーションの仕組みと、TwelveLabs Python SDKを使用して同様のソリューションを構築する方法について探っていきましょう。

アプリケーションのデモは、こちらから体験できます:動画マルチリンガル文字起こしデモ(Video Multilingual Transcriber)

コードにアクセスしてアプリを直接試してみたい場合は、こちらのReplit テンプレートをご利用ください。

前提条件

  • Twelve Labs Playgroundにサインアップして、APIキーを生成します。

  • このアプリケーションのリポジトリは Video Multilingual Transcriber で確認できます。

  • Flask、HTML、CSS、JavaScriptに関する基本的な知識があることを前提としています。

アプリケーションの仕組み

このセクションでは、マルチリンガル動画文字起こしアプリケーションを開発するための処理の流れを説明します。このプロセスは、動画を取得し、ユーザーの好みに応じて様々な言語と習得レベルの文字起こしを生成します。単なる単純な文字起こしにとどまらず、より包括的なソリューションを提供します。

システム構成は、フロントエンド層、バックエンド層、ストレージ層、そしてTwelveLabsサービスの4つの主要コンポーネントで構築されています。仕組みは次の通りです:ユーザーが(外国語の可能性がある)動画をアップロードし、希望する翻訳言語と習得レベル(初級、中級、上級)を選択します。

__wf_reserved_inherit

動画アップロード後に「送信(submit)」ボタンをクリックすると、インデックスID(Index ID)が生成され、今後の使用に備えてセッション状態で保存されます。次に、システムは動画をアップロードしてタスクID(Task ID)を作成し、インデックス処理が完了するとビデオID(Video ID)が取得されます。インデックス処理のエンベディングエンジンにはMarengo 2.6が使用され、文字起こしの生成には、Generate APIを介してPegasus 1.1エンジン(ジェネレーティブエンジン)が使用されます。

ユーザーの利便性を高めるため、アプリケーションは文字起こしとともにタイムスタンプを生成します。この機能により、文字起こしテキストと動画再生の間でインタラクティブな同期が可能になります。

準備手順

  1. Twelve Labs PlaygroundからAPIキーを取得し、環境変数を準備します。

  2. Githubからプロジェクトをクローンするか、Replit テンプレートを使用します。

  3. メインファイルと同じ階層に、APIキーを設定した.envファイルを作成します。

API_KEY=your_api_key_here

これらの手順が完了したら、いよいよアプリケーションの開発を開始しましょう!

VidScribe - 動画マルチリンガル文字起こしアプリのウォークスルー

このチュートリアルでは、最小限のフロントエンドを持つFlaskアプリケーションを構築します。ディレクトリ構造は以下の通りです:

.
├── app.py
├── requirements.txt
├── static
├── style.css
└── main.js
├── templates
└── index.html
└── uploads

Flaskアプリケーションの作成

準備手順が完了したので、Flaskアプリケーションを実装します。これにより、動画をアップロードしてユーザーの好みに応じた様々な言語の文字起こしを生成するためのシンプルな方法が構築できます。

仮想環境を準備して作成するために必要な依存関係は、こちらで確認できます:requirements.txt

Pythonの仮想環境を作成し、次のコマンドを実行してアプリケーションの環境をセットアップします:

pip install -r

1 - メインアプリケーションの構築

このセクションでは、重要なロジックと指示フローが含まれるメインアプリケーションのユーティリティ機能に焦点を当てます。メインアプリケーションをいくつかのセクションに分解して説明します:

  • インデックスの作成

  • 結果の生成

  • アップロード用コンポーネント

1.1 - インデックスの作成 

ここでは、Twelve Labs SDKを使用してインデックスを構成する方法について説明します。このFlaskアプリケーションは、ユーザーが動画をアップロードして様々な目的のために処理できるようにします。本番環境で確実に動作するように、安全なファイル名の処理システムとセッション管理を採用しています。

# 必要なモジュールのインポート

from flask import Flask, render_template, request, jsonify, send_from_directory, session
from werkzeug.utils import secure_filename
import os
import uuid
from twelvelabs import TwelveLabs
from twelvelabs.models.task import Task
from dotenv import load_dotenv

load_dotenv()

# 環境変数からTwelve LabsのAPIキーを読み込む
API_KEY = os.getenv("API_KEY")

app = Flask(__name__)
app.secret_key = os.urandom(24)  

UPLOAD_FOLDER = 'uploads'
ALLOWED_EXTENSIONS = {'mp4', 'avi', 'mov'}
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024


# Twelve Labs SDK クライアントの初期化
client = TwelveLabs(api_key=API_KEY)

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/')
def index():
    return render_template('index.html')

# ステータス確認用のユーティリティ関数
def on_task_update(task: Task):
    print(f"Status={task.status}")


def process_video(filepath, language, difficulty):
    try:
        if 'index_id' not in session:

	     # インデックス名の設定
            index_name = f"Translate{uuid.uuid4().hex[:8]}"

	     # エンジンの定義
            engines = [
                {
                    "name": "pegasus1.1",
                    "options": ["visual", "conversation"]
                },
                {
                    "name": "marengo2.6",
                    "options": ["visual", "conversation", "text_in_video", "logo"]
                }
            ]

	     # 設定を適用したインデックスの作成
            index = client.index.create(
                name=index_name,
                engines=engines
            )

            # インデックスIDをセッションに格納し、後の処理で再利用できるようにする
            session['index_id'] = index.id
            print(f"Created new index with ID: {index.id}")
        else:
            print(f"Using existing index with ID: {session['index_id']}")

	
	 # タスクの作成
        task = client.task.create(index_id=session['index_id'], file=filepath)
        task.wait_for_done(sleep_interval=5, callback=on_task_update)
        
        if task.status != "ready":
            raise RuntimeError(f"Indexing failed with status {task.status}")
        
        print(f"The unique identifier of your video is {task.video_id}.")

セッション状態を管理して、インデックスIDの作成を堅牢に処理します。UUIDによってインデックス名にランダムな番号が付加され、セッションごとに一意のインデックス名が作成されます。この新しいインデックスIDは、後で参照できるようにセッションに保存されます。

インデックス構成では、2つのエンジンを定義しています。動画のインデックス作成にはMarengo 2.6(エンベディングエンジン)を使用し、インデックスされた動画にアクセスして自由形式のプロンプトなどに基づいてコンテンツを生成するためにPegasus 1.1(ジェネレーティブエンジン)を使用します。

タスクを作成するには、インデックスIDと動画ファイルのパスを指定します。タスクの処理ステータスは `task.status` を使って追跡できます。インデックス作成が完了すると、次のステップで生成されたビデオIDが使用されます。

1.2  - 結果の生成

このセクションでは、ユーザーが選択した難易度レベルと希望する言語設定に基づいて、インデックス化された動画からテキストを生成する部分を扱います。私たちのプロンプトエンジニアリングシステムは、正確なタイムスタンプと翻訳を維持しながら、ユーザーの理解レベルに合わせて動画文字起こしの複雑さと詳細度を調整します。

# 難易度レベルに応じたアップデートされたプロンプト
        difficulty_prompts = {
            "beginner": "Provide a simplified and easy-to-understand",
            "intermediate": "Provide a moderately detailed",
            "advanced": "Provide a comprehensive and detailed"
        }
        
	 # ユーザーフォームから取得した難易度レベルを格納
        base_prompt = difficulty_prompts.get(difficulty, difficulty_prompts["intermediate"])
	 # 生成のためのオープンエンドプロンプト
        prompt = f"Provide the Only Transcript in the Translated {language.capitalize()} Language, {base_prompt} level with the timestamp duration (in the format of ss : ss) of the Indexed Video Content."
        
        res = client.generate.text(video_id=task.video_id, prompt=prompt, temperature=0.25)
        print(res)
        return {
            'status': 'ready',
            'message': 'File processed successfully',
            'transcript': res.data,
            'video_path': f'/uploads/{os.path.basename(filepath)}'
        }
        
    except Exception as e:
        print(f"Error processing video: {str(e)}")
        return {'status': 'error', 'message': str(e)}

辞書(dictionary)を用いたアプローチで難易度レベルを適切なプロンプトテンプレートにマッピングし、デフォルトのフォールバックとして中級レベルを設定します。一貫性があり信頼性の高い出力生成を実現するために、システムのTemperature値を0.25と低く維持しています。レスポンスには、タイムスタンプ付きの処理済み文字起こしデータが含まれます。

1.3  - アップロード用コンポーネント

このセクションでは、Flaskアプリケーションで安全なファイルアップロードのエンドポイントを生成および管理し、ユーザーからのファイル送信を処理する方法について解説します。このコンポーネントは、ファイルのアップロードプロセスを管理し、ファイルタイプを検証し、言語や難易度の設定を処理します。さらに、ワークフロー全体の適切なエラーハンドリングを維持しながら、アップロードされた動画の安全な保存を保証します。

# ファイルアップロードを処理するルート - POSTリクエストのみを許可
@app.route('/upload', methods=['POST'])
def upload_file():

    # リクエストにファイルが含まれているか確認
    if 'file' not in request.files:
        return jsonify({'status': 'error', 'message': 'No file part'}), 400
    
    # リクエストからファイルを取得
    file = request.files['file']
    # フォームデータから抽出した言語設定、未指定の場合はデフォルトでドイツ語を設定
    language = request.form.get('language', 'german')
    difficulty = request.form.get('difficulty', 'intermediate')
    
    # ファイルが実際に選択されているか検証
    if file.filename == '':
        return jsonify({'status': 'error', 'message': 'No selected file'}), 400

    # 許可されたファイル拡張子であるか確認し、処理を進行
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(filepath)
        
	 # 設定された言語と難易度で動画処理を実行
        result = process_video(filepath, language, difficulty)

        # 処理結果をJSON応答として送信
        return jsonify(result)
    # 許可されていないファイルタイプの場合はエラーを返す
    return jsonify({'status': 'error', 'message': 'File type not allowed'}), 400

# アップロードされたファイルを提供する配信ルート
@app.route('/uploads/<filename>')
def uploaded_file(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)

動画ファイルと関連するメタデータを処理するためのアップロード先エンドポイントを提供しています。このアップロードルートでは、安全なファイル名処理としてWerkzeugの secure_filename ユーティリティを使用し、適切なHTTPステータスコードを返す堅牢なエラーチェックを実装しています。言語や難易度の設定が指定されない場合、システムはそれぞれドイツ語と中級(intermediate)に設定します。また、ファイルを配信するルートがあることで、指定のディレクトリを通じてアップロード動画への安全なアクセスが可能になり、アクセス制限とファイルシステムのセキュリティが保護されます。

2 - JavaScriptによるコンポーネント処理

このセクションでは、app.jsファイルに含まれる主要な処理ユーティリティについて解説します。このJavaScriptファイルは、ファイルのアップロード、フォーム送信、タイムスタンプ付きの文字起こしデータと動画の同期再生、エラーハンドリング、そしてその他の画面のステータス管理を処理します。main.jsを以下の2つのセクションに分解して説明します:

  • ファイルのアップロードと検証

  • 文字起こしデータのパース処理

2.1 - ファイルのアップロードと検証

このセクションでは、クライアントサイドでの包括的な検証および送信処理の構成について紹介します。アップロード処理中に動画ファイルを検証し、選択ファイルの削除に対応し、フォームを非同期通信で送信する仕組みが組み込まれています。

 // アップロードされたファイルの検証
     function validateFile(file) {
        const validTypes = ['video/mp4', 'video/avi', 'video/quicktime'];
        const maxSize = 100 * 1024 * 1024; // 100MB

        if (!validTypes.includes(file.type)) {
            updateStatus('有効な動画ファイル(MP4, AVI, もしくは MOV)を選択してください', 'error');
            return false;
        }

        if (file.size > maxSize) {
            updateStatus('ファイルサイズは100MB未満にしてください', 'error');
            return false;
        }

        return true;
    }

    // 選択されたファイルの削除処理を行うユーティリティ関数
    function handleFileRemove(e) {
        e.preventDefault();
        fileInput.value = '';
        selectedFile.classList.add('hidden');
        uploadPrompt.textContent = '動画を選ぶか、ここにドラッグしてください';
        updateStatus('', '');
    }

    // フォーム送信の処理
    async function handleFormSubmit(e) {
        e.preventDefault();
        const formData = new FormData(e.target);
        
        if (!fileInput.files || !fileInput.files[0]) {
            updateStatus('まず始めにファイルを選択してください', 'error');
            return;
        }

        const loadingOverlay = document.getElementById('loading-overlay');
        loadingOverlay.classList.remove('hidden');
        updateStatus('ファイルをアップロードして処理中...', 'loading');
        // 過去の結果表示を切り替える
        hideResult();

        try {
         // フォームとともにPOSTリクエストを作成してエンドポイントへ送信
            const response = await fetch('/upload', {
                method: 'POST',
                body: formData
            });

	     // JSONレスポンスのパース
            const data = await response.json();
            console.log('Server response:', data);

            // アップロードと処理に成功したか確認
            if (response.ok && data.status === 'ready') {
                // 状態表示を完了に切り替えて取得データを反映表示
                updateStatus('処理が完了しました!', 'success');
                showResult();
                displayTranscript(data.transcript);
                displayVideo(data.video_path);
            } else {
                // 処理に失敗した場合はエラーをスローする
                throw new Error(data.message || 'データ処理中にエラーが発生しました');
            }
        } catch (error) {
            // エラーを検知してログを出力し表示
            console.error('Error:', error);
            updateStatus(`Error: ${error.message}`, 'error');
        } finally {
            // 処理が完了したら成否に関わらずローディング画面を非表示にする
            loadingOverlay.classList.add('hidden');
        }
    }

validateFile 関数により正しい形式の動画フォーマットと許容されるサイズ以外の不正なファイルが除外され、handleFileRemove が呼び出されると選択されていたファイルの削除と共にステータス表示がリセットされます。また、handleFormSubmit が非同期処理としての送信を仲介し、適切なエラー制御やロード時の進行状況を処理します。

この処理には、状況に合わせた表示を画面上に反映するために、ローディングオーバーレイの有効化・完了通知メッセージの切り替え・エラー発生時の通知表示といった細やかなステータス制御が含まれています。アップロードの各種段階で、常に一貫した使いやすさを提供します。

2.2 - オープンエンドプロンプトを処理する文字起こしパースの処理

このセクションでは、頑健な文字起こしパーサー(解析処理)のJavascript記述に焦点を当てます。このプログラムは、生成AIの自由なテキスト回答において検出される、様々な形式のタイムスタンプ情報や構文構成を網羅し、各構成の時間ベース位置に整合させてテキストを適切に配列します。

この処理により原型のテキストを構造化配列データに編集することで動画プレーヤーと文字起こしテキストのタイムラグの同期表現を行い、文字起こしの際に想定される例外部分や形式のブレに対する汎用的な適応を備えます。

 // Parses a transcript string into structured data with timestamps and text
    function parseTranscript(transcript) {
        console.log('Raw transcript:', transcript);
        // Initialize Map to store unique entries (prevents duplicates)
        const entries = new Map();
        
        if (!transcript) {
            console.error('Empty transcript received');
            return [];
        }
    
        // Extract data from JSON response and handle escapes
        let transcriptText = transcript;
        try {
            if (typeof transcript === 'string' && (transcript.includes('"id":') || transcript.includes("'id':"))) {
                const dataMatch = transcript.match(/['"]data['"]\s*:\s*['"]([^]+?)['"]\s*$/);
                if (dataMatch && dataMatch[1]) {
                    transcriptText = dataMatch[1]
                        .replace(/\\n/g, '\n')
                        .replace(/\\'/g, "'")
                        .replace(/\\"/g, '"')
                        .replace(/\\\\/g, '\\');
                }
            }
        } catch (e) {
            console.error('Error parsing JSON response:', e);
        }
    
        // Different timestamp patterns
        const patterns = [
            // HH:MM - HH:MM : "text"
            /(\d{2}):(\d{2})\s*-\s*(\d{2}):(\d{2})\s*:\s*["']([^"']+)["']/g,
            // HH:MM - HH:MM: text
            /(\d{2}):(\d{2})\s*-\s*(\d{2}):(\d{2})\s*:\s*([^"\n]+)/g,
            // Simple format with quotes
            /(\d{2}):(\d{2})\s*-\s*(\d{2}):(\d{2})\s*["']([^"']+)["']/g
        ];
    
        // Try each pattern against the transcript text for parsing
        for (const pattern of patterns) {
            let match;
            while ((match = pattern.exec(transcriptText)) !== null) {
                try {
                    const [_, startMin, startSec, endMin, endSec, text] = match;
                    // Convert timestamps to seconds
                    const startTime = parseInt(startMin) * 60 + parseInt(startSec);
                    const endTime = parseInt(endMin) * 60 + parseInt(endSec);
    
                    // Skip invalid timestamps
                    if (isNaN(startTime) || isNaN(endTime)) continue;
                    // Clean up theo transcript text, if appears
                    const cleanText = text
                        .replace(/^["'\s]+|["'\s]+$/g, '')
                        .replace(/\\n/g, ' ')
                        .replace(/\*\*/g, '')
                        .replace(/\\'/g, "'")
                        .replace(/\\"/g, '"')
                        .replace(/\\\\/g, '\\')
                        .replace(/\s+/g, ' ')
                        .trim();
    
                    if (cleanText && !cleanText.includes('Note:')) {
                        const key = `${startTime}-${cleanText.substring(0, 50)}`;
                        entries.set(key, {
                            start: startTime,
                            end: endTime,
                            text: cleanText
                        });
                    }
                } catch (e) {
                    console.error('Error processing match:', e);
                    continue;
                }
            }
        }
    
        // Convert Map to Array and sort by start time
        const sortedEntries = Array.from(entries.values())
            .sort((a, b) => a.start - b.start);
        // Log processed result
        console.log('Parsed entries:', sortedEntries);
        return sortedEntries;
    }

パースを機能させるために、各種パターンに対応する正規表現を使用し、時間指定を秒表記へと変換し、タイム表示に伴う不要ノイズを取り除くよう文字列処理を行います。Map型オブジェクトを用いて重複登録を抑止しつつ時系列を適正順に構成します。また、任意のタイミングで稼働追跡ができるようにデバッグを意識した実用的なログ記述が散りばめられています。

この最終生成物を配列としてまとめることで、動画再生プレーヤーや字幕描写処理等にそのまま適用でき、正確なタイミングで画面文字起こしを表示させることができます。

上で説明されたJavaScriptファイルのコード全体は、こちらのapp.jsファイルからご確認いただけます。

デモアプリケーションの実行

初めに、対象とする翻訳変換先の言語および希望する難易度習得レベルを画面から選択し、動画をアップロードします。アップロードが完了すると、自動的にインデックス化処理が開始されます。

__wf_reserved_inherit

インデックス作成と準備タスクが完了したら、動画IDを介してユーザーの選択設定に合わせて文字起こし文を生成します。下図は、Twelve Labs SDKを用いた生成結果を表示するデモ画面です。

__wf_reserved_inherit

Twelve Labsをさらに深く試してみたい場合は、教育用途、コンテンツ配信制作、その他の分野で動画認識を活用した独自ソリューションのアイデア生成などを進めてみてください。

このチュートリアルを応用するさらなるアイデア

マルチリンガル文字起こしアプリの基本構成を理解することで、多種多様なアイデアの着想やビジネス実用に向けた発展、新たな動画処理機能の開発などが可能になります。以下に、ユーザーの動画利用体験を豊かにする実用的な活用案を紹介します:

🌍 グローバルコンテンツクリエイター: 瞬時に多言語翻訳をまとめて自動文字起こしとして生成し、即時動画の現地ローカライズを進めオーディエンス層をグローバルに広げます。

🎓 国際遠隔教育・レッスンプログラム: ウェブ講義等の動画から自動で他国言語の字幕を作成し、生徒の語学学習レベル(初〜上級)に合わせて理解補助を行い、リソースの制限を軽減します。

💼 多国籍ビジネスにおけるミーティング効率化: 多国間のリモートビデオ面談を一斉にローカル言語へと書き出し、スムーズな多国間商談の要約や確認を可能にします。

まとめ

Twelve Labsを使用した、マルチリンガル動画自動文字起こしアプリケーションの開発と仕組みについてのチュートリアルに最後までお付き合いいただき、ありがとうございました。動画を高度に理解して処理する機能と、今回のアプリ構成フローを組み合わせることで、皆様の新しいアプリ開発の発想に繋がれば幸いです。機能追加や開発上の課題に対する皆様からの積極的なご意見をお待ちしています。

その他のドキュメント関連リソース

動画理解とテキスト生成処理に使用している各エンジンの仕様詳細は、こちらの資料をご覧ください:Marengo 2.6 (エンベディングエンジン) および Pegasus 1.1 (ジェネレーティブエンジン)。また、Twelve Labsを活用するためのリソースや動画データ解析の学習のために、以下の便利なコンテンツをご覧ください:

  • Discord コミュニティ: 開発仲間が多く籍を置く公式Discordにぜひ登録してください。不明点に関するQ&Aや開発アイデアの進捗共有などが活発に行われています。

  • サンプルアプリケーション群: すぐに試して理解を深められるアイデア別の導入デモなどを、次の開発のきっかけにお役立てください。

  • チュートリアル閲覧: Twelve Labsが有する多様なマルチメディア理解機能をさらに深く引き出すための方法を、詳細な手順書とともに追求できます。

ぜひこれらの公式リソースをフルに活用いただき、Twelve Labsの高度な映像理解技術によるまったく新しい革新的ビデオアプリケーションの創出に挑戦してください。

異なる言語の動画コンテンツを理解するのに苦労していませんか?あるいは、世界中のオーディエンスに向けてコンテンツをアクセシブルにすることに難しさを感じていませんか? 🌍

このチュートリアルでは、「マルチリンガル動画文字起こしアプリケーション(MultiLingual Video Transcriber Application)」を紹介し、その開発プロセスについて解説します。このアプリケーションは、Twelve LabsのAIモデルを活用して動画を理解し、複数の言語でシームレスな文字起こしを提供します。

このプログラムのユニークな点は、ユーザーが選択した習得レベル(初級、中級、上級)に合わせて文字起こしの内容を調整できることです。ユーザーは選択したレベルに最適化された文字起こしや翻訳テキストを受け取ることができます。さらに、このアプリケーションは正確なタイムスタンプを提供するため、ユーザーは発話された言葉と文字起こしテキストの同期を追うことができます。この機能により、コンテンツのナビゲーションや理解が容易になります。このアプリケーションの仕組みと、TwelveLabs Python SDKを使用して同様のソリューションを構築する方法について探っていきましょう。

アプリケーションのデモは、こちらから体験できます:動画マルチリンガル文字起こしデモ(Video Multilingual Transcriber)

コードにアクセスしてアプリを直接試してみたい場合は、こちらのReplit テンプレートをご利用ください。

前提条件

  • Twelve Labs Playgroundにサインアップして、APIキーを生成します。

  • このアプリケーションのリポジトリは Video Multilingual Transcriber で確認できます。

  • Flask、HTML、CSS、JavaScriptに関する基本的な知識があることを前提としています。

アプリケーションの仕組み

このセクションでは、マルチリンガル動画文字起こしアプリケーションを開発するための処理の流れを説明します。このプロセスは、動画を取得し、ユーザーの好みに応じて様々な言語と習得レベルの文字起こしを生成します。単なる単純な文字起こしにとどまらず、より包括的なソリューションを提供します。

システム構成は、フロントエンド層、バックエンド層、ストレージ層、そしてTwelveLabsサービスの4つの主要コンポーネントで構築されています。仕組みは次の通りです:ユーザーが(外国語の可能性がある)動画をアップロードし、希望する翻訳言語と習得レベル(初級、中級、上級)を選択します。

__wf_reserved_inherit

動画アップロード後に「送信(submit)」ボタンをクリックすると、インデックスID(Index ID)が生成され、今後の使用に備えてセッション状態で保存されます。次に、システムは動画をアップロードしてタスクID(Task ID)を作成し、インデックス処理が完了するとビデオID(Video ID)が取得されます。インデックス処理のエンベディングエンジンにはMarengo 2.6が使用され、文字起こしの生成には、Generate APIを介してPegasus 1.1エンジン(ジェネレーティブエンジン)が使用されます。

ユーザーの利便性を高めるため、アプリケーションは文字起こしとともにタイムスタンプを生成します。この機能により、文字起こしテキストと動画再生の間でインタラクティブな同期が可能になります。

準備手順

  1. Twelve Labs PlaygroundからAPIキーを取得し、環境変数を準備します。

  2. Githubからプロジェクトをクローンするか、Replit テンプレートを使用します。

  3. メインファイルと同じ階層に、APIキーを設定した.envファイルを作成します。

API_KEY=your_api_key_here

これらの手順が完了したら、いよいよアプリケーションの開発を開始しましょう!

VidScribe - 動画マルチリンガル文字起こしアプリのウォークスルー

このチュートリアルでは、最小限のフロントエンドを持つFlaskアプリケーションを構築します。ディレクトリ構造は以下の通りです:

.
├── app.py
├── requirements.txt
├── static
├── style.css
└── main.js
├── templates
└── index.html
└── uploads

Flaskアプリケーションの作成

準備手順が完了したので、Flaskアプリケーションを実装します。これにより、動画をアップロードしてユーザーの好みに応じた様々な言語の文字起こしを生成するためのシンプルな方法が構築できます。

仮想環境を準備して作成するために必要な依存関係は、こちらで確認できます:requirements.txt

Pythonの仮想環境を作成し、次のコマンドを実行してアプリケーションの環境をセットアップします:

pip install -r

1 - メインアプリケーションの構築

このセクションでは、重要なロジックと指示フローが含まれるメインアプリケーションのユーティリティ機能に焦点を当てます。メインアプリケーションをいくつかのセクションに分解して説明します:

  • インデックスの作成

  • 結果の生成

  • アップロード用コンポーネント

1.1 - インデックスの作成 

ここでは、Twelve Labs SDKを使用してインデックスを構成する方法について説明します。このFlaskアプリケーションは、ユーザーが動画をアップロードして様々な目的のために処理できるようにします。本番環境で確実に動作するように、安全なファイル名の処理システムとセッション管理を採用しています。

# 必要なモジュールのインポート

from flask import Flask, render_template, request, jsonify, send_from_directory, session
from werkzeug.utils import secure_filename
import os
import uuid
from twelvelabs import TwelveLabs
from twelvelabs.models.task import Task
from dotenv import load_dotenv

load_dotenv()

# 環境変数からTwelve LabsのAPIキーを読み込む
API_KEY = os.getenv("API_KEY")

app = Flask(__name__)
app.secret_key = os.urandom(24)  

UPLOAD_FOLDER = 'uploads'
ALLOWED_EXTENSIONS = {'mp4', 'avi', 'mov'}
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024


# Twelve Labs SDK クライアントの初期化
client = TwelveLabs(api_key=API_KEY)

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/')
def index():
    return render_template('index.html')

# ステータス確認用のユーティリティ関数
def on_task_update(task: Task):
    print(f"Status={task.status}")


def process_video(filepath, language, difficulty):
    try:
        if 'index_id' not in session:

	     # インデックス名の設定
            index_name = f"Translate{uuid.uuid4().hex[:8]}"

	     # エンジンの定義
            engines = [
                {
                    "name": "pegasus1.1",
                    "options": ["visual", "conversation"]
                },
                {
                    "name": "marengo2.6",
                    "options": ["visual", "conversation", "text_in_video", "logo"]
                }
            ]

	     # 設定を適用したインデックスの作成
            index = client.index.create(
                name=index_name,
                engines=engines
            )

            # インデックスIDをセッションに格納し、後の処理で再利用できるようにする
            session['index_id'] = index.id
            print(f"Created new index with ID: {index.id}")
        else:
            print(f"Using existing index with ID: {session['index_id']}")

	
	 # タスクの作成
        task = client.task.create(index_id=session['index_id'], file=filepath)
        task.wait_for_done(sleep_interval=5, callback=on_task_update)
        
        if task.status != "ready":
            raise RuntimeError(f"Indexing failed with status {task.status}")
        
        print(f"The unique identifier of your video is {task.video_id}.")

セッション状態を管理して、インデックスIDの作成を堅牢に処理します。UUIDによってインデックス名にランダムな番号が付加され、セッションごとに一意のインデックス名が作成されます。この新しいインデックスIDは、後で参照できるようにセッションに保存されます。

インデックス構成では、2つのエンジンを定義しています。動画のインデックス作成にはMarengo 2.6(エンベディングエンジン)を使用し、インデックスされた動画にアクセスして自由形式のプロンプトなどに基づいてコンテンツを生成するためにPegasus 1.1(ジェネレーティブエンジン)を使用します。

タスクを作成するには、インデックスIDと動画ファイルのパスを指定します。タスクの処理ステータスは `task.status` を使って追跡できます。インデックス作成が完了すると、次のステップで生成されたビデオIDが使用されます。

1.2  - 結果の生成

このセクションでは、ユーザーが選択した難易度レベルと希望する言語設定に基づいて、インデックス化された動画からテキストを生成する部分を扱います。私たちのプロンプトエンジニアリングシステムは、正確なタイムスタンプと翻訳を維持しながら、ユーザーの理解レベルに合わせて動画文字起こしの複雑さと詳細度を調整します。

# 難易度レベルに応じたアップデートされたプロンプト
        difficulty_prompts = {
            "beginner": "Provide a simplified and easy-to-understand",
            "intermediate": "Provide a moderately detailed",
            "advanced": "Provide a comprehensive and detailed"
        }
        
	 # ユーザーフォームから取得した難易度レベルを格納
        base_prompt = difficulty_prompts.get(difficulty, difficulty_prompts["intermediate"])
	 # 生成のためのオープンエンドプロンプト
        prompt = f"Provide the Only Transcript in the Translated {language.capitalize()} Language, {base_prompt} level with the timestamp duration (in the format of ss : ss) of the Indexed Video Content."
        
        res = client.generate.text(video_id=task.video_id, prompt=prompt, temperature=0.25)
        print(res)
        return {
            'status': 'ready',
            'message': 'File processed successfully',
            'transcript': res.data,
            'video_path': f'/uploads/{os.path.basename(filepath)}'
        }
        
    except Exception as e:
        print(f"Error processing video: {str(e)}")
        return {'status': 'error', 'message': str(e)}

辞書(dictionary)を用いたアプローチで難易度レベルを適切なプロンプトテンプレートにマッピングし、デフォルトのフォールバックとして中級レベルを設定します。一貫性があり信頼性の高い出力生成を実現するために、システムのTemperature値を0.25と低く維持しています。レスポンスには、タイムスタンプ付きの処理済み文字起こしデータが含まれます。

1.3  - アップロード用コンポーネント

このセクションでは、Flaskアプリケーションで安全なファイルアップロードのエンドポイントを生成および管理し、ユーザーからのファイル送信を処理する方法について解説します。このコンポーネントは、ファイルのアップロードプロセスを管理し、ファイルタイプを検証し、言語や難易度の設定を処理します。さらに、ワークフロー全体の適切なエラーハンドリングを維持しながら、アップロードされた動画の安全な保存を保証します。

# ファイルアップロードを処理するルート - POSTリクエストのみを許可
@app.route('/upload', methods=['POST'])
def upload_file():

    # リクエストにファイルが含まれているか確認
    if 'file' not in request.files:
        return jsonify({'status': 'error', 'message': 'No file part'}), 400
    
    # リクエストからファイルを取得
    file = request.files['file']
    # フォームデータから抽出した言語設定、未指定の場合はデフォルトでドイツ語を設定
    language = request.form.get('language', 'german')
    difficulty = request.form.get('difficulty', 'intermediate')
    
    # ファイルが実際に選択されているか検証
    if file.filename == '':
        return jsonify({'status': 'error', 'message': 'No selected file'}), 400

    # 許可されたファイル拡張子であるか確認し、処理を進行
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(filepath)
        
	 # 設定された言語と難易度で動画処理を実行
        result = process_video(filepath, language, difficulty)

        # 処理結果をJSON応答として送信
        return jsonify(result)
    # 許可されていないファイルタイプの場合はエラーを返す
    return jsonify({'status': 'error', 'message': 'File type not allowed'}), 400

# アップロードされたファイルを提供する配信ルート
@app.route('/uploads/<filename>')
def uploaded_file(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)

動画ファイルと関連するメタデータを処理するためのアップロード先エンドポイントを提供しています。このアップロードルートでは、安全なファイル名処理としてWerkzeugの secure_filename ユーティリティを使用し、適切なHTTPステータスコードを返す堅牢なエラーチェックを実装しています。言語や難易度の設定が指定されない場合、システムはそれぞれドイツ語と中級(intermediate)に設定します。また、ファイルを配信するルートがあることで、指定のディレクトリを通じてアップロード動画への安全なアクセスが可能になり、アクセス制限とファイルシステムのセキュリティが保護されます。

2 - JavaScriptによるコンポーネント処理

このセクションでは、app.jsファイルに含まれる主要な処理ユーティリティについて解説します。このJavaScriptファイルは、ファイルのアップロード、フォーム送信、タイムスタンプ付きの文字起こしデータと動画の同期再生、エラーハンドリング、そしてその他の画面のステータス管理を処理します。main.jsを以下の2つのセクションに分解して説明します:

  • ファイルのアップロードと検証

  • 文字起こしデータのパース処理

2.1 - ファイルのアップロードと検証

このセクションでは、クライアントサイドでの包括的な検証および送信処理の構成について紹介します。アップロード処理中に動画ファイルを検証し、選択ファイルの削除に対応し、フォームを非同期通信で送信する仕組みが組み込まれています。

 // アップロードされたファイルの検証
     function validateFile(file) {
        const validTypes = ['video/mp4', 'video/avi', 'video/quicktime'];
        const maxSize = 100 * 1024 * 1024; // 100MB

        if (!validTypes.includes(file.type)) {
            updateStatus('有効な動画ファイル(MP4, AVI, もしくは MOV)を選択してください', 'error');
            return false;
        }

        if (file.size > maxSize) {
            updateStatus('ファイルサイズは100MB未満にしてください', 'error');
            return false;
        }

        return true;
    }

    // 選択されたファイルの削除処理を行うユーティリティ関数
    function handleFileRemove(e) {
        e.preventDefault();
        fileInput.value = '';
        selectedFile.classList.add('hidden');
        uploadPrompt.textContent = '動画を選ぶか、ここにドラッグしてください';
        updateStatus('', '');
    }

    // フォーム送信の処理
    async function handleFormSubmit(e) {
        e.preventDefault();
        const formData = new FormData(e.target);
        
        if (!fileInput.files || !fileInput.files[0]) {
            updateStatus('まず始めにファイルを選択してください', 'error');
            return;
        }

        const loadingOverlay = document.getElementById('loading-overlay');
        loadingOverlay.classList.remove('hidden');
        updateStatus('ファイルをアップロードして処理中...', 'loading');
        // 過去の結果表示を切り替える
        hideResult();

        try {
         // フォームとともにPOSTリクエストを作成してエンドポイントへ送信
            const response = await fetch('/upload', {
                method: 'POST',
                body: formData
            });

	     // JSONレスポンスのパース
            const data = await response.json();
            console.log('Server response:', data);

            // アップロードと処理に成功したか確認
            if (response.ok && data.status === 'ready') {
                // 状態表示を完了に切り替えて取得データを反映表示
                updateStatus('処理が完了しました!', 'success');
                showResult();
                displayTranscript(data.transcript);
                displayVideo(data.video_path);
            } else {
                // 処理に失敗した場合はエラーをスローする
                throw new Error(data.message || 'データ処理中にエラーが発生しました');
            }
        } catch (error) {
            // エラーを検知してログを出力し表示
            console.error('Error:', error);
            updateStatus(`Error: ${error.message}`, 'error');
        } finally {
            // 処理が完了したら成否に関わらずローディング画面を非表示にする
            loadingOverlay.classList.add('hidden');
        }
    }

validateFile 関数により正しい形式の動画フォーマットと許容されるサイズ以外の不正なファイルが除外され、handleFileRemove が呼び出されると選択されていたファイルの削除と共にステータス表示がリセットされます。また、handleFormSubmit が非同期処理としての送信を仲介し、適切なエラー制御やロード時の進行状況を処理します。

この処理には、状況に合わせた表示を画面上に反映するために、ローディングオーバーレイの有効化・完了通知メッセージの切り替え・エラー発生時の通知表示といった細やかなステータス制御が含まれています。アップロードの各種段階で、常に一貫した使いやすさを提供します。

2.2 - オープンエンドプロンプトを処理する文字起こしパースの処理

このセクションでは、頑健な文字起こしパーサー(解析処理)のJavascript記述に焦点を当てます。このプログラムは、生成AIの自由なテキスト回答において検出される、様々な形式のタイムスタンプ情報や構文構成を網羅し、各構成の時間ベース位置に整合させてテキストを適切に配列します。

この処理により原型のテキストを構造化配列データに編集することで動画プレーヤーと文字起こしテキストのタイムラグの同期表現を行い、文字起こしの際に想定される例外部分や形式のブレに対する汎用的な適応を備えます。

 // Parses a transcript string into structured data with timestamps and text
    function parseTranscript(transcript) {
        console.log('Raw transcript:', transcript);
        // Initialize Map to store unique entries (prevents duplicates)
        const entries = new Map();
        
        if (!transcript) {
            console.error('Empty transcript received');
            return [];
        }
    
        // Extract data from JSON response and handle escapes
        let transcriptText = transcript;
        try {
            if (typeof transcript === 'string' && (transcript.includes('"id":') || transcript.includes("'id':"))) {
                const dataMatch = transcript.match(/['"]data['"]\s*:\s*['"]([^]+?)['"]\s*$/);
                if (dataMatch && dataMatch[1]) {
                    transcriptText = dataMatch[1]
                        .replace(/\\n/g, '\n')
                        .replace(/\\'/g, "'")
                        .replace(/\\"/g, '"')
                        .replace(/\\\\/g, '\\');
                }
            }
        } catch (e) {
            console.error('Error parsing JSON response:', e);
        }
    
        // Different timestamp patterns
        const patterns = [
            // HH:MM - HH:MM : "text"
            /(\d{2}):(\d{2})\s*-\s*(\d{2}):(\d{2})\s*:\s*["']([^"']+)["']/g,
            // HH:MM - HH:MM: text
            /(\d{2}):(\d{2})\s*-\s*(\d{2}):(\d{2})\s*:\s*([^"\n]+)/g,
            // Simple format with quotes
            /(\d{2}):(\d{2})\s*-\s*(\d{2}):(\d{2})\s*["']([^"']+)["']/g
        ];
    
        // Try each pattern against the transcript text for parsing
        for (const pattern of patterns) {
            let match;
            while ((match = pattern.exec(transcriptText)) !== null) {
                try {
                    const [_, startMin, startSec, endMin, endSec, text] = match;
                    // Convert timestamps to seconds
                    const startTime = parseInt(startMin) * 60 + parseInt(startSec);
                    const endTime = parseInt(endMin) * 60 + parseInt(endSec);
    
                    // Skip invalid timestamps
                    if (isNaN(startTime) || isNaN(endTime)) continue;
                    // Clean up theo transcript text, if appears
                    const cleanText = text
                        .replace(/^["'\s]+|["'\s]+$/g, '')
                        .replace(/\\n/g, ' ')
                        .replace(/\*\*/g, '')
                        .replace(/\\'/g, "'")
                        .replace(/\\"/g, '"')
                        .replace(/\\\\/g, '\\')
                        .replace(/\s+/g, ' ')
                        .trim();
    
                    if (cleanText && !cleanText.includes('Note:')) {
                        const key = `${startTime}-${cleanText.substring(0, 50)}`;
                        entries.set(key, {
                            start: startTime,
                            end: endTime,
                            text: cleanText
                        });
                    }
                } catch (e) {
                    console.error('Error processing match:', e);
                    continue;
                }
            }
        }
    
        // Convert Map to Array and sort by start time
        const sortedEntries = Array.from(entries.values())
            .sort((a, b) => a.start - b.start);
        // Log processed result
        console.log('Parsed entries:', sortedEntries);
        return sortedEntries;
    }

パースを機能させるために、各種パターンに対応する正規表現を使用し、時間指定を秒表記へと変換し、タイム表示に伴う不要ノイズを取り除くよう文字列処理を行います。Map型オブジェクトを用いて重複登録を抑止しつつ時系列を適正順に構成します。また、任意のタイミングで稼働追跡ができるようにデバッグを意識した実用的なログ記述が散りばめられています。

この最終生成物を配列としてまとめることで、動画再生プレーヤーや字幕描写処理等にそのまま適用でき、正確なタイミングで画面文字起こしを表示させることができます。

上で説明されたJavaScriptファイルのコード全体は、こちらのapp.jsファイルからご確認いただけます。

デモアプリケーションの実行

初めに、対象とする翻訳変換先の言語および希望する難易度習得レベルを画面から選択し、動画をアップロードします。アップロードが完了すると、自動的にインデックス化処理が開始されます。

__wf_reserved_inherit

インデックス作成と準備タスクが完了したら、動画IDを介してユーザーの選択設定に合わせて文字起こし文を生成します。下図は、Twelve Labs SDKを用いた生成結果を表示するデモ画面です。

__wf_reserved_inherit

Twelve Labsをさらに深く試してみたい場合は、教育用途、コンテンツ配信制作、その他の分野で動画認識を活用した独自ソリューションのアイデア生成などを進めてみてください。

このチュートリアルを応用するさらなるアイデア

マルチリンガル文字起こしアプリの基本構成を理解することで、多種多様なアイデアの着想やビジネス実用に向けた発展、新たな動画処理機能の開発などが可能になります。以下に、ユーザーの動画利用体験を豊かにする実用的な活用案を紹介します:

🌍 グローバルコンテンツクリエイター: 瞬時に多言語翻訳をまとめて自動文字起こしとして生成し、即時動画の現地ローカライズを進めオーディエンス層をグローバルに広げます。

🎓 国際遠隔教育・レッスンプログラム: ウェブ講義等の動画から自動で他国言語の字幕を作成し、生徒の語学学習レベル(初〜上級)に合わせて理解補助を行い、リソースの制限を軽減します。

💼 多国籍ビジネスにおけるミーティング効率化: 多国間のリモートビデオ面談を一斉にローカル言語へと書き出し、スムーズな多国間商談の要約や確認を可能にします。

まとめ

Twelve Labsを使用した、マルチリンガル動画自動文字起こしアプリケーションの開発と仕組みについてのチュートリアルに最後までお付き合いいただき、ありがとうございました。動画を高度に理解して処理する機能と、今回のアプリ構成フローを組み合わせることで、皆様の新しいアプリ開発の発想に繋がれば幸いです。機能追加や開発上の課題に対する皆様からの積極的なご意見をお待ちしています。

その他のドキュメント関連リソース

動画理解とテキスト生成処理に使用している各エンジンの仕様詳細は、こちらの資料をご覧ください:Marengo 2.6 (エンベディングエンジン) および Pegasus 1.1 (ジェネレーティブエンジン)。また、Twelve Labsを活用するためのリソースや動画データ解析の学習のために、以下の便利なコンテンツをご覧ください:

  • Discord コミュニティ: 開発仲間が多く籍を置く公式Discordにぜひ登録してください。不明点に関するQ&Aや開発アイデアの進捗共有などが活発に行われています。

  • サンプルアプリケーション群: すぐに試して理解を深められるアイデア別の導入デモなどを、次の開発のきっかけにお役立てください。

  • チュートリアル閲覧: Twelve Labsが有する多様なマルチメディア理解機能をさらに深く引き出すための方法を、詳細な手順書とともに追求できます。

ぜひこれらの公式リソースをフルに活用いただき、Twelve Labsの高度な映像理解技術によるまったく新しい革新的ビデオアプリケーションの創出に挑戦してください。