「Kotoba-Whisper-v2.2」を試す
Kotoba-Whisper-v2.2
Kotoba-Whisper-v2.2は、kotoba-tech/kotoba-whisper-v2.0をベースに、後処理スタックをパイプラインとして統合した日本語ASRモデルです。新機能としては、(i)diarizersによる話者のダイアライゼーション、(ii)punctuatorsによる句読点の付加があります。このパイプラインは、Asahi UshioとKotoba Technologiesのコラボレーションにより開発されました。
インストール
Colaboratory L4で。
パッケージインストール。transformersのアップデートを行っている箇所で、おそらくランタイム再起動を促されると思うので、すべてのパッケージインストールが終わった段階でランタイムを再起動する。
!pip install --upgrade pip
!pip install --upgrade transformers accelerate torchaudio
!pip install "punctuators==0.0.5"
!pip install "pyannote.audio"
!pip install git+https://github.com/huggingface/diarizers.git
以下の2つの話者ダイアライゼーション用の事前学習モデルを使用する。両方とも事前にHuggingFaceのサイト上で規約に同意しておく必要がある。
モデルロード時にHF_TOKENを読み込めるようにColaboratortyのシークレットに登録しておく
話者ダイアライぜーションを有効にした、音声からの文字起こし
サンプルをダウンロードしておく。
!wget https://huggingface.co/kotoba-tech/kotoba-whisper-v2.2/resolve/main/sample_audio/sample_diarization_japanese.mp3
以下で再生できる。
from IPython.display import Audio, display
display(Audio("sample_diarization_japanese.mp3", autoplay=True))
二人の男性が会話していて、少し後ろで声が聞こえたり、最後に女性だけの声が含まれるようなサンプルになっている。
パイプラインを実行。モデルのダウンロードが行われるので少し時間がかかる。
import torch
from transformers import pipeline
import pprint
# 設定
model_id = "kotoba-tech/kotoba-whisper-v2.2"
torch_dtype = torch.float16 if torch.cuda.is_available() else torch.float32
device = "cuda:0" if torch.cuda.is_available() else "cpu"
model_kwargs = {"attn_implementation": "sdpa"} if torch.cuda.is_available() else {}
# モデルのロード
pipe = pipeline(
model=model_id,
torch_dtype=torch_dtype,
device=device,
model_kwargs=model_kwargs,
batch_size=8,
trust_remote_code=True,
)
# 推論の実行
result = pipe("sample_diarization_japanese.mp3", chunk_length_s=15)
pprint.print(result)
結果。
{'chunks': [{'speaker_id': 'SPEAKER_00',
'text': '水をマレーシアから買わなくてはならないのです',
'timestamp': [22.1, 24.97]},
{'speaker_id': 'SPEAKER_01',
'text': 'これも先ほどがずっと言っている自分の感覚的には大丈夫ですけれども',
'timestamp': [0.03, 13.85]},
{'speaker_id': 'SPEAKER_01',
'text': '今は屋外の気温',
'timestamp': [5.03, 18.85]},
{'speaker_id': 'SPEAKER_01',
'text': '昼も夜も上がってますので',
'timestamp': [7.63, 21.45]},
{'speaker_id': 'SPEAKER_01',
'text': '空気の入れ替えだけではかえって人が上がってきます',
'timestamp': [9.91, 23.73]},
{'speaker_id': 'SPEAKER_02',
'text': '愚直にその街の良さをアピールしていくという',
'timestamp': [13.48, 22.1]},
{'speaker_id': 'SPEAKER_02',
'text': 'そういう姿勢が基本にあった上での',
'timestamp': [17.26, 25.88]},
{'speaker_id': 'SPEAKER_02',
'text': 'こういうPR作戦だと思うんですよね',
'timestamp': [19.86, 28.48]}],
'chunks/SPEAKER_00': [{'speaker_id': 'SPEAKER_00',
'text': '水をマレーシアから買わなくてはならないのです',
'timestamp': [22.1, 24.97]}],
'chunks/SPEAKER_01': [{'speaker_id': 'SPEAKER_01',
'text': 'これも先ほどがずっと言っている自分の感覚的には大丈夫ですけれども',
'timestamp': [0.03, 13.85]},
{'speaker_id': 'SPEAKER_01',
'text': '今は屋外の気温',
'timestamp': [5.03, 18.85]},
{'speaker_id': 'SPEAKER_01',
'text': '昼も夜も上がってますので',
'timestamp': [7.63, 21.45]},
{'speaker_id': 'SPEAKER_01',
'text': '空気の入れ替えだけではかえって人が上がってきます',
'timestamp': [9.91, 23.73]}],
'chunks/SPEAKER_02': [{'speaker_id': 'SPEAKER_02',
'text': '愚直にその街の良さをアピールしていくという',
'timestamp': [13.48, 22.1]},
{'speaker_id': 'SPEAKER_02',
'text': 'そういう姿勢が基本にあった上での',
'timestamp': [17.26, 25.88]},
{'speaker_id': 'SPEAKER_02',
'text': 'こういうPR作戦だと思うんですよね',
'timestamp': [19.86, 28.48]}],
'speaker_ids': ['SPEAKER_00', 'SPEAKER_01', 'SPEAKER_02'],
'text/SPEAKER_00': '水をマレーシアから買わなくてはならないのです',
'text/SPEAKER_01': 'これも先ほどがずっと言っている自分の感覚的には大丈夫ですけれども今は屋外の気温昼も夜も上がってますので空気の入れ替えだけではかえって人が上がってきます',
'text/SPEAKER_02': '愚直にその街の良さをアピールしていくというそういう姿勢が基本にあった上でのこういうPR作戦だと思うんですよね'}
テキストは、すべてのチャンク(chunks
)、話者ごとのチャンク(chunks/SPEAKER_*
)、話者ごとにチャンクが結合されたテキスト(text/SPEAKER_*
)で返される様子。SPEAKER_00
が女性、SPEAKER_01
とSPEAKER_02
が二人の男性の声として別々認識されているのがわかる。
add_punctuation
を有効にすると句読点が追加される。
result = pipe("sample_diarization_japanese.mp3", add_punctuation=True)
pprint.pprint(result)
{'chunks': [{'speaker_id': 'SPEAKER_00',
'text': '水をマレーシアから買わなくてはならないのです',
'timestamp': [22.1, 24.97]},
{'speaker_id': 'SPEAKER_01',
'text': 'これも先ほどがずっと言っている自分の感覚的には大丈夫ですけれども',
'timestamp': [0.03, 13.85]},
{'speaker_id': 'SPEAKER_01',
'text': '今は屋外の気温',
'timestamp': [5.03, 18.85]},
{'speaker_id': 'SPEAKER_01',
'text': '昼も夜も上がってますので',
'timestamp': [7.63, 21.45]},
{'speaker_id': 'SPEAKER_01',
'text': '空気の入れ替えだけではかえって人が上がってきます',
'timestamp': [9.91, 23.73]},
{'speaker_id': 'SPEAKER_02',
'text': '愚直にその街の良さをアピールしていくという',
'timestamp': [13.48, 22.1]},
{'speaker_id': 'SPEAKER_02',
'text': 'そういう姿勢が基本にあった上での',
'timestamp': [17.26, 25.88]},
{'speaker_id': 'SPEAKER_02',
'text': 'こういうPR作戦だと思うんですよね',
'timestamp': [19.86, 28.48]}],
'chunks/SPEAKER_00': [{'speaker_id': 'SPEAKER_00',
'text': '水をマレーシアから買わなくてはならないのです',
'timestamp': [22.1, 24.97]}],
'chunks/SPEAKER_01': [{'speaker_id': 'SPEAKER_01',
'text': 'これも先ほどがずっと言っている自分の感覚的には大丈夫ですけれども',
'timestamp': [0.03, 13.85]},
{'speaker_id': 'SPEAKER_01',
'text': '今は屋外の気温',
'timestamp': [5.03, 18.85]},
{'speaker_id': 'SPEAKER_01',
'text': '昼も夜も上がってますので',
'timestamp': [7.63, 21.45]},
{'speaker_id': 'SPEAKER_01',
'text': '空気の入れ替えだけではかえって人が上がってきます',
'timestamp': [9.91, 23.73]}],
'chunks/SPEAKER_02': [{'speaker_id': 'SPEAKER_02',
'text': '愚直にその街の良さをアピールしていくという',
'timestamp': [13.48, 22.1]},
{'speaker_id': 'SPEAKER_02',
'text': 'そういう姿勢が基本にあった上での',
'timestamp': [17.26, 25.88]},
{'speaker_id': 'SPEAKER_02',
'text': 'こういうPR作戦だと思うんですよね',
'timestamp': [19.86, 28.48]}],
'speaker_ids': ['SPEAKER_00', 'SPEAKER_01', 'SPEAKER_02'],
'text/SPEAKER_00': '水をマレーシアから買わなくてはならないのです。',
'text/SPEAKER_01': 'これも先ほどがずっと言っている。自分の感覚的には大丈夫です。けれども。今は屋外の気温、昼も夜も上がってますので、空気の入れ替えだけではかえって人が上がってきます。',
'text/SPEAKER_02': '愚直にその街の良さをアピールしていくという。そういう姿勢が基本にあった上での、こういうPR作戦だと思うんですよね。'}
最後の話者ごとにチャンクが結合されたテキスト(text/SPEAKER_*
)は句読点が含まれる形になっている。
num_speakers
で話者数を制御できる。話者数を2にしてみる。
result = pipe("sample_diarization_japanese.mp3", num_speakers=2)
pprint.pprint(result)
{'chunks': [{'speaker_id': 'SPEAKER_00',
'text': 'これも先ほどがずっと言っている自分の感覚的には大丈夫ですけれども',
'timestamp': [0.03, 13.85]},
{'speaker_id': 'SPEAKER_00',
'text': '今は屋外の気温',
'timestamp': [5.03, 18.85]},
{'speaker_id': 'SPEAKER_00',
'text': '昼も夜も上がってますので',
'timestamp': [7.63, 21.45]},
{'speaker_id': 'SPEAKER_00',
'text': '空気の入れ替えだけではかえって人が上がってきます',
'timestamp': [9.91, 23.73]},
{'speaker_id': 'SPEAKER_00',
'text': '水をマレーシアから買わなくてはならないのです',
'timestamp': [22.1, 24.97]},
{'speaker_id': 'SPEAKER_01',
'text': '愚直にその街の良さをアピールしていくという',
'timestamp': [13.48, 22.1]},
{'speaker_id': 'SPEAKER_01',
'text': 'そういう姿勢が基本にあった上での',
'timestamp': [17.26, 25.88]},
{'speaker_id': 'SPEAKER_01',
'text': 'こういうPR作戦だと思うんですよね',
'timestamp': [19.86, 28.48]}],
'chunks/SPEAKER_00': [{'speaker_id': 'SPEAKER_00',
'text': 'これも先ほどがずっと言っている自分の感覚的には大丈夫ですけれども',
'timestamp': [0.03, 13.85]},
{'speaker_id': 'SPEAKER_00',
'text': '今は屋外の気温',
'timestamp': [5.03, 18.85]},
{'speaker_id': 'SPEAKER_00',
'text': '昼も夜も上がってますので',
'timestamp': [7.63, 21.45]},
{'speaker_id': 'SPEAKER_00',
'text': '空気の入れ替えだけではかえって人が上がってきます',
'timestamp': [9.91, 23.73]},
{'speaker_id': 'SPEAKER_00',
'text': '水をマレーシアから買わなくてはならないのです',
'timestamp': [22.1, 24.97]}],
'chunks/SPEAKER_01': [{'speaker_id': 'SPEAKER_01',
'text': '愚直にその街の良さをアピールしていくという',
'timestamp': [13.48, 22.1]},
{'speaker_id': 'SPEAKER_01',
'text': 'そういう姿勢が基本にあった上での',
'timestamp': [17.26, 25.88]},
{'speaker_id': 'SPEAKER_01',
'text': 'こういうPR作戦だと思うんですよね',
'timestamp': [19.86, 28.48]}],
'speaker_ids': ['SPEAKER_00', 'SPEAKER_01'],
'text/SPEAKER_00': 'これも先ほどがずっと言っている自分の感覚的には大丈夫ですけれども今は屋外の気温昼も夜も上がってますので空気の入れ替えだけではかえって人が上がってきます水をマレーシアから買わなくてはならないのです',
'text/SPEAKER_01': '愚直にその街の良さをアピールしていくというそういう姿勢が基本にあった上でのこういうPR作戦だと思うんですよね'}
話者数指定なしの場合には女性の声は別の話者として認識され合計話者数3だったが、2を指定した場合は別の話者の発話と同じ扱いになっている。
min_speakers
とmax_speakers
で、話者数の最大・最小を設定できる。
result = pipe("sample_diarization_japanese.mp3", min_speakers=2, max_speakers=5)
pprint.pprint(result)
何も指定しない場合は話者の特定も推論にお任せになると思うので、事前にわかっている場合には指定したほうがより精度が上がるのだろうと思われる。
音声の先頭・最後に無音期間を付与することができる。これにより文字起こし精度が向上する場合がある。
result = pipe("sample_diarization_japanese.mp3", add_silence_end=0.5, add_silence_start=0.5)
pprint.pprint(result)
{'chunks': [{'speaker_id': 'SPEAKER_00',
'text': '水をマレーシアから買わなくてはならないのです',
'timestamp': [22.1, 24.97]},
{'speaker_id': 'SPEAKER_01',
'text': 'そうですねこれも先ほどがずっと言っている自分の感覚的には大丈夫ですけれども',
'timestamp': [0.03, 13.85]},
{'speaker_id': 'SPEAKER_01',
'text': 'もう今は屋外の気温',
'timestamp': [5.51, 19.33]},
{'speaker_id': 'SPEAKER_01',
'text': '昼も夜も上がってますので空気の入れ替えだけでは',
'timestamp': [8.11, 21.93]},
{'speaker_id': 'SPEAKER_01',
'text': 'かえって人が上がってきます',
'timestamp': [12.29, 26.11]},
{'speaker_id': 'SPEAKER_02',
'text': 'やっぱり愚直にやっぱりその街の良さをアピールしていくっていう',
'timestamp': [13.48, 22.1]},
{'speaker_id': 'SPEAKER_02',
'text': 'そういう姿勢が基本にあった上でのこういうPR作戦だと思うんですよね',
'timestamp': [17.74, 26.36]}],
'chunks/SPEAKER_00': [{'speaker_id': 'SPEAKER_00',
'text': '水をマレーシアから買わなくてはならないのです',
'timestamp': [22.1, 24.97]}],
'chunks/SPEAKER_01': [{'speaker_id': 'SPEAKER_01',
'text': 'そうですねこれも先ほどがずっと言っている自分の感覚的には大丈夫ですけれども',
'timestamp': [0.03, 13.85]},
{'speaker_id': 'SPEAKER_01',
'text': 'もう今は屋外の気温',
'timestamp': [5.51, 19.33]},
{'speaker_id': 'SPEAKER_01',
'text': '昼も夜も上がってますので空気の入れ替えだけでは',
'timestamp': [8.11, 21.93]},
{'speaker_id': 'SPEAKER_01',
'text': 'かえって人が上がってきます',
'timestamp': [12.29, 26.11]}],
'chunks/SPEAKER_02': [{'speaker_id': 'SPEAKER_02',
'text': 'やっぱり愚直にやっぱりその街の良さをアピールしていくっていう',
'timestamp': [13.48, 22.1]},
{'speaker_id': 'SPEAKER_02',
'text': 'そういう姿勢が基本にあった上でのこういうPR作戦だと思うんですよね',
'timestamp': [17.74, 26.36]}],
'speaker_ids': ['SPEAKER_00', 'SPEAKER_01', 'SPEAKER_02'],
'text/SPEAKER_00': '水をマレーシアから買わなくてはならないのです',
'text/SPEAKER_01': 'そうですねこれも先ほどがずっと言っている自分の感覚的には大丈夫ですけれどももう今は屋外の気温昼も夜も上がってますので空気の入れ替えだけではかえって人が上がってきます',
'text/SPEAKER_02': 'やっぱり愚直にやっぱりその街の良さをアピールしていくっていうそういう姿勢が基本にあった上でのこういうPR作戦だと思うんですよね'}
最初の文字起こしでは取得できていなかった「そうですね」や「やっぱり」「もう」などが取得できている
Flash Attention2にも対応している。Flash Attentionのビルドは時間がかかるので、事前ビルドしたものを使用する。
!wget https://github.com/kun432/flash-attention-prebuild-wheels/releases/download/v0.0.0-test/flash_attn-2.6.3+cu121torch2.5-cp310-cp310-linux_x86_64.whl
!pip install --no-dependencies --upgrade flash_attn-2.6.3+cu121torch2.5-cp310-cp310-linux_x86_64.whl
のだが、どうもKotoba-Whisperの推論を実行するとロケールが書き換わってしまうようで、wgetに失敗する。
NotImplementedError: A UTF-8 locale is required. Got ANSI_X3.4-1968
ロケールを確認してみる。
import locale
print(locale.getpreferredencoding())
ANSI_X3.4-1968
ロケールをUTF−8に戻す
import locale
def getpreferredencoding(do_setlocale = True):
return "UTF-8"
locale.getpreferredencoding = getpreferredencoding
これでwgetは成功するようになるのだが、このあとFlash Attention2を有効にして推論した場合でもエラーになってしまう。
ので、Flash Attention2を有効化する場合は最初からそれを踏まえて進めるのが良いと思う。一旦ランタイム再起動して、最初からやる場合は以下のような手順で進めれば問題ない。
!pip install --upgrade pip
!pip install --upgrade transformers accelerate torchaudio
!pip install "punctuators==0.0.5"
!pip install "pyannote.audio"
!pip install git+https://github.com/huggingface/diarizers.git
!wget https://github.com/kun432/flash-attention-prebuild-wheels/releases/download/v0.0.0-test/flash_attn-2.6.3+cu121torch2.5-cp310-cp310-linux_x86_64.whl
!pip install --no-dependencies --upgrade flash_attn-2.6.3+cu121torch2.5-cp310-cp310-linux_x86_64.whl
!wget https://huggingface.co/kotoba-tech/kotoba-whisper-v2.2/resolve/main/sample_audio/sample_diarization_japanese.mp3
"attn_implementation": "flash_attention_2"
を指定する。
import torch
from transformers import pipeline
import pprint
model_id = "kotoba-tech/kotoba-whisper-v2.2"
torch_dtype = torch.float16 if torch.cuda.is_available() else torch.float32
device = "cuda:0" if torch.cuda.is_available() else "cpu"
# Flash Attention2を有効化
model_kwargs = {"attn_implementation": "flash_attention_2"} if torch.cuda.is_available() else {}
pipe = pipeline(
model=model_id,
torch_dtype=torch_dtype,
device=device,
model_kwargs=model_kwargs,
batch_size=8,
trust_remote_code=True,
)
result = pipe("sample_diarization_japanese.mp3")
pprint.pprint(result)
結果。
{'chunks': [{'speaker_id': 'SPEAKER_00',
'text': '水をマレーシアから買わなくてはならないのです',
'timestamp': [22.1, 24.97]},
{'speaker_id': 'SPEAKER_01',
'text': 'これも先ほどがずっと言っている自分の感覚的には大丈夫ですけれども',
'timestamp': [0.03, 13.85]},
{'speaker_id': 'SPEAKER_01',
'text': '今は屋外の気温',
'timestamp': [5.03, 18.85]},
{'speaker_id': 'SPEAKER_01',
'text': '昼も夜も上がってますので',
'timestamp': [7.63, 21.45]},
{'speaker_id': 'SPEAKER_01',
'text': '空気の入れ替えだけではかえって人が上がってきます',
'timestamp': [9.91, 23.73]},
{'speaker_id': 'SPEAKER_02',
'text': '愚直にその街の良さをアピールしていくという',
'timestamp': [13.48, 22.1]},
{'speaker_id': 'SPEAKER_02',
'text': 'そういう姿勢が基本にあった上での',
'timestamp': [17.26, 25.88]},
{'speaker_id': 'SPEAKER_02',
'text': 'こういうPR作戦だと思うんですよね',
'timestamp': [19.86, 28.48]}],
'chunks/SPEAKER_00': [{'speaker_id': 'SPEAKER_00',
'text': '水をマレーシアから買わなくてはならないのです',
'timestamp': [22.1, 24.97]}],
'chunks/SPEAKER_01': [{'speaker_id': 'SPEAKER_01',
'text': 'これも先ほどがずっと言っている自分の感覚的には大丈夫ですけれども',
'timestamp': [0.03, 13.85]},
{'speaker_id': 'SPEAKER_01',
'text': '今は屋外の気温',
'timestamp': [5.03, 18.85]},
{'speaker_id': 'SPEAKER_01',
'text': '昼も夜も上がってますので',
'timestamp': [7.63, 21.45]},
{'speaker_id': 'SPEAKER_01',
'text': '空気の入れ替えだけではかえって人が上がってきます',
'timestamp': [9.91, 23.73]}],
'chunks/SPEAKER_02': [{'speaker_id': 'SPEAKER_02',
'text': '愚直にその街の良さをアピールしていくという',
'timestamp': [13.48, 22.1]},
{'speaker_id': 'SPEAKER_02',
'text': 'そういう姿勢が基本にあった上での',
'timestamp': [17.26, 25.88]},
{'speaker_id': 'SPEAKER_02',
'text': 'こういうPR作戦だと思うんですよね',
'timestamp': [19.86, 28.48]}],
'speaker_ids': ['SPEAKER_00', 'SPEAKER_01', 'SPEAKER_02'],
'text/SPEAKER_00': '水をマレーシアから買わなくてはならないのです',
'text/SPEAKER_01': 'これも先ほどがずっと言っている自分の感覚的には大丈夫ですけれども今は屋外の気温昼も夜も上がってますので空気の入れ替えだけではかえって人が上がってきます',
'text/SPEAKER_02': '愚直にその街の良さをアピールしていくというそういう姿勢が基本にあった上でのこういうPR作戦だと思うんですよね'}
個人的には、句読点がきちんと入るのが良き。