音声データセットMozilla Common Voiceに関する覚え書き
初めての方、初めまして。そうでない方、こんれる🐻
バ美肉凡夫VTuberの夜御牧(やみまき)れるです。
Mozilla Common VoiceはCC0(≒パブリックドメイン=著作権放棄)で提供されている音声データセットです。運営しているのはWebブラウザFirefox(最盛期はIEに次ぐシェアのあった……)の開発などを手がけているMozilla Foundationです。
「話者の身元を特定しないこと」という条件はありますが、利用規約上各話者はCC0で公開されることに自ら同意しており、かつこのプロジェクトに参加したりしなかったりすることで何らかの不利益(たとえば、Firefoxが使えなくなるとか)が発生するわけでもない字義通りのボランティア※によるものです。
※ボランティアの原義は「志願兵」だよ! 「ただ働きする人」じゃないよ!(なので別に有償ボランティアは矛盾ではないのである……Common Voiceには何も直接的なリワードもありませんが)
2023年4月25日の時点で、検証済みの音声だけで全世界17,000時間という大規模なデータセットです。
が、あまりに巨大すぎるために凡夫には扱いにくい部分もあったので、悩んだところとその対応について覚え書きを書いたのがこちらの記事になります。
Mozilla Common Voiceの内容
全世界で何万時間という膨大なデータですので、全世界ではなくまず言語ごとに分かれています。
バージョン13.0では、日本語話者は73時間・3.42GBですが、英語話者の検証済み音声はなんと2,429時間・76.39GB(!)もあります。
日本語版だと同一話者で1000ファイル以上あるものはあまりないので、今回は英語話者のデータを用いました。が、でかい……。
さすがに数十GBもあると、その倍のストレージ容量を空けて一括ダウンロードして展開して……は躊躇しますが、Hugging Faceからであれば音声MP3本体は4万ファイル毎に分割したtarballをダウンロードできます。
ただし、Hugging FaceからのDLにはアカウント登録が必要です。
言語とは別に、データセット内の物理ファイルは大きく分けて2つのデータに分かれています。
audio 発話1件ごとのMP3音声。64kbps・48kHz・16ビット・モノラル。
transcript 発話された文章や、その他のメタデータ(話者のID、性別、年齢層などの属性や、検証者の評価)。
TSV(タブ区切りデータ)という若干マイナーな形式ですが、CSVの区切り文字をタブに変えただけなので、普通にExcel等の表計算ソフトで読めます。
また、各データは検証結果などに応じ、invalidated, test, trainなどに分類されています。
このうち、検証結果良好なのはvalidatedまたはdev, test, trainで、特にデータが多いものはtrainなので、とりあえずtrainを使っておけば問題ないでしょう。
困ったこと
録音品質がまちまち
ご厚意で参加されているボランティアであって別に発話のプロフェッショナルではないので、録音品質がまちまちです。そもそもが音声認識用に集められているデータです。リップノイズや反響音みたいな機械的に取り切らないノイズはもちろんのこと、データによっては環境音もけっこう入ってたりします。
こればかりは仕方ないので、ある程度録音の質が良いデータを絞りつつ、多少ノイズが入っててもいいような使い方をするほかないですね。。
メタデータTSVが巨大で処理に時間がかかる
transcript内のTSV(タブ区切りデータ)は、CSVの区切り文字をタブに変えただけなのでExcelなどの表計算ソフトで読めますが、言語によってはデータ量が多すぎ、読めるだけになってほぼビューワとしてしか使えません。
ExcelやLibreOfficeは105万行弱までのデータしか扱えませんが、英語話者のtrain.tsvは100万行ちょっとあります。ギリギリです。
Googleスプレッドシートは1,000万セルしか扱えません。
いちおう、LibreOfficeで100万行まるまる読み込ませてもピボットテーブルは使えました。さすが……❣
しかし100万行のデータとなると読み込みと保存だけで数十秒待たされます。Googleスプレッドシートに変換しようとしたら大きすぎると怒られました(手作業で分割して写せばいけたかもしれませんが)。
100万行に下手な数式を打とうものなら時間単位で固まります。1行10ミリ秒の処理だとしても3時間ぐらいかかる計算になりますからね。。
最初から絶対使わないなと思ったデータは削りつつ、重い突合処理はPython書いてやってました。
MP3が多すぎてエクスプローラが重い
NTFSは辞書順ソート(文字コード順ソート)なはずですが、一つのフォルダに何万ファイルも入っているとファイル名が分かっていてもなかなか見つからなかったりします。
パスが分かっている場合、GUIに頼るほうがかえって手間がかかってしまうので、DOS窓開いて「start (MP3のパス)」一択でした。
MP3がサブフォルダに分かれている
バージョン13.0ですと日本語話者のMP3はそんなに多くないので一つのフォルダにまとまっていますが、英語話者は100万件ちょっともあるので4万件ごとにサブフォルダに分かれています。
だもので、ファイル名が分かっても、そのファイルがどのフォルダにあるかも知らないとパス指定でMP3を開けません。
そのため、フォルダごとのファイル一覧を書き出して、それとTSVに書かれているファイル名を突き合せていました。
カレントフォルダ以下のフォルダごとのファイル名一覧を書き出すやつ。
#!/usr/bin/env python
# Common Voiceの「ファイル名,ディレクトリ名」のCSVを吐き出すために書いたスクリプト
import os
import math
def main():
buf = ''
dir_count = 0
cur_dir = os.path.dirname(__file__)
with open(os.path.join(cur_dir, 'files.csv'), mode='w') as f:
with os.scandir(cur_dir) as dir_it:
for dir_ent in dir_it:
if not dir_ent.is_dir():
continue
dir_count += 1
for file_ent in os.listdir(os.path.join(cur_dir, dir_ent)):
buf += f"{file_ent},{dir_ent.name}\n"
f.write(buf)
buf = ''
if __name__ == '__main__':
main()
上のスクリプトの出力と、カレントフォルダの「target.csv」(ファイル名のみ1列)を突き合せ、各ファイルの親フォルダを書き出すやつ。
#!/usr/bin/env python
# Common Voiceの各mp3が入っているフォルダを探しCSVを吐き出すために書いたスクリプト
import os
def main():
file_dir_map = dict()
cur_dir = os.path.dirname(__file__)
with open(os.path.join(cur_dir, 'files.csv')) as f:
for line in f:
line = line.strip()
if 0 == len(line):
break
(fname, dname) = line.split(',')
file_dir_map[fname] = dname
buf = ''
with open(os.path.join(cur_dir, 'targets.csv')) as f:
for line in f:
line = line.strip()
if 0 == len(line):
break
buf += file_dir_map[line] + "\n"
with open(os.path.join(cur_dir, 'out-targets.csv'), mode='w') as f:
f.write(buf)
if __name__ == '__main__':
main()
おまけ。同じフォルダにある「to-copy.csv」(フォルダ名,ファイル名の2列)で指定されたMP3を作業フォルダ「C:\work\in\cv」にコピーするやつ。
#!/usr/bin/env python
# Common Voiceの各mp3を作業用フォルダにコピーするためのスクリプト
import os
import shutil
def main():
cur_dir = os.path.dirname(__file__)
dest_dir = 'C:\work\in\cv'
os.makedirs(dest_dir, exist_ok=True)
with open(os.path.join(cur_dir, 'to-copy.csv')) as f:
for line in f:
line = line.strip()
if 0 == len(line):
break
(dname, fname) = line.split(',')
shutil.copy(os.path.join(cur_dir, dname, fname),
os.path.join(dest_dir, fname))
if __name__ == '__main__':
main()
バ美肉VTuber夜御牧れるはMozilla Common Voiceを応援しています(ちゆ12歳構文)。