こんにちは、たびとです。
前回、Microsoft Azure AI サービスの Video Analyzer (旧 : Video Indexer) の概要を説明しました。 Web 画面から簡単に学習済みの AI サービスを利用できるため、映像・音声の AI 解析としては敷居が低いと思います。 今回は、Video Analyzer を C# を使ってアプリ化する 方法を紹介します。
今回の内容は、Video Analyzer の基礎知識が前提となります。 初めての方は、前回の記事を参考にしてください。
この記事の対象者
事前準備
今回のサンプルは、Visual Studio 2022 のコンソールアプリ (.NET 6) で作成しています。 また、 Newtonsoft.Json と RestSharp を使うため、 NuGet パッケージマネージャーからインストールしてください。
Newtonsoft.Json (Json.Net)
AI サービスの結果は、JSON データになるため、JSON ツールは必須となります。 Microsoft の JSON も使えるようになってきましたが、 まだ性能的にはこちらの JSON の方がよさそうです。
RestSharp
Microsoft AI サービスを REST API から利用するための便利ツールです。 そのまんまの名称ですね。
昔、C# ライブラリも試してみましたが、 当時は REST API の方が新しかったという過去があります。
今回実装する API
今回のアプリに実装したのは、5 つの基本的な API です。 ここでは「ビデオ=動画ファイル」とします。
API | 説明 |
---|---|
Get Access Token | Video Analyzer を利用するためのトークンを取得します。 |
Upload Video | ビデオをアップロードし、学習済みAIで解析します。 |
List Video | アップロードしたビデオの一覧をJSON形式で取得します。 |
Get Video Index | 学習済みAIで解析した情報をJSON形式で取得します。 |
Get Video Captions | 音声認識により音声をテキストに変換した文字列を取得します。 |
アップロード
最初は、ビデオ(動画ファイル)をアップロードする API からです。 この API を使うと Video Analyzer にビデオをアップロードした後、 学習済み AI による解析が始まります。
動作済みのソースコードを掲載します。 accountId と apiKey の値は、前回の記事を参考にしてください。
このアップロードは同じファイルを何回もアップロードしても、 別のビデオ ID が割り振られる ことに注意してください。
using Newtonsoft.Json; using RestSharp; using System.Net; //Video Analyzer定義 const string VideoIndexerUri = @"https://api.videoindexer.ai"; var location = "trial"; var accountId = "<アカウントID>"; var apiKey = "<Video Analyzer Subscriptions の Primary Key または Secondary Key>"; //アクセス準備 var client = new RestClient(baseUrl: VideoIndexerUri); // Video Indexer URL client.AddDefaultHeader("x-ms-client-request-id", string.Empty); client.AddDefaultHeader("Ocp-Apim-Subscription-Key", apiKey); var accessToken = GetToken(); if (accessToken == null) { Console.Error.WriteLine("ERROR:アクセストークンの取得に失敗しました。"); return; } Console.WriteLine(accessToken); var path = @"<適当なビデオファイル名>.mp4"; if (!Upload(path)) { Console.Error.WriteLine("ERROR:ビデオファイルのアップロードに失敗しました。"); return; } //VideoAnalyzerトークン取得 string? GetToken() { Console.WriteLine("Access-Token."); var request = new RestRequest($"Auth/{location}/Accounts/{accountId}/AccessToken", Method.Get); request.AddParameter("allowEdit", "true", ParameterType.GetOrPost); var response = client.ExecuteAsync(request).Result; if (HttpStatusCode.OK != response.StatusCode) { Console.Error.WriteLine($"ERROR: {response.StatusCode}={response.Content}"); return null; } var token = JsonConvert.DeserializeObject<string>(response.Content); return token; } //アップロード(AIに解析を依頼する) bool Upload(string path) { Console.WriteLine("Upload."); var name = "サンプルビデオ"; var privacy = "Private"; // Private|Public var language = "auto"; // auto|en-GB|en-US|ja-JP var indexingPreset = "Default"; // Default|AudioOnly|VideoOnly|DefaultWithNoiseReduction var streamingPreset = "Default"; // NoStreaming|Default|SingleBitrate|AdaptiveBitrate var request = new RestRequest($"{location}/Accounts/{accountId}/Videos", Method.Post); request.AlwaysMultipartFormData = true; request.AddHeader("Content-Type", "multipart/form-data"); request.AddParameter("name", name, ParameterType.QueryString); request.AddParameter("privacy", privacy, ParameterType.QueryString); request.AddParameter("language", language, ParameterType.QueryString); request.AddParameter("indexingPreset", indexingPreset, ParameterType.QueryString); request.AddParameter("streamingPreset", streamingPreset, ParameterType.QueryString); request.AddParameter("accessToken", accessToken, ParameterType.QueryString); request.AddFile("video_file", path, "application/octet-stream"); var response = client.ExecuteAsync(request).Result; if (HttpStatusCode.OK != response.StatusCode) { Console.Error.WriteLine($"ERROR: {response.StatusCode}={response.Content}"); return false; } Console.WriteLine("Uploaded."); return true; }
リクエストのパラメータは、標準のままでも特に問題がないと思いますが、 細かく指定したい方はマニュアルを参照してください。
ビデオをアップロードすると、Video Analyzer の Web 画面からも解析状態を確認することができます。
例えば、昔 Channel 9 (現在 Microsoft Learn の一部) からダウンロードしたクラウディアの ビデオ(動画ファイル)が手元にあったのでアップロードしてみました。 解析中の背景にクラウディアが薄っすらと表示されて、解析中のビデオを確認することができます。
解析データの取得
Video Analyzer の解析結果を取得するソースコードの全文です。
using Newtonsoft.Json; using Newtonsoft.Json.Linq; using RestSharp; using System.Net; //Video Analyzer定義 const string VideoIndexerUri = @"https://api.videoindexer.ai"; var location = "trial"; var accountId = "<アカウントID>"; var apiKey = "<Video Analyzer Subscriptions の Primary Key または Secondary Key>"; //アクセス準備 var client = new RestClient(baseUrl: VideoIndexerUri); // Video Indexer URL client.AddDefaultHeader("x-ms-client-request-id", string.Empty); client.AddDefaultHeader("Ocp-Apim-Subscription-Key", apiKey); var accessToken = GetToken(); if (accessToken == null) { Console.Error.WriteLine("ERROR:アクセストークンの取得に失敗しました。"); return; } Console.WriteLine(accessToken); var videosJson = ListVideos(); if (videosJson == null) { Console.Error.WriteLine("ERROR:ビデオ一覧の取得に失敗しました。"); return; } //ビデオ情報の取得 var obj = JObject.Parse(videosJson); var items = (JArray)obj["results"]; foreach( var item in items) { var videoId = (string)item["id"]; var videoTitle = (string)item["name"]; var state = (string)item["state"]; var progress = (string)item["processingProgress"]; var lang = (string)item["sourceLanguage"]; Console.WriteLine($"{state}({progress}), {videoId}:{videoTitle}"); if (state.Equals("Processing")) continue; //解析情報の出力 var index = VideoIndex(videoId, lang); if (index != null) { using var sw1 = new StreamWriter(@$"va_{videoId}.json"); sw1.WriteLine(index); } //文字起こし内容の出力 var caption = VideoCaptions(videoId, lang); if (caption != null) { using var sw2 = new StreamWriter($@"va_{videoId}.srt"); sw2.WriteLine(caption); } } //VideoAnalyzerトークン取得 string? GetToken() { Console.WriteLine("Access-Token."); var request = new RestRequest($"Auth/{location}/Accounts/{accountId}/AccessToken", Method.Get); request.AddParameter("allowEdit", "true", ParameterType.GetOrPost); var response = client.ExecuteAsync(request).Result; if (HttpStatusCode.OK != response.StatusCode) { Console.Error.WriteLine($"ERROR: {response.StatusCode}={response.Content}"); return null; } var token = JsonConvert.DeserializeObject<string>(response.Content); return token; } // ビデオ一覧取得 string? ListVideos() { Console.WriteLine("ListVideos."); var request = new RestRequest($"{location}/Accounts/{accountId}/Videos", Method.Get); request.AddParameter("accessToken", accessToken, ParameterType.GetOrPost); var response = client.ExecuteAsync(request).Result; if (HttpStatusCode.OK != response.StatusCode) { Console.Error.WriteLine($"ERROR: {response.StatusCode}={response.Content}"); return null; } return response.Content; } //解析情報の取得 string? VideoIndex(string id, string lang) { // リクエスト作成 var request = new RestRequest($"{location}/Accounts/{accountId}/Videos/{id}/Index", Method.Get); request.AddParameter("language", lang, ParameterType.GetOrPost); request.AddParameter("accessToken", accessToken, ParameterType.GetOrPost); var response = client.ExecuteAsync(request).Result; if (HttpStatusCode.OK != response.StatusCode) { Console.Error.WriteLine($"ERROR: {response.StatusCode}={response.Content}"); return null; } return response.Content; } //文字起こしデータ取得 string? VideoCaptions(string id, string lang) { // リクエスト作成 var request = new RestRequest($"{location}/Accounts/{accountId}/Videos/{id}/Captions", Method.Get); request.AddParameter("indexId", id, ParameterType.GetOrPost); request.AddParameter("format", "Srt", ParameterType.GetOrPost); //Vtt/Ttml/Srt/Txt/Csv request.AddParameter("language", lang, ParameterType.GetOrPost); request.AddParameter("accessToken", accessToken, ParameterType.GetOrPost); var response = client.ExecuteAsync(request).Result; if (HttpStatusCode.OK != response.StatusCode) { Console.Error.WriteLine($"ERROR: {response.StatusCode}={response.Content}"); return null; } return response.Content; }
List Video
まずは、 List Video でビデオ一覧を JSON 形式で取得します。 この JSON データの中にステータス(state)と進捗(processingProgress)が入っているので、 ステータスの値をみて解析が終了しているのかを判定します。
項目 | 説明 |
---|---|
state | 'Processed'(完了) か 'Processing'(解析中) |
processingProgress | '100%' で完了 |
解析が終了すると、 ビデオID(id) を使って解析情報を取得することができるようになります。 List Video API の結果を JSON配列として扱えるように変換する必要があります。 JObject.Parse() を使って平文の JSON テキストを JObject に変換し、"results" の項目を JArray 配列にキャストします。
必要な個所を抜粋したソースは以下の通り。
var videosJson = ListVideos(); var obj = JObject.Parse(videosJson); var items = (JArray)obj["results"]; foreach( var item in items) { var videoId = (string)item["id"]; var state = (string)item["state"]; var progress = (string)item["processingProgress"]; }
ビデオ ID が取得できれば、後は必要な API を呼び出して解析データを取得するだけです。
ただし、1回のリクエストで取得できる件数の上限 (25件だったか) があるため、 解析したビデオが増えた場合、ページ制御が必要になります。 これは、API のパラメータで設定することができます。
Get Video Index
Video Analyzer で解析した結果を JSON 形式で取得するのですが、 ビデオによってはサイズが大きくなるため、拡張子 .json でファイルに保存しています。
そのまま中身を見てもインデントされてないため、読めないと思います。 Visual Studio Code で整形、または Linux の jq コマンド等を使って、 見やすい形式に変換して解読してください。
Get Video Captions
音声認識したテキストを取得するのですが、 こちらもサイズが大きくなるため、拡張子 .srt でファイルに保存します。 ファイル形式は選べるので、必要に応じてパラメータを変更してください。
この srt ファイルをビデオファイルと同じ名前に変更し、 同じフォルダの下に置くと、字幕として表示させることができます。
まとめ
今回は、Video Analyzer を C# から利用するための簡単なソースコードを紹介しました。 本当は、 List Video で取得した JSON の内容や、 Get Video Index で取得した JSON の内容を もう少し説明しようと思いましたが、説明が長くなったので、またの機会にしようと思います。
Video Analyzer を使ってみて思うのですが、実運用で使うには精度が不足していますが、 汎用 AI なので、仕方ないと思います。 これを一時解析として、次の AI 解析のインプットに使う選択はありだと思います。
実際に作ったアプリは、クライアントを WPF アプリで作成し、 サーバとは gRPC でファイルや DB 情報を HTTP/2 で送受信します。 また、クライアント側はビデオを流しながら解析結果を表示したり、 別の AI サービスの結果を表示したりと、かなり手が込んでいます。 サーバ側は、Web(Nginx)・AP(Kestrel)・DB(PostgreSQL) の三層構造で、 Docker ファイルで構築できるようになっています。 この辺りの話題も、いつか紹介していこうと思います。
では、皆さん、よい旅を。