アライさんになりきってみたかった(前編)
あらまし
2019年4月頃からTwitterなどで認知されてきたアライグマの「アライさん」のなりきり。 いくつかのポイントを押さえるだけで、簡単にかわいいキャラクターになりきれることが魅力です。 その一方、Twitterではテキストのみが対象なので、見た目などの要素を増やして、よりアライさんに近づきたい要求もあると考えられます。
そこで、本プロジェクトでは、声の質を変換する音声変換と呼ばれる技術を用いて、声の側面でアライさんになりきる方法を提案します。 本記事(前編)でデータの抽出、続く後編でyukarinライブラリによる音声変換を行います。
なお、前後編ともに結果が悪いため「なりきってみたかった」としました。 使える結果はここに無いので、探している人はごめんなさいなのだ。
前編では、次の手順で目的話者(アライさん)の声だけを切り出すことを試みました。
この手順をアニメ1期の初登場シーン(第1話最終パート)に適用しました。 結果を表す図です(横に長いので拡大推奨)。
背景の色(赤/青)は推定結果を、図下部は手動で付けたセリフを表します。 セリフの上段はアライさん、下段はフェネックのものです。 話者推定が上手くいっていないことが分かります。
(追記) 後編をアップロードしました。
目次
はじめに
理由は調べていませんが、2019年4月頃から自らを「アライさん」と名乗るアカウントの増加がTwitterで観測されました。 キャラクターそのものの可愛さや、語尾に「なのだ」と付ければなりきりが成立する手軽さが要因だと言われています[用出典]。
テキストで手軽になりきれる一方で、見た目などの要素をよりアライさんに近づけたい要求もあると考えられます。 そこで、本プロジェクトは、音声変換によって声の側面でアライさんになりきる方法を提案し、その方法がどこまで有効かを検証します。
抽出手順
あらましに示した通り、BGMの音量低下、x-vectorの分析の2ステップで抽出を行います。 以下、それぞれのステップの詳細を説明します。
BGMの音量低下
アニメの音声にはBGMなどの声でない成分が含まれており、音声認識では雑音として入力されてしまいます。 そのため、まずは以前実装した信号分離モデルChimeraNet(実装レポート1、リポジトリ2)でBGMの音量を低下させることから始めます。
こんな風に。
x-vectorのクラスタリングによる対象話者音声の取り出し
BGMを弱くした後、アライさんの発話のみを取り出す作業を行います。 非常に長いので次の結果セクションに移っていただいても構いません。 (x-vectorの抽出について、より詳しいチュートリアルを別記事に示しています。)
本記事では、音声処理プラットフォームKaldiが提供するx-vectorに基づいて行います。 Kaldiのissue3を参考に処理を行います。
Kaldiのインストール
- https://github.com/kaldi-asr/kaldi からクローン
- tools/INSTALL、src/INSTALLにある手順でインストール(UNIX系)
学習済みモデルのダウンロード
- https://kaldi-asr.org/models/m8 のSITW Xvector System 1aをダウンロード/展開
- 0008_sitw_v2_1a/exp を kaldi_root/egs/callhome_diarization/v2 にコピー
コマンドを実行
shellで以下を実行します。まずはKaldiが読み取るファイルの準備から。
cd ~/Repos/kaldi/egs/callhome_diarization/v2 # ~/Repos/kaldiにクローンしたとする export name=arai1 export wav_source=/media/sf_Desktop/$name # wavファイルがあるディレクトリ export sitw_dir=../../sitw/v2 # prepare for segmentation # wav.scp, spk2utt, utt2spk, segments を data/arai1 下に用意 # ファイルの仕様は https://kaldi-asr.org/doc/data_prep.html を参照 mkdir data/$name # script to make wav.scp export sox_conv_script=$(cat <<- _PY_ import os, sys print('\n'.join( os.path.splitext(os.path.basename(l))[0]+' sox ' +l +' -r 16k -t wav -e signed-integer -b 16 - |' for l in sys.stdin.read().splitlines() )) _PY_ ) find $wav_source -type f | python3 -c "$sox_conv_script" > data/$name/wav.scp awk '{print $1, $1}' data/$name/wav.scp > data/$name/spk2utt utils/spk2utt_to_utt2spk.pl data/$name/spk2utt > data/$name/utt2spk utils/data/get_utt2dur.sh data/$name awk '{print $1, $1, "0.0", $2}' data/$name/utt2dur > data/$name/segments # segmentation steps/make_mfcc.sh --mfcc-config $sitw_dir/conf/mfcc.conf data/$name steps/compute_vad_decision.sh --vad-config $sitw_dir/conf/vad.conf data/$name diarization/vad_to_segments.sh data/$name data/${name}_seg # extracting x-vector # data/arai1 下に utt2spk, spk2utt を準備 awk '{print $1, $1}' data/${name}_seg/segments > data/${name}_seg/utt2spk utils/utt2spk_to_spk2utt.pl data/${name}_seg/utt2spk > data/${name}_seg/spk2utt # prepare_feats.sh and extract_xvector.sh local/nnet3/xvector/prepare_feats.sh data/${name}_seg data/${name}_cmn exp/${name}_cmn cp data/${name}_seg/segments data/${name}_cmn/ diarization/nnet3/xvector/extract_xvectors.sh \ --config exp/xvector_nnet_1a/extract.config \ --window 2.0 --period 0.5 \ exp/xvector_nnet_1a data/${name}_cmn exp/${name}_xvector
次は各々のx-vectorをクラスタリングすることによって、どの区間で誰が喋っているのかを推定します。 Kaldi標準のクラスタリングアルゴリズム(diarization/cluster.sh)もありますが、 ここでは雑音に強いといわれる4スペクトラルクラスタリングを使います。
- クラスタリングのため、exp/arai1_xvector/wav2nspk (<録音ID(wav.scpの1列目)> <話者数>のスペース区切り)を次のように作成
ep01.pt03.wav 2 ep03.pt04.wav 3 ... (以下略)
- 以下のスクリプトを保存/実行
x-vectorのクラスタリングのより詳しいチュートリアルを後で別記事にて示します。
#!/usr/bin/env python3 import os import sys import glob import numpy as np import subprocess from collections import Counter from scipy.spatial.distance import cosine from sklearn.cluster import SpectralClustering from sklearn.metrics import pairwise_distances if len(sys.argv) < 2: exit(1) xvector_dir = sys.argv[1] copy_vector = '../../../src/bin/copy-vector' # load binary x-vector from xvector_dir contents = ''.join( subprocess.Popen( [copy_vector, 'ark:{}'.format(f), 'ark,t:'], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL ).communicate()[0].decode('utf-8') for f in glob.glob(os.path.join(xvector_dir, 'xvector.*.ark')) ) vectors = dict( (c.split()[0], np.array(list(map(float, c.split()[2:-1])))) for c in contents.splitlines() ) # load from sub-segments subsegments_file = os.path.join(xvector_dir, 'segments') with open(subsegments_file, 'r') as fp: seg_tuples = [l.split() for l in fp.read().splitlines()] seg_data = dict((s[0], s[1:]) for s in seg_tuples) wav_to_segs = dict() for k, v in seg_data.items(): if v[0] not in wav_to_segs: wav_to_segs[v[0]] = [] wav_to_segs[v[0]].append(k) # load from wav2nspk to get the number of speakers per wav wav2nspk_file = os.path.join(xvector_dir, 'wav2nspk') with open(wav2nspk_file, 'r') as fp: wav_to_nspk = list(zip(*[l.split() for l in fp.read().splitlines()])) wav_to_nspk = dict(zip(wav_to_nspk[0], map(int, wav_to_nspk[1]))) # clustering for w in wav_to_segs: n_cluster = wav_to_nspk.get(w, 1) subseg, X = zip(*[ (k, v) for k, v in vectors.items() if seg_data[k][0] == w ]) X = np.array(X) Xmat = np.exp(1 - pairwise_distances(X, metric='cosine')) model = SpectralClustering(n_cluster, affinity='precomputed').fit(Xmat) labels = dict(zip(subseg, model.labels_)) for k in labels.keys(): print('{} {} {} {}'.format(*seg_data[k], labels[k]))
python3 clustering-xvector.py exp/${name}_xvector > labels.txt
labels.txtは
<録音ID(ファイル名)> <開始時刻> <終了時刻> <ラベル>
の列です。
- audacity用にラベルを作成
mkdir label_audacity awk '{print $1}' labels.txt | sort | uniq | \ xargs -IX sh -c "grep X labels.txt | awk '{printf(\"%s\\t%s\\t%s\\n\", \$2, \$3, \$4)}\' > label_audacity/X.label.txt"
結果
ラベルをもとにどの区間で誰が喋っているのかの推定を手動で可視化しました。 用いた音声データはアニメ1期の初登場シーン(第1話最終パート)のものです。 横に長いので拡大してご覧ください。
背景の色(赤/青)は推定結果を、図下部は手動で付けたセリフを表します。 セリフの上段はアライさん、下段はフェネックのものです。 話者推定が上手くいっていないことが分かります。
どうしてこうなった
- データの特性の相違
- BGM除去が低品質
- 操作ミス
おわりに/次回予告
手動で切り出した音声とyukarin7を用いて音声変換を行うことでアライさんになりきります。なりきれませんでした