自然な「グロンギ語」を音声合成 / ギゼンバ「グロンギゴ」ゾゴンゲギゴグゲギ

# 概要 / ガギジョグ
合成音声ライブラリ Open JTalk を魔改造することにより、**自然なアクセント**を持つグロンギ語音声を合成することに成功しました。

<iframe width="560" height="315" src="https://www.youtube.com/embed/bHn0xufj08E" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

# グロンギ語とは / グロンギゴドパ
グロンギ語とは、「仮面ライダークウガ」における悪役である「グロンギ」たちが使う言語です。作中では言語学者も解明できない難解な言語とされていました。

しかし、実際のところグロンギ語は日本語を一定のルールで変換したものになっています。日本語とグロンギ語の変換ルールを踏まえればグロンギたちが何を話していたのか現実世界の視聴者にも分かるという、とても凝った作りの番組になっていました。

さて日本語からグロンギ語への変換ルールはおおむね以下のようになっています。

+ 「あ行」は「ガ行」に、「か行」は「バ行」にというように、子音を一律に変換する
    + 助詞の「が」「の」「は」は例外的にそれぞれ「グ」「ン」「パ」となる
    + 「グロンギ」「クウガ」など一部の固有名詞はそのまま
+ 促音は直後の音を重ねる
    + 例：「キ**ッ**ク」→「ビ**ブ**ブ」
+ 長音は直前の音を重ねる
    + 例：「仮面ライダ**ー**」→「バレンサギザ**ザ**」
+ 数体系は九進法に類似した独自のものを用いる
    + 例：99 は「バギングバギングパパンドバギングドググ」、直訳すると「9 が 9 が 1 と 9 が 2」で $9 \times 9 \times 1 + 9 \times 2$ という意味になる

変換ルールはこれだけなので、日本語の文字列をグロンギ語の文字列に変換するのも比較的簡単です。助詞の「が」と助詞でない「が」を見分けるところや漢字を処理するところが面倒ですが、そこは MeCab などの既存のツールを使えば楽々対処できてしまいます。

しかし、こうして作ったグロンギ語の文字列をただ音声合成エンジンに投げるだけではある問題が発生してしまいます。

# 音声合成における問題点 / ゴンゲギゴグゲギビゴベスロンザギデン
日本語とグロンギ語は同じアクセント位置で発音されます。たとえば「プログラミング」は「プロ**グ**ラミング」と 3 モーラ目にアクセント核がある（3 音目で高くなる）ので、グロンギ語でも「ムソ**グ**サリング」と 3 音目を上げて発音しなければなりません。

しかし既存の合成音声ライブラリは当然グロンギ語になんて対応しているわけがないので、グロンギ語文字列を入力してもアクセント位置はめちゃくちゃになってしまいます。

|<iframe width="560" height="315" src="https://www.youtube.com/embed/QctbcsX-2TY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>|<iframe width="560" height="315" src="https://www.youtube.com/embed/YgZiUWGzOas" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>|<iframe width="560" height="315" src="https://www.youtube.com/embed/bHn0xufj08E" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>|
|:-:|:-:|:-:|
|日本語|アクセントが不適切なグロンギ語|アクセントが適切なグロンギ語|

# Open JTalk / ゴゴムンジェギドドブ
この問題を解決するために合成音声ライブラリ **Open JTalk** に含まれる形態素解析用辞書 **NAIST[^naist] Japanese Dictionary** を利用しました。辞書本体は以下のような CSV ファイルになっています。

[^naist]: Nara Institute of Science and Technology, 奈良先端科学技術大学院大学

```
（前略）
りんこ,1345,1345,7477,名詞,一般,*,*,*,*,りんこ,リンコ,リンコ,1/3,C1
りんこう,1343,1343,7376,名詞,サ変接続,*,*,*,*,りんこう,リンコウ,リンコー,0/4,C2
りんこう,1345,1345,7433,名詞,一般,*,*,*,*,りんこう,リンコウ,リンコー,0/4,C2
りんご,1345,1345,7445,名詞,一般,*,*,*,*,りんご,リンゴ,リンゴ,0/3,C2
りんごく,1345,1345,7429,名詞,一般,*,*,*,*,りんごく,リンゴク,リンゴク,0/4,C2
りんさい,1343,1343,7376,名詞,サ変接続,*,*,*,*,りんさい,リンサイ,リンサイ,0/4,C2
りんさん,1345,1345,7575,名詞,一般,*,*,*,*,りんさん,リンサン,リンサン,0/4,C2
（後略）
```

MeCab を利用されたことがある方はお気づきになられたと思いますが、これは MeCab の辞書形式を拡張したものです。15 個のフィールドはそれぞれ以下を表します。

```
表層形,左文脈 ID,右文脈 ID,コスト,品詞,品詞細分類 1,品詞細分類 2,品詞細分類 3,活用型,活用形,原形,読み,発音,アクセント核位置/モーラ数,アクセント結合規則
```

<details>
  <summary>ちょっとだけ詳しい説明</summary>
  <dl>
    <dt>表層形</dt>
    <dd>実際に文面の中に現れている形。</dd>
    <dt>左 / 右文脈 ID</dt>
    <dd>形態素解析のときに内部的に使用する ID。自分で語彙を追加する場合は空にしておけばよしなにやってくれる。</dd>
    <dt>コスト</dt>
    <dd>その語の現れにくさ。</dd>
    <dt>品詞・品詞細分類 1・品詞細分類 2・品詞細分類 3</dt>
    <dd>品詞は言うまでもなく「名詞」「動詞」など。それ以外の付加的な情報が必要な場合は細分類として表記する。たとえば「赤十字」の項目は「赤十字,1352,1352,4554,名詞,<b>固有名詞,組織</b>,*,*,*,赤十字,セキジュウジ,セキジュージ,3/5,C1」と二つの細分類を、「田中」の項目は「田中,1350,1350,5150,名詞,<b>固有名詞,人名,姓</b>,*,*,田中,タナカ,タナカ,0/3,C1」と三つすべての細分類を使用している。</dd>
    <dt>活用型・活用形・原形</dt>
    <dd>読んで字のごとく。たとえば「立て」の項目は「立て,767,767,7079,動詞,自立,*,*,<b>五段・タ行,仮定形,立つ</b>,タテ,タテ,1/2,*」となっている。</dd>
    <dt>読み・発音</dt>
    <dd>一部の項目ではこの二つが微妙に異なる。例えば「太陽,1345,1345,5601,名詞,一般,*,*,*,*,太陽,<b>タイヨウ,タイヨー</b>,1/4,C1」など。</dd>
    <dt>アクセント核位置/モーラ数</dt>
    <dd>これも読んで字のごとく。例えば前述のとおり「プログラミング」であれば「プログラミング,1343,1343,4317,名詞,サ変接続,*,*,*,*,プログラミング,プログラミング,プログラミング,<b>3/7</b>,C1」となっており、これは 7 モーラ中 3 モーラ目にアクセント核があるという意味。</dd>
    <dt>アクセント結合規則</dt>
    <dd>
      複合語のアクセントのつけ方。以下は「条件付確率場に基づく日本語アクセント型予想モデルの改良と日本語教育システムへの応用」からの引用。
正直まったくわからん

      <table>
        <thead>
          <tr>
            <th>規則</th>
            <th>説明</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>C1（自立語結合保存型）</td>
            <td>後続語が 2 モーラ以上、かつ最終音節以外の位置にアクセント核を持つ場合後続語の単独発生アクセント型が保持される。</td>
          </tr>
          <tr>
            <td>C2（自立語結合生起型）</td>
            <td>後続語が 2 モーラ以上で、かつアクセント核を持たない、あるいは最終音節内に核を持つ場合結合アクセント価が 1 となる。</td>
          </tr>
          <tr>
            <td>C3（接辞結合標準型）</td>
            <td>接尾辞または 2 モーラ以下の名詞が後続した場合、先行後の末尾モーラに核を生じさせる。</td>
          </tr>
          <tr>
            <td>C4（接辞結合平板化型）</td>
            <td>接尾辞または 2 モーラ以下の名詞が後続した場合、結合した複合語を平板化させる。</td>
          </tr>
          <tr>
            <td>C5（従属型）</td>
            <td>後続語のモーラ数、アクセント型にかかわらず、先行後のアクセント型が保持される。</td>
          </tr>
        </tbody>
      </table>
    </dd>
  </dl>
</details>

とにかくこの CSV ファイルには読みもアクセントも全部書いてあるので、これを流用することで自然なグロンギ語を発音させることができるはずです。というわけでまずは Open JTalk をインストールします。

```bash
# HTS engine のインストール
$ wget http://downloads.sourceforge.net/hts-engine/hts_engine_API-1.10.tar.gz
$ tar xvf hts_engine_API-1.10.tar.gz
$ cd hts_engine_API-1.10
$ ./configure
$ make
$ make install
$ cd ..
```

```bash
# Open JTalk のインストール
$ wget http://downloads.sourceforge.net/open-jtalk/open_jtalk-1.11.tar.gz
$ tar xvf open_jtalk-1.11.tar.gz
$ cd open_jtalk-1.11
$ ./configure --with-hts-engine-header-path=/usr/local/include \
              --with-hts-engine-library-path=/usr/local/lib \
              --with-charset=UTF-8
$ make
$ make install
$ cd ..
```

これでインストールは済んだので、動作テストを行います。

```bash
# 音響モデルファイルのダウンロード。
# htsvoice ファイルならなんでもいいので、ググればいくらでも転がってると思います。
$ wget http://downloads.sourceforge.net/project/open-jtalk/HTS%20voice/hts_voice_nitech_jp_atr503_m001-1.05/hts_voice_nitech_jp_atr503_m001-1.05.tar.gz
$ tar xvf hts_voice_nitech_jp_atr503_m001-1.05.tar.gz

# wav ファイルの作成
# -x 辞書データのあるディレクトリの指定
# -m 音響モデルファイルの指定
# -ow 作成する wav ファイルの指定
$ open_jtalk -x (さっき解凍した Open JTalk のディレクトリ)/mecab-naist-jdic/ \
             -m hts_voice_nitech_jp_atr503_m001-1.05/nitech_jp_atr503_m001.htsvoice \
             -ow result.wav \
             <(echo こんにちは)
```

作成した wav ファイルを再生すると「こんにちは」という音声が合成されているはずです。

# 魔改造 / ラバギゾグ
上のスクリプトからもわかるように `open_jtalk` コマンドが利用する辞書データは Open JTalk 内の `mecab-naist-jdic` ディレクトリに置いてあります。

```bash
$ ls mecab-naist-jdic
COPYING      Makefile.in   _pos-id.def    char.bin     left-id.def  naist-jdic.csv  right-id.def    unk.def
Makefile     Makefile.mak  _rewrite.def   char.def     matrix.bin   pos-id.def      sys.dic         unk.dic
Makefile.am  _left-id.def  _right-id.def  feature.def  matrix.def   rewrite.def     unidic-csj.csv
```

このうち書き換えるべきなのは `naist-jdic.csv` です。さきほどお見せしたように、CSV 各レコードはこのようになっています。

```
置時計,1345,1345,5746,名詞,一般,*,*,*,*,置時計,オキドケイ,オキドケー,3/5,C1
```

第 13 フィールドの「オキドケー」が発音の部分なので、ここを書き換えることでグロンギ語を喋らせることができます。しかし注意しなければならないのが、正しいグロンギ語は「オキドケー」を変換して得られる「ゴビゾベベ」**ではなく**、「オキドケイ」を変換して得られる「ゴビゾベギ」だということです。つまり、第 12 フィールドをグロンギ語変換したものを第 13 フィールドに入れることでグロンギ語辞書を作れます。

```
置時計,1345,1345,5746,名詞,一般,*,*,*,*,置時計,オキドケイ,ゴビゾベギ,3/5,C1
```

`naist-jdic.csv` には 50 万近いレコードがあるので、書き換えには速度重視で C++ を使っていきます。まずは元のデータをとっておくためにリネームしておきます。

```bash
$ mv naist-jdic.csv naist-jdic.csv.original
```

そしてゴリ押しでグロンギ語変換していきます。

```c++
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <array>
#include <vector>
#include <optional>
#include <algorithm>
#include <numeric>
#include <stdexcept>

/**
 * @brief カタカナ文字列をグロンギ語に変換します。
 * @param[in] str 変換する文字列
 * @return グロンギ語に変換された文字列
 * @details ア/カ/サ/タ/ナ/ハ/マ/ヤ/ラ/ガ/ザ/ダ/バ/パの各行は
 *          ガ/バ/ガ/ダ/バ/ザ/ラ/ジャ/サ/ガ/ザ/ザ/ダ/マ行に変換する。
 * @details ワ/ヲ/ンはそれぞれパ/ゾ/ンに変換する。
 * @details ヰ/ヱはそれぞれイ/エと同一視し、ギ/ゲに変換する。
 * @details ヴァ行は変換しない。
 * @details ファ行はハ行と同一視し、ザ行に変換する。
 * @details ファ行以外の拗音は変換しない。
 * @details 小文字（ァ/ィ/ゥ/ェ/ォ/ャ/ュ/ョ/ヮ）が単独で現れた場合はそれぞれ
 *          大文字（ア/イ/ウ/エ/オ/ヤ/ユ/ヨ/ワ）と同一視し変換する。
 * @details 長音は直前のモーラと同じ音に変換する。
 *          （例：カメンライダー -> バレンサギザザ）
 *          直前に長音と促音以外のモーラが存在しない場合は例外が投げられる。
 * @details 促音は直後のモーラと同じ音に変換する。
 *          （例：キック -> ビブブ）
 *          直後のモーラが促音もしくは存在しない場合は変換しない。
 *          （例：イヤミカキサマッッ -> ギジャリバビガラッッ）
 *          直後のモーラが長音である場合は例外が投げられる。
 * @attention str は UTF-8 によるカタカナ文字列であることを想定していますが、
 *            バリデーションは一切行っていません。
 * @attention UTF-8 に含まれるカタカナ (U+30A0 - U+30FF) のうち、ヵ/ヶ/ヷ/ヸ/ヹ/ヺ、
 *            区切り文字（゠/・）、踊り字（ヽ/ヾ）、合字（ヿ）の変換は未定義です。
 */
std::string to_gurongi(const std::string &str)
{
    const std::array<std::string, 5> a_row { "ア", "イ", "ウ", "エ", "オ" };
    const std::array<std::string, 5> ka_row { "カ", "キ", "ク", "ケ", "コ" };
    const std::array<std::string, 5> sa_row { "サ", "シ", "ス", "セ", "ソ" };
    const std::array<std::string, 5> ta_row { "タ", "チ", "ツ", "テ", "ト" };
    const std::array<std::string, 5> na_row { "ナ", "ニ", "ヌ", "ネ", "ノ" };
    const std::array<std::string, 5> ha_row { "ハ", "ヒ", "フ", "ヘ", "ホ" };
    const std::array<std::string, 5> ma_row { "マ", "ミ", "ム", "メ", "モ" };
    const std::array<std::string, 3> ya_row { "ヤ", "ユ", "ヨ" };
    const std::array<std::string, 5> ra_row { "ラ", "リ", "ル", "レ", "ロ" };
    const std::array<std::string, 5> ga_row { "ガ", "ギ", "グ", "ゲ", "ゴ" };
    const std::array<std::string, 5> za_row { "ザ", "ジ", "ズ", "ゼ", "ゾ" };
    const std::array<std::string, 5> da_row { "ダ", "ヂ", "ヅ", "デ", "ド" };
    const std::array<std::string, 5> ba_row { "バ", "ビ", "ブ", "ベ", "ボ" };
    const std::array<std::string, 5> pa_row { "パ", "ピ", "プ", "ペ", "ポ" };
    const std::array<std::string, 5> fa_row { "ファ", "フィ", "フ", "フェ", "フォ" };
    const std::array<std::string, 5> xa_row { "ァ", "ィ", "ゥ", "ェ", "ォ" };
    const std::array<std::string, 3> xya_row { "ャ", "ュ", "ョ" };
    const auto get_index = [](const auto &container, const auto &val) {
        const unsigned long long dist = std::find(container.begin(), container.end(), val) - container.begin();
        return dist < container.size() ? std::optional{dist} : std::nullopt;
    };
    const auto is_yoon = [get_index, xa_row, xya_row](const std::string &str) {
        return str.size() == 6 && (
            get_index(xa_row, str.substr(3)).has_value() ||
            get_index(xya_row, str.substr(3)).has_value()
        );
    };
    std::string result;
    for (std::size_t i = 0; i <= str.size() - 3; i += 3) {
        if (const std::string katakana = str.substr(i, 6); is_yoon(katakana)) {
            if (const auto index = get_index(fa_row, katakana)) {
                result += za_row[index.value()];
            } else {
                result += katakana;
            }
            i += 3;
        } else if (const std::string katakana = str.substr(i, 3); katakana == "ー") {
            if (i >= 3) {
                const bool is_prev_yoon = i >= 6 && is_yoon(str.substr(i - 6, 6));
                result += to_gurongi(str.substr(i - (is_prev_yoon ? 6 : 3), is_prev_yoon ? 6 : 3));
            } else {
                throw std::runtime_error("Wrong use of 'ー'");
            }
        } else if (katakana == "ッ") {
            if (i + 3 < str.size()) {
                const bool is_next_yoon = is_yoon(str.substr(i + 3, 6));
                result += to_gurongi(str.substr(i + 3, is_next_yoon ? 6 : 3));
            } else {
                result += "ッ";
            }
        } else if (const auto index = get_index(a_row, katakana)) {
            result += ga_row[index.value()];
        } else if (const auto index = get_index(ka_row, katakana)) {
            result += ba_row[index.value()];
        } else if (const auto index = get_index(sa_row, katakana)) {
            result += ga_row[index.value()];
        } else if (const auto index = get_index(ta_row, katakana)) {
            result += da_row[index.value()];
        } else if (const auto index = get_index(na_row, katakana)) {
            result += ba_row[index.value()];
        } else if (const auto index = get_index(ha_row, katakana)) {
            result += za_row[index.value()];
        } else if (const auto index = get_index(ma_row, katakana)) {
            result += ra_row[index.value()];
        } else if (const auto index = get_index(ya_row, katakana)) {
            result += "ジ" + xya_row[index.value()];
        } else if (const auto index = get_index(ra_row, katakana)) {
            result += sa_row[index.value()];
        } else if (katakana == "ワ") {
            result += "パ";
        } else if (katakana == "ヰ") {
            result += "ギ";
        } else if (katakana == "ヱ") {
            result += "ゲ";
        } else if (katakana == "ヲ") {
            result += "ゾ";
        } else if (katakana == "ン") {
            result += "ン";
        } else if (const auto index = get_index(ga_row, katakana)) {
            result += ga_row[index.value()];
        } else if (const auto index = get_index(za_row, katakana)) {
            result += za_row[index.value()];
        } else if (const auto index = get_index(da_row, katakana)) {
            result += za_row[index.value()];
        } else if (const auto index = get_index(ba_row, katakana)) {
            result += da_row[index.value()];
        } else if (const auto index = get_index(pa_row, katakana)) {
            result += ma_row[index.value()];
        } else if (katakana == "ヴ") {
            result += "ヴ";
        } else if (const auto index = get_index(xa_row, katakana)) {
            result += a_row[index.value()];
        } else if (const auto index = get_index(xya_row, katakana)) {
            result += ya_row[index.value()];
        } else if (katakana == "ヮ") {
            result += "ワ";
        } else {
            throw std::runtime_error("Unexpected character: " + katakana);
        }
    }
    return result;
}

/**
 * @brief カタカナ文字による助詞をグロンギ語に変換します。
 * @param[in] particle 変換するカタカナ
 * @return グロンギ語に変換された助詞
 * @details 助詞ガ/ノ/ハはそれぞれグ/ン/パに変換する。
 *          それ以外の文字は一般のルールを適用し返却する。
 * @attention particle が UTF-8 によるカタカナ文字になっているかの確認は一切しません。
 */
std::string to_gurongi_particle(const std::string &particle)
{
    return
        particle == "ガ" ? "グ" :
        particle == "ノ" ? "ン" :
        particle == "ハ" ? "パ" :
        to_gurongi(particle);
}

/**
 * @brief 指定した区切り文字で文字列を分割する。
 * @param[in] str 区切る文字列
 * @param[in] delim 区切り文字
 * @param[out] out 区切った結果を出力するイテレータ
 * @note これくらいは標準ライブラリにあってほしい。
 */
template<class OutputIterator>
void split_string(const std::string &str, char delim, OutputIterator out)
{
    std::istringstream iss(str);
    for (std::string field; std::getline(iss, field, delim); ) {
        *out++ = field;
    }
}

/**
 * @brief 指定した文字を挟みながら文字列群を連結する。
 * @param[in] begin 連結する文字列のシーケンスの最初の要素を指すイテレータ
 * @param[in] end 連結する文字列のシーケンスの最後の要素の次を指すイテレータ
 * @param[in] delim 挟む文字
 * @return 連結した文字列
 * @note これくらいは（ｒｙ
 */
template<class InputIterator>
std::string join_string(InputIterator begin, InputIterator end, char delim)
{
    return std::accumulate(
        begin, end, std::string{},
        [delim](const std::string &acc, const std::string &str) {
            return acc.empty() ? str : acc + delim + str;
        }
    );
}

/**
 * @brief 指定した区切りで文字列を分割し、各要素に変換を施したのち連結する
 * @param[in] str 変換する文字列
 * @param[in] delim 区切り文字
 * @param[in] tf 変換（文字列から文字列への単項演算）
 * @return 変換した文字列
 */
template<class Transformer>
std::string transform_fields(const std::string &str, char delim, Transformer tf)
{
    std::vector<std::string> v;
    split_string(str, delim, std::back_inserter(v));
    std::transform(v.begin(), v.end(), v.begin(), tf);
    return join_string(v.begin(), v.end(), delim);
}

int main()
{
    std::ifstream ifs("naist-jdic.csv.original");
    std::ofstream ofs("naist-jdic.csv");
    int i = 0;
    for (std::string buffer; std::getline(ifs, buffer); ) {
        std::cout << ++i << '\r' << std::flush;
        std::vector<std::string> fields;
        split_string(buffer, ',', std::back_inserter(fields));
        if (fields[4] != "記号") {
            if (const std::string kana = fields[11]; kana != "、" && kana != "，" && kana != "．" && kana != "・") {
                /* 「サンマル:ニエー」のように読みにコロンを含むレコードが少数存在するので、
                   コロンで区切ってから各要素を変換する */
                fields[12] = (fields[4] == "助詞" ? to_gurongi_particle(kana) : transform_fields(kana, ':', to_gurongi));
            }
        }
        ofs << join_string(fields.begin(), fields.end(), ',') << std::endl;
    }
    return 0;
}
```

グロンギ語版 `naist-jdic.csv` を作ったら `make` コマンドを打つことで他のファイルもよしなに作成されます（`make install` も打ちなおす必要はありません）。それではまたさっきと同じコマンドで wav ファイルを作ってみましょう。

```bash
$ open_jtalk -x (さっき解凍した Open JTalk のディレクトリ)/mecab-naist-jdic/ \
             -m hts_voice_nitech_jp_atr503_m001-1.05/nitech_jp_atr503_m001.htsvoice \
             -ow result.wav \
             <(echo こんにちは)
```

「こんにちは」ではなく「ボンビヂザ」と発音しているはずです。しかしそれ以上に注目すべきなのは、**「こんにちは」と同じアクセントで発音している**ということです。

# 固有名詞に対応する / ボジュグレギギビダギゴググス
記事冒頭で述べたように、「クウガ」「グロンギ」などの一部の固有名詞は変換されません。そこでこれらの固有名詞も適切に辞書に登録するようにします。ついでに「パパン」を始めとする数詞も登録しておきます。コスト 4000 はテキトーに決めました。

```diff_cpp
+ /** グロンギ語における固有名詞 */
+ static constexpr std::array<const char*, 15> extra_records[] = {
+     { "グロンギ", "", "", "4000", "名詞", "固有名詞", "組織", "*", "*", "*", "グロンギ", "グロンギ", "グロンギ", "2/4", "C1" },
+     { "リント", "", "", "4000", "名詞", "固有名詞", "一般", "*", "*", "*", "リント", "リント", "リント", "1/3", "C1" },
+     { "クウガ", "", "", "4000", "名詞", "固有名詞", "一般", "*", "*", "*", "クウガ", "クウガ", "クウガ", "1/3", "C1" },
+     { "パパン", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "パパン", "パパン", "パパン", "1/3", "C3" },
+     { "ドググ", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "ドググ", "ドググ", "ドググ", "1/3", "C3" },
+     { "グシギ", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "グシギ", "グシギ", "グシギ", "1/3", "C3" },
+     { "ズゴゴ", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "ズゴゴ", "ズゴゴ", "ズゴゴ", "1/3", "C3" },
+     { "ズガギ", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "ズガギ", "ズガギ", "ズガギ", "1/3", "C3" },
+     { "ギブグ", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "ギブグ", "ギブグ", "ギブグ", "1/3", "C3" },
+     { "ゲヅン", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "ゲヅン", "ゲヅン", "ゲヅン", "1/3", "C3" },
+     { "ゲギド", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "ゲギド", "ゲギド", "ゲギド", "1/3", "C3" },
+     { "バギン", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "バギン", "バギン", "バギン", "1/3", "C3" }
+ };
+ 
  int main()
  {
      /* 中略 */
+     for (const auto &er : extra_records) {
+         ofs << join_string(er.begin(), er.end(), ',') << std::endl;
+     }
  }
```

# 数に対応する / バズビダギゴググス
ここまでで助詞を区別する問題やアクセントの問題、固有名詞の問題は解決されたわけですが、まだ一つ問題が残されています。グロンギたちは九進法を使っているので、それに合わせた変換をしなければならないという点です。

この問題を解決するにあたっては、数字の部分だけを事前に変換しておくという方法が考えられます。

> 「6 時間で 72 人殺す」
> ↓ 数字の部分だけ事前に変換しておく
> 「**ギブグ**時間で**バギングゲギド**人殺す」
> ↓ open_jtalk に投げる
> 「ギブグジバンゼバギングゲギドビンボソグ」と発音する wav ファイルができる

しかしこの方法には一つ問題があります。先ほどグロンギ語の数詞を辞書に登録しておいたので「ギブグ」などは恐らく問題なくパースされると思いますが、数詞と助詞の組み合わせである「バギングゲギド」などは正しくパースされるとは限りません。そこで、少々無理やりではありますがもう少し確実な方法を使います。

そもそも、実のところグロンギ語の数詞は英語の「ワン、ツー、スリー・・・」を変換したものなのです。

|変換元|グロンギ語の数詞|
|:-:|:-:|
|ワーン|パパン|
|トウー|ドググ|
|スリイ|グシギ|
|フオー|ズゴゴ|
|フアイ|ズガギ|
|シクス|ギブグ|
|セブン|ゲヅン|
|エイト|ゲギド|
|ナイン|バギン|

そこでこの「変換前のグロンギ語数詞」、つまり「ワーン」や「トウー」などを用いた変換を行うようにします。

> 「6 時間で 72 人殺す」
> ↓ 数字の部分だけ事前に変換しておく
> 「**シクス**時間で**ナインがエイト**人殺す」
> ↓ open_jtalk に投げる
> 「ギブグジバンゼバギングゲギドビンボソグ」と発音する wav ファイルができる

この方法を使うことで、数詞と助詞の組み合わせも「ナインがエイト」のようにカタカナとひらがなが混じった形になるので、より正確なパースが期待できます。

まずは辞書に登録するデータを微修正します。

```diff_cpp
  /** グロンギ語における固有名詞 */
  static constexpr std::array<const char*, 15> extra_records[] = {
      { "グロンギ", "", "", "4000", "名詞", "固有名詞", "組織", "*", "*", "*", "グロンギ", "グロンギ", "グロンギ", "2/4", "C1" },
      { "リント", "", "", "4000", "名詞", "固有名詞", "一般", "*", "*", "*", "リント", "リント", "リント", "1/3", "C1" },
      { "クウガ", "", "", "4000", "名詞", "固有名詞", "一般", "*", "*", "*", "クウガ", "クウガ", "クウガ", "1/3", "C1" },
-     { "パパン", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "パパン", "パパン", "パパン", "1/3", "C3" },
+     { "ワーン", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "パパン", "パパン", "パパン", "1/3", "C3" },
-     { "ドググ", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "ドググ", "ドググ", "ドググ", "1/3", "C3" },
+     { "トウー", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "ドググ", "ドググ", "ドググ", "1/3", "C3" },
-     { "グシギ", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "グシギ", "グシギ", "グシギ", "1/3", "C3" },
+     { "スリイ", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "グシギ", "グシギ", "グシギ", "1/3", "C3" },
-     { "ズゴゴ", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "ズゴゴ", "ズゴゴ", "ズゴゴ", "1/3", "C3" },
+     { "フオー", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "ズゴゴ", "ズゴゴ", "ズゴゴ", "1/3", "C3" },
-     { "ズガギ", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "ズガギ", "ズガギ", "ズガギ", "1/3", "C3" },
+     { "フアイ", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "ズガギ", "ズガギ", "ズガギ", "1/3", "C3" },
-     { "ギブグ", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "ギブグ", "ギブグ", "ギブグ", "1/3", "C3" },
+     { "シクス", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "ギブグ", "ギブグ", "ギブグ", "1/3", "C3" },
-     { "ゲヅン", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "ゲヅン", "ゲヅン", "ゲヅン", "1/3", "C3" },
+     { "セブン", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "ゲヅン", "ゲヅン", "ゲヅン", "1/3", "C3" },
-     { "ゲギド", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "ゲギド", "ゲギド", "ゲギド", "1/3", "C3" },
+     { "エイト", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "ゲギド", "ゲギド", "ゲギド", "1/3", "C3" },
-     { "バギン", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "バギン", "バギン", "バギン", "1/3", "C3" }
+     { "ナイン", "", "", "4000", "名詞", "数", "*", "*", "*", "*", "バギン", "バギン", "バギン", "1/3", "C3" }
  };
```

数字部分の変換は Python を使ってざっくりとやっていきます。

```python:to_gurongi_number.py
import sys
import re

def to_gurongi_number(n):
    GURONGI_DICT = {
        1 : 'ワーン',
        2 : 'トウー',
        3 : 'スリイ',
        4 : 'フオー',
        5 : 'フアイ',
        6 : 'シクス',
        7 : 'セブン',
        8 : 'エイト',
        9 : 'ナイン'
    }
    result = ''
    i = 0
    while n > 0:
        if n % 9 != 0:
            result = 'ナインが' * i + GURONGI_DICT[n % 9] + ('と' if result else '') + result
        n //= 9
        i += 1
    return result.removesuffix('がワーン')

print(re.sub(r"(\d+)", lambda m : to_gurongi_number(int(m.group())), sys.argv[1]))
```

```bash
$ python3.9 to_gurongi_number.py 6時間で72人殺す
シクス時間でナインがエイト人殺す
```

そういうわけで、最終的にはこのようにして助詞にも固有名詞にも数にも対応した変換プログラムが動きます。

```bash
open_jtalk -x (さっき解凍した Open JTalk のディレクトリ)/mecab-naist-jdic/ \
           -m hts_voice_nitech_jp_atr503_m001-1.05/nitech_jp_atr503_m001.htsvoice \
           -ow gurongi.wav \
           <(python3.9 to_gurongi_number.py 6時間で72人殺す)
```

# 現時点での問題点と今後の展望 / ゲンジデンゼンロンザギデンドボンゴンデンドグ
+ 変換対象の文字列の中に辞書にない言葉が含まれていた場合グロンギ語変換されないという致命的な欠点があります。たとえば「あのイーハトーヴォのすきとおった風」は「ガボ**イーハトーヴォ**ングビドゴダダバゼ」という発音になってしまいます。辞書にない語彙を使いたいときには都度辞書に追加する必要があります。
+ スマホや Arduino などの小型デバイスに移植し、マイクから拾った声をリアルタイム変換できれば面白そうだと思いました。私の技術力ではできませんでした😭

# まとめ
**<font color=#55C500>ここではリントの言葉で話せ！</font>**