Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all 8913 articles
Browse latest View live

Firebase Cloud Functionsでデフォルトで使える環境変数

$
0
0

前提

  • 2019/12/09現在の情報です
  • Firebase Cloud Functions
    • GCPのCloudFunctionsではない
  • Runtime Node.js 10
    • Node.js 10はBetaなのでご注意を

TL;DR

  • Nodejs 8で取れてた環境変数が10系だと取れない可能性があるので気をつけよう
  • Nodejs 10ではこいつらが使えるぞ

ドキュメント

環境変数とは

Node.jsだと process.envで取れるやつの話です。こいつは、FirebaseやGCP側が予め設定しておいてくれる変数が存在します。
下記のようにすれば自分でも設定できます。

firebase functions:config:set someservice.key="THE API KEY" someservice.id="THE CLIENT ID"

経緯

使える環境変数一覧

実際にNode.js 10のRuntimeでFirebase Cloud Functionsをデプロイして、process.envの中身を書き出してみました。 デプロイ時の関数名はSampleFunction、regionはasia-northeast1です。一部***でマスクしてます。あくまで2019/12/09現在で取れたものなので、今後取れなくなる可能性があります。

{"NO_UPDATE_NOTIFIER":"true","FUNCTION_TARGET":"SampleFunction","NODE_OPTIONS":"--max-old-space-size=256","NODE_ENV":"production","PWD":"/srv/functions","HOME":"/root","DEBIAN_FRONTEND":"noninteractive","PORT":"8080","K_REVISION":"2","K_SERVICE":"SampleFunction","SHLVL":"1","FUNCTION_SIGNATURE_TYPE":"http","PATH":"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","OLDPWD":"/srv","_":"../start-functions-framework","FIREBASE_CONFIG":"{\"projectId\":\"***\",\"databaseURL\":\"***\",\"storageBucket\":\"***.appspot.com\",\"locationId\":\"asia-northeast1\"}","GCLOUD_PROJECT":"***","VIPSHOME":"/target"}

ドキュメントに書いてあるとおり、いろいろ取れなくなったり名前が変わったりしています。 GCP側のドキュメントには GCLOUD_PROJECTはセットしないって書いてありますが、ここに書いてあるとおりFirebase側がセットしてくれているようです。


Reactで実現するUniversal JavaScript

$
0
0

はじめに

この記事はReact#2 Advent Calendar 2019 10日目の記事です。

先日某勉強会で以下のようなLTをさせていただきました。

はじめてのUniversal JavaScript

当記事ではこのLTを元に

・Universal JavaScriptについて
・LT内でも言及されているReact Routerを用いたSSRについて

深ぼっていこうと思います。

なぜUniversal JavaScriptか

まずはじめに、少しReactAdventCalendarという趣旨から少しはずれるかもしれませんが、Universal JavaScriptという言葉について触れておこようと思います。

Universal JavaScriptとは、
・サーバーサイドやクライアントのコードをJavaScriptのコードで共通化させていこうという設計論
・Isomorphic JavaScriptやSSRと同義語として扱われることも多い。
SSR(サーバーサイドレンダリング)という言葉の方が皆さんにとっても馴染みが深いと思います(ていうか私もです。Universal JacaScriptという用語は最近知りました。)

このUniversal JavaScriptという用語は2015年、こちらのUniversal JavaScriptという記事で言及させたのがはじまりだと言われています。

当記事でも

Because good names are important. A good name teaches about purpose and responsibility, so you have to spend some time thinking about it.

と言われている通り、物事を伝えるためには言葉選びというのは大切です。
私も教育に携わっている身なので、そのことはよくわかります。
私は最近プログラミングをはじめて、SPA+SSRというような感じでサーバーサイドレンダリングはSEOのため、ページ表示を早くするためプラスアルファでやるものという認識でした。(私の勉強不足というのもあるかもしれませんが)
ただこの用語に出会ってからはSSRは実装方法によっては、JavaScriptのクライアント・サーバー(Node.js)どちらも実装することが可能であるという利点を生かした設計ができるのではという考えをもつことができました。
(これはSSRへの理解にも繋がると考えています。)

なので、あえてタイトルは現在ではあまり目にすることはないUniversal JavaScriptというタイトルを使わせていただきました(決して興味を持たせようという釣りではありません。)

実装例

冒頭のLTの資料でも紹介させていただきましたが、こちらのUniversal JavaScriptの考えを理解するのに最適なのがNode.jsデザインパターンです.

こちらの例を少し紹介したいと思います。

環境
フロントエンド:React
サーバーサイド:Node(Express)

server.js
app.get('*',(req,res)=>{Router.match({routes:routesConfig,location:req.url},(error,redirectLocation,renderProps)=>{if(error){res.status(500).send(error.message)}elseif(redirectLocation){res.redirect(302,redirectLocation.pathname+redirectLocation.search)}elseif(renderProps){letmarkup=ReactDom.renderToString(<Router.RouterContext{...renderProps}/>);res.render('index',{markup});}else{res.status(404).send('Not found')}});

Expressでのルーティングのコードです。
Router.matchとはフロントエンドのReact.Routerのルーティングとマッチさせます。つまりはExpressのルーティングにReactRouterのルーティングを合わせ、ReactDOM.renderToStringによって受け取ったReactDOMをHTMLにして、返すことによってSSRを実現しています。
このようにフロント/サーバーのコードを同じ考えのもと共通かしていくのがUniversal JavaScriptの考えです。

現在のSSR

とはいえ、先ほどの例のコードは現在のReactRouterのバージョンに対応していないので、どのようなコードを書いて、実装していったら良いか考えてみましょう。

先ほどの例では、

{routes:routesConfig,location:req.url},

というようにフロントエンドのルーティングを一つのroutesConfigというモジュールにまとめることによって、ルーティングの共通化を実装しやすくしています。

これを実装するにはreact-routerの
react-router-config
が便利です。

こちらは、Routeを以下のようにまとめ、

route.js
constroutes=[{component:Root,routes:[{path:"/",exact:true,component:Home},{path:"/child/:id",component:Child,routes:[{path:"/child/:id/grand-child",component:GrandChild}]}]}];

このようにすることでルーティングを実装することができます。

import{BrowserRouter}from"react-routerdom";import{renderRoutes}from"react-router-config";importroutesfrom"./client/routes";<BrowserRouter>{renderRoutes(routes)}</BrowserRouter>;

以下のようにしてルーティングを実装します。サーバーサイドのルーティングにはStaticRouterを使います。

importhttpfrom"http";importReactfrom"react";importReactDOMServerfrom"react-dom/server";import{StaticRouter}from"react-router-dom";import{renderRoutes}from"react-router-config"importroutesfrom"./client/routes"http.createServer((req,res)=>{constcontext={};consthtml=ReactDOMServer.renderToString(<StaticRouterlocation={req.url}context={context}>{renderRoutes(routes)}</StaticRouter>
);if(context.url){res.writeHead(301,{Location:context.url});res.end();}else{res.write(`
      <!doctype html>
      <div id="app">${html}</div>
    `);res.end();}}).listen(3000);

※サンプルはReact Routerのドキュメントから引用

まとめ

いかがでしょうか?
SSRという用語だとなかなか気が進まない作業でもUniversal JavaScriptだとなんかかっこいい感じがします(笑)
というのは冗談で、UniversalJavaScriptという考えを知った上でSSRに取り組むと理解が進むのではないでしょうか?という初心者目線の投稿でした。

読んでくださり、ありがとうございました。

なんかReact要素少なかったような。。。すいません。

参考

ReactRouter
react-router-config

UniversalJavaScript

以下はIsomorphicJavaScriptの記事ですが、歴史がしれてとても興味深かったです。
https://speakerdeck.com/koichik/isomorphic-survival-guide)

Google Cloud Functions で obniz を 1分おきに動かしてみた

$
0
0

この記事は obniz Advent Calendar 2019の11日目の記事です。

obniz で高頻度なセンシングをラクに実現してみたい!

今年の首都圏を襲った台風をきっかけに、 obniz で自宅の気温や気圧などのログを取って可視化してみたいと考え、obniz Cloud の12分おきのセンシングではもの足りず、 Google Cloud Functions で obniz を 1分おきに動かしてみました。

obniz とは

obniz(オブナイズ)は日本の CambrianRobotics 社が開発したマイコンボードで、Wi-Fi に接続してインターネット経由で操作します。簡単に Wi-Fi に繋がり、ファームウェアの書き込み不要で API 経由で操作するので、インターネットと連携したハードウェアをサクッと作れるのが特徴です。

obniz の動かし方

obniz の動かし方の種類

obniz の動かし方は以下のように分類することができます。

obniz を1分おきに動かしてセンサの値を記録したい

obniz サーバーレスイベントは?

obniz サーバーレスイベントを使えば、定期的にセンサの値を記録するようなアプリケーションを作成することができます。ただ、 各イベントの実行は1日に120回までという制限があります。一定間隔で実行しても12分に1回実行できるわけで、IoTセンシング用途としては十分だと思いますが、 もっと高頻度なセンシングをラクに実現してみたい!ということで、別の方法を調べてみました。

専用のPCやサーバーを用意する?

Raspberry Piなど常時起動できるPCを1台用意して、定期的に走るように設定する方法もありますが、常に起動しておく必要があり、万が一エラーが起きてシャットダウンされた場合、復旧するのが面倒そうです。

FaaS

そこでクラウドサービスを利用することを検討しました。Node.js で数十秒間の関数を定期的に実行したいとなると、FaaSと呼ばれる、イベント駆動型コード実行サービスを使用するのが、容易で適していそうでした。
例えば Google Cloud Functionsでは、既にNode.js を動かせる環境が入っているため、環境設定が記載されたファイルや、実行する関数が記載されたファイルをアップロードするだけで、クラウド上で実行できるのです。

obniz 公式サイトにも、AWS Lambda で obniz を動かすレッスンがありますが、今回は無料制限回数が多かった(上無料体験期間が残っていた) Google Cloud Functions を使用してみました。
料金 - Google Cloud Functionsによると月200万回、100万秒、5GBまで無料で実行できる?ようです。
毎分実行しても1ヶ月に5万回未満なので、無料の範囲に収まりそう…?
(その他の部分で料金がかかるかもしれません。この記事を参考にして生じた損失に関して一切の責任を負いません。)

作るもの

Google Cloud Functions を使用し、 obniz を1分おきに動かしてセンサの値を読み取り、Google Spreadsheet に記録します。測定する量は、温度、湿度、気圧、照度とします。
https://qiita.com/y-hira/items/b8fe1268a12492bd865c
の記事のように、obniz のセンサの値を Google Spreadsheet に記録することができます。

手順

手元のPCで Node.js で obniz を動かすプロジェクトを作成し、 ZIP ファイルに固めて Google Cloud Functions にアップロードする流れになります。macOS 10.13 にて行いました。

obniz に配線

今回は BME280 とアナログ照度センサを配線しました。
image.png

ついでにテレビやエアコンの操作用に赤外線LEDも配線してありますが(笑)

データ記録用のスプレッドシートを作成

https://qiita.com/y-hira/items/b8fe1268a12492bd865c
の手順に従って、スプレッドシートと、GAS のプロジェクトを作成しました。GAS のプロジェクトの「現在のウェブアプリケーションのURL」をメモしておきます。

Node.js と npm をインストール

まず、手元の PC で Node.js と npm を使えるようにしておきます。

Node ファイルの作成

適当なフォルダ(ここでは obniz_gcf) を作成し、 obniz のライブラリをインストールします。

mkdir obniz_gcf
cd obniz_gcf
npm init

npm initでは簡単な質問に答えていくと package.jsonが生成されます。デフォルトのままで良ければ Enterを押していくことで進めます。

npm install obniz

obniz のコードを書くファイル index.jsを作成します。

touch index.js

コードを作成

POST するために、慣れているjQuery を使いたかったので、NodeJSでjQueryを使うを参考にコードを作成しました。

index.js
varObniz=require("obniz");constjsdom=require('jsdom');const{JSDOM}=jsdom;constdom=newJSDOM(`<html><body><div id="aaa">AAA<div></body></html>`);const{document}=dom.window;constjquery=require('jquery');const$=jquery(dom.window);exports.handler=function(event,context,callback){varobniz=newObniz(process.env.OBNIZ_ID);consturl=process.env.POST_URL;varsensor;obniz.onconnect=asyncfunction(){obniz.io8.output(true);sensor=obniz.wired("BME280",{gnd:9,sck:10,sdi:11,address:0x77});awaitsensor.applyCalibration();constillum=awaitobniz.ad7.getWait();constobj=awaitsensor.getAllWait();varparam={sheet:"log",obniz_id:obniz.id,temperature:obj.temperature,humidity:obj.humidity,pressure:obj.pressure,illuminance:illum};console.log(param);$.post(url,param).done(function(data){console.dir(data);obniz.close();callback(null,"success");});}};

追加で使用するライブラリもインストールします。

npm install jquery
npm install jsdom

Google Cloud Functions にアップロード

ZIP に圧縮

解凍したときに index.jsがトップの階層に来るように ZIP に圧縮します。

zip -r obniz_gcf_codes.zip index.js node_modules/ package-lock.json package.json

Google Cloud Platform のコンソールで新たにプロジェクトを作成し、 Cloud Functionsのメニューから新たに関数を作成します。

image.png

定期実行の設定

1分ごとに実行するため、トリガーには Cloud Pub/Subを選択します。
「新しいトピックを作成」し、適当な名前を付けます。
Cloud Scheduler を開き、「ジョブを作成」します。トピックには先程作成したトピック名を指定してください。毎分実行したい場合は、頻度として * * * * *を指定します。その他の時間間隔もこの文字列次第で設定することができます。

image.png

image.png

Google Cloud Functions の設定

ソースコードは ZIPアップロードとし、さきほど圧縮してできた ZIP ファイルを選択します。
実行する関数は handlerです。

image.png

また追加の設定項目を開き、環境変数として、 OBNIZ_IDに obniz の ID, POST_URLには GAS アプリケーションのURLを指定します。

image.png

最後に デプロイ !
アップロードに少々時間がかかるかもしれません。
うまくいけば、1分おきに、関数の実行が開始されます!
スプレッドシートを確認して、センサの値が正しく記録されているか確認してみましょう!

約2ヶ月間動かしてみた結果

既に2ヶ月近く動かしているのですが、安定して実行できています。 obniz はコンセントに挿した USB - AC アダプタに挿しっぱなしです。万が一停電が起きても、復旧時にも特に何もする必要がないのが楽でいいですね。

Google Cloud Functions の記録

obniz を接続している Wi-Fi ルーターがもともと不安定でときどき再起動してしまうこともあり、ときどきタイムアウトしていますが、おおむね安定して実行できているようです。Google Cloud Functions の設定では、50秒でタイムアウトとなっています。

image.png

どうやらタイムアウト時間の50秒近くかかっている場合もあるようです。
image.png

GAS の記録

GAS の方では1回あたり大体10秒ちょっとかかっているようですね。エラーもほぼないようです。

image.png

image.png

スプレッドシートの記録

2ヶ月もデータを溜めると、スプレッドシートを開くのにも時間がかかるようになってしまいました(笑)
GAS で毎日 Google スプレッドシート上のグラフを画像にして Google ドライブに保存するようにもしており、またデータにアクセスしたい場合は Google Colab を使って Python で処理すれば、 Google スプレッドシートを直接開く必要がないので、端末のスペックに依存しなくて済むかと思いますが、これ以上データを蓄積したい場合は、 IoT 向けのストレージを使うべきでしょう。

image.png

試しに11月1日0時0分〜11月30日23時59分59秒までにスプレッドシートに記録された行数を数えてみると、42951行ありました。
60*24*30=43200 なので、249回だけ記録できなかったことがあるようですね。でも、 99.4% 以上の割合で記録できています!

また台風通過時の気圧のグラフです。肝心の台風の目通過時は、 obniz Cloud で12分おきに取得していたのですが、それでも台風通過の様子がよく分かりますね。
たまに値が飛んでいますが、自宅の環境センシングとしては十分です。
image.png

気になる請求金額は

image.png

ちゃんと無料枠に収まっていました!
ただし他にもプロジェクトがある場合や設定次第で課金される可能性も十分あるので、無料枠で使いたい方は十分お気をつけください!

最後に

Google Cloud Functions で obniz を1分おきに動かすことができました!
台風通過時の気圧の変化、季節や時間による温度や湿度の変化を可視化できて満足です。
エアコンをつけたときの温度と湿度の変化など、グラフを見て気付かされることもありますね。逆にグラフを見ればエアコンをいつつけたかも分かります。もっとセンサの種類を増やしたくなりました。
ただ、蓄積したデータへのアクセスが問題なので、より良いデータの蓄積方法も考えてみたいと思います。

Let's IoT!
Let's obniz!

僕の個人プロジェクトを加速させたNuxt

$
0
0

はじめに

2019年は年間通してNuxtにお世話になったのでその感想を書きたかったのですが、感想という感想を書く時間がなくもう11日です。そのため内容はお察しください。

Nuxt とは何か?

Nuxt は、Vue の公式ガイドラインに沿って強力なアーキテクチャを提供するように設計されたフレームワークです。一部分から徐々に採用することが可能で、静的なランディングページから複雑な企業向け web アプリケーションの作成に使用できます。
本質的に汎用性があり、さまざまなターゲット(サーバー、サーバーレス、または静的)をサポートし、サーバーサイドのレンダリングは切り替えることができます。
強力なモジュールエコシステムにより拡張可能で、REST や GraphQL エンドポイント、お気に入りの CMS や CSS フレームワークなどさまざまなものに簡単に接続できます。PWA および AMP のサポートは、Nuxt プロジェクトにはないモジュールのみになります。
NuxtJS は Vue.js プロジェクトのバックボーンであり、柔軟でありながら自信を持ってプロジェクトを構築するための構造を提供します。

参考:https://ja.nuxtjs.org/guide/#nuxtjs-%E3%81%A8%E3%81%AF%E4%BD%95%E3%81%8B-

Nuxtを使ってみて

さまざまなものに簡単に接続できます。とNuxtも唱っている通り、とても簡単です。
日本語ドキュメントも充実しているため、Nuxtでの開発は簡単で楽しいものでした(ただStorybookの導入はちょっときつかった記憶、知識的な意味で)。
初めてVueを勉強しようと思った際に、以前はAPIをJavaで作ったりしていましたが言語とプロジェクトあっち行ったりこっち行ったりとをSwitchするのが僕はきつかった印象でした。
そのため中々進捗がなかったのですが、NuxtでserverMiddlewareプロパティがあることを知った際に別にJavaで作る必要ないんじゃない?(今更)と思いました。
そこから中々進捗のなかった個人プロジェクトに進捗の兆しが見えはじめました。

serverMiddlewareとは

Nuxt は内部で connect のインスタンスを作ります。 それはミドルウェアをスタックに登録したり、 外部サーバーを必要とせず に API などのルートを増やす事を可能にしてくれます。

参考:
https://ja.nuxtjs.org/api/configuration-servermiddleware/#servermiddleware-%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3

サクッというとNuxtだけでViewルート + APIルート作れるというものです。
これでプロジェクトあっち行ったりこっち行ったり問題が解決しました。更にNode + Expressを利用することで言語Switch問題も解決しました。嬉しい。

serverMiddlewareの導入

Nuxt.config.tsにserverMiddlewareを定義

Nuxt.config.ts
serverMiddleware:['redirect-ssl',{path:'/api',handler:'~/api/index.ts'}]

Nuxtプロジェクトにapiディレクトリを作成
apiディレクトリにindex.tsを作成

api/index.ts
constexpress=require('express')constapp=express()constroutes=require('./routes')app.use(express.urlencoded({extended:true}))app.use(express.json())constrouteKeys=Object.keys(routes)routeKeys.forEach((filename)=>app.use(routes[filename]))module.exports={path:'/api',handler:app}

apiディレクトリにroutesディレクトリを作成
routesディレクトリにindex.tsを作成

routes/index.ts
importtestfrom'./test'constroutes={test,}module.exports=routes

routesディレクトリにtest.tsを作成
'Hello World!'をただ返すだけです。loggerはそのままコピペでは使えないよ☆

routes/test.ts
import{Router}from'express'importcommonfrom'../common'constrouter=Router()// APIテスト用router.get('/test',(req,res)=>{res.send('Hello World!'),common.logger.LogAccessInfo('success: /api/test/')})exportdefaultrouter

localhost:3000の立ち上げ

$ yarn run dev

PostmanでHello world がget出来るか確認
image.png

とりあえずここまでくればあとはいい感じにAPIつくればよいだけです。
DBと繋ぎたいときもあると思います。そんなときはNuxt.config.tsenvはServer側でも利用出来るので便利です。(書いてる時思ったけど普通にimportしてるから使えるよね。。。)
僕はこんな感じで使っています。devは勿論Dockerで。

docker-compose.yml
version:'3.7'services:db:image:mysql:5.6container_name:mysqlenvironment:MYSQL_ROOT_PASSWORD:XXXXXXXXMYSQL_USER:XXXXXXXXMYSQL_PASSWORD:XXXXXXXXvolumes:-./sql:/docker-entrypoint-initdb.d-./my.cnf:/etc/mysql/my.cnfports:-3306:3306
Nuxt.config.ts
constmysql={dev:{host:'0.0.0.0',user:'XXXXXXXX',password:'XXXXXXXX',database:'XXXXXXXX'},production:{host:'XXXXXXXXX',user:'XXXXXXXXX',password:'XXXXXXXXX',port:3306,database:'XXXXXXXXX'}}env:{mysql:process.env.NODE_ENV==='production'?mysql.production:mysql.dev},
// 使用時
import config from '../../nuxt.config'
const connection = await mysql.createConnection(config.env.mysql)

あとがき

HerokuにもGithub連携して、環境変数設定(Nuxtのドキュメント通り)してとすぐにデプロイまで出来たので何かつくるならこの構成でいいなーって感じです。
しかしHerokuでMySQL使うにはクレジットカードが必須ですが、認証出来なくてそれが1番の難関でした。楽◯カードマンめ(´・ω・`)
あとMySQL5.6までしか使えなかったのでそれもトラップでしたが無料で使えるのはありがたかったです。
時間こそかかってしまいましたが結構手軽にWebアプリケーションつくれた印象でしたのでオススメしたいです!!!

明日は

「North Detail Advent Calendar」 12日目は @k_azmさんです!いえーい :clap:

Transcribeの英語ミーティング文字起こしをmarkdownにする

$
0
0

fushimiです。
この記事は Wanoグループ Advent Calendar 2019 Advent Calendar 2019の11日目の記事になります。

(英語)会議 is ...:innocent:

最近プロジェクトではニューヨークのチームと英語での会議をする機会が多いです。基本リスニングも不得手なので、会議同席中も熱が入って速度が速い時はなかなか聞き取れないことがあります。
そこで会議の後など、勉強/議事録がてらAmazon Transcribeでの音声文字起こしを見てわからなかったところの文脈を追ってみたりしています。
今回はTranscribeの出力をmarkdown化するやつをwebアプリに起こしてみました。

制作物

リポジトリ:
wano/aws-transcribe-render

アプリケーションのページ:
https://wano.github.io/aws-transcribe-render/

Amazon Transcribe

いわゆる文字起こしサービスです。S3上の音声ファイルを解析してくれます。
最近日本語対応したり東京リージョンで使えるようになったりしました。
ただ文字起こしをする、ってだけではなく、話者解析(誰がそのフレーズを喋っているか)が取得できるのがなかなか面白いところかな、と思います。
Termは割と適当なんだけど、話者解析もあって文脈が追えるので復習にはいいかな...というステータスのサービスです。

ちょっとした文ならコンソール上で結果がプレビューできるのですが、ある程度の長さになると出力結果であるオリジナルのjsonを使うしかありません。

...Canyouseetheseats?Nooption.Hello.It'snot"}],"speaker_labels":{"speakers":8,"segments":[{"start_time":"1.44","speaker_label":"spk_4","end_time":"2.35","items":[{"start_time":"1.44","speaker_label":"spk_4","end_time":"1.81"},{"start_time":"1.94","speaker_label":"spk_4","end_time":"2.35"}]},{"start_time":"11.94","speaker_label":"spk_4","end_time":"12.45","items":[{"start_time":"11.94","speaker_label":"spk_4","end_time":"12.45"}]},{"start_time":"13.71","speaker_label":"spk_4","end_time":"14.16","items":[{"start_time":"13.71","speaker_label":"spk_4","end_time":"14.16"}]},{"start_time":"14.71","speaker_label":"spk_4","end_time":"15.38","items":[{"start_time":"14.71","speaker_label":"spk_4","end_time":"15.38"}]},{"start_time":"16.24","speaker_label":"spk_4","end_time":"16.91","items":[{"start_time":"16.24","speaker_label":"spk_4","end_time":"16.91"}]},{"start_time":"25.86","speaker_label":"spk_1","end_time":"26.97",...

こういう感じ。なかなか辛い

初めはPHP製のパーサーaws-transcribe-transcriptを改変してコマンド叩いていたのですが、jsonパース/整形/改変くらいwebのクライアントサイドでサクッとやれるべきだよな...という感想があったので、今回のアドベントカレンダーを機にjsで書いてみました。

aws-transcribe-render

image.png

markdown化する、と書きましたが、mustache記法でテンプレを書いているだけなのでなんでもありといえばありです。
テンプレートの塊は、ある話者が話し始めてから終わるまでとなっています。

使い方

事前: まずは文字起こし

まず、会話/会議の音声データをs3に上げ、transcribeのコンソールからjson化しておきます。
image.png

jsonをアプリに入力/テンプレート編集

そのjsonをこちらのアプリに入力します。

  • speaker
  • text
  • time

がテンプレート変数として渡ってくるので、markdownでもhtmlでもお好みのフォーマットで出力できます。

話者名の上書き機能

Transcribeのデフォルトの話者名を上書きすることもできます。
spk_0 という身も蓋もないラベリングになってたりするので、多少記憶を掘り起こし話者の名前を書きましょう。

結果

ここまでの結果が以下のような感じです。

image.png

あとはコピーして議事録に貼り付けるなど。

まとめ

これで、出力結果をinputするだけでさっくりと編集する機能ができました。
英語で行われるミーティングでまた試す予定ですが、そういえば日本語会話でTranscribeを試したことがないので、そちらの精度も気になりますね。

課題

  • 外に出しても差し支えないような会話でアプリにサンプルを載っけたかったんだけど、著作権フリーな「会話音声データ」ってなかなか見つからないですね....
  • GUIと関係ないコア部分のロジックは切り離されていますが、肝心のnpmモジュール化とかはまだしていません。 1年以上ぶりくらいにReactを触ったら「useStateすげー!」「useEffectすげー!」ってなってたりしたのでそっちで時間が溶けました。
  • XSS対策真面目にやっていません。mustacheのテンプレをそのまま出しています。どこか永続化するサイトに使うわけでもないのでどうということはないのですが。

【Windows版】Node.js上でTensorFlow.jsを使う準備

$
0
0

はじめに

クライアントに極力負担をかけたくない!
サーバで学習も推論もするんだ!JavaScriptすき!!
そんなとき、Node.js上でTensorFlow.jsを動かしたいと考え、環境構築をしたとき、思ったほどめんどくさかったのでメモ!!

前提条件

  • Node.jsがインストールされていること
  • OSがWindows7以上であること

構築

手順
1. node-gypのインストール
2. tfjs-nodeのインストール

簡単そう...

node-gypの導入

node-gypとは?

node-gypNode.js用のネイティブアドオンモジュールをコンパイルするためのNode.jsで記述されたクロスプラットフォームコマンドラインツールです

node-gyp
横文字ばっかりでさっぱり。
要するに他の言語で書かれたモジュールとかをNode.jsで使えるようにビルドするぜってことみたい。

Pyhton2.7 と C++ビルドツールが入っていないと動作してくれないので、まずそいつらからインストールしていく
PowerShellを管理者権限で実行し、以下のコマンドを打ち込む。
※windows7を使っている人は.NET Framework 4.5.1が必要だぞ

npm install --global --production windows-build-tools

こいつはすげえ。ビルドツールとPyhton2.7を一緒に入れてくれる代物!!最高だ!!
windows-build-tools

そしてnode-gypをグローバルインストール!

npm -g install node-gyp

オワリ!!!

tfjs-nodeの導入

tfjs-nodeとは?

このリポジトリは、Node.jsランタイムの下でバックエンドJavaScriptアプリケーションでネイティブTensorFlow実行を提供し、内部のTensorFlow Cバイナリによって加速されます。TensorFlow.jsと同じAPIを提供します。

tfjs-node
Node.jsでTensorFlow.jsを使いたいならtfjs-node入れろ!てことだ。

CPU版

npm install @tensorflow/tfjs-node

GPU版

npm install @tensorflow/tfjs-node-gpu

これでオワリ!!!

実行するときは

//CPU版import*astffrom'@tensorflow/tfjs-node';//GPU版import*astffrom'@tensorflow/tfjs-node-gpu';

を書いてから実行!

最後に

私はこの作業でエラーに悩まされました。
こうして書いてみると意外に単純な作業だったんだなって思います。

悩まされた原因の一つとして、英語の文献を適当に読んでいたからだと思います。
反省してます。これからはちゃんと隅々まで読みます。

英語圏の人として生まれたかったぞ:frowning2:
多分ほとんどのエンジニアは一度はそう思ったことがあるはずだ:frowning2:

TypeScriptでステップ実行するときの設定の自分用のまとめ

$
0
0

私はTypeScript初心者です。よろしくお願いします。m(_ _)m

今回tsconfig.jsonはこのような設定で行いました。
sourceMaptrueに設定しておきます。

tsconfig.json
{"compilerOptions":{"target":"es6","module":"commonjs","sourceMap":true,"outDir":"./dist","strict":true},"include":["src"],"exclode":["node_modules"]}

ステップ実行するコードは、こちらの簡単なコードで行います。

https://gist.github.com/okumurakengo/8433019b8b525dd241c08cb357c414e7

src/index.ts
functionfizzbuzz(n:number):number|"Fizz"|"Buzz"|"FizzBuzz"{if(n%15===0){return"FizzBuzz";}if(n%3===0){return"Fizz";}if(n%5===0){return"Buzz";}returnn;}leta:number=1;console.log(fizzbuzz(a++));console.log(fizzbuzz(a++));console.log(fizzbuzz(a++));console.log(fizzbuzz(a++));console.log(fizzbuzz(a++));// ...

chromeでステップ実行する用にhtmlも作成

index.html
<!DOCTYPE html><metacharset="UTF-8"><title>Document</title><script src="dist/index.js"defer></script><p>ステップ実行のテスト</p>

1-1. chromeでステップ実行をしてみた

yarn add -D typescript
yarn tsc # src/index.ts をコンパイルして、 dist/index.js が出力

index.htmlを開き、開発者ツールのSourcesパネルでtypescriptのコードを確認できるので、ブレークポイントを設定してステップ実行することができました。

C566UY2YIa.gif

1-2. chromeでNode.jsのコードのステップ実行してみた

参考: ChromeDevToolを使ってNodeJSのデバッグ - Qiita

nodeで実行するときに --inspectフラグをつけて実行することで、chromeでステップ実行することができます。

node --inspect index.js

参考の通りに、ts-nodeを指定すると、typescriptをts-nodeで実行して、chromeでステップ実行することができました。
--inspect-brkをつけると、1行目にブレークポイントをつけたように実行してくれるようなので、それもつけておきました。

$ yarn add -D typescript ts-node
$ node --inspect--inspect-brk--require ts-node/register src/index.ts
Debugger listening on ws://127.0.0.1:9229/d715288d-7d07-4576-834e-4787eecadb0b

chrome://inspect を開いて、Remote Targetの部分にある、inspectを押すと、開発者ツールが開いてステップ実行できました。

Screen Shot 2019-11-24 at 16.20.19.png

2-1. vscodeでステップ実行してみた

参考:https://code.visualstudio.com/docs/typescript/typescript-debugging

vscodeの左の虫のマークを押して、左上の歯車を押すと、launch.jsonが開きます。

Screen Shot 2019-12-11 at 22.51.45.png

参考ページのlaunch.jsonを自分のディレクトリに合わせて設定します。

launch.json
{//UseIntelliSensetolearnaboutpossibleattributes.//Hovertoviewdescriptionsofexistingattributes.//Formoreinformation,visit:https://go.microsoft.com/fwlink/?linkid=830387"version":"0.2.0","configurations":[{"type":"node","request":"launch","name":"launch","program":"${workspaceFolder}/src/index.ts","preLaunchTask":"tsc: build - tsconfig.json","outFiles":["${workspaceFolder}/dist/**/*.js"]}]}

この状態で左上にある緑色の三角の実行ボタンを押すとステップ実行できました。

Screen Shot 2019-11-24 at 16.39.03.png

2-2. vscodeでクライアントコードのステップ実行してみた

vscode拡張の Debugger for Chrome をインストールする

Screen Shot 2019-11-24 at 16.44.56.png

launchi.json
{//UseIntelliSensetolearnaboutpossibleattributes.//Hovertoviewdescriptionsofexistingattributes.//Formoreinformation,visit:https://go.microsoft.com/fwlink/?linkid=830387"version":"0.2.0","configurations":[{"type":"chrome","request":"launch","name":"Launch local file","preLaunchTask":"tsc: build - tsconfig.json","url":"file:///Users/kengookumura/dev/sample/index.html","webRoot":"${workspaceFolder}"}]}

この状態でデバッグの実行ボタンを押すと、chromeが起動し、デバッグすることができました。

oyK8cxjCHw.gif


↑の設定だと、ローカルファイルとしてデバッグしているので、例えば、自分でローカルサーバを起動していて、http://localhost:8000/として実行したい場合は、launch.jsonの設定を変更するか、追加すると実行できます。

launch.json
{//UseIntelliSensetolearnaboutpossibleattributes.//Hovertoviewdescriptionsofexistingattributes.//Formoreinformation,visit:https://go.microsoft.com/fwlink/?linkid=830387"version":"0.2.0","configurations":[{"type":"chrome","request":"launch","name":"Launch local file","preLaunchTask":"tsc: build - tsconfig.json","url":"file:///Users/kengookumura/dev/sample/index.html","webRoot":"${workspaceFolder}"},{"type":"chrome","request":"launch","name":"Launch localhost","preLaunchTask":"tsc: build - tsconfig.json","url":"http://localhost:8000","webRoot":"${workspaceFolder}"}]}

jsonの"name"の部分でどれを実行できるか指定できるので、Launch localhostを指定した状態で実行すると

Screen Shot 2019-11-24 at 17.05.24.png

http://localhost:8000/を指定して、vscodeでクライアントコードのデバッグができました。

Screen Shot 2019-11-24 at 17.06.27.png

3-1. WebStormでステップ実行してみた

参考

右上の「Add Configulation」を押す

Screen Shot 2019-11-24 at 18.07.36.png

左上の「+」を押して、「Node.js」 を選択

Screen Shot 2019-11-24 at 18.08.13.png

  • Working directory
  • JavaScript file

をそれぞれコンパイル後のJavaScriptを指定する。

Before launch: activate tool window に Compile TypeScriptを指定すると、
最初の一回はコマンドでtscと実行し(※最初の1回目をWebStormで実行するとdist/index.jsがないとエラーになる)、
それ以降WebStormで実行するときは自動でTypeScriptをコンパイル後にデバッグ実行してくれました。

Screen Shot 2019-11-24 at 18.10.27.png

この状態で右上のデバッグ実行のボタンを押すと、ステップ実行できました。
※私の環境ではなぜか不安定で、ブレークポイントを2つ設定しないと止まってくれなかったり、普通に動いてくれることもあったりで、よくわかりませんでした。

LfENC9nON0.gif

3-2. WebStormでクライアントコードのステップ実行してみた

右上の「Edit Configulation」から、
左上の「+」を押して、「JavaScript Debug」 を選択

URLにhttp://localhost:63342/<index.htmlへのパス>で設定すると、WebStormの組み込みのWebサーバーからステップ実行してくれました。
※もちろんhttp://localhost:8000などとして自分のローカルサーバーを指定しても大丈夫でした。

先ほどと同じように、Before launch: activate tool window に Compile TypeScriptを指定すると、
デバッグ実行するときは毎回自動でTypeScriptをコンパイルした後にデバッグ実行してくれました。

Screen Shot 2019-11-24 at 18.49.41.png

I2sPT0jeKg.gif


以上です。見ていただいてありがとうございました。m(_ _)m

wordpress投稿データをcontentfulに移植するスクリプトを公開しました。

$
0
0

最近contentfulを業務で使うことが多くなった上に、非エンジニアの方々にcontentfulを教える作業が辛くなったため、エディタをwordpress、データベース・APIをcontentfulにと役割分担させるべく「wordpressで書いた記事をcontentfulに同期しちゃう」というスクリプトを書きました。

その際、contentfulのfieldsの仕組みを鑑みてwordpressの記事投稿は通常投稿(post)を許容せず、いわゆるカスタム投稿(custom post type)のみを許容するようにしました。

正直、通常記事しかないとしたらcontentfulの高機能なAPIを使うこともないと思うので、wordpress rest apiでゴニョゴニョして運用した方がいいと思います。

早速使ってみる

Githubからzipなりpullなり適当にダウンロードしてもらって、ローカルで下準備をします。

必要なもの

とりあえず、必要なものは

  • Wordpress Information

    Or (Only one of these two)
    - "Wordpress custom posts and custom post type slug names of those" (ex:
    { "example_slug1": ["example_post1", ...], ... } )

  • Contentful Information

    • "Contentful Delivery API" (see here)
    • "Contentful Management API"(see here)
    • "Contentful Space ID"
    • "Contentful Environment ID" (default: master)
    • "Contentful Area"(default: en-US)

以上になります。(githubから横流しですみません)
日本語で軽く説明しますと、
wordpress rest apiからデータをとるため、rest apiのurl(これは、wp-json/wp/v2という末尾を考えています)、データの対象であるslugsやposts・slugsのペアのいずれかが必要になります。(どっちもはダメです)
次に、contentfulにデータをアップロードする際に諸々の情報が必要になりますが、基本的にググれば出るので割愛します。気をつけることとしては、management apiとdelivery apiが割と区別されているので惑わされないようにするくらいです。(management apiは作成とかで、delivery apiは閲覧専門的な感じです)

wordpress側の設定と入力事項

今回テスト環境に適当なwordpressをインストールして、rest apiは全て公開するというノンセキュアな環境のみで実行しています。そのため、セキュリティプラグインが邪魔するなどの方は、リポジトリを改変するかセキュリティプラグインの設定をうまくrest apiが使えるようにチューニングしてください。

wordpressに Custom Post Type UIを入れてカスタム投稿量産体制を整える

このプラグインを使用すると、カスタム投稿がボタンぽちぽちするだけで作れて、カスタムタクソノミーもボタンぽちぽちで瞬時に作れます。
とりあえず、テストというカスタム投稿とカスタムタグ、カスタムカテゴリーを作成しちゃいます。
スクリーンショット 2019-12-12 2.37.43.png
スクリーンショット 2019-12-12 2.38.12.png
(同様にカスタムカテゴリー。写真略)
脱線しますが、wordpressって無料プラグインのくせにあり得ないほどの高機能なプラグインがあったりしてたまに怖いです。その中でもこのプラグインはシンプルさと高機能・カスタマイズ性の高さがすごいので是非この機会に今後も使ってもらいたいですね。

wordpress上でデータを作っとく

僕は、以下のようにデータを作っときました。
スクリーンショット 2019-12-12 2.39.54.png
下書きを入れているのは、テスト用で、下書きはrest api上で取得できないです。そのためcontentfulへのデータ移植は公開済みのものしか対応していません。

Contentfulでapiを取得する

contentfulアカウント作成→スペース作成(スペースIDが後に必要)→Delivery api keyを取得(後に必要)→management api keyを取得(後に必要)→地域設定が必要なら済ませてdefault areaを設定(後に必要)
加えて、ENVIRONMENT_IDも必要だったりしますが、基本的にmasterで問題ないはずです。

スクリプトを実行する

二通りのパターンで実行できるようにしましたが、今回は楽チンに設定ファイルで済ませてしまいます。

.env.jsonに直接環境設定を記載。ローカル環境なのでこうしています。
スクリーンショット 2019-12-12 2.50.37.png

setup.jsonにwordpress側の欲しいデータの設定を記載。
スクリーンショット 2019-12-12 2.50.51.png

なお、WORDPRESS_POST_TYPE_SLUGSとWORDPRESS_POST_TYPE_SLUG_IDSの片方は空でないといけないようにしました。両方を考慮するパターンは、需要がなさそうな上に処理が煩雑になったためです。

run script

node index.jsで実行。
色々出力していますが、終わってcontentfulを確認して中身がいい感じなら完了です。
なお、後半忙しくなって、entry titleを設定してないので、最後にそれだけ設定してしまいましょう。

Entry Titleの設定

Content Modelに入って、作成したcontent modelをクリックします。
その後titleのSettingsをクリックして、Field optionsというところにあるThis field represents the Entry titleにチェックをつければ完了です。

注意点とまとめ

基本的に、個人的なスクリプトなので共通して雑に作成しています。暇な方はプルリク投げてくれると仕事した感が出るのでお願いします。

注意点

今回個人的な使用目的に限定しているためいくつかの制約があります。
・カスタム投稿しか使えない
・カスタムフィールドとかに対応していない
・カスタムタクソノミーがcontent modelのshort textのlistとして扱われる(※1)
・親カテゴリーなどに対応していない
などなど
個人的には、通常投稿で固めていてカスタムフィールドをデータベースとして使用している方も多い印象なので、そうした方々はそれらデータをエクスポートしてカスタム投稿とカスタムタクソノミーに合うように工夫してもらうとかがいいかと思います。例えば、wordpressダッシュボードのツールからエクスポートして、ローカルでそのファイルのpost名をカスタム投稿のpost名に置換してインポートするとカスタム投稿として複製されます。

まとめ

用途が限られるかもしれませんが、簡易的なサイトであったり個人用サイトではこのスクリプトで物足りることは多い気もします。保守メンテはとても暇な際にやる予定なので、エラーが出たりしたら一応共有して頂けると嬉しいです。

※1:contentfulの仕様的に、いわゆるタグやカテゴリーを実現するには、1.content modelに作成するパターンと2.fieldsに書いてしまうパターンの二つがあります。1のパターンだと絞り込みでgetする際にrelationから取得しますが、2のパターンだと標準のfieldsの絞り込みapiで実現できます。好き好きではありますが、個人的にcontentfulの無料枠がasset 5000まで(1だとasset扱いにならざるをない)なのでケチな私は2しか選べなかっただけです。すみません。


「結び目」の図を簡単に描きたい

$
0
0

「結び目」について

この記事でいう「結び目」というのは、一つの閉じた円環がどのように絡まっているかを分類したものです。
参照:結び目理論(Wikipedia)

470px-Knot_table.svg.png

「結び目」に関する文書などを書く際に、簡単に結び目の画像を描くことが出来ないかと考えて、調査してみたことと合わせて記事を書いてみたいと思います。なお、今回の記事は、計算によって曲線を生成する、ということに着目したもので、「結び目理論」とは本質的に関係がありません。

グラフィックツールで描く

手作業で描く手順

「交叉」している部分以外は、各グラフィックツールで閉曲線を描くことになるので省略します。
「交叉」している部分の描き方は様々な方法があると思いますが、今回は以下のようなやり方で表現しています。

svg-knot-layers.jpg

  1. 下の線を「黒・細いストローク」で描く
  2. 交叉部分によって下の線を隠すため、上の線を「白・太いストローク」で描く
  3. 上の線を「黒・細いストローク」で描く

難点

  • 曲線を上手に描くのが難しい

多くのグラフィックツールでは曲線を描く場合に、「ベジェ曲線」を使用して編集します。しかし、多数の点を滑らかに通過するような曲線を手作業で描くのは、ツールに慣れていないとなかなか骨が折れる作業です。

bezier-edit.png

  • 交叉部分を作るのが面倒

前述した「交叉する部分」の編集は単純ながら、グラフィックツールを使って手作業で行うのはなかなか面倒です。結び目が複雑になってくると、この交叉する箇所が多くなり編集時間や間違いの可能性が増えることが予想されます。

自動的に描くために

今回は、指定した点を通り、SVGを使って滑らかな曲線を表現する調整を計算により求め、また、交叉する部分のSVG編集を同時に行う、ということを目標とします。
入力は、グラフィックツール(Inkscapeを使用)により作成したSVGとして、下記のようなSVG画像を出力します。

knot_input_output.jpg

基本の3次Bezier曲線

SVGでサポートされている曲線で、簡単な計算式によって様々な形の弧からなる曲線を描くことができます。

Bézier curves, (Cubic Bézier curves)

800px-Bezier_curve.svg.png

B(t) = (1-t)^3 P_0 + 3(1-t)^2t P_1 + 3(1-t)t^2 P_2 + t^3 P_3 \hspace{10mm} (0 \leq t \leq 1)

このベジェ曲線の複数の弧を使って、指定した点を通る曲線を生成します。

Catmull-Rom曲線

Bezier曲線を使って、指定した点を滑らかにつなぐには上記の$P_1$、$P_2$を別途計算することが必要になります。今回はCatmull-Rom曲線というものを利用して、Bezier曲線を生成しました。

"Centripetal Catmull–Rom spline" (Wikipedia)

catmullrom-example.jpg

この曲線はBezier同様に簡単な式で表現できる上に、3次Bezier曲線と相互に変換することが可能です。入力として与えられた点を通るCatmull-Rom曲線をBezier曲線に変換することで、SVGの基本機能のみで描画することが可能になります。

Catmull-RomからBezierへの変換

catmullrom-bezier.jpg

図中、$P_0, P_1, P_2, P_3$ を通るCatmull-Rom曲線の弧 $P_1P_2$ に相当するBezier曲線の点 $b_0, b_1, b_2, b_3$ を求める変換式は、下のようになります。

\begin{align}
b_0 & = P_1 \\
b_1 & = P_1 + \small{\frac{P_2 - P_0}{6}} \\
b_2 & = P_2 - \small{\frac{P_3 - P_1}{6}} \\
b_3 & = P_2
\end{align}

(注) 一般的なCatmull-Rom曲線には曲がり具合を調整するために計算に $\alpha$ という設定値を用いていますが、今回は、簡単のため $\alpha = 0$ として計算しています。

作業、および計算処理の流れ

直線で概形を描く(手作業)

最初に入力するSVGファイルを作成します。ここは手作業です。
このファイルを作る際に、下記の制限を設定しています。

  • 制限1:2つの線分が交叉する場合は端点以外の場所で交叉する
  • 制限2:全体として1個の閉じた曲線を構成する

作成した曲線(線分で構成された閉曲線)をSVGファイルとして保存し、プログラムの入力とします。

SVGファイルを読み込み、path要素内のd属性を見つける

以下、node.jsを使用して作成したものについて説明します。

上記のSVGドキュメント内に存在するpath要素のd属性を読み込みます。
ここでは、jsdomを利用してSVGをDOMとして扱っています。

(SVGドキュメントの構造例)
svg-dom1.png

constfs=require("fs");constjsdom=require('jsdom');const{JSDOM}=jsdom;fs.readFile(process.argv[2],'utf8',function(error,data){// obtain svg path stringconstdom=newJSDOM(data);constpath_d=dom.window.document.querySelector("svg path").getAttribute("d");

path要素のd属性を解析して頂点を求める

path要素のd属性には、SVGの描画命令文が格納されています。命令文を解析して座標を計算し、頂点として必要なものを抽出します。

(SVG path 描画命令文の例)
svg-dom2.png

今回は、MoveTo命令とLineTo命令のみを読み取れば良いので、ごく簡単な解析で済ませています。

// parse path string to get end pointsletpathSentence=path_d.match(/[mzlhvcsqta][0-9\-., ]*/gi)for(letphraseofpathSentence){letcmd=phrase[0];if(cmd==="m"||cmd==="l"){// move and lineToletpoints=phrase.slice(1).trim().split(/[\s,]+/)letx=points.shift();lety=points.shift();//... (以下略、必要であれば後述のソースコードを参照してください)

交叉する弧を見つける

全ての2つの線分の組み合わせについて、「交叉するか否か」を判断し、その情報から配列を生成します。後の工程で曲線の弧が「上を通る」か「下を通る」かを切り替える場所となります。

なお、「入力の線分が交叉するなら、生成するCatmull-Rom曲線の弧が交叉する」、また「線分が交叉しないなら、弧は交叉しない」という前提です(その前提が成立するように入力用SVGファイルを作成する必要があります)。

functionisCrossing(segments0,segments1){let[p0,p1]=segments0;let[p2,p3]=segments1;let[x0,y0]=p0;let[x1,y1]=p1;let[x2,y2]=p2;let[x3,y3]=p3;// bounding boxes testletb=Math.max(x0,x1)>Math.min(x2,x3)&&Math.min(x0,x1)<Math.max(x2,x3)&&Math.max(y0,y1)>Math.min(y2,y3)&&Math.min(y0,y1)<Math.max(y2,y3);if(b){// cross product testletcp0=(x2-x0)*(y3-y0)-(y2-y0)*(x3-x0);letcp1=(x2-x1)*(y3-y1)-(y2-y1)*(x3-x1);letcp2=(x0-x2)*(y1-y2)-(y0-y2)*(x1-x2);letcp3=(x0-x3)*(y1-y3)-(y0-y3)*(x1-x3);return(cp0*cp1<0)&&(cp2*cp3<0);}returnfalse;}

Catmull-Rom曲線をSVGのBezierで描く

前述の変換式を使って、Catmull-Rom曲線を生成するBezire曲線の各点を求めます。

//// convert Catmull-Rom endpoints to cubic Bezier nodes//functioncatmullRomToBezier(endPoints){letbezierNode=[];bezierNode.push(endPoints[0]);endPoints.forEach((p,i,a)=>{letP0=a[(i-1+a.length)%a.length]letP1=pletP2=a[(i+1)%a.length]letP3=a[(i+2)%a.length]// Catmull-Rom to Bezier conversion (alpha=0)letCb1x=P1[0]+(P2[0]-P0[0])/6letCb1y=P1[1]+(P2[1]-P0[1])/6letCb2x=P2[0]-(P3[0]-P1[0])/6letCb2y=P2[1]-(P3[1]-P1[1])/6// push cubic bezier pointsbezierNode.push([Cb1x,Cb1y])bezierNode.push([Cb2x,Cb2y])bezierNode.push(P2)});returnbezierNode;}

交叉部分を作る

生成されたCatumull-Rom曲線(SVG内ではBezier曲線で表現)の弧で交叉する部分を作ります。
g要素をレイヤーとして用いることによって、基本的には手作業で作成したのと同じ表現方法です。

svg-dom3.png

出力

jsdomのDOMを利用して全体をSVGドキュメントとして完成し、最終的にテキストとして出力して終了です。

//// create simple svg document with a Catmull-Rom curve//functiondraw_svg(endPoints,crossing,viewBox){// create document root and bodyconstdocument=newJSDOM().window.document;constbody=document.body;// create svg elementconstsvgns="http://www.w3.org/2000/svg";constsvg=document.createElementNS(svgns,"svg");// ... (中略)// construct svg document and output as textbody.appendChild(svg);returndocument.body.innerHTML;}

完成したもの

以上を、コマンドラインから使用できるプログラムとして作成したものをGitHub上にまとめました。

draw_knot (GitHub)

node draw_knot.js input.svg > output.svg

生成したSVGドキュメントは標準出力に表示されます。
なお、今のところ、Inkscapeで作成したSVGでのみ動作を確認しています。今回の記事の内容のデモンストレーションが目的であって、あまり汎用的なツールとなることを目的とはしておりません。動作不良などは悪しからずご了承ください。

作図例(入力と出力)

Prime Knot 3 (三つ葉結び)
prime3.jpg

Prime Knot 4 (8の字結び)
prime4.jpg

まとめ

今回の調査の元々のきっかけは、様々なデータを可視化する際に、matplotlibやd3.jsを使ってカスタマイズした図を作るよりも、SVGなどの基本的な機能で描画したほうが手っ取り早く、見やすいものができるのではないか、という考えからSVGの基本機能を調査してみよう、というものでした。

簡素ではあるけれども、「特殊な用途のための図」にぴったりのものがない場合に、ひとつの選択肢として考慮してみるのも良いと思います。

参考にした記事など

Centripetal Catmull–Rom spline

Bézier curves, (Cubic Bézier curves)

A Primer on Bézier Curves: 34. Bézier curves and Catmull-Rom curves

東大の学園祭でLINE BOTをつくった話【Node.js】

$
0
0

この記事は学園祭のシステム業務に携わった人々などがお送りする「学園祭プログラマーAdvent Calendar 2019」の12日目の記事です。
よかったら他の人の記事も見ていってくださいね!

はじめに

みなさんLINE Messaging APIってご存知ですか?この記事を見ている人ならおおよそ知っている人が多いと思いますが、LINEで自動返信をする"BOT"を作ることができるAPIです。約3年前にリリースされてから様々な機能が追加されてきました。今回の記事はこれを使って学園祭でLINE BOTを作成したことに関するお話を色々していきたいと思います。

自己紹介

自己紹介が遅れました。私、東京大学での秋の学園祭「駒場祭」でLINE BOT関係を担当させていただいた者です。yuukamiと呼んでください…

数年前、ちょうどBOTブーム最盛期?にLINE BOTに出会いまして、BOTで色々遊んだり、高校の文化祭の公式LINEを担当したりしておりました。その後、大学に入学したのち、その場の気分で学園祭の実行委員会の方に入りまして、システム担当の方をやることになりました。そこで、過去の経験などもありまして、LINE BOTの制作に携わった次第であります。

やったこと

端的に言えば学園祭の公式LINEを作りました。最初の方は企画投票を公式LINEでやるので、その機能を作って欲しい、みたいな感じだったのですが、LINE BOT作りが楽しくなってしまっていろいろな機能を入れたくなり、結局企画検索や単純な会話の機能も追加することになりました。

目次

長いので必要な部分だけ読むことをお勧めします。

  • 技術編

    • LINE Messaging API
      • LIFFの紹介
      • FLEX designの紹介
    • pm2
    • (番外編) chalk.js
    • (番外編) express.js router
  • 問題解決編

    • 投票機能の困難
      • 困難1 : 状態管理
      • 困難2 : 条件分岐の多さ
      • 困難3 : 一旦対話から離れて別ページへ飛んで対話に復帰する
    • 検索機能の困難
      • 困難1 : 「もっとみる」の実装
      • 困難2 : 「"」と 「’」の扱い
    • 会話機能の困難
      • 困難1. 対話パターンの多さ
      • 困難2. スタンプへの返信
    • 開発時の困難
      • 困難1. pm2で環境変数が更新されない

技術編

ここではLINEBOTを作るに当たって必要となった技術的な知見を一挙に書いていきたいと思います。
初めて使ったものも割とあったので、正しい使い方とは異なる場合がありますが、そこはご容赦くださいm(_ _)m

LIFF(LINE Front-end Framework)

一言で言えばWEBページにLINEの機能を少し混ぜられる機能です。
HTMLのscriptでliffのsdkを読み込むことで、ウェブページ上でJSを使ってLINEを使っている人のuserid・プロフィールが取得できるようになったり、トーク画面にメッセージをユーザーの代わりに送ったりできます。

LIFFの公式ドキュメントはこちら

今回での使用例

今回のBOTではLIFFページにアンケートを置いて、最後に送信を押すことでaxiosでサーバーにアンケートのデータをPOSTしつつ、トーク画面に「次へ」のようなメッセージを送信することで、次にある企画投票のフェーズにスムーズに移ることを計画していました。

ただ、駒場祭の直前の頃、この「トーク画面にメッセージを送る機能」が使えないことがわかりました。おそらく、理由としては、11/11のリリースノートにある、「LINEログインを中核とした機能拡張」に向けた改変だと思われます。
私が数ヶ月前に作っていたテスト用LIFFでは「トーク画面にメッセージを送る機能」は使えていました。しかし、いざ駒場祭も近づいてきたことだし本番環境でもその機能を追加しようとした時、そうすることはできませんでした。新しく機能追加することを防いでいるのでしょう。

そのせいで、アンケートから投票までの流れがかなり無理のあるぎこちないものとなってしまいました。みなさん、何かAPIなど開発者が他のものを使う時は急な仕様変更に気をつけましょう…

機能紹介

LINE Developpers の項目を紹介したいと思います。(一部加工しています。)

スクリーンショット 2019-12-09 0.24.18.png
スクリーンショット 2019-12-09 0.25.02.png

LIFF URL

LIFF URLはこのURLにアクセスするとこのLIFFのページが開きますよ、というものです。
変なことを言うと混乱の原因となるので謹みますが、とにかくLIFF URLはここに書いてあるものを利用してください。(私はこれとは違うURLを利用した結果、色々と不具合が起きました…)

サイズ

LIFFは基本的にはLINEアプリ中の内部ブラウザで開かれる利用例が想定されているようで、全面を覆う"Full", 若干上部分が空く"Tall", 半分ぐらいを占める"Compact"がある模様です。
アンケートのようにウェブページ的な利用の場合は"Full"か"Tall"を、トークの途中の要素としての利用なら"Compact"といった使い分けでしょうか。

エンドポイントURL

LIFFページが置かれているサーバーのURLを指定すれば良いです。
LIFF URLを踏んでLIFFのページを開こうとすると、このURLにページをGETしにいく、という感じなのでしょう。

SCOPE

アクセス許可です。現在は"profile"(プロフィールへのアクセス権限)しかないと思います。
昔は"~.write"という、トークへの送信権限が設定できたのですがね…

オプション

LIFFはLINE Thingsと呼ばれるBluetooth機器との連携もできるようです。
scanQRという、LINEについているQRコードを読み取る機能を起動する権限もあるようですが、この機能はiOS版LINEで機能を一時停止している模様です。

Flex design

LINEのMessaging APIに新しく追加されていた機能で、今まではテンプレートの型しか利用できなかったボタンやカルーセルを自由にカスタマイズできるようにする機能です。

(LINE公式ドキュメントよりテンプレートメッセージの例)

テンプレートメッセージ

これがFlexメッセージを使うと

LINE_capture_597675374.738383.JPG

このようにできます。
独自のアイコンを追加したり、ボタンもデザインできたりするので、グッとデザインの統一が可能になります。ただ、書式がJSONやYAMLで、CSSのように見やすくかけないところが欠点だと思います。

Flex Message Simulatorやその続編 Flex Message Simulator(ß)もあるので、一からデザインするのであれば、割と楽だと思います。(このツール、インポートができないので、若干苦労しました。)

pm2

この存在を上司の方から教わりまして、ひどく感動しました。

Node.jsをターミナルの裏で稼働し続けるようにできるものです。

pm2.config.jsonと言うものを作れば、環境変数なども設定できて、
pm2 start pm2.config.json --env <環境名>
を実行すると、<環境名>に紐づけられた環境変数を読み込んで実行できます。

pm2 start <環境名>で稼働開始、
pm2 restart <環境名>でコードを再び読み込んで稼働開始
pm2 stop <環境名>で稼働停止
pm2 logs <環境名>でログの垂れ流し
pm2 monit <環境名>でCPU使用率などの確認
などなど色々機能があります。
詳しくは pm2 公式ページをご覧ください。(すごくかっこいいです。)

ただ、pm2で環境変数を追加した後にrestartをするだけだと、新しい環境変数が更新されないと言う事態が発生したので、ご利用の際はしっかりstopした後もういちどstartするようにした方がいいかと思いました。

(番外編) chalk.js

chalk.jsのサイト

LINE_capture_596900003.999261_Original.JPG

(画像:chalk.jsのサイトより)

個人的に大好きなnpmパッケージです。
上のようにconsoleに色がつけられます。
console.log(chalk.blue.bgRed.bold("あああ"))
とすると赤背景に太字で青文字の「あああ」が表示されます!

consoleがすごく華やかになるのでおすすめです!
あと、普通にエラーなどが見やすくなります。

(番外編) express.js router

今までexpressを使ったことがなかったため、全く慣れていませんが、使えて良かったことを紹介します。

index.js
app.post('/line',line.middleware(lineConfig),(req,res)=>{// ... メッセージの返信処理}

LINEのサーバーからメッセージが送られた時はlineの公式のパッケージを利用してメッセージの認証を行います。

ただ、lineのミドルウェアを間に挟む際、データ通信でよく使う、JSONパースの設定

index.js
app.use(express.json())app.use(express.urlencoded({extended:true}));

を使うとうまく動きません。

その時に助かったのがrouterです。

index.js
app.use('/other',router_http)

のようにすると/otherで送られたリクエストはrouter_httpで捌いてくれるようになります。このrouter_http内で

index.js
router.use(express.json())router.use(express.urlencoded({extended:true}));

などとすることで、lineのミドルウェアに干渉することなくJSONパースができました。

expressのルーターを使えば、"/"以下をそれぞれ別のサーバーとして扱う、ということができるので、とても便利ですね。(駒場祭のウェブサイトもそのルーターを使っているみたいです。)


問題解決編

BOT制作にあたって各機能ごとにぶつかった困難と、それをどう解決したかを書いておきます。これからの制作でこれらが少しでも役立つことがあれば幸いです。

1. 開発時の困難

困難1. pm2で環境変数が更新されない

pm2は先述の通り、環境名を使って環境変数を分けて使えるのですが、開発環境でできたものを本番環境にあげようとしたところ、原因不明のエラーが発生しました。開発環境ではちゃんと動く処理が動かないのです。なんでだろうと、本番環境でテストしたところ、環境変数が参照できないと言うエラーでした。

解決法

先述の通り、新しく環境変数を追加した場合は、restartではなく、stop=>startの手順を踏むことが必要なようです。

2. 投票機能の困難

うちの学園祭では来場者の方に気に入った企画のIDを投票していただき、それをもとにランキングを作成し、表彰するというものがあります。投票は投票所でタブレットやコンピュータに入力するのが主流ではあるのですが、数年前から来場者の多数が利用しているLINEを活用して、より気軽に投票ができるようにと、LINE投票が始まりました。

困難1 : 状態管理

LINEをはじめ、ほとんどのBOTではどのような文脈でユーザーが発話してきたかについては知ることができません。そのため、会話の状態管理もこちらでしなければなりませんでした。
(Dialogflowなどの外部ツールを使えば、そのような状態管理は丸投げできるのですが、利用制限などの面から導入はしませんでした。)

解決法

やることはそんなに複雑ではなく、データベースにユーザーIDと状態の番号を一対一で保存しました。ただ、そうすると状態の読み込みが一発話ごとになるので、データベース読み込み回数が大変なことになるのですが、今回使っていたサーバーが非常に強いものだったので、全くそのことを気にせずできました。(サーバー管理者の方ありがとうございます。m(_ _)m)

(もっと大規模なBOTではどのように状態管理を行なっているのでしょうか。このBOTと同じようにデータベースで管理しているのでしょうか。興味が湧きました。)


困難2 : 条件分岐の多さ

投票時には企画IDを入力する必要があるのですが、おおよそ一般の方はそんなIDのことを気にすることなく、「〜をやっていた企画」などというふうに記憶しています。そのため、投票時に企画のIDを検索する機能が必要でした。LINEは対話式に物事が進んで行きますから、投票の流れの中で企画IDの検索機能を入れると状態管理が複雑になってしまいます。これにとても苦しまされました。

解決法

本当は状態管理用のツールを作るべきなのですが、当時の私にそのような能力・時間はなく、とりあえずswitchで大量に条件分岐しました。(力技ですごめんなさい)
例えばユーザーが3と言う状態で「投票」と発話すると〜、「番号を検索」と発話すると〜、「やめる」と発話すると〜、それ以外は〜、などという感じです。

おかげで、投票機能の条件分岐だけで1000行いってしまい、とても管理しづらくなってしまいました。また、「この状態でこうなった場合はどの状態に移るのか」といった遷移の関係が非常に見辛く、そこは失敗した点だと考えています。自分で遷移の関係を図示してみましたが、カオスです。

IMG_4979.JPG

ただ、システムの裏はこのように複雑なことになっていますが、表面はなるべくユーザーのキーボード入力が少なくなるよう多くのボタンを配置したつもりです。
LINE_capture_596900090.487445_Original.JPG
(上のように企画ID以外はなるべくボタンでの操作にしました。)


困難3 : 一旦対話から離れて別ページへ飛ぶ

投票前には来場者の方への簡単なアンケートがあります。そのページを別で作った関係で、アンケートはLINEの対話の中には組み込まず、一つのウェブページに飛ばすことになりました。ただ、それではアンケートの後にある企画への投票のデータとユーザーのデータが結びつかない、また、アンケートをせずに企画への投票に移ることができてしまう、などの問題点がありました。

解決法

そこで利用したのが LIFF(LINE Front-end Framework)というものです。(詳細は技術編を参照)、それを使えばLINEでの対話と連携したウェブページを作成することができます。
ただ、こちらも色々とトラブル続きで、当日まで様々な不具合が発生しており、担当者の方にお多大なご迷惑をかけてしまいました。また、LIFFの機能が直前になってテスト環境と異なることに気づいたため、急遽導線を変更し、少し使いづらくなってしまいました…

LINE_capture_596899960.155579_Original.JPG

上のようにアンケートをしないと企画投票に進めない"風"のデザインにして、アンケートに答えてもらいました。(実際アンケートせずには投票できないようになっています。)

(元々は、LIFFの機能を使ってアンケート画面で「投票へ進む」を押すとそのままトーク画面に戻って接続する予定でした…本当に残念です…)

3. 検索機能の困難

公式LINEと言えばやはり企画検索だと思います。実は駒場祭には検索APIというものが作られていまして、別の記事にもあると思いますが、そこでキーワードを入力すると、個数指定で企画の情報を持ってくることができます。(便利です!担当者の方ありがとうございます。)それを利用して、入力されたキーワードに対して情報を表示する機能を制作しました。ウェブサイトの検索機能も同じAPIを使っているので、情報は同じなのですが、サイトではCSSなどデザインの部分を多く読みこむのでLINEの方がスピードが出せるということを、当日使用していて思いました。今後はその検索スピードの速さを売りにしていきたいですね。


困難1 : 「もっとみる」の実装

検索ワードに対して30件ヒットしたとします。しかし、LINE Messaging APIによればカルーセルメッセージで送ることができる要素は最大10こまでです。前述の通りLINE BOTでは会話の文脈が保存されないので、ユーザーが「もっとみる」を押した時に残りの20個をどう取ってきて、表示したら良いかが問題でした。

解決法

簡単に言えば検索API側の機能Postbackを利用しました。

これもまた優秀な検索APIに助けられたのですが、検索にはページという概念がありまして、例えばヒット数30で、一ページあたりの個数を10に設定すると、一ページ目を要求すると1~10が、2ページ目を要求すると11~20の企画情報が返ってくるようになっています。それを利用することで、必要十分なデータだけを取得することが可能になりました。(担当者の方ありがとうございます。)

ただ、これだけではまだ完成ではありません。何度も言っていると思いますが、LINE BOTでは文脈が記憶されませんので、今表示されているのが「どういうキーワードで検索した結果の何ページ目か」を記録しなければなりません。データベースにその情報を書き込んでもいいのですが、わざわざそのために列を新しく作るのは躊躇われたので、Postbackという機能を利用しました。これは送信するメッセージにメタ情報的なものを付け加えることができる機能です。

「もっとみる」ボタンのメタデータに「検索ワード」と「次のページ番号」などをPostback用データとして埋め込んでおくことで、ユーザーがそのボタンを押すと、ただのメッセージではなく、Postbackメッセージとしてこちらが受け取ることができます。データを読み込むことでその状況にあった続きの検索結果を返答することができました。


困難2 : 「"」と 「’」の扱い

今回のLINE BOTではFlex Message(詳しくは技術編)というテンプレートのデザインよりも遥かに自由度の高いデザインが可能なメッセージ形式を多く利用したのですが、その記述方式がJSON型で、さらに文言を状況によって変える必要があったため、オブジェクト型を文字列型に変換したり、また戻したりしました。そこで問題になったのが「"」と「'」です。企画名に時々この記号が含まれているのですが、この扱いに非常に苦労しました。

解決法

とはいっても解決法は簡単なものでした。JSON用の引用点は全て「'」を使い、企画名に含まれる「"」を「\\"」(\は2本)に置き換えればエスケープされてうまくいきます。

template.json
{'name':'{{name}}'}

と言うjsonをテンプレートとして用意します。

データによって{{name部分を変えたいので}}このjsonを文字列として取得します。そしてreplaceで置き換えて、再びJSON parseします

main.js
// kikaku_name は 「event '19 and "20"」です。//データから読み込む時に「"」は邪魔しないはずです。 consttemplate_string="{'name':'{{name}}'}"constkikaku_name=kikaku_name.replace(/"/g,'\\"')constdata=template_string.replace(/\{\{name\}\}/g,kikaku_name)

こうするとparse時にエラーを吐かず、さらにLINE からも「メッセージ形式がおかしい」と言うエラーが来なくなりました。("や'をちゃんとエスケープしないとJSONの形が崩れてLINE側が読み込めなくなります。)

result
//data={"name":"event '19 and \\"20\\""}

JSON.parseすると"となってしまうのでこのような対策が必要みたいです。

4. 会話機能の困難

LINE BOTの醍醐味の一つは自由な会話だと思います。ということで様々なキーワードに反応できるように工夫しました。また、駒場祭では公式キャラクター「こまっけろ」のLINEスタンプも販売しているということで、それを用いた機能も何かできないかと考えました。

困難1. 対話パターンの多さ

普通BOTなどが会話するときは機械学習などで言葉の表記ゆれや曖昧さを加味して発話の意図を捉えます。ただ、今回はそのようなデータが用意できなかったので、キーワードと返答を一対一対応させることになりました。そこで生じたのがパターンの多さです。トイレというものを表すためにも「トイレ」「便所」「お手洗い」など様々な言葉が考えられます。それら一つ一つに対して返答を考える必要があり、数が膨大となってしまいました。

解決法

同じ意味を表す言葉に対しては同じ返答で良いので、それをまとめられるアイデアを考えました。

結果的に採用されたのは、スプレッドシートにキーワードをカンマ区切りで書き、その隣の列に返答を書き、それをtsv(タブ区切り)でエクスポートした後に、加工してJSON形式の返答例集を自動生成するというものでした。

conversations.spreadsheet

入力出力
トイレ,お手洗い,便所トイレはこちらにあります。

== (tsvで出力) =>

トイレ,お手洗い,便所 \t トイレはこちらにあります。

== (「,」でsplitして整理する。) =>

reply.json
[{"トイレ":"トイレはこちらにあります。"},{"お手洗い":"トイレはこちらにあります。"},{"便所":"トイレはこちらにあります。"}]

という感じです。
このようにすることで、返答が膨大であってもスクリプト一つ走らせるだけで簡単に返答集が生成されるようになりました。


困難2. スタンプへの返信

これは困難というほどではありません。
スタンプには「こんにちは」などの言葉が書かれており、それに対応する返答を返してあげようという完全に遊んだ機能を作りました。また、この返信にはランダム性を持たせて、複数ある選択肢から一つ選択して返答するという機能も追加しました。

解決法

スタンプにはパッケージIDとスタンプIDが割り振られており、それで区別が可能です。それを利用して、特定のスタンプが送られたときはこのメッセージを返す、というものを作れば簡単にスタンプに対する返信が可能になります。ランダムな返答については返答をカンマ区切りにして上と同じくtsv出力した後、カンマを用いてsplitした後にランダムな数字で配列にアクセスすれば可能です。


以上長くなってしまいましたが、最後まで読んでくださりありがとうございました。

また来年の駒場祭で会いましょう! さようなら!

Node.jsから画像をmultipart/form-dataでPOSTするメモ (axios利用)

$
0
0

某APIを試していて、少しハマったのでメモ。
axiosで画像POSTとかを調べると、最近はVue.jsだったりフロントエンド側からPOSTする記事が多く、Node.js側から送るサンプルはあまりヒットしない印象です。

環境

Node.js v13.2.0

準備

mkdir myapp
cd myapp
npm init -y
npm i axios

こんな感じでaxiosだけ追加インストールです。

コード

post.js
'use strict';constfs=require('fs');constFormData=require('form-data');constaxios=require('axios');consturl=`https://hogehoge.com/hogehoge`;//ポスト先のエンドポイントURLconstimagePath=`./public/image.png`;//画像のパスconstfile=fs.createReadStream(imagePath);constform=newFormData();form.append('image',file);constconfig={headers:{'X-HOGEHOGE-HEADER':'xxxxxxx',//APIごとのヘッダーなど...form.getHeaders(),}}axios.post(url,form,config).then(res=>console.log(res.data))//成功時.catch(err=>console.log(err));//失敗時

所感

参考にさせてもらった記事にもありましたが、色々調べててform.getHeaders()の箇所が直感的ではないのでちょっとハマりました。

あとform-dataって標準モジュールになってるんですね(?) とくにnpm installしてないのに使えました。

参考

LINE Clova を使って怠惰なDrink Bar を開発する

$
0
0

この記事の概要

この記事では、6/25に開催された「スマートスピーカーを遊びたおす会 vol.6」の登壇ネタとして開発した『LINE Clova Drink Bar』のコンセプトや実装内容について解説します。

また、LINE Clova Drink Bar はヒーローズ・リーグのVUI 部門決勝にノミネートいただきました。ありがとうございました。

LINE Clova Drink Bar のコンセプト

LINE Clova Drink Bar(以下、Drink Bar)は、「怠惰」を目指した作品です。
VUI として、Drink Bar というサービスをどこまで人の手を使わずに声だけで操作できるかを試しています。

単純に指示したドリンクを抽出するだけはなく、怠惰のためには人間からの指示を状況に応じて置き換えることもあります。

出来ること

音声操作で好みのドリンクを抽出

これは単純に指示されたドリンクを抽出する機能です。
カスタムスロット「DRINK」として麦茶やオレンジジュースなどをスロットタイプに追加しておき、Clova で認識できるようにしておきます。

  • お茶・ジュースなど複数から選択できる
    • スロット値のドリンク(麦茶、コーラなど)に応じ、抽出するドリンクを切り替える

同種のドリンクで代替して抽出

Drink Bar とはいえ在庫がないドリンクもあります。そんな時でも変わりのドリンクを提供するための機能です。
予めドリンクをグルーピング(麦茶、緑茶などはTEAグループ、オレンジジュース、りんごジュースはSOFTDRINKグループ など)しておき、在庫がないドリンクを指示されても同じグループ(同種)で在庫があるドリンクを抽出する機能です。

例えば、以下のように置き換えます。

  • 緑茶が在庫に無ければ、同じお茶で在庫のある麦茶を抽出する
  • 同様にオレンジジュースが無ければ、りんごジュースを抽出する

市販の銘柄でも認識可能

もう一つの置き換え機能が、市販されている商品名でもドリンクとして認識できるものです。
コーラを飲みたいと頭で考えていても、思わず「ペプシをちょうだい」と指示してしまうこともあります。そんな時にこの銘柄認識機能が役に立ちます。

例えば、以下のように認識します。

  • 緑茶として認識する語句
    • 伊右衛門、生茶、綾鷹
  • オレンジジュースとして認識する語句
    • なっちゃんオレンジ、つぶつぶみかん
  • コーラとして認識する語句
    • ペプシ、ドクターペッパー、メッツコーラ

デモ動画

言葉で説明しても難しいので、デモ動画を見ていただければ分かっていただけると思います。

Clovaスキルで怠惰にドリンクバーを操作する_20190801.png

システム構成

Drink Bar のシステム構成です。
スキル側はnode.js で実装し、ngrok で開発PC をサーバーとして動かしています。

システム構成01.png

実際にドリンクを抽出するハードウェア部分はobniz とエアーポンプ(DC モーター)、シリコンチューブを組み合わせた構成となっています。

システム構成02.png

スキルの実装はシンプル

スキル部分はとてもシンプルです。
カスタムインテントは、飲みたいドリンクを指示する「DispenseDrinkIntent」のみです。

DispenseDrinkIntent では「麦茶をちょうだい」「オレンジジュースをいれて」などの指示を受け付けます。
あとは認識したドリンクを基に、JSON で定義しておいたDrinkModel で在庫有無や、どのエアーポンプを動かせば抽出されるかの情報を取得します。
在庫があればそのまま、なければ同種で在庫があるドリンクに置き換えて、抽出するためのエアーポンプを動作させるだけです。

clova.js(抜粋)
letmessageText='';letd=findDrinkById(drink);if(d['available']===true){// ドリンクバーに設置されているドリンクの場合messageText=`わかりました。${d['name']}をついでおくね!`}else{letsameTypeDrink=findSameTypeDrinkModel(d['type']);if(sameTypeDrink){// 同じ種類のドリンクが設置されている場合はそちらを注ぐmessageText=`おっと、${d['name']}が無かったので、同じ種類の${sameTypeDrink['name']}をついでおくね!`;}else{// ドリンクが設置されてない場合は断りのメッセージを返すmessageText=`ごめんなさい。${d['name']}を用意してなかったよ。他の飲みたいドリンクを教えてね。`;}d=sameTypeDrink;}// Clova のセリフを組み立てるconstspeechList=[];// 注ぐセリフspeechList.push(clova.SpeechBuilder.createSpeechText(messageText));
DrinkModel.json(抜粋)
{"list":[{"available":false,"id":"OolongTea","name":"ウーロン茶","slot":0,"type":"TEA"},{"available":false,"id":"GreenTea","name":"緑茶","slot":0,"type":"TEA"},{"available":true,"id":"BarleyTea","name":"麦茶","slot":0,"type":"TEA"},{"available":false,"id":"BrownRiceTea","name":"玄米茶","slot":0,"type":"TEA"},{"available":false,"id":"JasmineTea","name":"ジャスミン茶","slot":0,"type":"TEA"},{"available":true,"id":"OrangeJuice","name":"オレンジジュース","slot":1,"type":"SOFTDRINK"},{"available":false,"id":"AppleJuice","name":"リンゴジュース","slot":1,"type":"SOFTDRINK"},...後略

市販銘柄の認識は同義語をひたすら登録する

出来ることで挙げていた「市販の銘柄でも認識可能」ですが、こちらはカスタムスロットの同義語としてひたすら登録します。

スクリーンショット 2019-12-12 15.21.35.png

怠惰のためには地道な作業もこなします。
ひたすら画面に入力するのも大変なので、Google スプレッドシートなどを活用してTSV ファイルとして出力し、Clova Developer Center のアップロード機能を活用すると少し楽ができます。

スクリーンショット 2019-12-12 15.24.42.png

アップロードファイル例

OolongTea   ウーロン茶 烏龍茶   うーろん茶 黒烏龍茶                                                                            
GreenTea    緑茶  りょくちゃ おちゃ   お茶  伊右衛門    いえもん    綾鷹  あやたか    生茶  なまちゃ    おーいお茶 贅沢緑茶    特茶  ヘルシア緑茶  濃い茶                               
BarleyTea   麦茶  むぎちゃ    ミネラル麦茶  ゴマ麦茶    胡麻麦茶    むぎ茶   健康ミネラル麦茶    六畳麦茶    六条麦茶    やさしい麦茶  香り薫るむぎ茶                                               
BrownRiceTea    玄米茶   げんまい茶 げんまいちゃ                                                                              
JasmineTea  ジャスミン茶  ジャスミンティー    ジャスミン

アップロード機能はサンプル発話の登録にも有効

アップロード機能はサンプル発話の登録にも有効です。
サンプル発話のテンプレートを決めて、そこにタグとドリンク名を連結してサンプル発話の文字列を一挙に生成します。

アップロードファイル例

[INTENT SLOT]
Drink   DRINK

[INTENT EXPRESSION]
<Drink>ウーロン茶</Drink>を頂戴
<Drink>烏龍茶</Drink>を頂戴
<Drink>うーろん茶</Drink>を頂戴
<Drink>黒烏龍茶</Drink>を頂戴
<Drink>緑茶</Drink>を頂戴
<Drink>りょくちゃ</Drink>を頂戴
<Drink>おちゃ</Drink>を頂戴
<Drink>お茶</Drink>を頂戴
<Drink>伊右衛門</Drink>を頂戴
<Drink>いえもん</Drink>を頂戴
<Drink>綾鷹</Drink>を頂戴
<Drink>あやたか</Drink>を頂戴
<Drink>生茶</Drink>を頂戴
<Drink>なまちゃ</Drink>を頂戴
<Drink>おーいお茶</Drink>を頂戴
<Drink>贅沢緑茶</Drink>を頂戴
<Drink>特茶</Drink>を頂戴
<Drink>ヘルシア緑茶</Drink>を頂戴

obniz を使ってドリンクを抽出する

ハードウェア構成もシンプルです。まずはobniz とエアーポンプを繋ぎます。
エアーポンプに繋いだシリコンチューブと、ドリンクが出てくる側のシリコンチューブをPET ボトルキャップに接続します。接続できるようにPET ボトルキャップに電気ドリルで穴を開けています。
うまくドリンクを抽出するポイントはボトルの気密を保つことです。チューブ外径にぴったりな穴が開けられない場合は、ホットボンドで穴とチューブを接着したり、キャップにパッキンを装着するなどして気密を保ってください。

システム構成02.png

以上で実装は終わりです。
とてもシンプルな作りとなっています。あとは外装などにこだわるのも良いですね。

まとめ

実はこの「LINE Clova Drink Bar」は最初から作ろうと思っていたのではなく、別で作成していた「LINE Things Drink Bar」というLINE Things とLINE Pay を組み合わせた作品のスピンオフとして生まれた作品でした。
もう少し明かすと、「スマートスピーカーを遊びたおす会 vol.6」の登壇ネタに困っていた筆者が、時間かけずに作れるネタがないものか、という怠惰な発想から生まれた怠惰な作品なのでした。
実際の実装時間も半日程度です。怠惰ですませたいという気持ちを持っていると、作業効率の良い作品が生まれるのかもしれません。

そんな怠惰な作品が日本最大級のスマートスピーカーコミュニティでの登壇ネタとなり、日本最大級の開発コンテストの部門決勝にノミネートされてしまい、とても恐縮です。
あの有名なミステリーの女王 アガサ・クリスティが「発明は怠惰から生まれる」と言っていたように、怠惰にしたいがために動いていれば良い作品が生まれるのかもしれませんね。

Azure Searchのハイライト機能 &ハイライト機能の癖を回避した実装について

$
0
0

Azure Searchのハイライト機能 & ハイライト機能の癖を回避した実装について

はじめに

Azure Searchの検索結果はデフォルトだと、キーワードにヒットした本文をハイライトしてくれません
なのでAzure Searchの検索結果のハイライトを実装したいと思います。

ハイライト機能はやや癖があるので、癖を回避した実装について書きます
 
 
 
<ハイライトを使用しない場合の検索結果イメージ>
image.png

基礎部分の作成

npm install

npm install npm express ejs request --save

 

index.js ※Azure Searchの設定関連は未入力状態になっています

// /////////////////////////////////////////////////////////////////////////////////////// Azure Searchの設定関連 // /////////////////////////////////////////////////////////////////////////////////////// Azure Searchのサービス名constsearchServiceName='';// Azure SearchのクエリキーconstqueryKey='';// Azure Searchのインデクス名constindexName='';// コンテンツを保持しているフィールド名(ほとんどの場合はcontent、OCRのマージフィールドを指定する場合はmerged_content)    constcontent_field_name="content";// /////////////////////////////////////////////////////////////////////////////////////// 定義関連 // /////////////////////////////////////////////////////////////////////////////////////// MVCフレームワークとしてexpressを利用するための設定varexpress=require('express');varapp=express();// ejsをビューに使う為の設定app.set('view engine','ejs');// 非同期処理における例外発生時にエラーに繋ぐためのラッパーconstasyncwrap=fn=>(req,res,next)=>fn(req,res,next).catch(next);// 静的コンテンツを外部ファイル化(publicフォルダ配下を<ROOT>/staticでアクセス許可)app.use('/static',express.static('public'));// /////////////////////////////////////////////////////////////////////////////////////// 検索の初期表示 と 検索実施// http://localhost:8080/にアクセスしたときの処理// /////////////////////////////////////////////////////////////////////////////////////app.get('/',asyncwrap(async(req,res)=>{// 画面から投げた検索キーワードの設定。 キーワードが投げられていない場合はワイルドカード(*=条件未指定)を設定するconstq=req.query.keyword||'*';console.log(q);// キーワードをエンコードして設定constquery=encodeURIComponent(q)+'&count=true&searchMode=all';// 検索実行varsearchResult=awaitnewPromise((resolve,reject)=>{constrequest=require('request');request({method:'GET',url:`https://${searchServiceName}.search.windows.net/indexes/${indexName}/docs?api-version=2019-05-06&search=${query}`,headers:{'Content-type':'application/json','api-key':queryKey},json:true,},function(err,res,body){if(err){reject(err);}else{resolve(body);}});});// 通常の検索結果とハイライト付の検索結果はそれぞれ異なるフィールドに設定されるので、ハイライトを優先的に取得しますvarresult=[];for(vari=0;i<searchResult.value.length;i++){// 1.タイトル(ファイル名)の取得vartitle=searchResult.value[i].metadata_storage_name;// 2.本文の取得varbody=searchResult.value[i][`${content_field_name}`];result.push({'title':title,'body':body});}// index.ejsに検索結果を渡して画面描画res.render('index',{searchResult:result,inputKeyword:q});}));// /////////////////////////////////////////////////////////////////////////////////////// 起動// /////////////////////////////////////////////////////////////////////////////////////app.listen(8080,()=>console.log('access -> http://localhost:8080/'))

 

index.ejs ※viewsフォルダ配下に入れましょう

<!DOCTYPE html><html><head><metacharset="utf-8"/><linkrel="stylesheet"media="all"href="./static/style.css"/><title>ハイライト</title></head><body><%//************************************************%><%//検索条件を設定し、検索を行う為のフォームエリア%><%//***********************************************%><formstyle="position:relative; margin-bottom:20px;"action="/"><%//キーワード入力%><inputid="keyword"class="keyword"name="keyword"type="text"placeholder="キーワードを入力"value="<%= inputKeyword %>"/><%//検索ボタン%><inputtype="submit"class="submitsearch"value="検索"/></form><%//************************************************%><%//検索結果表示%><%//***********************************************%><%for(vari=0;i<searchResult.length;i++){%><%//ファイル名%><pclass="filename"><%=searchResult[i].title%></p><%//本文%><pid="a<%= i %>"class="docmain"><%-searchResult[i].body%></p><%//隙間調整%><br><%}%></body></html>

 

style.css ※publicフォルダ配下に入れましょう

.docmain{width:850px;margin:0000;padding:12px15px;color:#777;background:#fafafa;border:1pxsolid#ddd;position:relative;left:40px;}.keyword{outline:0;height:50px;padding:010px;left:0;top:0;width:230px;border-radius:2px;background:#eee;}.submitsearch{width:70px;height:50px;left:260px;top:0;border-radius:2px;background:#7fbfff;color:#fff;font-weight:bold;font-size:16px;border:none;}

 
 

ハイライトの実装

ハイライトの実装に必要な要素は大きく2つです。

 

1つ目がハイライト検索を行う為のクエリの作成です

ハイライトの要求はクエリで行う為、クエリに以下の3つのパラメータを追加します。

パラメータ説明
highlightどのフィールドをハイライトしたいか本記事ではcontentフィールド等を指定
highlightPreTagハイライト開始に指定したいタグ本記事ではmarkタグを指定
highlightPostTagハイライトの終了に指定したタグ本記事では/markタグを指定

※markタグは囲った文字列をマーカー調にハイライトしてくれます
 
クエリ周りを以下のように実装します

// Azure Searchのハイライトは以下のようにクエリで指定します。// ハイライトの設定(検索結果に含まれるキーワードを<mark>タグで囲うように設定)varhighlight=`&highlight=${content_field_name}-3&highlightPreTag=<mark>&highlightPostTag=</mark>`;// キーワードをエンコードして設定constquery=encodeURIComponent(q)+'&count=true&searchMode=all'+highlight;

 
 

2つ目がハイライトされた検索結果の取得です

ハイライトされた文字列は、本文とは異なるフィールドにマップされる為
以下のような実装が必要になります。

ハイライトされている場合 → ハイライトフィールドをサマリとして活用
ハイライトされていない場合 → デフォルトフィールドをサマリとして活用

// 2.本文の取得varbody=null;if(searchResult.value[i]['@search.highlights']!=undefined){// ハイライトが存在する場合lethighlights=searchResult.value[i]['@search.highlights'];body=highlights[`${content_field_name}`].join('\n');}else{// ハイライトが存在しない場合body=searchResult.value[i][`${content_field_name}`];}

ハイライトの動作確認をします

キーワードにマッチする本文がハイライトされていることがわかります
しかしハイライトされているのは一部で本文全体がハイライトされているわけではありません。

 
 
<ハイライト機能をさくっと実装した際のイメージ>
image.png

 
 
 
 
 
 

ハイライト処理のカスタマイズ

本文全体の中からマッチするワードをハイライトするようにカスタマイズします

一番オーソドックスなやりかたとしては

①『ハイライト文章』からハイライトタグを除去して、『未ハイライト文章』を作成
② 本文から『未ハイライト文章』を検索して、『ハイライト文章』で置換します

①②を繰り返すことで、本文全体の中からマッチするワードがハイライトされるようになります。

// 2.本文の取得varbody=null;if(searchResult.value[i]['@search.highlights']!=undefined){// 本文body=searchResult.value[i][`${content_field_name}`];// ハイライトが存在する場合lethighlights=searchResult.value[i]['@search.highlights'];for(varj=0;j<highlights[`${content_field_name}`].length;j++){// ハイライトを1つずつ取得            lethighlight=highlights[`${content_field_name}`][j];// ハイライトタグを除去して、未ハイライト文章を作成するletnotHighlight=highlight.replace(/<mark>/g,'').replace(/<\/mark>/g,'');// 本文から未ハイライト文章を検索し、ハイライト済文章で置換するbody=body.replace(notHighlight,highlight);}}

 
 
 

カスタマイズしたハイライトの動作確認をします

本文全体がハイライトされていることが確認できました

<ハイライト機能の癖を回避した実装のイメージ>
image.png

 ↑ 構造が複雑なPDFファイル等をこの手法でハイライトする場合は
  構造データが本文フィールドに混じる場合があり、ハイライトフィールドには混じらないことがあるので
  そういった場合は、もう1工夫が必要です。(上記手法だけだと、置換の為の検索対象が本文に存在しないので、置換がうまくいかない場合があります。)
  ※現時点ではそうなってしまう状態ですが、バージョンアップでいずれ解消するかもしれません。
 
 
 
 

 

最終的なindex.jsのソースです

// /////////////////////////////////////////////////////////////////////////////////////// Azure Searchの設定関連 // /////////////////////////////////////////////////////////////////////////////////////// Azure Searchのサービス名constsearchServiceName='';// Azure SearchのクエリキーconstqueryKey='';// Azure Searchのインデクス名constindexName='';// コンテンツを保持しているフィールド名(ほとんどの場合はcontent、OCRのマージフィールドを指定する場合はmerged_content)    constcontent_field_name="content";// /////////////////////////////////////////////////////////////////////////////////////// 定義関連 // /////////////////////////////////////////////////////////////////////////////////////// MVCフレームワークとしてexpressを利用するための設定varexpress=require('express');varapp=express();// ejsをビューに使う為の設定app.set('view engine','ejs');// 非同期処理における例外発生時にエラーに繋ぐためのラッパーconstasyncwrap=fn=>(req,res,next)=>fn(req,res,next).catch(next);// 静的コンテンツを外部ファイル化(publicフォルダ配下を<ROOT>/staticでアクセス許可)app.use('/static',express.static('public'));// /////////////////////////////////////////////////////////////////////////////////////// 検索の初期表示 と 検索実施// http://localhost:8080/にアクセスしたときの処理// /////////////////////////////////////////////////////////////////////////////////////app.get('/',asyncwrap(async(req,res)=>{// 画面から投げた検索キーワードの設定。 キーワードが投げられていない場合はワイルドカード(*=条件未指定)を設定するconstq=req.query.keyword||'*';console.log(q);// Azure Searchのハイライトは以下のようにクエリで指定します。// ハイライトの設定(検索結果に含まれるキーワードを<mark>タグで囲うように設定)varhighlight=`&highlight=${content_field_name}-3&highlightPreTag=<mark>&highlightPostTag=</mark>`;// キーワードをエンコードして設定constquery=encodeURIComponent(q)+'&count=true&searchMode=all'+highlight;// 検索実行varsearchResult=awaitnewPromise((resolve,reject)=>{constrequest=require('request');request({method:'GET',url:`https://${searchServiceName}.search.windows.net/indexes/${indexName}/docs?api-version=2019-05-06&search=${query}`,headers:{'Content-type':'application/json','api-key':queryKey},json:true,},function(err,res,body){if(err){reject(err);}else{resolve(body);}});});// 通常の検索結果とハイライト付の検索結果はそれぞれ異なるフィールドに設定されるので、ハイライトを優先的に取得しますvarresult=[];for(vari=0;i<searchResult.value.length;i++){// 1.タイトル(ファイル名)の取得vartitle=searchResult.value[i].metadata_storage_name;// 2.本文の取得varbody=null;if(searchResult.value[i]['@search.highlights']!=undefined){// 本文body=searchResult.value[i][`${content_field_name}`];// ハイライトが存在する場合lethighlights=searchResult.value[i]['@search.highlights'];for(varj=0;j<highlights[`${content_field_name}`].length;j++){// ハイライトを1つずつ取得            lethighlight=highlights[`${content_field_name}`][j];// ハイライトタグを除去して、未ハイライト文章を作成するletnotHighlight=highlight.replace(/<mark>/g,'').replace(/<\/mark>/g,'');// 本文から未ハイライト文章を検索し、ハイライト済文章で置換するbody=body.replace(notHighlight,highlight);}}result.push({'title':title,'body':body});}// index.ejsに検索結果を渡して画面描画res.render('index',{searchResult:result,inputKeyword:q});}));// /////////////////////////////////////////////////////////////////////////////////////// 起動// /////////////////////////////////////////////////////////////////////////////////////app.listen(8080,()=>console.log('access -> http://localhost:8080/'))

Node.jsでLINE BRAIN OCR APIを使う #linebrain #ood2019

$
0
0

先日のLINE DEVDAYでbeta公開されていたLINE BRAIN COR APIをNode.jsで利用してみます。

Node.jsから画像をmultipart/form-dataでPOSTするメモ (axios利用)

LINE BRAIN & LINE BRAIN COR API

LINE BRAINとは企業がチャットボット・OCR・音声認識・音声合成・画像認識などの AI技術をより簡単に利用できる、各種サービスの総称らしいです。 AIがどんどん民主化されて嬉しい限りです。

公式サイトから引用 https://www.linebrain.ai/

LINE BRAIN OCR APIはLINE BRAINのサービス群の一つというイメージですね。

https://ocrdemo.linebrain.ai/

汚い字でも読み込んでくれた

実際にDEMOページで試してみたら、手書きの結構汚い字でもちゃんと読み込んでくれました。
GCPなどでもOCRはあると思いますが、その辺の精度の違いは僕は比較してないのでよく分かりません。


instagram https://www.instagram.com/p/B57Yq7ijJg2/

OCR APIはまだベータ版

2019/12/12時点ではまだベータ版で、ベータ版のエンドポイントは

https://ocr-devday19.linebrain.ai/v1/

となっています。URLから分かる通りLINE DEVDAY 2019に参加した人だけに公開されたっぽい雰囲気です。利用料金なども現時点では分かりません。(既にどこかで公開されてるかもしれないですが)

ドキュメント画面はこんな雰囲気です。ドキュメントのURLは非公開かもしれないので載せないでおきます。

DetectionとRecognition

大きく分けるとこの二つの機能になっているみたいで、

  • Detection - 文字領域の検出のみを行います。
  • Recognition - 文字認識のみを行います。もしくは、文字領域の検出と認識を順に行います。

CURLで試す (ローカルファイル)

Recognitionの方を試してみました。
以下のようなコマンドで結果が返ってきます。

ここで書いてるサービスIDのPMqTDgBsucfsyvi7pJEsbIxMIUeNQWDgはドキュメントに書いてあるサンプル例なので、このまま書いても使えません。LINE DEV DAY 2019で登録した人はメールでサービスIDが届いてると思います。

curl -X POST https://ocr-devday19.linebrain.ai/v1/recognition \
                    -H 'X-ClovaOCR-Service-ID: PMqTDgBsucfsyvi7pJEsbIxMIUeNQWDg' \
                    -H "Content-Type: multipart/form-data" \
                    -F "image=@./image.png" \
                    -F "entrance=detection" \
                    -F "language=jp" \
                    -F "segments=false" \

CURLから試すが画像URL指定がうまくいかなかった

ドキュメントにあるサンプルリクエストを見るとこんな感じで画像URL指定でも使えそうでした。

curl -X POST https://ocr-devday19.linebrain.ai/v1/recognition \-H'X-ClovaOCR-Service-ID: PMqTDgBsucfsyvi7pJEsbIxMIUeNQWDg'\-H"Content-Type: application/json"\-d'{
           "imageURL":["https://xxxx/images/ocr_sample.jpg"],
           "entrance":"detection",
           "scaling":false,
           "segments":false
         }'

ただ、Gyazoに載せた画像URLを指定した場合に

errorConnection reset by peer

などのエラーが出たり、エラーが出た時のメッセージからどこに問題があるのかの判断がつきにくいとこが現時点ではありました。

取り急ぎの原因は不明で、時間があれば再調査しますが、とりあえずローカルファイルを投げつけてみます。

Node.jsから扱ってみる

ということで実際のアプリケーションに組み込みやすいようにNode.jsから扱ってみます。

環境や準備など

こちらで書いた内容がそのまま使えます。

Node.jsから画像をmultipart/form-dataでPOSTするメモ (axios利用)

axiosの準備をしましょう。

コード

post.js
'use strict';constfs=require('fs');constaxios=require('axios');constFormData=require('form-data');constOCR_SERVICE_URL=`https://ocr-devday19.linebrain.ai/v1/recognition`;constOCR_SERVICE_ID=`PMqTDgBsucfsyvi7pJEsbIxMIUeNQWDg`;//サービスIDconstIMAGE_PATH=`./public/image.png`;// 画像パスconstfile=fs.createReadStream(IMAGE_PATH);constform=newFormData();form.append('image',file);form.append('entrance','detection');constconfig={headers:{'X-ClovaOCR-Service-ID':OCR_SERVICE_ID,...form.getHeaders(),}}axios.post(OCR_SERVICE_URL,form,config).then(res=>console.log(res.data))//成功時.catch(err=>console.log(err));//失敗時

ちなみに、entranceをdetectionにしないとうまいこと文字が抽出されませんでした。

実行結果

$ node post.js
{
  words: [
    {
      boundingBox: [Array],
      text: '斎場御獄',
      confidence: 0.6916899085044861,
      lineBreak: false,
      segments: [Array]
    }
  ]
}

元画像はこちらですね。斎場御獄という文字が抽出されました。


instagram https://www.instagram.com/p/B57Yq7ijJg2/

所感

使い勝手は割と直感的なAPIで分かりやすい気がしました。

他社のOCR系のAPIと比較してどうなのか、どこかの誰かが検証してくれると幸いです。

画像URLからOCR APIに投げつけたかった問題はまたどこかで...

JavaScriptで木構造をラクに扱う

$
0
0

はじめに

この記事はJavaScriptで木構造をラクに扱う方法について、ロゴスウェア株式会社の社内勉強会で取り上げたものです。

1. 木構造を楽に扱うためのライブラリ

以下の2つがオススメです。

2. tree-model-js

木構造のデータについて、ノードの検索やフィルタ等々の操作をうまいことやってくれるライブラリです。

2-1. 呼び出し方

constTreeModel=require('tree-model');consttree=newTreeModel();

2-2. 期待するデータの形式

tree-model-jsでは、入れ子になっている木構造のオブジェクトを入力にとります。
ここでは以下のような組織図の木構造のデータを例にします。

組織図

org.png

上記組織図を表すオブジェクト

以下のようにchildrenプロパティの配列に子組織のオブジェクトを入れて表現します。

consttreeDataStructure={id:1,name:'全社',children:[{id:11,name:'つくばオフィス'children:[{id:111,name:'システムアンドサービスグループ'}]},{id:12,name:'東京オフィス',children:[{id:121,name:'スイートプロダクトデザイングループ'},{id:122,name:'アクティブ・ラーニングデザイングループ'}]},{id:13,name:'不明のグループ'}]};

2-3. Rootノードオブジェクトを作成する

tree-model-jsparseヘルパーに上記の入れ子のデータを入れて、対象の木構造のRootノードのオブジェクトを作成します。

// 木構造のオブジェクトをパースしてRootノードオブジェクトを作成constroot=tree.parse(treeDataStructure);

2-4. ノードを検索する

idが121のノードを検索してノードを取得する例です。

constnode121=root.first(node=>node.model.id===121);console.log(node121.model);// modelプロパティを使えば、ノードのプロパティを取得できる。// -> { id: 121, name: 'SPD' }

2-5. ノードをフィルターする

idが100以上のノードを全て取得する例です。

constnodesGt100=root.all(node=>node.model.id>100);

2-6. ノードを走査する

ツリーを上から辿って、ノードのidを順番に出力する例です。

root.walk(node=>{console.log(node.model.id)});/*
1
11
111
12
121
122
13
*/

2-7. 探索アルゴリズムを指定する

上記いずれのAPI(first, all, walk)も、オプションを指定すれば探索アルゴリズムを指定できます。

root.walk({strategy:'breadth'/* 幅優先探索 */},node=>{console.log(node.model.id)});/*
1
11
12
13
111
121
122
*/

以下の3種類がサポートされています。

種類オプション
幅優先探索{ strategy: 'breadth' }
深さ優先探索(ルートから){ strategy: 'pre' }
深さ優先探索(末端から) { strategy: 'post' }

3. list-to-tree

フラットなリストから、2-2. 期待するデータの形式で記載した 入れ子になっている木構造のオブジェクトに変換するライブラリです。

constLTT=require('list-to-tree');constnodeList=[{id:1,parent:0},{id:11,parent:1},{id:111,parent:11}];consttreeDataStructure=newLTT(nodes,{key_id:'id',key_parent:'parent',key_child:'children'}).GetTree()[0];console.log(JSON.stringfy(treeDataStructure,null,2));/*
{
  "children": [
    {
      "children": [
        {
          "id": 111,
          "parent": 11
        }
      ],
      "id": 11,
      "parent": 1
    }
  ],
  "id": 1,
  "parent": 0
}
*/

4. tree-model-js と list-to-tree を組み合わせる

2つを組み合わせれば、
1. フラットな木構造のデータから
2. 入れ子になっている木構造のオブジェクトに変換し、
3. tree-model-jsのRootノードオブジェクトを作成できます。

constTreeModel=require('tree-model');constLTT=require('list-to-tree');// 1. フラットな木構造のデータconstnodeList=[{id:1,parent:0},{id:11,parent:1},{id:111,parent:11}];// 2. 入れ子になっている木構造のオブジェクトconsttreeDataStructure=newLTT(nodes,{key_id:'id',key_parent:'parent',key_child:'children'}).GetTree()[0];// 3. tree-model-jsのRootノードオブジェクトを作成constroot=tree.parse(treeDataStructure);

最後に

間違いや改善点等があればご指摘ください。

参考リンク


YouTubeのJukeboxを作ってみた

$
0
0

どんなもの

YouTubeのURLを登録しておくと指定した時間帯にランダム再生するジュークボックスです。
音専用で、映像は流れません。
ブラウザでサーバにアクセスし、曲登録や編集ができます。

screenshot.png

注意事項

  • ssl設定はしていないので、VPNやローカルネットワークなどのセキュアなネットワーク内で利用してください
  • 著作権は守ってください。ご自分の演奏動画などが推奨されます。

構成

structure.png

インストール

GitHubからファイル一式をクローンするかダウンロードしてください。

バックエンド

  1. Node.jsをインストールしてください

  2. MongoDBをインストールしてください

  3. back/app/environment.tsを環境に合わせて編集してください

  4. 下記のコマンドを実行しプラグインをインストールしてください

    $ npm install
    
  5. 下記のコマンドを実行すると起動します

    $ npm start
    

フロントエンド

  1. Angular7をインストールしてください

  2. front/src/environments/environment.tsを環境に合わせて編集してください

  3. 下記のコマンドを実行しプラグインをインストールしてください

    $ npm install
    
  4. 下記どちらかの方法でWEBページを公開してください

    • 下記のコマンドを実行するとnginxやapacheなしでWEBページが公開されます
      $ ng serve --host 0.0.0.0
    
  • 下記のコマンドを実行するとdistディレクトリにindex.htmlやjs, css一式が生成されるので、apacheやnginxでWEBページを公開してください

      $ ng build
    

利用しているモジュール

バックエンド

  • @microlink/youtube-dl
  • @types/express
  • @types/mongodb
  • @types/node
  • body-parser
  • cluster
  • crypto
  • express
  • moment
  • mongodb
  • node-cron
  • typescript

フロントエンド

  • angular

Chromeで音声認識して、Discordに書き込み

$
0
0

1. 概要

Google Chromeの Web Speech API を利用して音声認識し、結果を Discordのテキストチャンネルに書き込みをする方法を紹介します。
やっていることは、NAMAROIDの音声認識をChromeでやってみたとほぼ同様ですが、本記事では実装面を解説します。

本実装は Webサーバを介した、クライアント・サーバ形式で、相互のメッセージングに socket.ioを利用しています。
socket.ioを利用することにより、Web APIに比べて実装が簡単で、送受信の高速化が期待できます。

アイコンはおむ烈 様のフリーアイコンをお借りしております!

参考記事

動作環境

  • OS: Windows 10
  • node: v9.11.1
  • chrome: 78.X

ソースコード

https://bitbucket.org/YoshikazuOota/post_stt

2. 使い方

2.1 ライブラリのインストール

初回のみ npm install (setting.bat) を実行して

2.2 Discord Bot・テキストチャンネルの設定

config/discord.jsonで下記の2つを指定する
- Botのトークン
- 書き込み先のテキストチャンネル

2.3 サーバサイドの実行

  1. node server.js実行(start.bat) を実行
  2. ブラウザで http://localhost:4000を開く

2.4 ブラウザ操作

  1. 『音声認識開始』ボタンをクリックして、音声認識かいし
  2. 『停止』ボタンで音声認識を終了

補足
音声入力がうまく出来ていないときは、マイクの使用許可設定が適切か確認してください。

3. 実装の解説

3.A クライアントサイド(index.html)

Chrome で音声認識して、サーバにテキストを送る

'server.js' を起動すると、http://localhost:4000で 'index.html' にアクセスすることができます。
この index.htmlがクライアントサイドになります。

音声認識のメイン処理は speech_recognition.jsで行われます。

index.html
<!DOCTYPE html><htmllang="ja"><head><metacharset="UTF-8"><title>音声認識 Discordブリッジ</title><linkrel="stylesheet"href="assets/bootstrap/css/bootstrap.min.css"><linkrel="stylesheet"href="assets/bootstrap/css/bootstrap-grid.min.css"><linkrel="stylesheet"href="assets/bootstrap/css/bootstrap-reboot.min.css"><script src="assets/lib_js/jquery-3.3.1.min.js"></script><script src="assets/bootstrap/js/bootstrap.js"></script><script src="/socket.io/socket.io.js"></script><!-- socket.io を読み込み --><script src="assets/js/speech_recognition.js"></script><!-- メインロジック --><style>.row{margin-top:20px;}</style></head><body><divclass="container"><divclass="row"><divclass="col-sm-12"><h3>Chromeで音声認識して、Discordに書き込み</h3></div></div><divclass="row"><divclass="col-sm-5"><buttonclass="btn btn-primary"id="start_btn">音声認識開始</button><!-- 音声認識開始ボタン --><buttonclass="btn btn-secondary"id="end_btn">停止</button><!-- 音声認識停止ボタン --></div><divclass="col-sm-7 d-flex align-items-center"><strong><spanid="status"style="font-size: 24px"></span></strong><!-- 音声認識 ON/OFF 表示 --></div></div><divclass="row"><divclass="col-sm-12"><divclass="card-text"><spanid="prosess"></span><!-- 音声認識イベント表示 --></div></div></div><divclass="row"><divclass="col-sm-12"><divclass="card"><divclass="card-body"><divclass="card-text"id="content"></div><!-- 認識したテキスト表示 --></div></div></div></div></div></body></html>
speech_recognition.js
constsocket=io.connect();// ソケットioconstspeech=newwebkitSpeechRecognition();// 音声認識APIの使用speech.lang="ja";// 言語を日本語に設定letkeep_standby=false;functionupdate_prosess(text){$("#prosess").text(text);}// 音声認識 スタンバイ/停止 表示functionupdate_status(text){$("#status").html(text);}// 音声認識 イベント 表示// 音声認識した文字表示functionupdate_result_text(text){update_prosess('[結果表示]');$("#content").text(text);console.log(text);}// 音声認識の結果取得時の処理speech.onresult=result=>{consttext=result.results[0][0].transcript;update_result_text(text);socket.emit('get_word',{word:text});// 認識文字を socket.io 経由でサーバに送信する};// 音声認識の継続継続処理speech.onend=()=>{if(keep_standby)speech.start();elsespeech.stop();};speech.onspeechstart=()=>update_prosess('[音声取得開始]');speech.onspeechend=()=>update_prosess('[解析開始]');$(function(){$("#start_btn").on('click',()=>{update_status('<span class="text-success">『音声認識スタンバイ』</span>');keep_standby=true;speech.start();});$("#end_btn").on('click',()=>{update_status('<span class="text-danger">『停止中』</span>');update_prosess('[音声認識停止中]');keep_standby=false;speech.stop();});$("#end_btn").trigger('click');// 初期$("#start_btn").trigger('click');// 自動スタート});

音声認識の処理フロー

1. index.html: socket.io, speech_recognition.jsを読み込み
/socket.io/socket.io.jsの実態ファイルがソースコード一式にないため、不思議な感じですが、このファイルは server.jssocket.ioの設定をすると勝手に用意してくれます。

<scriptsrc="/socket.io/socket.io.js"></script> <!-- socket.io を読み込み -->
<scriptsrc="assets/js/speech_recognition.js"></script> <!-- メインロジック -->

2. speech_recognition.js: socket.io オブジェクト取得
サーバ側(server.js)へのテキスト送信はこのオブジェクトを介して行います。

constsocket=io.connect();// ソケットio

3. speech_recognition.js: "音声認識APIの使用"
音声認識のオブジェクト(speech)を取得します。
音声認識に関わる処理はこちらを介して行います。
その際、speech.lang = "ja"をお忘れなく。

constspeech=newwebkitSpeechRecognition();// 音声認識APIの使用speech.lang="ja";

4. speech_recognition.js: $("#start_btn").on
index.html で表示している、『音声認識開始』ボタンをクリックした時のイベント。
ここの speech.start();で音声認識が開始されます。
以降、音声認識処理が完了すると speech.onresultで、音声認識した文字列を取得出来ます。

$("#start_btn").on('click',()=>{update_status('<span class="text-success">『音声認識スタンバイ』</span>');keep_standby=true;speech.start();});

5. speech_recognition.js: speech.onresultでサーバ(server.js)への文字送信
音声認識した文字列は result.results[0][0].transcript;に格納されています。大抵このまま決め打ちでいいようです。
細かい仕様は https://wicg.github.io/speech-api/#speechreco-resultを参照してください。

6. speech_recognition.js: speech.onendで音声認識を継続

7 の処理を継続するための処理です。
5 keep_standby = trueとして、このフラグがtrueであれば、speech.start()をコールして継続処理をします。

B. サーバーサイド(server.js)

音声認識文字を受け取り、Discord の テキストチャンネル書き込む

server.jsでは クライアント側のspeech_recognition.jssocket.emitで送信される文字を取得して、Discord のテキストチャンネルに書き込みをします。

server.js
constPORT=4000;constexpress=require('express');constos=require('os');constapp=express();app.use(express.static(__dirname));constserver=require('http').createServer(app).listen(PORT);constio=require('socket.io').listen(server);// ログイン処理constconfig=require("./config/discord");constDiscord=require('discord.js');constclient=newDiscord.Client();consttoken=config.token;client.on('ready',()=>{console.log('start server');console.log(`open chrome: http://localhost:${PORT}`);consttargetTextChannel=client.channels.find(val=>val.id===config.channel_id);// 指定テキストチャンネルを取得io.sockets.on('connection',function(socket){socket.on('get_word',function(data){if(data===undefined||data===""||data===null)return;console.log(data);targetTextChannel.send(data.word);});});});client.login(token);

音声認識の文字取得・テキストチャンネル書き込み処理フロー

  1. Web expressを利用して index.html 用の Webサーバを立ち上げ、Webサーバに socket.ioを追加する。
server.js
constapp=express();app.use(express.static(__dirname));constserver=require('http').createServer(app).listen(PORT);constio=require('socket.io').listen(server);
  1. Discord Botの設定 discord.jsを利用して Botを起動させる。 client.login(token)で Botが起動して、起動完了後client.on('ready', () => {})がコールされます。
server.js
constconfig=require("./config/discord");constDiscord=require('discord.js');constclient=newDiscord.Client();consttoken=config.token;...client.login(token);
  1. 指定のテキストチャンネルを取得 config.channel_idに一致する channelを取得します。 find(val => val.name === 'チャンネル名')とすると、チャンネル名でチャンネルを取得できます。
server.js
consttargetTextChannel=client.channels.find(val=>val.id===config.channel_id);// 指定テキストチャンネルを取得
  1. socket.io の接続待ち
    index.html側で socket.ioに接続するのを待ち、接続があった場合はio.sockets.on('connection', () => {})が実行されます。
    この処理は index.htmlを開いたページ毎にコールされ、音声入力する index.htmlは複数でも対応可能です。

  2. 取得文字をテキストチャンネルにポスト
    index.html側で get_wordイベントを発火すると、下記のget_wordがコールされます。
    data.wordに音声認識した文字列が格納されているので、それを送信するだけで、テキストチャンネルに書き込みをします。

server.js
socket.on('get_word',function(data){if(data===undefined||data===""||data===null)return;console.log(data);targetTextChannel.send(data.word);});

最後に

google chromeの音声認識は無料で、APIなどを発行する必要もないので、個人利用するのであればすごく便利ですね。
google Cloud の STT(Speech-to-Text)も低料金で利用できるのですが、音声処理周りの実装やAPI利用のための設定が大変ですですので、Chromeの Web Speechが一番楽です。

音声入力は未来感があって、筆者はとてもワクワクするインターフェイスだと思っています!

掃除当番つぶやきボットを作ってみた

$
0
0

はじめに

お掃除当番を抽選し、Mattermostに発言するボットです。
抽選されるとちょっとした楽しみになりますよ。

スクリーンショット 2019-12-12 19.13.29.png

インストール

  1. GitHubからファイル一式をクローンもしくはダウンロードする

  2. Mattermostにボット用のアカウントを登録する

  3. Node.jsをインストールする

  4. モジュールをインストールする

    $ npm ci
    
  5. app/environment.tsファイルを設定する

  6. ビルドする

    $ npm run build
    

発言させる

  $ npm start

定時実行させたい時はcronなどを利用してください。

利用しているモジュール

  • @types/node
  • request
  • typescript

さいごに

よければいいねをお願いします。

【Node.js×PostgreSQL】ExpressとPostgreSQLでローカル環境のAPIサーバーを構築する

$
0
0

自分の環境

・Windows10
・Docker
・Docker-composer
・Node.js 10.15.3

やりたいこと

DockercomposerでPostgresとExpressを使用して、ローカル環境にAPIサーバーを作る。

とりあえず、Docker-Composer書く

docker-composer.yaml
version:'3.7'services:api:image:'node:12-alpine'volumes:-'./:/api:cached'working_dir:'/api'environment:HOST:'0.0.0.0'ports:-'3000:3000'command:'sh-c"npminstall&&npmrunstart"'db:image:postgres:12-alpinerestart:alwaysenvironment:POSTGRES_PASSWORD:postgresPOSTGRES_USER:postgresPOSTGRES_DB:postgresDATABASE_HOST:localhostports:-"5432:5432"volumes:-./migration:/docker-entrypoint-initdb.d

npmで必要なものをいろいろ入れる

・Express
・pg

npm init
npm install Express pg

とりあえず、ローカルでExpressが上手く動いているか確認する

node index.js

localhost:3000にアクセスする。Hello World!って表示されたらOK!

Expressのルーティングをいじってみる

index.js
constexpress=require('express')constapp=express()app.get('/',(req,res)=>res.send('Hello World!'))app.get('/user',(req,res)=>res.send('Got a PUT request at /user'));app.get('/hoge',(req,res)=>res.send('ほげほげ'));app.listen(3000,()=>console.log('Example app listening on port 3000!'))

localhost:3000/userにアクセスしてみて、Got a Put~って書いてあればOK。
他にもルーティングを足したい時はhogeのように足していく。

ExpressとPostgreSQLを接続させる

index.js
constexpress=require('express')constapp=express()const{Pool,Client}=require('pg')constconnectionString='postgres://postgres:postgres@db:5432/postgres'constpool=newPool({connectionString:connectionString,})pool.query('SELECT NOW()',(err,res)=>{console.log(err,res)pool.end()})app.get('/',(req,res)=>res.send('Hello World!'))app.get('/user',(req,res)=>res.send('Got a PUT request at /user'));app.listen(3000,()=>console.log('Example app listening on port 3000!'))

ログでSELECT NOW()のクエリの結果が表示されていればOK!

マイグレーション用のSQLを作成する

今のままだと、DBに何も入っていないので、テーブル作成用のSQLを作成し、Docker-compose up時に自動でSQL文を実行させるようにする。

/migration/migration.sql
CREATETABLECompany(idserial,nametextNOTNULL,descriptiontextNOTNULL,PRIMARYKEY(id));INSERTINTOresume(name,description)VALUES('株式会社〇〇''老舗のIT系の会社。人材派遣も行っている',),('△△株式会社''人材系の会社。自社開発は行っていない',),('株式会社✖✖''最近設立したばかりの伸び盛りのIT系ベンチャー企業',);

こんな感じで、ファイルを作成し、docker-compose upをすると、Companyテーブルとレコードが追加されます。

実際にレコードが追加されたことを確かめるために、index.jsを修正します。

index.js
constexpress=require('express')constapp=express()const{Pool,Client}=require('pg')constconnectionString='postgres://postgres:postgres@db:5432/postgres'constpool=newPool({connectionString:connectionString,})pool.query('SELECT NOW()',(err,res)=>{console.log(err,res)pool.end()})//idの箇所に数字を入れることで、パラメータを渡すことができる。company/10//みたいな感じで渡すと、10を受け取ることができる。//Laravelのルーティングの中に処理を書くイメージだと思われる。(本当はもっといい方法がありそう)// app.post('/company/:id',(req,res)=> //     res.send(req.param('id') + ":POSTデータのお返しです!")// );/*
URL設計
/company⇒Companyの全件取得
/company/:id⇒Companyテーブルのidに応じたレコードの取得
*/app.get('/company',(req,res)=>{constclient=newClient({connectionString:connectionString,})client.connect()client.query('SELECT * FROM Company',(err,result)=>{res.send(result.rows);client.end()})});app.get('/company/:id',(req,res)=>{constclient=newClient({connectionString:connectionString,})client.connect()//idを使ってクエリclient.query({name:'fetch-user',text:'SELECT * FROM Company WHERE id = $1',values:[req.param('id')]},(err,result)=>{res.send(result.rows);client.end()})});app.listen(3000,()=>console.log('Example app listening on port 3000!'))

localhost:3000/companyでテーブルの中身が全件表示されて、
localhost:3000/company/1で「株式会社〇〇」のレコードが表示されていれば成功です。
ちなみに、app.getのところを、app.postにすることもできます。

こんな感じで、Dockerを使用してExpressとPostgreSQLでの
API開発環境の構築が出来ました!!!

TypeScript×Nodeを勉強した時に参考にしたソースまとめ

$
0
0

はじめに

TypeScriptのおすすめ記事をまとめました。

言語仕様

TypeScriptの型入門

JavaScript

イマドキのJavaScriptの書き方2018

webpack

webpack 4 入門

終わり

TypeScriptの標準のAPIはJavaScriptと同じなので、やりたい処理はJSで調べて書き方をTSの記事をみて修正するというフローで勉強していました。
TSの書き方がわかるとフロントのVueとかReactでも使えるのでコスパが良いかなと思いました!
それでは!!

Viewing all 8913 articles
Browse latest View live