はじめに
要は「今日は良い天気ですなぁ。」という文章を「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 で使えるようにしておきます。
カタカナをローマ字の読みに変換
次はカタカナをローマ字の読みに変換します。以下のエントリでも書いた IBM の Unicode ライブラリ 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解析時に格助詞とか係助詞とかとれるので、ここを「@」とかでマーキングしておいて読んでる途中に空白が入る可能性のある箇所に