Asteriskを使って通話を録音し、文字起こしをしてみた

## はじめに
本記事は、Asteriskを用いた通話録音＋文字起こしに関する紹介となります。

## モチベーション
社内でも活用が進んでいる生成AIを私のメイン業務であるIP電話の分野にも取り入れたく、
まずは触ってみようという事で簡単に導入できるPBXソフトを利用して文字起こしをしてみました。

## Asteriskについて
AsteriskはオープンソースのIP-PBXソフトウェアです。

https://www.asterisk.org/

安価にPBXを構築できるだけでなく、別のPBXに接続したりGWを利用してアナログ電話をPBXに繋いだりと様々なことができます。何よりオープンソースという事で、気軽に自宅PCに導入して遊ぶるのが個人的にとてもメリットかなと思っています。
また社内でも簡単な検証や通話試験等さまざまな場面で活用しています。

##  実行環境
環境は以下となります。

・raspberry pi 5
・Raspberry Pi OS 12
・Asterisk 20.8.1
・Python 3.11.2
・Google Cloud Speech-to-Text 

今回は社内検証環境も利用して以下構成としています。
一人で細々とやっていたので、着信側は自動応答メッセージを流してもらうようにし、録音も着信側で行ってみようと思います。
※外部PBXに接続せずとも、Asterisk配下の端末のみでも同様のことができると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929504/9fb8abee-c864-ec4c-3104-868ff2b6c465.png)

## Asteriskインストール
今回は以下の公式ドキュメントを参考に、ソースコードからインストールしました。

https://docs.asterisk.org/Getting-Started/Installing-Asterisk/Installing-Asterisk-From-Source/

今回実施したインストール手順は以下となります。


・ソースをダウンロードして解答
```:bash
# cd /usr/local/src/
# wget http://downloads.asterisk.org/pub/telephony/asterisk/asterisk-20-current.tar.gz
# tar -zxvf asterisk-18-current.tar.gz
```

・依存関係となるパッケージのインストール(環境に応じて)
```:bash
# sudo build-essential wget libedit-dev libssl-dev libncurses5-dev libsqlite3-dev libjansson-dev uuid-dev libxml2-dev
```
※必要なパッケージが不明の場合、公式が用意してくれているスクリプトを実行することで
必要ものをまとめてインストールしてくれます。
```:bash
# cd contrib/scripts
# ./install_prereq install
# ./install_prereq install-unpackaged
```

・インストールチェックのためのコマンド
```:bash
# cd asterisk-20.8.1
# ./configure

               .$$$$$$$$$$$$$$$=..
            .$7$7..          .7$$7:.
          .$$:.                 ,$7.7
        .$7.     7$$$$           .$$77
     ..$$.       $$$$$            .$$$7
    ..7$   .?.   $$$$$   .?.       7$$$.
   $.$.   .$$$7. $$$$7 .7$$$.      .$$$.
 .777.   .$$$$$$77$$$77$$$$$7.      $$$,
 $$$~      .7$$$$$$$$$$$$$7.       .$$$.
.$$7          .7$$$$$$$7:          ?$$$.
$$$          ?7$$$$$$$$$$I        .$$$7
$$$       .7$$$$$$$$$$$$$$$$      :$$$.
$$$       $$$$$$7$$$$$$$$$$$$    .$$$.
$$$        $$$   7$$$7  .$$$    .$$$.
$$$$             $$$$7         .$$$.
7$$$7            7$$$$        7$$$
 $$$$$                        $$$
  $$$$7.                       $$  (TM)
   $$$$$$$.           .7$$$$$$  $$
     $$$$$$$$$$$$7$$$$$$$$$.$$$$$$
       $$$$$$$$$$$$$$$$.
       
→ 上記のようなAAが表示されればOK
```

・コンパイル＆インストール
```:bash
# make

+--------- Asterisk Build Complete ---------+
+ Asterisk has successfully been built, and +
+ can be installed by running: +
+ +
+ make install +
+-------------------------------------------+
+--------- Asterisk Build Complete ---------+

→ 上記のような表示となればOK

# make install

+---- Asterisk Installation Complete -------+
+ +
+ YOU MUST READ THE SECURITY DOCUMENT +
+ +
+ Asterisk has successfully been installed. +
+ If you would like to install the sample +
+ configuration files (overwriting any +
+ existing config files), run: +
+ +
+ make samples +
+ +
+-------------------------------------------+
+---- Asterisk Installation Complete -------+

→ 上記のような表示となればOK

# make samples
# make config
```

# Asteriskの設定
/etc/asterisk内の以下ファイルに必要な設定をします。

pjsip.conf：SIPアカウントの設定。
extensions.conf：ダイヤルプランの設定。発着信時の動作を設定

pjsip.confの基本的な設定は省略しますが、着信側のエンドポイントに今回利用するダイヤルプラン名を設定する所がポイントです。
※以下contextの部分
```:pjsip.conf
[150006]
type=endpoint
context=call-recording
disallow=all
allow=ulaw
aors=aor
transport=transport_udp
```

extensions.confには以下の通り設定します。

```:extensions.conf
[call-recording]
exten => _X.,1,Ringing()               ; 180 Ringingを送信
 same => n,Wait(5)                    ; 5秒間リンギング
 same => n,Answer()                   ; 応答
 same => n,Set(FILE=${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}) ; ファイル名を指定
 same => n,Set(MONITOR_DIR="/var/spool/asterisk/monitor/") ; 録音データの保存場所の指定
 same => n,MixMonitor(${FILE}.wav,at(${FILE}-out.wav)r(${FILE}-in.wav)) ; 録音開始
 same => n,Playback(Test)          ; 音声を再生
 same => n,Wait(5)                    ; 5秒待機
 same => n,Hangup()                   ; 通話を終了
```
MixMonitor部に通話録音に関する設定があります。
今回は発信者側と着信者側それぞれで録音をします。書き方は以下公式ドキュメントを参考にしました。

https://docs.asterisk.org/Asterisk_20_Documentation/API_Documentation/Dialplan_Applications/MixMonitor/

t(file)：送信した音声の録音
r(file)：受信した音声の録音
発着両方向を1ファイルに録音し文字起こしの方で話者分離する方法も試しましたが、上手く分離できない場合があり、今回はこの手法を取りました。

また自動で流す音声は以下ディレクトリにgsm形式で保存する必要があるようです。

```:bash
$ pwd
/var/lib/asterisk/sounds/en
$ ls -la |grep Test
-rwxr-xr-x 1 root root   9372 Sep  2 17:07 Test.gsm
```

私はffmpegを利用してMP3→GSM形式の変換をしました。
```:bash
$ ffmpeg -i Test.mp3 -ar 8000 -ac 1 -c:a gsm Test.gsm
```

一通り設定が完了したらサービス再起動をします。
```:bash
# systemctl restart asterisk
```

これで他端末から150006に発信すると、extensions.confに設定したシナリオ通り動くはずです...
保存された録音ファイルを確認すると、発着両方向の音声が確認できると思います。
※今回の設定だと以下ディレクトリに保存されます。
```:bash
$ ls -U /var/spool/asterisk/monitor/ |grep 20241224-094723
20241224-094723-in.wav
20241224-094723.wav
20241224-094723-out.wav
```
上記WAVファイルを聞いて見ると、確かに音声が録音されていることを確認できます。

# Google Cloud Speech-to-Textによる文字起こし
ここまでで内線通話の内容をwavファイルに保存することができたので、次は文字起こしです。

今回は無料で手軽に利用できるGoogle Cloud Speech-to-Text APIを使います。
Speech-to-Text V1とV2がありますが、１ヶ月60分まで無料で利用できるV1を使います。

https://cloud.google.com/speech-to-text/pricing?hl=ja

利用手順はネットに情報が多く出回っているため省略しますが、以下を実施しキーファイル(JSON)を取得してください。
※この後のPythonスクリプトで利用します。

・Google Cloud Platform の利用登録
・Cloud Speech-to-Text API の有効化
・キーファイル(JSON)の作成

キーファイルが取得できたら、文字起こしスクリプトを作成します。今回はPythonを利用しました。
```python:google-stt.py
import os
import sys
from google.cloud import speech
import wave

api_key_path = 'XXXXXXXX.json'　← 作成したキー情報を記載
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = api_key_path

client = speech.SpeechClient()

def transcribe_audio_with_timestamps(file_path, speaker_label):
    # サンプリングレートを確認
    with wave.open(file_path, 'rb') as f:
        fr = f.getframerate()  # ここで取得したサンプリングレートを使用
    
    # 音声ファイルを読み込み
    with open(file_path, "rb") as audio_file:
        content = audio_file.read()

    audio = speech.RecognitionAudio(content=content)
    config = speech.RecognitionConfig(
        encoding=speech.RecognitionConfig.AudioEncoding.LINEAR16,
        sample_rate_hertz=fr,
        language_code="ja-JP",
        enable_word_time_offsets=True  # タイムスタンプを有効にする
    )

    response = client.recognize(config=config, audio=audio)

    # タイムスタンプ付きの発話をリストに格納
    transcript_with_timestamps = []
    for result in response.results:
        words = result.alternatives[0].words
        if words:
            start_time = words[0].start_time.total_seconds()
            end_time = words[-1].end_time.total_seconds()
            transcript = ' '.join(word.word for word in words)
            transcript_with_timestamps.append({
                'transcript': transcript,
                'start_time': start_time,
                'end_time': end_time,
                'speaker': speaker_label  # 発話者を追加
            })

    return transcript_with_timestamps

def main(base_file_name):
    # 発信側と受信側のファイルパスを生成
    in_file = f"{base_file_name}-in.wav"
    out_file = f"{base_file_name}-out.wav"

    # 録音ファイルから文字起こしを実行
    inbound_transcript = transcribe_audio_with_timestamps(in_file, "発信者")
    outbound_transcript = transcribe_audio_with_timestamps(out_file, "受信者")

    # 時系列順に統合
    combined_transcript = inbound_transcript + outbound_transcript

    # タイムスタンプでソート
    combined_transcript.sort(key=lambda x: x['start_time'])

    # 結果を表示
    for entry in combined_transcript:
        print(f"{entry['start_time']} ({entry['speaker']}): {entry['transcript']}")

if __name__ == "__main__":
    # 引数からファイル名を取得
    if len(sys.argv) != 2:
        print("Usage: python script.py <base_file_name>")
        sys.exit(1)

    base_file_name = sys.argv[1]

    main(base_file_name)

```

以下の公式ドキュメントを参考にコードを書いています。

https://cloud.google.com/speech-to-text/docs/sync-recognize?hl=ja

今回は受信音声と送信音声をそれぞれ文字起こしをし、結果を時系列順に表示できないか試します。以下の様な出力を期待しています。

```:bash
$ python3 google-stt.py /var/spool/asterisk/monitor/20241224-014723
(タイムスタンプ) (発信者): (文字起こし内容)
(タイムスタンプ) (受信者): (文字起こし内容)
```
余談ですが、Cloud Speech-to-Textに渡す音声ファイルはモノラルである必要があるようです。

https://cloud.google.com/speech-to-text/docs/optimizing-audio-files-for-speech-to-text?hl=ja

実行してみます。引数には音声ファイル名の日時部分までを指定します。
```:bash
$ python3 google-stt.py /var/spool/asterisk/monitor/20241224-014723
0.0 (発信者): ありがとう ござい ます 通話 録音 の テスト を 終了 し ます
0.0 (受信者): こんにちは 本日 は 2 0 2 4 年 1 2 月 2 3 日 です 天気 は 曇り です
```

(発信者)は私が話した内容で、(受信者)は自動音声の内容です。出力を見てみると、ほぼ正確に文字起こしされている事が確認できました。
ただタイムスタンプが正しく表示されておらず、会話の順序が分かりませんでした。
デバッグをしてみると、音声に入っているノイズ等で上手くタイムスタンプが取れていないようでしたが、通話録音＋文字起こしまでできたので一旦はここまで。

# 今後は...
今後は以下をやりたいと思っています。
・別の文字起こしサービスを使ってより高精度な文字起こしを実現する。
・話者分離をし、かつ時系列順に出力する。
・自宅のひかり電話にて通話録音＋文字起こし機能を実装する。