Calmery.me

みっかぼうずにならないようがんばる

JPHACKS2016で作ったものまとめ

追記
追加オンライン審査で選ばれた決勝進出チーム9組を発表 | JPHACKS | 全国6都市で開催する、日本最大級の学生向けハックイベント
追加オンライン審査で Award Day に進むことができました!

昨年に引き続き今年も JPHACKS に参加してきた.今年は熊本ではなく福岡で.Rubyコンテンツ産業振興センターでの開催だからか Ruby を使用したチームから選ばれる賞というものもあった.
f:id:calmery:20161103212816j:plain
今年は「FlowerTalk」という,実際に自分が撮影したお花さんとお話しできるアプリを作った.メルヘンチック.
f:id:calmery:20161103213206j:plain
詳細は Flower Talk | Devpost にまとめてある.開発は GitHub - jphacks/KS_1604 で行うはずだったが,権限をもらっていなかったので GitHub - calmery/ks_1604: JPHACKS 2016 を作ってそちらで行った.

結果はダメだったが,せっかくなのでまとめておこうと思う.

Devpost にある動画を見てもらえればわかるが,アプリでは背景として映像を流し続けている.このアプリ自体,ウェブベースなので getUserMedia を使用し Web カメラから映像を取得,そのまま画面に描写した.

var video = document.getElementById( 'video' )

navigator.webkitGetUserMedia( {
    video: true, 
    audio: false
}, function( localMediaStream ){
    video.src = window.URL.createObjectURL( localMediaStream )
}, function(){
    console.log( 'Error' )
} )

花が写っているかの判定を行う際に画像が必要になる.画像は取得した映像の現在のフレームを Canvas に描写し,送信するようにしている.

var canvas = document.getElementById( 'capture' ),
    ctx    = canvas.getContext( '2d' )

canvas.width  = 640
canvas.height = 480

function capture(){
    ctx.drawImage( video, 0, 0, 640, 480 )
    socket.emit( 'capture', canvas.toDataURL( 'png' ) )
}

NodeJS 側で画像データを受け取り Microsoft Computer Vision API を使用し写っているものを判別する.花が写っていれば次の処理へ進む.そういえば花が写っていない場合の処理を書いていない.本当は Vision API - Image Content Analysis  |  Google Cloud Platform を使いたかったがクレジットカードが必須だった.残念.

function checkType( filePath ){
    child_process.exec( 'python ./vision/msVision.py ' + filePath, function( error, stdOut, stdError ){
        if( !error && !stdError ){
            const data = JSON.parse( stdOut )
            const tags = data.description.tags.filter( ( e ) => e.toLowerCase() ).join( '' )
            if( tags.indexOf( 'flower' ) !== -1 || tags.indexOf( 'plant' ) !== -1 )
                setNewFlower( data )
        } else
            console.log( error )
    } )
}

socket.on( 'capture', ( raw ) => {
    const path = utility.fixPath( __dirname, 'tmp', 'captured.png' )
    fs.writeFile( path, utility.decodeBase64Image( raw ).data, function( error ){ 
        if( !error ) checkType( path )
    } )
} )

画像からの判別は時間がなかったので Python で書かれたものをほとんどそのまま使った.結果を出力して NodeJS 側で受け取り,使用する.

import httplib
import urllib

import sys

file_name = sys.argv[1]

headers = {
    'Content-Type': 'application/octet-stream',
    'Ocp-Apim-Subscription-Key': 'YOUR_KEY',
}
params = urllib.urlencode( {
    'visualFeatures': 'Description',
} )

connect = httplib.HTTPSConnection( 'api.projectoxford.ai' )

img = open( file_name, 'rb' ).read()

connect.request( 'POST', '/vision/v1.0/analyze?%s' % params, img, headers )
response = connect.getresponse()
caption_data = response.read()
connect.close()

print( caption_data )

判定の結果として以下のようなレスポンスが得られる.

{
    "description": {
        "tags": ["plant", "flower", "daisy", "vase"],
        "captions": [{
            "text": "a vase with flowers in it",
            "confidence": 0.363922346433229
        }]
    },
    "requestId": "8e3b3a10-1763-4e68-ab2c-308d9a68595a",
    "metadata": {
        "width": 640,
        "height": 480,
        "format": "Png"
    }
}

この結果のタグ部分を見て植物,花かどうかを判定する.発表の際に,花の種類は判別できるのかと聞かれた.その場はできないと答えたが,この結果の場合,タグの中に花の名前が含まれている.だが,結果によっては含まれていないこともあったので API の利用は植物,花かどうかの判定のみに絞った.

その後の会話部分はユーザーローカルの人工知能ボット API を使用した.簡単な使い方は ユーザーローカルの人工知能ボットAPIを使ってTwitterの自動返答ボットを作った - Calmery.me にまとめてある.今回はこれに加えキャラクター会話変換 API を使用している.使い方はいつの間にかここにまとめられていた.実を言うと本当は docomo の雑談対話 API を使ってみたかった.

返答は一度,ユーザーローカルの人工知能ボット API から返答のメッセージを受け取り,そのメッセージをキャラクター会話変換 API に投げる.「ワン」とか「ニャン」とかついて返ってくる.花が「ワン」とか「ニャン」とか喋る.この語尾だが,花の性格で決まるようになっている.性格が決まっていなければお嬢様っぽく「ですわ」と喋ってくれる.性格は花の色,その色の濃さで決めようと思っていた.結局間に合わず,ランダムで決めている.色相の角度で決めている.

request.get( config.message.base + config.message.endPoint.chat + '?message=' + encodeURI( message ) + '&key=' + config.message.key, ( error, response, body ) => {
    if( !error && response.statusCode == 200 ){
        var responseMessage = JSON.parse( body ).result

        if( me.personality !== undefined ){
            request( config.message.base + config.message.endPoint.character + '?message=' + encodeURI( responseMessage ) + '&key=' + config.message.key + '&character_type=' + me.personality, ( error, response, body ) => {
                if( !error && response.statusCode == 200 )
                    io.sockets.to( socket.id ).emit( 'message', JSON.parse( body ).result )
            })
        } else {
            responseMessage = responseMessage.replace( /[?|?|!|!]/g, '' )
            responseMessage += 'ですわ'
            io.sockets.to( socket.id ).emit( 'message', responseMessage )
        }
    }
} )

動くからいいがとても遅い.さらにメッセージによっては「ワン」や「ニャン」にうまく変換されていない.

会話を行う際に「名前ちゃんLINE教えて」と打つとこの花に「名前」という名前をつけることができる.「ふらわぁちゃんLINE教えて」と言えば名前は「ふらわぁ」となる.これで LINE を使ってこの花と会話できる.と言っても今のところ自分の名前を呼んでくれるだけなので意味がない.

var isCheckName = false
var key

if( isCheckName || message.toLowerCase().indexOf( 'line' ) !== -1 && message.indexOf( '教' ) !== -1 ){
    if( !isCheckName ){
        key = message.replace( /[LINE|教えて|ちゃん|!|!]/g, '' )
        io.sockets.to( socket.id ).emit( 'message', 'あなたの名前は?' )
        isCheckName = true
    } else {
        // key = require('crypto').createHash('md5').update(Date(), 'buffer').digest('hex')
        var value = message
        request.get( 'http://calmery.me/postNameData.php?key=' + encodeURI( key ) + '&value=' + encodeURI( message ), function( error, response, body ){
            if( !error )
                io.sockets.to( socket.id ).emit( 'message', message + 'さん!<br>LINEで「召喚!' + key + '」とメッセージを送ってね' )
        } )
        isCheckName = false
    }
}

花の名前はユーザの名前と関連付けられて別に用意したサーバ上に保存される.LINE の Bot で,その名前のリストを受けとりユーザの名前を呼ぶようにしている.

LINE の Business アカウントを作成し Messaging API を利用する.
f:id:calmery:20161103203526p:plain
さくらインターネット上で動かした.共有 SSL を使用し Flask で動かしている.さくらインターネットでは Python のインストールから必要となるがそこは調べれば出てくる.個人的に Flask をインストールして動かすところには時間がかかったのでメモしておく.

$ pip install Flask
...
Successfully installed Flask-0.11.1 Jinja2-2.8 MarkupSafe-0.23 Werkzeug-0.11.11 click-6.6 itsdangerous-0.24

$ cd www
$ mkdir ~/www/flask-cgi-test
$ mkdir ~/www/flask-cgi-test/flowertalk
$ cd ~/www/flask-cgi-test/flowertalk
$ vim .htaccess

1 RewriteEngine On
2 RewriteCond %{REQUEST_FILENAME} !-f
3 RewriteRule ^(.*)$ /flask-cgi-test/flowertalk/index.cgi/$1 [QSA,L]

$ vim appFlask.py

1 # coding: utf-8
2 
3 from flask import Flask
4 app = Flask(__name__)
5 
6 @app.route('/')
7 def index():
8     return "Hello!"
9         
10 if __name__ == '__main__':
11     app.run()

$ vim index.cgi

1 # coding: utf-8
2 
3 from flask import Flask
4 app = Flask(__name__)
5 
6 @app.route('/')
7 def index():
8     return "Hello!"
9 
10 if __name__ == '__main__':
11     app.run()

パーミッションは 755 にする必要がある.

suexec failure: could not open log file, referer: http://sojo-patchworks.sakura.ne.jp/flask-cgi-test/
fopen: Permission denied, referer: http://sojo-patchworks.sakura.ne.jp/flask-cgi-test/
Premature end of script headers: index.cgi, referer: http://sojo-patchworks.sakura.ne.jp/flask-cgi-test/
suexec failure: could not open log file, referer: http://sojo-patchworks.sakura.ne.jp/flask-cgi-test/
fopen: Permission denied, referer: http://sojo-patchworks.sakura.ne.jp/flask-cgi-test/
Premature end of script headers: index.cgi, referer: http://sojo-patchworks.sakura.ne.jp/flask-cgi-test/

動作確認後,以下のように書き換えた.

# -*- codinf: utf-8 -*-
#! /home/sojo-patchworks/local/python/bin/python3
from flask import Flask, request, abort
import json
import requests
import urllib.parse

from linebot import ( LineBotApi, WebhookHandler )
from linebot.exceptions import ( InvalidSignatureError )
from linebot.models import ( MessageEvent, TextMessage, TextSendMessage, )

app = Flask(__name__)

line_bot_api = LineBotApi('CHANNEL_ACCESS_TOKEN')
handler = WebhookHandler('CHANNEL_SECRET')

@app.route("/callback", methods=['POST'])
def callback():
    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']

    # get request body as text
    body = json.loads(request.get_data(as_text=True))
    
    if not ("召喚!" in body['events'][0]['message']['text']) :
        r = requests.get( 'https://chatbot-api.userlocal.jp/api/chat?message=' + urllib.parse.quote(body['events'][0]['message']['text']) + '&key=USERLOCAL_API_KEY' )
        line_bot_api.reply_message(body['events'][0]['replyToken'],TextSendMessage(text=json.loads(r.text)['result']))
    else :
        line_bot_api.reply_message(body['events'][0]['replyToken'],TextSendMessage(text=json.loads( requests.get( 'http://calmery.me/getNameData.php' ).text )[body['events'][0]['message']['text'].replace('召喚!', '')] + 'さん!おかえりなさい!'))

    return 'OK'

@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    line_bot_api.reply_message(
        event.reply_token,TextSendMessage(text='Hello World!'))

無理やりとか言っちゃダメ.ここも単純にユーザーローカルの API にメッセージを投げて返しているだけ.「召喚!」が含まれている場合のみユーザ名と花の名前を取得しユーザの名前を呼ぶ.重複の判定がないので他のユーザに名前を上書きされるとその名前で呼んでしまう.それはそれで浮気っぽくていいかもしれない.ちなみに「召喚!」というのは眠さのあまり適当につけたもので特に意味はない.
f:id:calmery:20161103212920p:plain
遠出したときも花を見たいよねということで SkyWay の WebRTC の API も使用した.SkyWay に登録後,利用可能ドメインの設定を行う必要がある.ローカルでもいけた.これは全くと言っていいほど時間がかからず,サンプルそのままで動いた.
f:id:calmery:20161103205808p:plain

navigator.getUserMedia( {
    audio: true, 
    video: true
}, function( stream ){
    localStream = stream
}, function(){ 
    console.log( 'Error' ) 
} )

var call = peer.call( peer_id, localStream )
call.on( 'stream', function( stream ){
    var url = URL.createObjectURL( stream )
    document.getElementById( 'peerVideo' ).src = url
} )

peer.on( 'call', function( call ){
    connectedCall = call
    call.answer( localStream )
    call.on( 'stream', function( stream )
        var url = URL.createObjectURL( stream )
        document.getElementById( 'peerVideo' ).src = url
    } )
} )

結局,色々な API を使っただけなのではという気はしている.スマートフォンでも同じような動作をするアプリを作ってもらっていたがこちらは間に合わず断念.撮影の際に植木鉢を画面下からスライドして投げるようにしてある.名前は「FlowerG○」にしたい.

センサーで植物,花の管理を LINE 上で行いたかった.「元気?」と聞くと「喉が渇いた」と返してくれるような.そのために Raspberry Pi も持って行ってはいたが画面もなかったしセンサーもないのでどうしようもなかった.ただ会話だけ,遊ぶ分には面白いのかなと思う.

ふらわぁちゃんは弄ぶのがうまい.
f:id:calmery:20161103212952p:plain
f:id:calmery:20161119105623p:plain