🤗

自作データセットでWhisperをファインチューニングしたら、独自用語だらけのクラロワ実況でも使えるようになった:「データセット作成編」

2023/01/06に公開

とりあえず結果

40分くらいの動画で一旦文字起こししてとりあえずファインチューニングしてみた。
いったん試しくらいでやったから適当だったが、その学習済みモデルで別動画の推論をした結果以下の結果になった


クラロワ実況の一文
正解の文章

めっちゃしやすくてで迫撃にもアチャクイを当てられるでしょ だもうマジで環境でゴレとかにもまあポイズンウッドだから普通に強くてエリポンも別にディガーで潰せると三銃士が来ても勝てるロイホグ系もねゴーストアチャクイゴブリンウッドだからめっちゃ強いんですよ

元のWhisperでpredictした文章

めっちゃしやすくてで迫撃にもあ着いを当てられるでしょ だもうマジで環境で5例とかにもはポイズングッドだから普通に強くてエリポンも別にリガーで潰せると30人が来ても勝てるロイホグ系もねゴーストアチャクイゴブリングッドだからめっちゃ強いんですよ

流石にゴレが5例になってたりディガーがリガーになってたり三銃士が30人になってたりします。

ファインチューニング後Whisper

めっちゃでしやすくてで迫撃にもアチャクイを当てられるでしょだからもうマジで環境でゴレとかにもポイズンウッドだから普通に強くてでエリポンも別にディガーで潰せると三銃士とか来ても勝てるロイホグ系もねゴーストアチャクイゴブリングッドだからめっちゃ強いんですよ

最後のゴーストアチャクイゴブリングッドのグッドはウッドが正解なのですが、それ以外全部完璧に文字起こしできている!!えぐい。


マジですごかった笑
この記事でも解説されているが、whisperはそもそもゼロショット転移が可能らしく、ここまですごくても当然かもしれない。

Whisperすごい。ただ流石に独自用語は難しい。

WhisperはOpenAIが開発した音声認識モデルで、人と同じくらいの精度で音声を文字起こしすることが可能です。
実際僕は自社用に会議の文字起こしとGPT-3を使った要約するプログラムを書き、普通に会社の会議とか文字起こしするのには実用に足るレベルで使えてます。

しかし弊社は製造業の分析報告会が結構あるので、流石に会社独自用語等だと聞き間違いも起こります。
ただ、おそらく弊社の社員じゃない人間に音声聞かせて文字起こしさせたら同じような聞き間違いを起こすだろうと言ったものが多いので、実際に人レベルの音声認識精度が出ていると言えます。

ただそれならば、もしWhisperに独自用語を理解させられれば自社の社員の文字起こしくらいの精度出せるのでは? というのが今回の試みです。
毎年僕は年末年始にハッカソンを一人でして物を作ったりすることが多いのですが、今年は専門用語でも理解できるWhisperの作成が目標になりました。

今回はこの年末年始に行ったことを書いていこうと思います。

いきなり余談: 検証のためにWhisperでも文字起こしが難しい動画を探す方が難易度が高かった。Whisper凄すぎ。

まずはvalorant実況を試してみた。

まず専門用語だらけの会社の音声でもよかったのですが、音が悪かったりする部分もあり専門用語が悪いのか音質が悪いのか微妙なところなのと、会社の音声だと公開できないのでyoutubeにあるゲーム実況の動画にすることにしました。

最初にやろうと思ったのがvalorantの実況です。
ただこれはwhisperの凄さを体感するだけに終わりました。
以下のVCTの動画で試したのですが、
https://www.youtube.com/watch?v=3YLIC2kpzmA&t=4932s

これがWhisperで自動文字起こしした文章

結構ノーセプション側は攻めの時には7割方ぐらいですかね 70%ぐらいでA側に結構ノーセプション側は攻めの時には7割方ぐらいですかね 70%ぐらいでA側にセットアップを組むことが多いのでそのあたりもある程度情報収集しつつ行くでしょうねあと設置後のこのキルジョイあとはコントローラーですね アストラですねここでの解除遅延というところも非常に強力なこのセットを組めるノーセプションです

以下は僕が手で文字起こしした正解の文章

結構ノーセプション側は攻めの時には7割方ぐらいですかね 70%ぐらいでA側に結構ノーセプション側は攻めの時には7割方ぐらいですかね 70%ぐらいでA側にセットアップを組むことが多いのでそのあたりもある程度情報収集しつつ行くでしょうねあと設置後のこのキルジョイあとはコントローラーですね アストラですねここでの解除遅延というところも非常に強力なこのセットを組めるノーセプションです

いや完璧やん
なんでキルジョイとか解除遅延とかノーセプションとか完璧なの。お前さてはゲームみまくってるだろ。

ギリギリ間違えている所探してこれ

ガレージであったりとか まあファーストラウンドCロングあとはB中を抑えるということもできますしある時アラームボット あとはナノスワム2個そのあたりのこのボット周辺がかなり強いので自ら動くのもありですしお互いほぼほぼミラーですかあの相場かスカイの違いかというところになりましたね最近ピック率が減りましたけどね 平分相場そうですね まあドローンが短くなったというところもありますけど

これどこが間違えてるかわかりますか?ソーヴァが相場になってます。
ちなみにそこ以外完璧。Cロングとかナノスワームとかvalorantやってなければ人でも何言ってるのかよくわからんやろ。

あと、さらにガチ余談なんすけど、文章はあってるんですがなぜか兄者弟者という文字が入る部分があった。
これWhisperの元のデータセットにFPS系のyoutube動画入ってるのでは・・・FPSの兄者弟者と言えば思い当たるのいるなぁ

弟者)( アーそのチャンスを見て赤目が前に詰めました兄者)( まぁただここに入らせても兄者)( 最悪こうブリーチのフラッシュフォールドラインで 取り返せるよっていうところがあるんで兄者)( ノーセプションも別に兄者)( まぁ取られても ここから取り返せばいいじゃんっていうところの時間の使い方をしていくでしょう弟者)( あと手前からのトレイルブレイザーとかもありますからね 後ろのだからあろうがどれだけ

といわけでどうやらWhisper君はvalorantとか大分やりこんでるみたいなので、valorantで検証するのはやめました。
もっと独自用語だらけの意味不明な実況するゲームでやってやろう。

独自用語すぎて意味不明動画代表。クラロワ実況。

僕がよく見ているクラロワというゲームのyoutuberライキジョーンズさんの動画を利用しました。
https://youtu.be/LGM4MM-jD34
クラロワって独自用語知らないと結構意味わからんくて、一部書くと

結局ガゴポイズンなんだなっていうのとフェニレイジはどうなのかはちょっとわかんないけどさぁゴーストんーとこれはヒースピゴルナイ樽なんだ?三銃士かな

この一文でガーゴイル・ポイズン・フェニックス・レイジ・ゴースト・ヒースピ・ゴルナイ・ローリングバーバリアン・三銃士というカード名が出てきます。
流石にwhisperでもローリングバーバリアンが樽に入って転がってくるから樽ババと呼ばれていることまでは理解できるわけありません。
ちなみにクラロワ一ミリも知らない人間に文字起こしさせてみたらwhisperより酷かったのでwhisperは悪くなかったです。

以下Whisperで文字起こしした文章の一部

唯一警戒しなきゃいけないのは30死に対して工場過去とかをした時に30死受けされるのがめちゃくちゃ嫌なんですよねそうなんで30死に対してはとりあえずなんか1倍対魔特に3コストのねこういう

以下正解

唯一警戒しなきゃいけないのは三銃士に対して攻城ガゴとかをした時に三銃士受けされんのがめちゃくちゃ嫌なんですよねそうなんで三銃士に対してはとりあえずなんか1倍タイムは特に3コストのねこういう

いやー流石にきついね。三銃士が30死になってるし1倍タイムが1倍対魔になってるし。
ていうかこのライキジョーンズさんとかめちゃくちゃ面白いんですけど自分で言ってる独自用語も多すぎて文字起こしには大敵すぎる。

このライキジョーンズさんの動画音声ですら認識できるようにできたら製造業の独自用語なんて余裕余裕。というわけで試していく。

Whisperのファインチューニングのやり方をHugging Faceが公開してくれている

ありがたいことにWhisperをHugging FaceでFine Tuningする方法はめちゃくちゃちゃんと公開されてて、日本語データセットでやるなら以下記事が非常に参考になります。
https://dev.classmethod.jp/articles/whisper-fine-tuning-by-huggingface/

ただし、自分でデータセット作って試すというのはあんまり記事がないので、どういう風にデータセット作るのが効率いいかとかを考える必要がありました。

そもそもブログで公開されている方法を見る

ブログでは、MozillaのCommon Voiceからデータセットをとってくる方法を使っています。
以下ほとんどブログ内容なので詳細は割愛しますが、データの形が重要なのでそこだけ抜粋します。
ブログでは以下のようにデータセットを用意してます。

from datasets import load_dataset, DatasetDict

common_voice = DatasetDict()

common_voice["train"] = load_dataset("mozilla-foundation/common_voice_11_0"
    , "ja", split="train", use_auth_token=True)
common_voice["validation"] = load_dataset("mozilla-foundation/common_voice_11_0"
    , "ja", split="validation", use_auth_token=True)
common_voice["test"] = load_dataset("mozilla-foundation/common_voice_11_0"
    , "ja", split="test", use_auth_token=True)

Hugging Faceにはまず辞書に色々なメソッドが生えてデータを使いやすくしたようなDatasetというオブジェクトを使ってデータセットを用意します。
さらにそれをDatasetDictという、これまた辞書のようなオブジェクトでtrain,validation,testといったデータセットをそれぞれ格納します。

DatasetDict({
    train: Dataset({
        features: ['client_id', 'path', 'audio', 'sentence', 'up_votes', 'down_votes'
            , 'age', 'gender', 'accent', 'locale', 'segment'],
        num_rows: 6505
    })
    validation: Dataset({
        features: ['client_id', 'path', 'audio', 'sentence', 'up_votes', 'down_votes'
            , 'age', 'gender', 'accent', 'locale', 'segment'],
        num_rows: 4485
    })
    test: Dataset({
        features: ['client_id', 'path', 'audio', 'sentence', 'up_votes', 'down_votes'
            , 'age', 'gender', 'accent', 'locale', 'segment'],
        num_rows: 4604
    })
})

そしてこのデータセットの中身がこんな感じ

common_voice["train"][0]
{'client_id': '02a8841a00d762472a4797b56ee01643e8d9ece5a225f2e91c007ab1f94c49c99e50d19986ff3fefb18190257323f34238828114aa607f84fbe9764ecf5aaeaa',
 'path': '/root/.cache/huggingface/datasets/downloads/extracted/0bd2523a2521d7780854389657031a0664717a7eec1f7dc907e32a8ea92856aa/cv-corpus-11.0-2022-09-21/ja/clips/common_voice_ja_25310216.mp3',
 'audio': {'path': '/root/.cache/huggingface/datasets/downloads/extracted/0bd2523a2521d7780854389657031a0664717a7eec1f7dc907e32a8ea92856aa/cv-corpus-11.0-2022-09-21/ja/clips/common_voice_ja_25310216.mp3',
  'array': array([-2.1230822e-08,  1.3050334e-08,  1.8151752e-08, ...,
          2.8415348e-05,  3.8682865e-05,  2.0064232e-05], dtype=float32),
  'sampling_rate': 48000},
 'sentence': 'わたしは音楽がすきです。',
 'up_votes': 2,
 'down_votes': 0,
 'age': '',
 'gender': '',
 'accent': '',
 'locale': 'ja',
 'segment': ''}

こんな感じでaudioのarrayにその音声の配列、sentenceに正解のテキストが入っているという形です。
そして実際にこのブログではaudioのarrayとsentence以外除いているのでこれだけ用意します。

ただこの音声バイナリの配列とかどう作るのがいいのかとか色々考えてました。
最終的にスプレッドシートに音声データのパスと正解を書き、それをDataset化する方法で行いました。

Whiperのファインチューニングに必要なデータ

Whisperのファインチューニングがなぜやりやすいかというと、データセットの構造がかなり簡単に作れる点にあります。
以下公式のhugginfaceブログに書いてありますが

The Whisper feature extractor performs two operations. It first pads/truncates a batch of audio samples such that all samples have an input length of 30s. Samples shorter than 30s are padded to 30s by appending zeros to the end of the sequence (zeros in an audio signal corresponding to no signal or silence). Samples longer than 30s are truncated to 30s. Since all elements in the batch are padded/truncated to a maximum length in the input space, we don't require an attention mask when forwarding the audio inputs to the Whisper model. Whisper is unique in this regard - with most audio models, you can expect to provide an attention mask that details where sequences have been padded, and thus where they should be ignored in the self-attention mechanism. Whisper is trained to operate without an attention mask and infer directly from the speech signals where to ignore the inputs.

DeepL翻訳

Whisper特徴抽出器では、2つの処理を行う。まず、すべてのサンプルが30秒の入力長になるように、オーディオサンプルのバッチをパッド/トランケートする。30秒より短いサンプルは、シーケンスの末尾にゼロを追加することで30秒になるようにパッドされる(音声信号のゼロは無信号または無音に対応する)。30秒より長いサンプルは、30秒に切り詰められる。バッチ内のすべての要素が入力空間の最大長になるようにパディング/トランケートされるので、音声入力をWhisperモデルに転送する際にアテンションマスクは必要ありません。多くのオーディオモデルでは、アテンションマスクを提供することが期待されますが、これはシーケンスがパディングされ、自己アテンション機構で無視されるべき場所の詳細を示すものです。Whisperはアテンションマスクなしで動作するように訓練されており、入力を無視する場所を音声信号から直接推測します。

つまり普通の音声データセットだとアテンションマスクというどこに音声があるかというデータもセットにしてあげたりする必要があるのと、本来は30秒の音声にしなければいけないが、Whisperのデータセットの場合30秒以下なら30秒になるように勝手に無音を挿入し、30秒以上なら勝手に切ってくれるということで、結構適当にデータセットを作っても行けるということです。

必要なものは30秒以下の音声データと正解の文章のみ。なんて簡単なんだ。
実際common voiceは5秒くらいの文章と正解が大量にあるだけなので、これでも問題ないみたいです。
ただ一応大体30秒くらいになるように音声を切ってあげる方がwhisperのモデルに合うかな?くらいの気持ちで30秒に近くなるような音声データセットと正解の音声を作る作業をしました。

本編:Whisperを独自データセットでのファインチューニング:「データセット作成編」

前置きが長くなりましたが、ファインチューニングしていきます。基本環境はgoogle colabで行ってます。
基本的にgoogle driveとスプレッドシートを利用することで簡単にデータセットを作れるようにすると言うのが目標です。

ちなみにcolab proのハイメモリ版でやらないとwhipserのlargeモデルが落ちますので、pro版にはした方がいいかも

一旦colabでdriveをマウント

from google.colab import drive
drive.mount('/content/drive')

Step1: Pydubを使ってmp3をセンテンスごとに大体分割しスプレッドシートに書き込み

pydubは音声の編集等に使われるライブラリで、これを使ってセンテンスごとに音声を大体で区切ります。
pydubのインストール

!pip install pydub

まずはmp3でもなんでも音声データをpydubfrom_file()でAudioSegmentにします。
これはffmpegの音声コーデックと同じように対応しているらしく、自分でやった時はaacとかも第二引数をaacにしたら読めました。

from pydub import AudioSegment
source = AudioSegment.from_file("/content/drive/.../クラロワ/2.mp3", "mp3")

AudioSegmentはpydubの独自オブジェクトで、簡単に色々な操作ができます。
例えばこのAudioSegmentで10秒から30秒の間だけ音声切り出したければ

source[10*1000:30*1000]

というようにms単位の配列をスライスするようにするだけで切り出せます。
また、音声の合成等も簡単で、10秒から30秒の音声と40秒から50秒の音声を合成するなら

merged_source = source[10*1000:30*1000]+ source[40*1000:50*1000]

基本immutableオブジェクトだそうなので操作も安心です。

さらにpydubにはsplit_on_silenceという便利メソッドがあり、これによって大体無音の部分で音声を切ってくれます。pythonのsplit(" ")みたいなイメージで、それによって無音部分もなくなります。

from pydub.silence import split_on_silence
chunks = split_on_silence(source, 
                          min_silence_len = 100, # 無音部分の長さ(ms)
                          silence_thresh = -40, # 無音判定の閾値(dB)
                          keep_silence = 200) # 無音時間をどのくらい残すか(ms)

音声に関する知識が疎いのですが、デシベルってある基準に対しての大きさらしいのでマイナスもありえるそうです。知らんかった。ちなみにこれらの数字は結構適当にパクってきたんですが、結構これで問題なく、普通に大抵の動画の音声この数字でいけました。

このsplit_on_silenceの戻り値はAudioSegmentの配列で、これらを使って色々な操作ができます。
AudioSegmentの配列を利用し、30秒を超えるまで合算し続け、30秒超えたら出力するというのを繰り返すコードを書きます。

実験がてらで結構適当に書いてしまったので汚いですが、まあデータセットは意外と適当でもwhisperが優秀なのでなんとかなります。

outputDirname="/content/drive/.../data" #googleドライブのディレクトリのパスに置き換えて

if os.path.exists(outputDirname):
  shutil.rmtree(outputDirname)
  
os.makedirs(outputDirname,exist_ok=True)

fileList=[]
# 30秒ごとにchunkを連結
current_chunk = None

for i, chunk in enumerate(chunks):
    if current_chunk is None:
      current_chunk = chunk
    else:
      current_chunk+=chunk
    
    # current_chunkを連結したものが30秒以上になるなら出力
    if len(current_chunk)/1000>30:
      # 30秒区切りにする場合何個になるかを計算
      num_of_chunk = int(len(current_chunk)/30000)
      # 30秒ごとに区切って出力
      for j in range(num_of_chunk+1):
        outFilePath = f'{outputDirname}/out_{i + 1}_{j + 1}.wav'
        current_chunk[j*30000:(j+1)*30000].export(outFilePath, format="wav")
        fileList.append(outFilePath)
      current_chunk = None

ポイントはここです。

current_chunk[j*30000:(j+1)*30000].export(outFilePath, format="wav")

AudioSegmentは先ほど言った通り30*1000ミリ秒で30秒をcurrent_chunk[0:30000]とかやると切り出せるのですが、切り出したAudioSegmentはexportメソッドで保存できます。まじ便利。
それら保存したfileのpathをfilelistという配列に入れます。

こうするとoutputDirnameで指定したパスの中にファイルが保存されます。以下イメージ。

Step2: 作った音声データを一旦もとのwhisperで推論させる

これはなくてもいいのですが、音声データをゼロから文字起こしするとめっちゃ時間かかるんですが、予めwhisperに読ませておいてそれの間違っているところを修正するやり方だと非常に簡単に正解のテキストデータが作れます。
そのため一旦元のwhipserを使って文字起こしさせておきます。

from datasets import  Dataset,Audio
from whisper.audio import N_FRAMES, pad_or_trim, log_mel_spectrogram
import whisper
from whisper.tokenizer import get_tokenizer

model = whisper.load_model("large")
datasets = Dataset.from_dict({"audio": fileList}).cast_column("audio", Audio())
predict_data = []
for i, f in enumerate(fileList):
  mel = log_mel_spectrogram(f)
  # 30秒データに整形
  segment = pad_or_trim(mel, N_FRAMES).to(model.device).to(torch.float16)
  # デコード
  result = model.decode(segment)
  # トークナイザ取得`
  tokenizer = get_tokenizer(multilingual=True, language="ja", task="transcribe")
  # トークナイザのデコード
  outputText = tokenizer.decode(result.tokens)
  predict_data.append({**datasets[i], "sentence":outputText})

ポイントとしては、

model = whisper.load_model("large")

まずはwhisperのモデルをロードします。基本largeでやった方がいいと思います。

datasets = Dataset.from_dict({"audio": fileList}).cast_column("audio", Audio())

その後30秒ごとに保存した音声データを配列にしてデータセット作るのですが、
これはHugging Faceのデータセットオブジェクトが便利で、audioのファイルパスの配列からaudioのデータセットを自動生成してくれます。
まずfrom_dictfrom_pandasというメソッドがあるのですが、これによってデータセットの辞書を作ることができます。
今回の場合、audioというカラムにファイルパスが入ってるのですが、これに対してcast_column("audio",Audio())というメソッド実行をすると、

{"audio":"/path/to/file"}

という形が

{
"audio":{
    "path":"/path/to/file",
    "array":[0.3131,0.6655...],
    "sampling_rate": 44000
}
}

という形に変換されます。
これによりあとでファインチューニングする時もめっちゃ簡単にできます。HuggingFaceすげえ。

そこから以下のようにwhisperでの推論をします。

for i, f in enumerate(fileList):
  mel = log_mel_spectrogram(f)
  # 30秒データに整形
  segment = pad_or_trim(mel, N_FRAMES).to(model.device).to(torch.float16)
  # デコード
  result = model.decode(segment)
  # トークナイザ取得`
  tokenizer = get_tokenizer(multilingual=True, language="ja", task="transcribe")
  # トークナイザのデコード
  output_text = tokenizer.decode(result.tokens)

これはwhisperが内部で行っている処理をほぼパクっているだけなので深く理解しなくてもいいのですが、まず
pad_or_trimのところで30秒以下や以上の音声を30秒に揃えます。
その後whisperによってその作ったsegmentを推論すると、最終的にoutput_textに推論されたデータが入ります。

そのoutput_textをdatasetから辞書を作って、sentenceカラムを追加します。

  predict_data.append({**datasets[i], "sentence":output_text})

これによってpredict_dataには

[
{
"audio":{
    "path":"/path/to/file",
    "array":[0.3131,0.6655...],
    "sampling_rate": 44000
},
"sentence":"これはwhisperが推測したテキストです。"
},...
]

みたいな感じでwhisperが推測したテキストとaudioが入った辞書が入ります。

Step3: これらのデータをスプレッドシートに書き込み

推測したデータを用いてスプレッドシートに書き込みます。

from google.colab import auth
auth.authenticate_user()
import gspread
from google.auth import default
creds, _ = default()
gc = gspread.authorize(creds)

from subprocess import getoutput
!apt-get install xattr > /dev/null

spread_sheet_dir_id = getoutput("xattr -p 'user.drive.id' " + "'" + outputDirname[:-5] + "'")
ss = gc.create('学習データ', folder_id=spread_sheet_dir_id)
ss.values_append("シート1", {"valueInputOption": "USER_ENTERED"}, {"values": list(map(
    lambda out: [
        f"""https://drive.google.com/file/d/{getoutput("xattr -p 'user.drive.id' " + "'" + out["audio"]["path"] + "'")}""",
        out["audio"]["path"], 
        out["audio"]["sampling_rate"],
        "",
        out["sentence"]
    ], correct_data))})

ポイントとしては

from google.colab import auth
auth.authenticate_user()
import gspread
from google.auth import default
creds, _ = default()
gc = gspread.authorize(creds)

colabではspreadsheet等も簡単に扱うことができ、このコード実行すると認証画面が開き、gcにスプレッドシートを操作するオブジェクトが入ります。

from subprocess import getoutput
!apt-get install xattr > /dev/null

spread_sheet_dir_id = getoutput("xattr -p 'user.drive.id' " + "'" + outputDirname[:-5] + "'")

さらにここもちょっと癖があるのですが、outputDirのフォルダに学習データをおきたかったので、spread_sheetのディレクトリidを取るためにcolabのファイルパスからdriveのディレクトリidを推測します。
そのためdriveのリンクを取得する必要があるのですが、他の方法もある気がするのですが、ファイル拡張属性に持たせているらしく、xattrで取れることがわかりました。
それをsubprocessで実行しgetoutputするとリンクが取得できます。

最後にそれを利用してspreadsheetを作ります。

ss = gc.create('学習データ', folder_id=spread_sheet_dir_id)
ss.append_row( values= ['url', 'colab_path','sampling_rate','correct','whisper'] )
ss.values_append("シート1", {"valueInputOption": "USER_ENTERED"}, {"values": list(map(
    lambda out: [
        f"""https://drive.google.com/file/d/{getoutput("xattr -p 'user.drive.id' " + "'" + out["audio"]["path"] + "'")}""",
        out["audio"]["path"], 
        out["audio"]["sampling_rate"],
        "",
        out["sentence"]
    ], correct_data))})

spread_sheetを新規作成するのですが、先ほどのディレクトリidをfolder_idという引数に入れるとそのディレクトリにシートを作成することができます。
そしてヘッダー行を1行追加します。
urlはgoogle driveにおけるリンク、colab_pathはcolabでそのファイルにアクセスするパスを表します。correct列は後で正解のテキストを手動で入れます。whisperはwhisperで推論したテキストを入れます。

ss = gc.create('学習データ', folder_id=spread_sheet_dir_id)
ss.append_row( values= ['url', 'colab_path','sampling_rate','correct','whisper'] )

次に値を書き込みます。


ss.values_append("シート1", {"valueInputOption": "USER_ENTERED"}, {"values": list(map(
    lambda out: [
        f"""https://drive.google.com/file/d/{getoutput("xattr -p 'user.drive.id' " + "'" + out["audio"]["path"] + "'")}""",
        out["audio"]["path"], 
        out["audio"]["sampling_rate"],
        "",
        out["sentence"]
    ], correct_data))})

そもそもスプレッドシートにはリクエスト制限があり、1行1行書き込むとすぐに超過してしまいます。
そのため配列のデータとかを書き込みたい場合values_appendを使います。
これは

ss.values_append("シート1", {"valueInputOption": "USER_ENTERED"}, {"values":[["aaa",1] , ["bbb",2]]})

という感じでvaluesに2重配列を入れるとそれを

aaa 1
bbb 2

というように値に入れてくれます。

google driveの音声データのリンクをスプレッドシートのセルに入れておくことで音声データをクリックすれば聞けるようになるので、やりやすくなります。
https://drive.google.com/file/d/{file_id}でdriveのリンクになるので、xattrでfile_idを取ってリンクを作成します。

f"""https://drive.google.com/file/d/{getoutput("xattr -p 'user.drive.id' " + "'" + out["audio"]["path"] + "'")}"""

そのリンクと他のデータをまとめて保存します。

ss.values_append("シート1", {"valueInputOption": "USER_ENTERED"}, {"values": list(map(
    lambda out: [
        f"""https://drive.google.com/file/d/{getoutput("xattr -p 'user.drive.id' " + "'" + out["audio"]["path"] + "'")}""",
        out["audio"]["path"], 
        out["audio"]["sampling_rate"],
        "",
        out["sentence"]
    ], correct_data))})

1カラム空文字を入れているのはここに自分で文字起こしした正解文字列を入れるためです。
これで保存されたのがこんな感じです。

このスプレッドシートは一番左のurlの列をクリックすると音声データが聞けるので、whisperのテキストを一回正解の列にコピペした後修正していくみたいな使い方をすると結構簡単に文字起こしができます。

実際に手で書いたのがこちら

correctに正解テキストの音声を入れました。30分の動画で50分くらいかかりましたが、逆にその程度ですみました。
whisperはかなり精度がいいので、実際一回聞けば修正できる程度だったのでめちゃくちゃ楽でした。

これをもとにwhisperを学習させてみます。
続きはこちら

Discussion