JavaScript
GoogleAppsScript
Web
Line
GoogleCloudPlatform

素人がLINEの「文字起こし君」を1日で作る

自分専用の文字おこし君欲しくないですか?
プログラミングわからないって?
コピペだコピペ!
以下, 初心者向けに文字おこし君の作り方を解説していきます.

まえがき

ある日Twitterで次のようなTweetを見ました.

なるほどこれはおもしろい.
そこで作者様(@never_be_a_pm)の解説記事で機能の概要を読んでみたところ…

なんやこれ! 単純すぎやろ!

平易に言えば, LINEで送られてきた画像をGoogleのAIに流して, 返ってきた結果をLINEでまた送り返すという仕組みです.
てっきり画像解析の部分をゴリゴリ自作しているのだと思いこんでいました…
でもこの仕組みなら自分でも作れるんじゃないか…! と思い, 正直プログラミングとかよくわかりませんがとりあえずGoogle先生を頼りにやってみることにしました.

確証はありませんが, 多分誰でもできます. (制限はありますが全て無料です)

スタンス

上で作者様を平気でディスるようなことを書いていますが, その意図はありません.
要素自体は高度なものではなくても, パッケージとして新しい価値を生み出せるというのは素晴らしい能力だと思います.
正直憧れます. だって, このBotの考え方自体はすごく単純だし個々のAPIは以前から存在するけど, それを一つのプロダクトにまとめてこれだけバズるんですから.

既存の技術を組み合わせて全く新しいものにしてしまうってカッコいいです.
(当方作者様とは面識等一切ございません. 勝手に批評しておりますことお許しください.)

下準備

さてBotを作るにあたって, 目標は文字おこし君と(ほぼ)同じものを作ること と定めます.

流れとしては,
1. LINEで画像が送られてくる
2. Google Apps Script(GAS)で受け取り, Google Cloud Vision APIに投げる
3. 返ってきたテキストをLINEでリプライする
のようにします. 作者様と同じ方式であることや, GASに少しFamiliarだったのでこのようにしました.

必要な準備は,
- GASでとりあえず空のスクリプトを作成し, web appとして公開しURLを取得
- LINEでBotを作り, GASで取得したURLをBotのWebhook URLに指定する
- Botのアクセストークンを取得する
- Google Cloud Consoleでプロジェクトを作りVision APIを有効化
- 同ConsoleでAPI Keyを取得
といった所でしょうか. (めちゃめちゃ端折りました)

初めてであればちょっとややこしいと感じるかもしれませんが, 挫折せずに頑張りましょう! ここを抜けたら後はコピペです!
このあたりの話は書くのがめんどくさい先人たちの記事が沢山あるので調べてみて下さい.
先程紹介した作者様の解説記事にも, 詳しくステップが記載されています.

ソースコード

下準備で作成しておいた空のGASスクリプトを開き, コードを書いていきます.
以下をコピペし, 2箇所ある【hogehoge】の部分を自分の値に変更しましょう.

//LINEのアクセストークン設定
var CHANNEL_ACCESS_TOKEN = '【自分のBotのアクセストークン】';

//LINEでメッセージが送られてきた時に動く部分
function doPost(e) {
  var user = JSON.parse(e.postData.contents).events[0].source.userId;
  var type = JSON.parse(e.postData.contents).events[0].message.type;
  var reply_token= JSON.parse(e.postData.contents).events[0].replyToken;

  //reply_tokenの正常取得を確認
  if (typeof reply_token === 'undefined') {
    return;
  }

  //ポスト内容が正常なら進む
  if (typeof e === "undefined"){
    return;

  } else if (type==='image'){ //画像のときのみ処理
    var messageId = JSON.parse(e.postData.contents).events[0].message.id;
    var blob = get_line_content(messageId); //画像をLINEに取りに行く
    var result = imageAnnotate(blob); //画像をVision APIに投げる
    if(result==='0'){ //画像に文字が入っていなかったときのエラー文
      result='文字を読み取れなかったよ\uDBC0\uDC29\nごめんね!\uDBC0\uDC8E';
    }
    message_post(reply_token,result); //結果のテキストをLINEで返す

  } else { //画像以外のときは定型文返信
    var name = get_line_name(user);
    var txt = name+'っち!\uDBC0\uDCB1 わたしのこと呼んだ?\n画像を送ってくれればテキストにして返信するよ!\uDBC0\uDC2D\uDBC0\uDC2D'
    message_post(reply_token,txt);
  } 
  return;
}

//LINEから名前を取ってくる関数
function get_line_name(userId) {
  var url = 'https://api.line.me/v2/bot/profile/'+userId;

  var tmp = UrlFetchApp.fetch(url, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
    },
    'method': 'get'
  });

  var name = JSON.parse(tmp.getContentText());
  tmp = name.displayName;
  return tmp;
}

//LINEから画像を取ってくる関数
function get_line_content(messageId) {
  var url = 'https://api.line.me/v2/bot/message/' + messageId + '/content';

  //blobに画像を格納
  var blob = UrlFetchApp.fetch(url, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
    },
    'method': 'get'
  }); 
  return blob;
}

//Vision APIでOCRする関数
function imageAnnotate(file){
  var googleToken = '【取得したAPI Key】'; //Google Cloud APIのAPI KEY
  var payload = JSON.stringify({
    "requests":[
      {
        "image": {
          "content": Utilities.base64Encode(file.getBlob().getBytes())
        }, 
        "features": [
          {
            "type": "TEXT_DETECTION"
          }
        ],
      }
    ]
  });

  var url="https://vision.googleapis.com/v1/images:annotate?key="+googleToken;
  try {
    var res = UrlFetchApp.fetch(url, //Vison APIに投げて,結果をresに格納
                                {
                                  method : 'post',
                                  contentType: 'application/json', 
                                  payload : payload
                                });
  } catch(e) {
    //error handling
    console.log(e);
    return false;
  }

  var obj = JSON.parse(res.getContentText()); //結果のJSONから必要なテキストだけ抜く
  var res = obj.responses;

  //一番最初の配列に, 全てのテキストがつながったものが入っている
  var txt;
  var textAnnotations = res[0].textAnnotations;
  if(textAnnotations===undefined){ //OCRに失敗したとき
    txt='0';
    return txt;
  }
  txt = textAnnotations[0].description

  return txt;
}

//LINE返信用関数
function message_post(token,message){
  var url = 'https://api.line.me/v2/bot/message/reply';

  UrlFetchApp.fetch(url, {

    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': token,
      'messages': [{
        'type': 'text',
        'text': message,
      }],
    }),
  }); 
}

コードを書き終えたら上書き保存をし, 再度web appとして公開ボタンを押しましょう.
バージョンの数字を上げるといいと思います.

免責

上のコードはGoogle先生を頼りに初心者の私がポチポチ書いたものであり, 本エントリを利用・参照することによって生じた如何なる事象に対しても責任は負いかねます. 自己責任によりご利用下さい.
我ながら汚いコードですが, Botとしての機能を満たして動作はすると思います. (私は使えています.)

具体的に怪しい部分を書いておくと, 7行目のtype判別で, 画像以外が送られてきた場合にコンソールで警告を発する場合があるとわかっていますが, 直すのがめんどくさい今回のBotの動作に支障はないため放置しています.
また, API Keyなどをソースコードの中に保存することはセキュリティ面から推奨されておりません.
加えて, API Keyを用いてCloud APIのサービスを構築することも推奨されていません. OAuthとかちょっとよくわからないのでモラルをお持ちの方は調べて見て下さい.

無料であることの制限

ここまで全て無料でできるわけですが, 当然無料ながらの制限があります.
使用した3つのサービスそれぞれにおける主な制限を記載します.

これ以外にも色々と制限はありますが, 主に引っかかりそうなのはこんな感じだと思います.
LINE Messaging APIに関してはデフォルトだとDeveloper Trialになっているかと思いますが, フリープランにすると友達登録数制限が解除される一方でPUSH送信(Bot側から話しかけること)ができなくなります.
またVision APIは従量課金制ですので, 月に1000枚以上の画像をBotに送った場合登録した支払い方法に請求が来る可能性があります. (支払いを無効にすれば1000ユニットで自動的に処理が止まるらしいですが, 詳しいことはわかりません.)

まとめ

ここまでやってみた方, 如何でしたか?
こんな記事でも, 少しでも役に立ったら幸いです.

Botの見た目や細かい詰めはLINE Developerの方から設定できます.
またソースコードの定型文は自分の好きなように変えてもいいでしょう.

感想, 批判等歓迎しておりますので是非コメント下さい.

良いTechライフを!