話者ダイアライゼーション
話者ダイアライゼーション機能は、複数の人が話している音声から、どの部分を誰が話しているかを識別する機能です。
会議での議事録作成や書紀などに応用が考えられますね。
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
話者 | 言葉 |
---|---|
姉 | おはようございます |
- | <1秒あける> |
姉 | こんにちは |
- | <1秒あける> |
姉 | こんばんは |
- | <1秒あける> |
妹 | おはようございます |
- | <1秒あける> |
妹 | こんにちは |
- | <1秒あける> |
妹 | こんばんは |
- | <1秒あける> |
妻 | おはようございます |
- | <1秒あける> |
妻 | こんにちは |
- | <1秒あける> |
妻 | こんばんは |
パターン2
話者 | 言葉 |
---|---|
姉 | おはようございます |
- | <1秒あける> |
妹 | おはようございます |
- | <1秒あける> |
妻 | おはようございます |
- | <1秒あける> |
姉 | こんにちは |
- | <1秒あける> |
妹 | こんにちは |
- | <1秒あける> |
妻 | こんにちは |
- | <1秒あける> |
姉 | こんばんは |
- | <1秒あける> |
妹 | こんばんは |
- | <1秒あける> |
妻 | こんばんは |
パターン3
姉->妹の順だったので逆にし、2回つづけて同じ言葉を言ってみる
話者 | 言葉 |
---|---|
妹 | おはようございます |
- | <1秒あける> |
妹 | おはようございます |
- | <1秒あける> |
姉 | おはようございます |
- | <1秒あける> |
姉 | おはようございます |
パターン4
間隔をあけずに、つなげるパターン
話者 | 言葉 |
---|---|
妹 | おはよう |
姉 | ございます |
- | <1秒あける> |
姉 | おはよう |
妹 | ございます |
いざ勝負
まずはデフォルトパラメータで試してみます
パターン1
使用データ | API | 結果 |
---|---|---|
録音データ① | 同期 |
妻は認識できているが双子は同じ話者として認識された |
録音データ② | 同期 |
すべて同じ話者として認識された |
録音データ① | 非同期 |
妻は認識できているが双子は同じ話者として認識された |
録音データ② | 非同期 |
妻は認識できているが双子は同じ話者として認識された |
パターン2
使用データ | API | 結果 |
---|---|---|
録音データ① | 同期 |
ほぼ判定できていない。話者をラベリングする label 属性自体が無いデータがほとんどになってしまった・・・ |
録音データ② | 同期 |
ほぼ判定できていない。話者をラベリングする label 属性自体が無いデータがほとんどになってしまった・・・ |
録音データ① | 非同期 |
双子は同じ話者、双子妹と妻が同じ話者となっている部分がある |
録音データ② | 非同期 |
双子は同じ話者、双子姉と妻が同じ話者となっている部分がある |
パターン3
使用データ | API | 結果 |
---|---|---|
録音データ① | 同期 |
全てが同じ話者として認識された |
録音データ② | 同期 |
全てが同じ話者として認識された |
録音データ① | 非同期 |
録音データ通りに認識された |
録音データ② | 非同期 |
録音データ通りに認識された |
パターン4
使用データ | API | 結果 |
---|---|---|
録音データ① | 同期 |
全てが同じ話者として認識された |
録音データ② | 同期 |
全てが同じ話者として認識された |
録音データ① | 非同期 |
全てが同じ話者として認識された |
録音データ② | 非同期 |
全てが同じ話者として認識された |
結果
なかなか厳しい結果になりました。
声が似てるとやっぱり厳しそう。
また、同期APIよりも非同期APIのほうが精度が高そう。
ただ、同期APIはパラメータ調整できるので、次はそちらを試してみます。
同期API パラメータ調整
パターン1
デフォルトでは、双子が同一話者、妻は別として認識されていました。
--diarizer-alpha 1e10
として実行してみます。
使用データ | API | 結果 |
---|---|---|
録音データ① | 同期 |
妻は認識できているが双子は同じ話者として認識された |
録音データ② | 同期 |
全てが同じ話者として認識された |
1e10
ではまだ足りないのかな
では最大の--diarizer-alpha 1e50
でやってみます
使用データ | API | 結果 |
---|---|---|
録音データ① | 同期 |
妻は認識できているが双子は同じ話者として認識された |
録音データ② | 同期 |
全てが同じ話者として認識された |
ん〜 diarizerAlpha
側のパラメータだけでは無理なのかも
diarizerTransitionBias
側のデフォルトが 1e-40
であり、切り替わりづらいと思うので、こちらも調整していきます。
切り替わりやすさを最大にした--diarizer-alpha 1e50 --diarizer-transition-bias 1e-10
の組み合わせで試してみます。
使用データ | API | 結果 |
---|---|---|
録音データ① | 同期 |
妻は認識できているが双子は同じ話者として認識された |
録音データ② | 同期 |
妻は認識できている。双子は・・・おしい |
パターン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"
}
]
双子姉が話している「おはようございます」は認識されました。
双子姉が話している「こんにちは」「こんばんは」が妹として認識されてしまいました。
パラメータ調整により結構精度が上がった感じです
パターン2
パターン1で顕著な改善が見られた、--diarizer-alpha 1e50 --diarizer-transition-bias 1e-10
の組み合わせでいきます。
使用データ | API | 結果 |
---|---|---|
録音データ① | 同期 |
ほぼ判定できていない。話者をラベリングする label 属性自体が無いデータがほとんどになってしまった・・・ |
録音データ② | 同期 |
ほぼ判定できていない。話者をラベリングする 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 | 結果 |
---|---|---|
録音データ① | 同期 |
全てが同じ話者として認識された |
録音データ② | 同期 |
全てが同じ話者として認識された |
パターン4
こちらも--diarizer-alpha 1e50 --diarizer-transition-bias 1e-10
の組み合わせで。
使用データ | API | 結果 |
---|---|---|
録音データ① | 同期 |
全てが同じ話者として認識された |
録音データ② | 同期 |
全てが同じ話者として認識された |
結果&まとめ
- 同期APIで、
diarizerAlpha
やdiarizerTransitionBias
パラメータ調整をすることで、認識精度が向上する可能性があることが確認された。
ただ、この調整がすべてのパターンで効果的とは限らない。 - 非同期APIは細かいパラメータ調整ができないが、一部のパターンで正確に話者を識別することができた。
話者ダイアライゼーションは、音声が似ている場合には特に難易度が高いことがわかりました。
技術的な限界と、APIの選択やパラメータの調整が重要であることが分かりました。
この記事が話者ダイアライゼーションの理解に役立ち、技術の可能性をさらに探るきっかけになればと思います。
AmiVoice様の「音声認識APIを使ってみよう!」投稿キャンペーンで、AmiVoice賞いただきました