アライさんになりきってみたかった(前編)

あらまし

2019年4月頃からTwitterなどで認知されてきたアライグマの「アライさん」のなりきり。 いくつかのポイントを押さえるだけで、簡単にかわいいキャラクターになりきれることが魅力です。 その一方、Twitterではテキストのみが対象なので、見た目などの要素を増やして、よりアライさんに近づきたい要求もあると考えられます。

そこで、本プロジェクトでは、声の質を変換する音声変換と呼ばれる技術を用いて、声の側面でアライさんになりきる方法を提案します。 本記事(前編)でデータの抽出、続く後編でyukarinライブラリによる音声変換を行います。

なお、前後編ともに結果が悪いため「なりきってみたかった」としました。 使える結果はここに無いので、探している人はごめんなさいなのだ。

前編では、次の手順で目的話者(アライさん)の声だけを切り出すことを試みました。

  1. ChimeraNetでBGMを弱くする
  2. Kaldiが提供するx-vectorによって話者推定

この手順をアニメ1期の初登場シーン(第1話最終パート)に適用しました。 結果を表す図です(横に長いので拡大推奨)。

f:id:leichtrhino:20191005143326p:plain

背景の色(赤/青)は推定結果を、図下部は手動で付けたセリフを表します。 セリフの上段はアライさん、下段はフェネックのものです。 話者推定が上手くいっていないことが分かります。

(追記) 後編をアップロードしました。

目次

はじめに

理由は調べていませんが、2019年4月頃から自らを「アライさん」と名乗るアカウントの増加がTwitterで観測されました。 キャラクターそのものの可愛さや、語尾に「なのだ」と付ければなりきりが成立する手軽さが要因だと言われています[用出典]。

テキストで手軽になりきれる一方で、見た目などの要素をよりアライさんに近づけたい要求もあると考えられます。 そこで、本プロジェクトは、音声変換によって声の側面でアライさんになりきる方法を提案し、その方法がどこまで有効かを検証します。

抽出手順

あらましに示した通り、BGMの音量低下、x-vectorの分析の2ステップで抽出を行います。 以下、それぞれのステップの詳細を説明します。

BGMの音量低下

アニメの音声にはBGMなどの声でない成分が含まれており、音声認識では雑音として入力されてしまいます。 そのため、まずは以前実装した信号分離モデルChimeraNet(実装レポート1リポジトリ2)でBGMの音量を低下させることから始めます。

こんな風に。

f:id:leichtrhino:20191008114243p:plain
元音声

f:id:leichtrhino:20191008114331p:plain
BGM除去後

x-vectorクラスタリングによる対象話者音声の取り出し

BGMを弱くした後、アライさんの発話のみを取り出す作業を行います。 非常に長いので次の結果セクションに移っていただいても構いません。 (x-vectorの抽出について、より詳しいチュートリアルを別記事に示しています。)

本記事では、音声処理プラットフォームKaldiが提供するx-vectorに基づいて行います。 Kaldiのissue3を参考に処理を行います。

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スペクトラルクラスタリングを使います。

以下の手順でx-vectorクラスタリングを行います。

  • クラスタリングのため、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(ファイル名)> <開始時刻> <終了時刻> <ラベル>

の列です。

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話最終パート)のものです。 横に長いので拡大してご覧ください。

f:id:leichtrhino:20191005143326p:plain

背景の色(赤/青)は推定結果を、図下部は手動で付けたセリフを表します。 セリフの上段はアライさん、下段はフェネックのものです。 話者推定が上手くいっていないことが分かります。

どうしてこうなった

  • データの特性の相違
    • x-vectorの学習に用いられたvoxcelebとsitwデータセットは主に英語
      • 言語が違っていても良い結果(等価エラー率10%程度)が得られることを確認しました。詳しくは別記事で。
  • BGM除去が低品質
    • BGMを除去した音声はマスクがかかって"自然"でないため、何らかの方法で"自然"にしたい(S. Seki et al., MLSP, 2017.5など)
    • 本実験で採用したマスクはIBM(Ideal Binary Mask)と呼ばれるものですが、ChimeraNetはIBMと同時にIRM(Ideal Real Mask)も推論します。そして結果もIRMの方が優れていると報告されています6
  • 操作ミス
    • 2択で(主観的な)正解率50%は致命的な何かを見落としている可能性が
    • 話者"照合"といえば準備した正解データを照合する作業でクラスタリングは不向きなのでは
      • この仮説を支持する結果が得られました。詳しくは別記事をご覧ください。

おわりに/次回予告

手動で切り出した音声とyukarin7を用いて音声変換を行うことでアライさんになりきります。なりきれませんでした