7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

音声認識APIを使ってみよう!

[話者分離] 最強の双子 vs AmiVoice 話者ダイアライゼーション

Last updated at Posted at 2024-04-20

話者ダイアライゼーション

話者ダイアライゼーション機能は、複数の人が話している音声から、どの部分を誰が話しているかを識別する機能です。
会議での議事録作成や書紀などに応用が考えられますね。
Speech to textのAIやAPIは色々あるけど、話者分離をする機能を標準提供しているものは少ないので、これを使ってみようと思います。

双子の声

私には一卵性の双子の娘がいます。
双子姉と妹の声質は非常に似ていて、 親でもどっちが話しているのかわからない ときもあります。
ただ、わずかに差があり、姉に比べて妹のほうが少し低音という感じです。
このわずかな違いを見分けられるのか、話者ダイアライゼーションを試してみることにしました。

環境

本当は指向性マイクがあれば良かったんですが・・・私のスマホのGoogle Pixel 6aにて録音を行い、そのファイルを読み込ませることにしました。
双子だけだと面白みがないので、遺伝的のつながりがある妻の音声も入れて3人での話者分離をさせることにしました。

使用するPythonスクリプト

こんな感じのPythonスクリプトを作りました。
同期/非同期で使える調整パラメータが変わるので、せっかくなのでそのへんも選べるようにして、どのような変化があるのか見られるようにしました。
非同期の場合は2秒間隔でジョブの状態をチェックするようにしています。

import requests
import argparse
import json
import time
from dotenv import load_dotenv
import os

# 環境変数の読み込み
load_dotenv()


def check_job_status(session_id, api_key):
    url = f"https://acp-api-async.amivoice.com/v1/recognitions/{session_id}"
    headers = {"Authorization": f"Bearer {api_key}"}
    while True:
        response = requests.get(url, headers=headers)
        if response.status_code != 200:
            print("Error checking job status:", response.text)
            break
        data = response.json()
        if data["status"] in ["completed", "error"]:
            print(f"Job {data['status']}:")
            print(json.dumps(data, indent=4, ensure_ascii=False))
            break
        else:
            print("Job status:", data["status"])
            time.sleep(2)


def amivoice_api(args):
    api_key = os.getenv("API_KEY")
    if not api_key:
        print("APIキーが設定されていません。.envファイルを確認してください。")
        return

    if args.use_async:
        url = "https://acp-api-async.amivoice.com/v1/recognitions"
        diarization_settings = f"speakerDiarization=True diarizationMinSpeaker={args.diarization_min_speaker} diarizationMaxSpeaker={args.diarization_max_speaker}"
    else:
        url = "https://acp-api.amivoice.com/v1/recognize"
        diarization_settings = f"segmenterProperties=useDiarizer=1 diarizerAlpha={args.diarizer_alpha} diarizerTransitionBias={args.diarizer_transition_bias}"

    params = {
        "u": api_key,
        "d": f"grammarFileNames=-a-general {diarization_settings}",
    }

    # 音声ファイルを読み込む
    files = {"a": open(args.audio_file, "rb")}

    # POSTリクエストを送信
    response = requests.post(url, files=files, data=params)

    # レスポンスを確認
    if response.status_code == 200:
        print("Response from AmiVoice:")
        data = response.json()
        print(json.dumps(data, indent=4, ensure_ascii=False))
        if args.use_async:
            # ジョブの状態を確認する
            session_id = data.get("sessionid")
            if session_id:
                check_job_status(session_id, api_key)
    else:
        print("Error:", response.status_code)


def diarization():
    parser = argparse.ArgumentParser(
        description="AmiVoice APIを使用して話者の識別を行います。"
    )
    parser.add_argument("audio_file", help="音声ファイルのパス")
    parser.add_argument(
        "--diarizer-alpha",
        default="1",
        help="新しい話者の出現のしやすさ\n大きな値を指定するほど新規話者が出現しやすくなり、小さな値を指定するほど新規話者が出現しづらくなる(デフォルト:1、同期APIのみ)",
    )
    parser.add_argument(
        "--diarizer-transition-bias",
        default="1e-40",
        help="話者の切り替わりやすさ\n大きな値を指定するほど話者が切り替わりやすくなり、小さな値を指定するほど話者が切り替わりづらくなる(デフォルト:1e-40、同期APIのみ)",
    )
    parser.add_argument(
        "--diarization-min-speaker",
        type=int,
        default=2,
        help="最小話者数(デフォルト:2、非同期APIのみ)",
    )
    parser.add_argument(
        "--diarization-max-speaker",
        type=int,
        default=3,
        help="最大話者数(デフォルト:3、非同期APIのみ)",
    )
    parser.add_argument(
        "--use-async",
        action="store_true",
        help="非同期APIを使用",
    )

    args = parser.parse_args()

    amivoice_api(args)

ヘルプは以下

$ rye run diarization --help                       
usage: diarization [-h] [--diarizer-alpha DIARIZER_ALPHA] [--diarizer-transition-bias DIARIZER_TRANSITION_BIAS] [--diarization-min-speaker DIARIZATION_MIN_SPEAKER] [--diarization-max-speaker DIARIZATION_MAX_SPEAKER] [--use-async] audio_file

AmiVoice APIを使用して話者の識別を行います。

positional arguments:
  audio_file            音声ファイルのパス

options:
  -h, --help            show this help message and exit
  --diarizer-alpha DIARIZER_ALPHA
                        新しい話者の出現のしやすさ 大きな値を指定するほど新規話者が出現しやすくなり、小さな値を指定するほど新規話者が出現しづらくなる(デフォルト:1、同期APIのみ)
  --diarizer-transition-bias DIARIZER_TRANSITION_BIAS
                        話者の切り替わりやすさ 大きな値を指定するほど話者が切り替わりやすくなり、小さな値を指定するほど話者が切り替わりづらくなる(デフォルト:1e-40、同期APIのみ)
  --diarization-min-speaker DIARIZATION_MIN_SPEAKER
                        最小話者数(デフォルト:2、非同期APIのみ)
  --diarization-max-speaker DIARIZATION_MAX_SPEAKER
                        最大話者数(デフォルト:3、非同期APIのみ)
  --use-async           非同期APIを使用

rye を使用して、以下のようなコマンドで実行できるようにしました

# 同期の場合
$ rye run diarization path_to_your_audio_file.wav
# 非同期の場合
$ rye run diarization path_to_your_audio_file.wav --async --diarization-min-speaker 3 --diarization-max-speaker 3

録音データの採取

以下のパターンを、それぞれ2回録音しました

パターン1

話者 言葉
:raising_hand: おはようございます
- <1秒あける>
:raising_hand: こんにちは
- <1秒あける>
:raising_hand: こんばんは
- <1秒あける>
:raising_hand_tone1: おはようございます
- <1秒あける>
:raising_hand_tone1: こんにちは
- <1秒あける>
:raising_hand_tone1: こんばんは
- <1秒あける>
:bride_with_veil_tone1: おはようございます
- <1秒あける>
:bride_with_veil_tone1: こんにちは
- <1秒あける>
:bride_with_veil_tone1: こんばんは

パターン2

話者 言葉
:raising_hand: おはようございます
- <1秒あける>
:raising_hand_tone1: おはようございます
- <1秒あける>
:bride_with_veil_tone1: おはようございます
- <1秒あける>
:raising_hand: こんにちは
- <1秒あける>
:raising_hand_tone1: こんにちは
- <1秒あける>
:bride_with_veil_tone1: こんにちは
- <1秒あける>
:raising_hand: こんばんは
- <1秒あける>
:raising_hand_tone1: こんばんは
- <1秒あける>
:bride_with_veil_tone1: こんばんは

パターン3

姉->妹の順だったので逆にし、2回つづけて同じ言葉を言ってみる

話者 言葉
:raising_hand_tone1: おはようございます
- <1秒あける>
:raising_hand_tone1: おはようございます
- <1秒あける>
:raising_hand: おはようございます
- <1秒あける>
:raising_hand: おはようございます

パターン4

間隔をあけずに、つなげるパターン

話者 言葉
:raising_hand_tone1: おはよう
:raising_hand: ございます
- <1秒あける>
:raising_hand: おはよう
:raising_hand_tone1: ございます

いざ勝負

まずはデフォルトパラメータで試してみます

パターン1

使用データ API 結果
録音データ① 同期 :x:
妻は認識できているが双子は同じ話者として認識された
録音データ② 同期 :x:
すべて同じ話者として認識された
録音データ① 非同期 :x:
妻は認識できているが双子は同じ話者として認識された
録音データ② 非同期 :x:
妻は認識できているが双子は同じ話者として認識された

パターン2

使用データ API 結果
録音データ① 同期 :x:
ほぼ判定できていない。話者をラベリングする label 属性自体が無いデータがほとんどになってしまった・・・
録音データ② 同期 :x:
ほぼ判定できていない。話者をラベリングする label 属性自体が無いデータがほとんどになってしまった・・・
録音データ① 非同期 :x:
双子は同じ話者、双子妹と妻が同じ話者となっている部分がある
録音データ② 非同期 :x:
双子は同じ話者、双子姉と妻が同じ話者となっている部分がある

パターン3

使用データ API 結果
録音データ① 同期 :x:
全てが同じ話者として認識された
録音データ② 同期 :x:
全てが同じ話者として認識された
録音データ① 非同期 :ok_hand_tone1:
録音データ通りに認識された
録音データ② 非同期 :ok_hand_tone1:
録音データ通りに認識された

パターン4

使用データ API 結果
録音データ① 同期 :x:
全てが同じ話者として認識された
録音データ② 同期 :x:
全てが同じ話者として認識された
録音データ① 非同期 :x:
全てが同じ話者として認識された
録音データ② 非同期 :x:
全てが同じ話者として認識された

結果

なかなか厳しい結果になりました。:sweat:
声が似てるとやっぱり厳しそう。
また、同期APIよりも非同期APIのほうが精度が高そう。
ただ、同期APIはパラメータ調整できるので、次はそちらを試してみます。

同期API パラメータ調整

パターン1

デフォルトでは、双子が同一話者、妻は別として認識されていました。
--diarizer-alpha 1e10 として実行してみます。

使用データ API 結果
録音データ① 同期 :x:
妻は認識できているが双子は同じ話者として認識された
録音データ② 同期 :x:
全てが同じ話者として認識された

1e10ではまだ足りないのかな
では最大の--diarizer-alpha 1e50でやってみます

使用データ API 結果
録音データ① 同期 :x:
妻は認識できているが双子は同じ話者として認識された
録音データ② 同期 :x:
全てが同じ話者として認識された

ん〜 diarizerAlpha 側のパラメータだけでは無理なのかも
diarizerTransitionBias 側のデフォルトが 1e-40 であり、切り替わりづらいと思うので、こちらも調整していきます。
切り替わりやすさを最大にした--diarizer-alpha 1e50 --diarizer-transition-bias 1e-10 の組み合わせで試してみます。

使用データ API 結果
録音データ① 同期 :x:
妻は認識できているが双子は同じ話者として認識された
録音データ② 同期 :white_flower:
妻は認識できている。双子は・・・おしい

パターン1 録音データ②の応答詳細

"tokens": [
    {
        "written": "おはようございます",
        "confidence": 0.98,
        "starttime": 1760,
        "endtime": 3376,
        "spoken": "おはようございます",
        "label": "speaker1"
    },
    {
        "written": "。",
        "confidence": 0.86,
        "starttime": 3376,
        "endtime": 3488,
        "spoken": "_",
        "label": "speaker1"
    },
    {
        "written": "こんにちは",
        "confidence": 1.0,
        "starttime": 4238,
        "endtime": 5086,
        "spoken": "こんにちは",
        "label": "speaker2"
    },
    {
        "written": "こんばんは",
        "confidence": 1.0,
        "starttime": 6204,
        "endtime": 7036,
        "spoken": "こんばんは",
        "label": "speaker2"
    },
    {
        "written": "おはようございます",
        "confidence": 1.0,
        "starttime": 8190,
        "endtime": 9470,
        "spoken": "おはようございます",
        "label": "speaker2"
    },
    {
        "written": "。",
        "confidence": 0.88,
        "starttime": 10142,
        "endtime": 10238,
        "spoken": "_",
        "label": "speaker2"
    },
    {
        "written": "こんにちは",
        "confidence": 1.0,
        "starttime": 10238,
        "endtime": 11022,
        "spoken": "こんにちは",
        "label": "speaker2"
    },
    {
        "written": "こんばんは",
        "confidence": 1.0,
        "starttime": 11920,
        "endtime": 12656,
        "spoken": "こんばんは",
        "label": "speaker2"
    },
    {
        "written": "おはようございます",
        "confidence": 0.99,
        "starttime": 13836,
        "endtime": 15180,
        "spoken": "おはようございます",
        "label": "speaker3"
    },
    {
        "written": "。",
        "confidence": 0.89,
        "starttime": 15180,
        "endtime": 15388,
        "spoken": "_",
        "label": "speaker3"
    },
    {
        "written": "こんにちは",
        "confidence": 1.0,
        "starttime": 15872,
        "endtime": 16768,
        "spoken": "こんにちは",
        "label": "speaker3"
    },
    {
        "written": "こんばんは",
        "confidence": 1.0,
        "starttime": 17770,
        "endtime": 18522,
        "spoken": "こんばんは",
        "label": "speaker3"
    }
]

双子姉が話している「おはようございます」は認識されました。
双子姉が話している「こんにちは」「こんばんは」が妹として認識されてしまいました。
パラメータ調整により結構精度が上がった感じです :thumbsup:

パターン2

パターン1で顕著な改善が見られた、--diarizer-alpha 1e50 --diarizer-transition-bias 1e-10 の組み合わせでいきます。

使用データ API 結果
録音データ① 同期 :x:
ほぼ判定できていない。話者をラベリングする label 属性自体が無いデータがほとんどになってしまった・・・
録音データ② 同期 :x:
ほぼ判定できていない。話者をラベリングする label 属性自体が無いデータがほとんどになってしまった・・・

パターン2 録音データ①の応答詳細

パターン2での応答がおかしい。
相変わらず label の属性自体が欠損しているデータが多い。

{
    "written": "おはようございます",
    "confidence": 0.93,
    "starttime": 1792,
    "endtime": 3264,
    "spoken": "おはようございます",
    "label": "speaker0"
},
{
    "written": "。",
    "confidence": 0.79,
    "starttime": 3264,
    "endtime": 3488,
    "spoken": "_",
    "label": "speaker0"
},
{
    "written": "おはようございます",
    "confidence": 1.0,
    "starttime": 3796,
    "endtime": 5204,
    "spoken": "おはようございます"
},
{
    "written": "。",
    "confidence": 0.9,
    "starttime": 5204,
    "endtime": 5444,
    "spoken": "_"
},
{
    "written": "おはようございます",
    "confidence": 1.0,
    "starttime": 5854,
    "endtime": 7134,
    "spoken": "おはようございます"
},

他と比べてlabel属性が欠損したり、ちょっとおかしい

パターン3

--diarizer-alpha 1e50 --diarizer-transition-bias 1e-10 の組み合わせで。

使用データ API 結果
録音データ① 同期 :x:
全てが同じ話者として認識された
録音データ② 同期 :x:
全てが同じ話者として認識された

パターン4

こちらも--diarizer-alpha 1e50 --diarizer-transition-bias 1e-10 の組み合わせで。

使用データ API 結果
録音データ① 同期 :x:
全てが同じ話者として認識された
録音データ② 同期 :x:
全てが同じ話者として認識された

結果&まとめ

  • 同期APIで、diarizerAlphadiarizerTransitionBiasパラメータ調整をすることで、認識精度が向上する可能性があることが確認された。
    ただ、この調整がすべてのパターンで効果的とは限らない。
  • 非同期APIは細かいパラメータ調整ができないが、一部のパターンで正確に話者を識別することができた。

話者ダイアライゼーションは、音声が似ている場合には特に難易度が高いことがわかりました。
技術的な限界と、APIの選択やパラメータの調整が重要であることが分かりました。

この記事が話者ダイアライゼーションの理解に役立ち、技術の可能性をさらに探るきっかけになればと思います。


AmiVoice様の「音声認識APIを使ってみよう!」投稿キャンペーンで、AmiVoice賞いただきました :bow_tone1:

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?