凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

ICU + Mecab で文章をローマ字の読みに変換

はじめに

要は「今日は良い天気ですなぁ。」という文章を「ky o: w a y o i t e n k i d e s u n a:」にして欲しい訳です。音声認識エンジン Julius での言葉の音の定義を行う voca ファイルを自動生成する目的で作りました。

MeCabで文章をカタカナに変換

Tagger を作って parseToNode で形態素解析します。結果は取り敢えずコンテナに突っ込んでおきます。

// MeCab による形態素解析
std::string input = "今日は良い天気ですなぁ。";
boost::shared_ptr<MeCab::Tagger> tagger(MeCab::createTagger(""));
const MeCab::Node* node = tagger->parseToNode(input.c_str());

// 結果をコンテナに突っ込む
std::vector<std::string> features;
for (node = node->next; node->next; node = node->next) {
	features.push_back(node->feature);
}

すると features の中には次のようなものが格納されます。

features[0] = "名詞,副詞可能,*,*,*,*,今日,キョウ,キョー,,";
features[1] = "助詞,係助詞,*,*,*,*,は,ハ,ワ,,";
features[2] = "形容詞,自立,*,*,形容詞・アウオ段,基本形,良い,ヨイ,ヨイ,よい/良い,";
features[3] = "名詞,一般,*,*,*,*,天気,テンキ,テンキ,,";
features[4] = "助動詞,*,*,*,特殊・デス,基本形,です,デス,デス,,";
features[5] = "助詞,終助詞,*,*,*,*,なぁ,ナァ,ナー,,";
features[6] = "記号,句点,*,*,*,*,。,。,。,,";

読みは「,」で区切った8、9番目あたりなのでここをパースして取ってきてつなげれば読みの出来上がりです。
今回は Boost.Spirit.Qi を使ってパースしてみます。

ソースコード
// 発音箇所だけ取り出す
std::string s;
for (const std::string& x : features) {
	std::vector<std::string> v;
	std::string::const_iterator
		first = x.begin(),
		last  = x.end();
	qi::parse(first, last, +(char_-',')%',', v);
	s += v[8];
}
std::cout << s << std::endl;

こんな感じです。ソースコード全体は 文章をカタカナに変換 に書きました。
コンパイルは以下のようにします。

g++-4.6 str2kana.cpp -std=c++0x -lmecab

実行すると以下のようになります。

キョーワヨイテンキデスナー。

今後色々変形していくのでパース部分は分離して Boost.Adaptors.Transformed で使えるようにしておきます。

カタカナをローマ字の読みに変換

次はカタカナをローマ字の読みに変換します。以下のエントリでも書いた IBMUnicode ライブラリ ICU を使います。

struct kana2yomi
{
	typedef std::string result_type;

	result_type operator() (const result_type& str) const {
		UnicodeString input = str.c_str();

		// カタカナ --> Latin 変換
		UErrorCode error = U_ZERO_ERROR;
		boost::shared_ptr<Transliterator> t(
			Transliterator::createInstance("Katakana-Latin", UTRANS_FORWARD, error)
		);
		t->transliterate(input);

		// 伸ばす音の表記変更
		std::map<UnicodeString, UnicodeString> long_map =
			{ {"\u0101","a:"}, {"\u0113","i:"}, {"\u012B","u:"}, {"\u014D","e:"}, {"\u014D","o:"} };
		for (const auto& x : long_map) {
			input.findAndReplace(x.first, x.second);
		}

		// 変換結果取得
		size_t length = input.length();
		char* result = new char[length + 1];
		input.extract(0, length, result, "utf8");

		return result;
	}
};

Transliterator.transliterate(UnicodeString) でさっくりとカタカナをローマ字にしてくれます。が、ローマ字というか引数でも指定しているようにラテン文字になるので、長音に関しては「ā, ē, ī, ō, ū」のようにバーがついた文字になります。Julius では長音は「a:, i:, u:, e:, o:」のように「:」がついた形式なので UnicodeString.findAndReplace() メンバ関数で変換をかけます。ちなみに上のソースコードでは、はてダだと文字化けするのでエスケープシーケンスで書いてますが、āēīōū を直打ちでもいけました。
そして最後に UnicodeString をマルチバイト文字列に戻すために UnicodeString.extract() を使います。
この時点で、「今日は良い天気ですなぁ。」-->「キョーワヨイテンキデスナー。」-->「kyo:wayoitenkidesuna:.」となっています。

ローマ字の読みにスペースを挿入する

正規表現でやりました。

struct insert_space
{
	typedef std::string result_type;

	result_type operator() (const result_type& str) const {
		std::string result(str);

		std::map<std::string, std::string> regex_map =
			{
				{"[aiueon]:?", "$0 "},
				{"[^aiueon]{1,2}", "$0 "},
				{"[^a-z:]", ""},
				{"\\s+", " "},
			};

		for (const auto& x : regex_map) {
			boost::regex r(x.first);
			result = boost::regex_replace(result, r, x.second, boost::format_all);
		}

		return result;
	}
};

母音または n の場合は問答無用で後ろにスペースを挟んで、これら以外の子音については「kyo」のように ky と2個文字が連なる場合もあるのでこれも考慮します。そしてこれら以外の文字はすべて消去し、スペースが2個以上つながってしまっている箇所に関しては1個にする、というような変換をしています。

コード全体

コンパイル
g++-4.6 str2yomi.cpp -lmecab -lboost_regex -licuio -std=c++0x
結果
ky o: w a y o i t e n k i d e s u n a:

めでたく変換できました!

今後の展望

MeCab解析時に格助詞とか係助詞とかとれるので、ここを「@」とかでマーキングしておいて読んでる途中に空白が入る可能性のある箇所に を挿入したい(Julius の voca ファイルのチューニング用)。