こんにちは、エンジニアの @akase244 です。
当社のFacebookページの投稿を見ていただくと、こんな感じでイノベーター・ジャパンではなにかと外国の方との接する機会が多いことが分かるかと思います。
去年、「日本語がほとんど話せないメンバーが入社した」ことで、社内で英語によるコミュニケーションを迫られるシーンがさらに増えました。
しかしながら、英語で問題なくコミュニケーションできるのはほんの数名です。。。
オンラインミーティングでは、Google翻訳の音声入力を試してみたり、英語が話せるメンバーにサポートしてもらったりと色々工夫してますが、なかなかうまくいかない場合もあり、最終的には翻訳サービスで翻訳した内容をSlackに貼り付けて黙って筆談という状態になってたりもします。
だったら、Slack上でもっと手軽に翻訳できるようにしたらいいじゃん。ということで今回翻訳ボットを作ってみました。
開発前夜
翻訳ボットを開発するちょっと前に「Translate for Slack」という /translate
で翻訳できるSlackのスラッシュコマンドを導入していました。
しかし、スラッシュコマンドで動くということは、翻訳前の文章が実行後に消えます。
スラッシュコマンド実行前
そして、この機能の残念なところは翻訳結果が「Only visible to you」状態で表示される点です。(翻訳後に表示される「Post This」ボタンをクリックすることで、投稿することも可能なようですが正直手間)
スラッシュコマンド実行後
翻訳結果が本人にしか見えないのでは、コミュニケーションのツールとしては使えません。
これを踏まえ、ボットを作る際にはスラッシュコマンドではなくSlackのOutgoing Webhooksで投稿に反応するものにしようと考えました。
作る意気込み的なもの
翻訳ボットは巷に溢れまくってますし何番煎じだよという感じですが、作る意味みたいなものを自分なりに考えて着手しました。
- 最近、技術調査をやってなかったので、時間を取ってやりたかった。
- サーバーレスアーキテクチャ、FaaSと呼ばれるものについて触れとかないとな、と以前から思っており、ボット開発は規模的にちょうど良さそうだった。
- AWS Lambdaの実装例は社内にあったが、Azure Functionsは無かったので一例目にしたかった。
仕様をざっくりと説明
- アーキテクチャ
- SlackのOutgoing Webhooks
- Azure Functions(無料範囲内で利用) + Node.js
- Microsoft Translator Text API(無料範囲内で利用)
- パラメータに「&category=generalnn」を渡すことでニューラルネットワークモデルによる翻訳を行う。
- Slackの投稿から「trans 翻訳したい文章」という文字列を発見したら「翻訳したい文章」の部分を何語か判定。
- 翻訳対象が日本語の場合は英語に翻訳。日本語以外の場合は日本語に翻訳。
- 翻訳結果をSlackへ投稿。
翻訳している様子
日本語から英語
英語から日本語
韓国語から日本語
なぜか擬人化
デザイナーの9ookyがこの翻訳機能をいたく気に入ってくれたようで、ある日突然擬人化されました。
本人曰く、「transちゃんの「T」型を模した髪型がポイント」だそうです。
最初のバージョンはこれ。
その後、「髪の毛青いのはイメージと違う!」との意見があったようでモデルチェンジ(しかも解説付き)しました。
「Slackボットのアイコンに使いたい」とお願いをすると、正方形に収まるようにヘアアレンジが入りました。
そして、「ブログ用に大きめの画像がほしいんですけど。。。」とお願いしてみると、最終的に渡されたのはこの画像でした。ちょっと気合い入れすぎでしょ。。。
実際のコード
module.exports = function (context, req) { const qs = require('querystring'); const request = require('request'); const xml2js = require('xml2js'); const body = qs.parse(req.body); if (body.token != process.env['SLACK_WEBHOOKS_TOKEN']) { context.res .status(500); } const text = body.text; const user_name = body.user_name; const matches = text.match(/^trans[\s ](([\n\r]|.)*)/); if (matches && user_name !== 'slackbot') { const translate_target = matches[1]; const subscription_key = process.env['TRANSLATOR_TEXT_API_KEY']; const token_headers = { 'Accept': 'application/jwt', 'Ocp-Apim-Subscription-Key': subscription_key }; const token_url = 'https://api.cognitive.microsoft.com/sts/v1.0/issueToken'; const token_options = { url: token_url, method: 'POST', headers: token_headers, }; request(token_options, function (error, response, body) { const access_token = body; const detect_url = 'http://api.microsofttranslator.com/v2/Http.svc/Detect?text=' + encodeURIComponent(translate_target); const detect_headers = { 'Authorization': 'Bearer ' + access_token }; const detect_options = { url: detect_url, method: 'GET', 'headers': detect_headers }; request(detect_options, function (error, response, body) { language_xml = body; xml2js.parseString(language_xml, function (err, res){ detect_language = res.string._; const from = detect_language; let to = 'ja'; let translated = '翻訳しました'; if (from === 'ja') { to = 'en'; translated = 'Translated'; } const translate_url = 'http://api.microsofttranslator.com/v2/Http.svc/Translate' + '?from=' + from + '&to=' + to + '&category=generalnn' + '&text=' + encodeURIComponent(translate_target); const translate_headers = { 'Authorization': 'Bearer ' + access_token }; const translate_options = { url: translate_url, method: 'GET', 'headers': translate_headers }; request(translate_options, function (error, response, body) { translate_xml = body; xml2js.parseString(translate_xml, function (err, res){ translated_text = res.string._; const payload = { 'username' : 'transちゃん', 'text' : translated + ' : ' + translated_text, 'icon_emoji' : ':trans:' }; context.res .status(200) .json(payload); }); }); }); }); }); } };
作ってみた感想、課題、今後対応したいこと
- ざっくりではあるが、Azure Functions(Function App)の使い方が把握できた。
- Translator Text APIのレスポンスがXML形式でビビった。
- コールバック地獄的な書き方になってしまってる。。。
- かなりの頻度で使われているようで、作った甲斐があった。
- 「Azure Functions の JavaScript 開発者向けガイド」を参照すると、「Node のバージョンは、現在、 6.5.0にロックされています。 」との記載があるが、「アプリケーション設定」の「WEBSITE_NODE_DEFAULT_VERSION」を修正することでバージョンが変更できるようなので、バージョンを7.6以上に上げたい。
- Node.jsの7.6からasync/awaitが正式に対応されているので、「co」や「async-await」といったパッケージを追加せずとも、コールバック地獄的なコードから脱却できるはず。。。
- Azure Functionsは数分間利用がない場合アイドル状態となり、次の実行時には時間が掛かってしまいます。(コールドスタートというらしいです)なので、同一リソース内で「"200 OK"だけを返す」といった単純な関数を作成して、アイドル状態にならないくらいの間隔で定期実行するといった対策を入れたい。
- たまに翻訳に失敗してレスポンスを返さない場合がある(Translator Text APIでタイムアウトになってるのでは?)ので、そういったエラー処理も対応したい。