CCCMKホールディングス TECH Labの Tech Blog

TECH Labスタッフによる格闘記録やマーケティング界隈についての記事など

ブログタイトル

NEUTRINO(AI きりたん)を使ってテキスト読み上げをしてみた

こんにちは。技術開発チームの高橋です。

AI きりたん、流行っていますね。

youtube などでたくさんの音源が公開されていますが、とても人間らしい歌声のものが多く、非常に驚きました。

自分もこのような曲を作ってみたい!と思ったのですが、残念ながら、自分は作曲や調声を行う技術を持ち合わせていません。そこで、まずはできることからということで、 手元の環境で NEUTRINO の実行環境を作成し、テキストの読み上げを行ってみました。

はじめに

NEUTRINOはニューラルネットワークを用いた歌声合成ソフトです(実装は非公開、のはず...)。 ライブラリとして東北きりたんを選択でき、こちらを用いた作品群が俗に AI きりたんと呼ばれているようです。

NEUTRINO はMusicXMLというデータ形式を入力として用いることができます。

MusicXML はMuseScoreなどのソフトを使って打ち込むことができますが、今回はテキストを MusicXMLに流し込むスクリプトを作成し、打ち込みの手間を無くすことを目標としました。

環境設定

まずは公式サイトに従って NEUTRINO をインストールしてください。

自分の環境は Ubuntu だったため

https://n3utrino.work/596/

の手順に従って設定を行いました。

具体的な環境は以下のとおりです。

OS: Ubuntu 18.04 LTS
GPU: GeForce RTX 2060
gpu-driver: NVIDIA 435.21
cuda: 10.1

MusicXML生成スクリプトの実行には Python を用いました。

Python 3.6.9

MusicXMLの作成

試しに MuseScore で簡単な楽譜を作成してみます。

f:id:takahashii:20200427131326p:plain
楽譜

こちらを export すると、出力したMusicXMLは以下のような内容になっています。

f:id:takahashii:20200427131330p:plain
musicxml

ここで、measure タグが小節、note タグが音符に対応しています。

そこで、この measureタグ、noteタグ をテキストから生成し、擬似的にスコア化することで、NEUTRINO での読み上げを行うこととします(音の高低は無視します)。

テキストの前処理

今回は MusicXMLを 1 小節 8 音として作成することとし、それに合わせてテキストをチャンクにします。

例を出すと、

"こんにちわみなさん きょうわ よい ひです"

->

[["こ", "ん", "に", "ち", "わ", "み", "な", "さ"], ["ん"], ["きょ", "う", "わ"], ["よ", "い"], ["ひ", "で", "す"]]
  • スペース -> 分割
  • 拗音、促音 -> 1 音にまとめる
  • 8 音以上 -> 8 音毎に分割

というルールを設定しました。 入力するテキストは、ひらがなのみ対応します。また、NEUTRINOが認識できるよう、"は" -> "わ"というように表記と音がずれる場合は、実際の音に合わせて読み替えて入力してください。

(計算量などを無視して)こちらを実装したものが、以下です。

def chunks(lst, n):
    # https://stackoverflow.com/questions/312443/how-do-you-split-a-list-into-evenly-sized-chunks
    for i in range(0, len(lst), n):
        yield lst[i:i + n]


def chunk_text(text):
    skip_letters = ('ゃ', 'ゅ', 'ょ', 'っ')
    max_chunk_length = 8
    splited_texts = text.split()
    result = []
    for splited_text in splited_texts:
        buf = []

        for i, x in enumerate(splited_text):
            if x in skip_letters:
                continue

            if i < len(splited_text) - 1 and splited_text[i+1] in skip_letters:

                buf.append(splited_text[i: i+2])
                continue

            buf.append(x)

        for chunk_text in chunks(buf, max_chunk_length):
            result.append(chunk_text)

    print(result)
    return result

xml の生成

次に、上のテキストデータを xml に変換します。

Python3.6 には xml.etree.ElementTree という xml を操作するライブラリが同梱されていますので、それを用いると、

import xml.etree.ElementTree as ET

tree = ET.parse('sample1.xml'))  # NEUTRINOのサンプルファイルをロード
root = tree.getroot()
part = root.find('part')
measure = ET.SubElement(part, 'measure')
measure.text = 'any text'

のように、xml を読み込み、タグを追加・編集することができます。

あとは 楽譜の表示情報などは無視し、最低限 NEUTRINO の入力として成立する MusicXML を出力できるように実装していきます。

class Lyric:
    def __init__(self, letter):
        self.default_x = ''  # 表示情報は設定しない
        self.default_y = ''
        self.relative_y = ''
        self.number = '1'
        self.syllabic = 'single'
        self.text = letter

    def set_el(self, element):
        element.set('default-x', self.default_x)
        element.set('default-y', self.default_y)
        element.set('relative-y', self.relative_y)
        element.set('number', self.number)
        syllabic = ET.SubElement(element, 'syllabic')
        syllabic.text = self.syllabic
        text = ET.SubElement(element, 'text')
        text.text = self.text
        return element

class Pitch:
    # 略


class Note:
    # 略


class Measure:
    def __init__(self, text):
        self.number = '100'
        self.width = ''
        self.text = text

    def set_el(self, element):
        max_text_len = 8
        if len(self.text) > max_text_len:
            raise ValueError('text length is over {}'.format(max_text_len))

        for letter in self.text:
            note = ET.SubElement(element, 'note')
            Note(letter).set_el(note)

        for _ in range(max_text_len - len(self.text)):
            # 長音防止のために休符を挿入
            note = ET.SubElement(element, 'note')
            Note('', rest=True).set_el(note)

        element.set('number', self.number)
        element.set('width', self.width)
        return element

def edit_xml(tree, text):
    tree = clean_xml(tree)
    root = tree.getroot()
    part = root.find('part')
    first_measure = part.find('measure')
    direction = first_measure.find('direction')
    sound = direction.find('sound')
    sound.set('tempo', '150')  # テンポの変更

    chunked_texts = chunk_text(text)
    for text_by_measure in chunked_texts:
        measure = ET.SubElement(part, 'measure')
        Measure(text_by_measure).set_el(measure)
    return tree

def wite_xml(tree, filename):
    tree.write(filename,
               encoding='utf-8',
               xml_declaration=True)


def main(text):
    # NEUTRINOのサンプルを読み込み
    filename = 'score/sample1.musicxml'
    tree = load_xml(filename)
    tree = edit_xml(tree, text)
    wite_xml(tree, filename)

実行

このスクリプトで作成した MusicXML を NEUTRINO に入力した結果、しっかり日本語として認識できるレベルの音声が出力できました。

いまのところいわゆる「棒読み」ですが、イントネーションに合わせたピッチの変更などは MusicXMLを編集すれば可能なため、イントネーションの公開データ等を用いてそこを調整できれば、もっと流暢な音声が出力できるかもしれません。

おわりに

以上、NEUTRINO を使って簡易的なテキスト読み上げを行うことができました。

NEUTRINO は学習データとして歌唱データベースを用いているのでそもそも今回の用途には適していないのですが、「音楽はわからないけど試しに触ってみたい」という方など、このような触れ方も面白いかと思います。

今回の仕組みは間接的に AI きりたん歌唱データベースや NEUTRINO を使用しているため、 試す際は各ライセンスに従って動かしてみてください。