凹みTips

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

Node.js 用 Open JTalk アドオンを作ってみた

はじめに

Twitterの内容をベラベラ喋ったり、こっちの問いかけに応答してくれるようなシステムを作ろうと思っているので、その一環として作ってみました。
内容は過去の継ぎ接ぎのような感じです。

ダウンロード

(2012/12/03 node-gyp 版を作ったためリンク変更)

↑から git clone して下さい。
別途、音声ファイルが必要です。

のいずれからか、音声ファイルを入手して下さい。

Node.js アドオンの作り方

このあたりを見ておくと何をすれば良いか分かります。
要約すると、v8 のお作法に従って C++JavaScript をお決まりの手順でつなぎこむ感じです。

openjtalk.cc
#include <node.h>
#include "v8.hpp"
#include "text_to_speech.hpp"

using namespace v8;

/* ------------------------------------------------------------------------- */
//  JavaScript へ Export する : TextToSpeech
/* ------------------------------------------------------------------------- */
class TextToSpeechJS : public hecomi::V8::ExportToJSIF
{
private:
	//! TextToSpeech で喋らせるクラス
	boost::shared_ptr<TextToSpeech> tts_;

	//! 初期化したかどうか
	bool if_initialized_;

	//! 初期化したかどうかを返す
	bool initialized() {
		if (!if_initialized_) {
			std::cerr << "Error! TextToSpeech has not been initialized yet." << std::endl;
			return false;
		}
		return true;
	}

public:
	//! コンストラクタ
	TextToSpeechJS() : if_initialized_(false) {}

	//! JavaScript へエクスポートする関数
	boost::any func(const std::string& func_name, const v8::Arguments& args)
	{
		// 初期化
		if (func_name == "init") {
			if (args.Length() < 2) {
				std::cout << "Error! TextToSpeech.init must have 2 or 3 arguments." << std::endl;
				return false;
			}
			OpenJTalkParams params;
			v8::String::Utf8Value voice_dir(args[0]);
			v8::String::Utf8Value dic_dir(args[1]);
			if (args[2]->IsObject()) {
				v8::Local<v8::Object> obj = v8::Local<v8::Object>::Cast(args[2]);
				if (obj->Get(v8::String::New("sampling_rate"))->IsInt32())
					params.sampling_rate = obj->Get(v8::String::New("sampling_rate"))->Int32Value();
				if (obj->Get(v8::String::New("stage"))->IsInt32())
					params.stage = obj->Get(v8::String::New("stage"))->Int32Value();
				if (obj->Get(v8::String::New("audio_buff_size"))->IsInt32())
					params.audio_buff_size = obj->Get(v8::String::New("audio_buff_size"))->Int32Value();
				if (obj->Get(v8::String::New("alpha"))->IsNumber())
					params.alpha = obj->Get(v8::String::New("alpha"))->NumberValue();
				if (obj->Get(v8::String::New("beta"))->IsNumber())
					params.beta = obj->Get(v8::String::New("beta"))->NumberValue();
				if (obj->Get(v8::String::New("uv_threshold"))->IsNumber())
					params.uv_threshold = obj->Get(v8::String::New("uv_threshold"))->NumberValue();
				if (obj->Get(v8::String::New("gv_weight_mgc"))->IsNumber())
					params.gv_weight_mgc = obj->Get(v8::String::New("gv_weight_mgc"))->NumberValue();
				if (obj->Get(v8::String::New("gv_weight_lf0"))->IsNumber())
					params.gv_weight_lf0 = obj->Get(v8::String::New("gv_weight_lf0"))->NumberValue();
				if (obj->Get(v8::String::New("gv_weight_lpf"))->IsNumber())
					params.gv_weight_lpf = obj->Get(v8::String::New("gv_weight_lpf"))->NumberValue();
			}
			tts_ = boost::make_shared<TextToSpeech>(*voice_dir, *dic_dir, params);
			if_initialized_ = true;
			return true;
		}
		// 指定した言葉を喋る
		else if (func_name == "talk") {
			if (!initialized()) return false;
			std::string str = *(v8::String::Utf8Value(args[0]));
			int fperiod = 220; // default
			if (args[1]->IsInt32()) fperiod = args[1]->Int32Value();
			tts_->talk(str, fperiod);
			return true;
		}
		// 喋りを止める
		else if (func_name == "stop") {
			if (!initialized()) return false;
			tts_->stop();
			return true;
		}
		return 0;
	}
};

void init(Handle<Object> target) {
	hecomi::V8::ExportToJS<TextToSpeechJS> openjtalk("OpenJTalk");
	openjtalk.add_func<bool>("init");
	openjtalk.add_func<bool>("talk");
	openjtalk.add_func<bool>("stop");
	target->Set(
		String::NewSymbol(openjtalk.get_class_name().c_str()),
		openjtalk.get_class()->GetFunction()
	);
}

NODE_MODULE(openjtalk, init)

v8 用に前作った資産を流用してますのでコードはアレですが…。

NODE_MODULE ってなんぞやってことについては以下のページに書いてあります。

wscript

ではこれをコンパイルします。コンパイルは wscript に必要な情報を記述し、Waf で行います。Waf とは Python ベースのコンパイルツールです。


サンプルに従って書くと、wscript は以下のようになりました。

srcdir = '.'
blddir = 'build'
VERSION = '0.0.1'

def set_options(opt):
  opt.tool_options('compiler_cxx')

def configure(conf):
  conf.check_tool('compiler_cxx')
  conf.check_tool('node_addon')
  conf.env['CXX']       = 'g++-4.6'
  conf.env['CXXFLAGS']  = '-std=c++0x'
  conf.env['LINKFLAGS'] = ['/home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/text2mecab/libtext2mecab.a', '/home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/mecab/src/libmecab.a', '/home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/mecab2njd/libmecab2njd.a', '/home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/njd/libnjd.a', '/home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/njd_set_pronunciation/libnjd_set_pronunciation.a', '/home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/njd_set_digit/libnjd_set_digit.a', '/home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/njd_set_accent_phrase/libnjd_set_accent_phrase.a', '/home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/njd_set_accent_type/libnjd_set_accent_type.a', '/home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/njd_set_unvoiced_vowel/libnjd_set_unvoiced_vowel.a', '/home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/njd_set_long_vowel/libnjd_set_long_vowel.a', '/home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/njd2jpcommon/libnjd2jpcommon.a', '/home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/jpcommon/libjpcommon.a', '/home/hecomi/Program/cpp/node/openjtalk/openjtalk/hts_engine_API-1.06/lib/libHTSEngine.a']

def build(bld):
  obj = bld.new_task_gen('cxx', 'shlib', 'node_addon')
  obj.target = 'openjtalk'
  obj.source = 'openjtalk.cc text_to_speech.cpp'
  obj.lib      = ['alut', 'openal']
  obj.includes = '-DHAVE_CONFIG_H /home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/ /home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/mecab /home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/text2mecab /home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/mecab/src /home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/mecab2njd /home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/njd /home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/njd_set_pronunciation /home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/njd_set_digit /home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/njd_set_accent_phrase /home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/njd_set_accent_type /home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/njd_set_unvoiced_vowel /home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/njd_set_long_vowel /home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/njd2jpcommon /home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/jpcommon /home/hecomi/Program/cpp/node/openjtalk/openjtalk/home/hecomi/Program/cpp/node/openjtalk/openjtalk/hts_engine_API-1.06/include -finput-charset=UTF-8 -fexec-charset=UTF-8 -MT open_jtalk.o -MD -MP -MF /home/hecomi/Program/cpp/node/openjtalk/openjtalk/open_jtalk-1.05/bin/.deps/open_jtalk.Tpo'

Open JTalk は static library をリンクしないといけないので、こんな面倒なことになっています。
そして次のコマンドでコンパイルすれば Node.js 用 Open JTalk アドオンの完成です。

$ node-waf configure build
openjtalk.js

これをテストするコードを書いてみました。

// sample

var util     = require('util');
var https    = require('https');
var host     = 'stream.twitter.com';

var OpenJTalk = require('./build/Release/openjtalk').OpenJTalk;
var mei = new OpenJTalk();
mei.init("data/mei_normal", "openjtalk/open_jtalk_dic_utf_8-1.05");

/* 以下のようにパラメタを指定することも出来ます
mei.init("data/mei_normal", "openjtalk/open_jtalk_dic_utf_8-1.05", {
	beta: 0.5,
	uv_threshold: 0.2,
	alpha: 0.2,
});
*/

var request  = https.get({
	host : host,
	port : 443,
	path : encodeURI('/1/statuses/filter.json?track=初音ミク'),
	auth : 'TwitterID:TwitterPass'
}, function(response) {
	console.log('Response: ' + response.statusCode);
}).on('error', function(e){
	console.log(e);
}).on('response', function(response) {
	response.on('data', function(chunk) {
		var tweet = JSON.parse(chunk);
		if ('user' in tweet && 'name' in tweet.user) {
			util.puts('[' + tweet.user['name'] + ']\n' + tweet.text);
			mei.talk(tweet.user.name);
		}
	});
});

process.on('uncaughtException', function (err) {
	console.log('uncaughtException => ' + err);
});

実行:

$ node openjtalk.js

これでひたすらミクさんのことについて喋ってるユーザの名前を読み上げ続けます。

課題

現状では読み上げ終わるまで操作が戻って来ません。読み上げは別スレッドで行うようにしようとしたのですが、どうにもうまくいかなかったので、一旦ここまでで公開しました。うまく行きましたら別途エントリを書きたいと思います。