AmiVoiceでTRPG/マダミスのログの文字起こしをする

いろいろあって、文字起こしAPIをとっかえひっかえして試してます。

whisperとか、Google CloudのSpeech to textとか、いろいろあるんですが、ちょうど良さげなキャンペーンがありましたので、AmiVoiceを使ってみることにしました。

https://qiita.com/official-events/23333339b694ca71810a

## AmiVoiceって何だ

音声認識エンジンです。
汎用のモデルのほか、医療、介護、保険などに特化したモデルがあるようです。

個人的には、SDKがあってオフラインで利用可能なのがすごく気になります。

https://acp.amivoice.com/amivoice_api/


## 目標
今回はAmiVoiceを使用して、マーダーミステリーの動画ログの文字起こしをしてみましょう。
（とはいえ、この記事にログを載せているわけでもありませんし、また、マーダーミステリーのシナリオのネタバレはありませんので、ご安心ください。）

TRPGのボイスセッションのログおこしにも良いかも、と思ったのですが、AmiVoiceの無料時間が月６０分までなので、お金を節約しようと思うとマーダーミステリー向きですね。

マーダーミステリーも（シナリオに寄りますが）なんだかんだ長尺になりがちではありますが、議論時間のみ、最終議論のみとすればハイライトとして使えそうです。

超過した場合は１分１.５円、１時間９０円ほどなので、下手なことをしない限りは破産せずにはすみそうです。
（今回はお試しキャンペーン中なので、クーポンで月６００分まで使えます。）

## AmiVoiceの導入

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663038/d73105f3-525b-101e-ed02-a56cd02141ab.png)

公式サイトからユーザー情報を登録して、利用を開始します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663038/cfbf7b40-7369-4a10-30b8-0cc97dedba0c.png)

使用には**クレジットカードの登録が必要**なタイプのAPIです。

なお、クレジットカードの支払いについてはGMOペイメントです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663038/e7b15782-236e-b3dd-4dc8-fa3f2073c028.png)

ダッシュボードがシンプルで、かなり好きです。
日本語でわかりやすいサイト、それだけで、嬉しい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663038/e1b74a4c-72ca-6060-7942-86bf51534581.png)

あとは、退会手続きがメニュー欄の分かりやすいところにあるのはユーザーフレンドリーで好感が持てます。
いいと思うんだよな……文字を小さくするより……。

## 早速使ってみる

ドキュメントを見てみましょう。

https://docs.amivoice.com/amivoice-api/manual/#async-http-interface

三つのユースケースが紹介されています。

- 同期 HTTP インタフェース (短い音声ファイル)
- 非同期 HTTP インタフェース (長時間の音声ファイル)
- WebSocket インタフェース (ストリーミング)

今回は手元にある動画ファイルの音声をAPIに送り、結果を得るということをしたいので、非同期HTTPインタフェースがよさそうです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663038/253e09de-2b97-08c0-09e7-92c48480c07b.png)

チュートリアルの「長い音声ファイルの書き起こし」を参考にして、コードを実装していくことにしましょう。

https://docs.amivoice.com/amivoice-api/manual/tutorial/long-speech-transcription

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663038/6e0412ff-6316-c4e8-87ba-97a7c4addd1c.png)

エンジンは素直に会話_汎用（ログ保存あり）でいきましょうか。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663038/e683d02f-fb5e-9982-0aeb-c86369585c20.png)

Window標準の「サウンドレコーダー」で、マイクに向かって１０秒ほど喋ってみました。

私はじゃっかんコンソール黒い画面アレルギーがあるのでC#で呼び出すことにしました。
わざわざWindows Form　アプリケーションです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663038/95eb33b0-f66d-6504-e72b-b1fe33f0492c.png)

作っておいてなんだが、コンソールからの方がかっこつくかもしれん……。

MainFormの中身はこんな感じです。

``` C#

using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Windows.Forms;
using Newtonsoft.Json.Linq;

namespace AmiTest
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();
        }

        private readonly HttpClient client = new HttpClient();
        private string appKey = "API_KEY"; // AmiVoiceのAPP_KEYを設定

        private async Task<string> SendTranscriptionRequest(string audioFilePath)
        {
            var content = new MultipartFormDataContent
        {
            { new StringContent("-a-general"), "d" },
            { new StringContent(appKey), "u" },
            { new ByteArrayContent(File.ReadAllBytes(audioFilePath)), "a", Path.GetFileName(audioFilePath) }
        };

            HttpResponseMessage response = await client.PostAsync("https://acp-api-async.amivoice.com/v1/recognitions", content);
            response.EnsureSuccessStatusCode();
            string responseBody = await response.Content.ReadAsStringAsync();
            JObject jsonResponse = JObject.Parse(responseBody);
            return jsonResponse["sessionid"]?.ToString();
        }

        private async Task<string> PollForTranscriptionResult(string sessionId)
        {
            string url = $"https://acp-api-async.amivoice.com/v1/recognitions/{sessionId}";
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", appKey);

            while (true)
            {
                HttpResponseMessage statusResponse = await client.GetAsync(url);
                string statusResponseBody = await statusResponse.Content.ReadAsStringAsync();
                JObject statusJson = JObject.Parse(statusResponseBody);

                string status = statusJson["status"]?.ToString();
                if (status == "completed")
                {
                    return statusResponseBody;  // JSON形式での結果をそのまま返します
                }
                else if (status == "failed")
                {
                    throw new InvalidOperationException("Transcription failed.");
                }

                await Task.Delay(5000);  // 5秒ごとにステータスをチェック
            }
        }

        private async void buttonTranscribe_Click(object sender, EventArgs e)
        {
            OpenFileDialog openFileDialog = new OpenFileDialog
            {
                Filter = "WAV Files (*.wav)|*.wav|All files (*.*)|*.*",
                Title = "Select an audio file"
            };

            if (openFileDialog.ShowDialog() == DialogResult.OK)
            {
                string audioFilePath = openFileDialog.FileName;
                try
                {
                    // 音声ファイルをAPIに送信して書き起こしをリクエスト
                    var sessionId = await SendTranscriptionRequest(audioFilePath);
                    if (sessionId == null)
                    {
                        MessageBox.Show("Failed to start transcription job.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                        return;
                    }

                    // 結果の取得
                    var result = await PollForTranscriptionResult(sessionId);
                    MessageBox.Show(result, "変換が完了しました。", MessageBoxButtons.OK, MessageBoxIcon.Information);
                    textBox1.Text = result;
                }
                catch (Exception ex)
                {
                    MessageBox.Show("Error: " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
        }
    }
}


```

wavファイルを選んで、「あれ……俺、あいつを怒らせたかな？」というくらい待つと結果が出力されます。
（＊速度を求めない長い音声の変換用処理なので、こんなものだと思います。）

コーヒーを、あーいや、すみません、見栄を張りました。カルピスを飲んでいると、返事が返ってきました。
中身はこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663038/a8c982e2-71bc-b0d3-8c3f-b764f539ee8f.png)

大まかな区切りで確からしさと評価値があり、最後にすべて連結されたテキストが入っています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663038/bda971a7-852b-ef52-e78b-de8136f20d63.png)

**パーフェクト**です。

## 実際に文字起こしをしてみた

とはいえ、これはかなり意識的に音読してみた音声ファイルですから、実用性があるかというとまた別のお話です。
マーダーミステリーをプレイしたときの実際のログを変換してみることにしました。

Shotcut(フリーの動画編集ソフト）で動画から音声を抜き出しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663038/b037da8f-5497-2da5-195b-27569c3cffea.png)

……。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663038/54663588-5373-b193-2a38-b9d47dd74da4.png)

いいですね。
実装があんまりだったので、ちょっと改善して、text部分（変換後結果）のみ抜き出すようにしてみました。

（追記）
上のつくりはあまりよくなくて、今はなんとなく小分けにして送るよりもいっぺんに３時間くらいで送る方がよさそうだと思っています。

## 結果
３時間超えの長尺のマダミスを文字起こししてもらうことにしました。

長いファイルを変換するために、５分ごとに音声ファイルを分割して、順次文字起こしをしてもらうようにします。
エンジンを選択できるようにして、進捗を表示するようにしました。

およそ３時間３０分かかりました。
６万字近くありますね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663038/bcbe836c-29e6-68a2-a966-56bf69741f6f.png)

ＳＴＴ結果はかなり良かった（＝手で１から起こすよりも早そうなくらいは使えそう）です。

漢字の変換は文脈がつかめないのか難しいところがあるようですが（例：勘で選ぶ→間で選ぶ、となっていたり）８割～９割は起こせてますし、会話の流れもつかんでいそうでした。

あと、認識できない部分が少ないです。
試しに、Google Document＋音声文字起こしという方法でやってみたんですが、なんとなく声が大きい話者ひとりにフォーカスしてしまうのか「よろしくお願いします！」に続くほかのプレイヤーの「よろしくお願いします」というような声はとれていませんでした。AmiVoiceだと、ちゃんと、人数分のよろしくお願いしますが入っていました。
私がやった感触では、小さい声の抑揚にもかなり強いように思えます。

この用途（ＴＲＰＧ/マダミスのセッションの文字起こし）についてはじゅうぶん実用レベルはあるんじゃないかと思います。

Google CloudのSpeech to textも、まあ、こっちも月60分無料ではあるんですけれども、やっぱりほかの選択肢は持っておきたい……。
AmiVoiceもなかなか面白そうです。

ほかは、オープンソースがゆえに自前で構築できれば無料のwhisperのkotoba-whisperが気になります。

https://huggingface.co/kotoba-tech/kotoba-whisper-v1.0

ストリーミングでの速度も気になるところなので、時間があれば、リアルタイムでいろいろやってみたいなと思います。