画像・音声・動画を文字起こしするLINE BotをNode.jsでつくってみよう! 〜ngrokによる高速Bot開発〜

はじめましてこんにちは、ひょうどうです。

今回は、画像・音声・動画を文字起こしするLINE Botアプリを作りながらNode.jsを学んでいきましょう!

以下が完成イメージです。

文字起こしアプリとは、画像や動画から文字または音声を判別してテキストに変換するアプリです。

【本記事で学べること】
・LINE Botの作り方
・FFmpegを用いた音声・動画のエンコーディング
・画像、音声、動画の文字起こし方法
・ngrokによる高速Bot開発
・Cloud Vision APIとCloud Speech-to-Textの使い方

【必要なもの】
・LINE Developerアカウント
・GCPアカウント
・PC(macであればなお良い)
・スマートフォン(LINE Botを試すため)

【前提知識】
・Gitの使い方
・Node.jsの基礎
・Expressの基礎
・LINE Botの基礎

本記事では、開発環境構築とアカウント、LINE Botチャンネルの作成手順は割愛させていただきます。

以下の記事を参考に開発環境構築とアカウント、LINE Botチャンネルの設定を行ってください。

 

・nodeの環境構築

➜ ~ node -v
v13.13.0
➜ ~ npm -v
6.14.4

この2つのコマンドを叩いてバージョンが表記されていれば問題ありません。

多少のバージョン違いはあると思いますが、できればメジャーバージョンが一致するようにお願いします。
13.13.0で例えると○○.13.0の〇〇の数値。ここでは13です。

 

・ngrokの環境構築

本記事で一番紹介したいサービスです。

ngrokは開発中で使用するもので、完成したコードは本記事ではgaeにデプロイします。

「開発段階でもngrokではなく別のサービスを使ってデバッグを行いたい」、という方がおられましたらこの章はスキップしてください。

ngrokを使用する理由は、開発段階で簡単かつ最速でデバッグを行えるからです。

LINE Botを動作させるには、webhookを作成しなければなりません。

Webhookとは、LINE Botにチャットを送信した際、送信内容を受信し処理するサーバのURLです。

自前のサーバがあれば、特に問題はありません。

しかし今のご時世、gcpやaws、herokuといったサービスを使ってサーバを構築するのが当たり前となっています。

本記事でも最終的にはgcpのgaeというサービスに公開するのですが、ちょっとした動作確認をするために毎回サーバにデプロイしていたのでは時間を浪費してしまいます。

そこでngrokの登場です。

このサービスを使うことで、PC内で立ち上げたサーバ(localhost)を1つのコマンドを叩くだけで外部に公開することができます。

このサービスと出会ったときの僕の心情は下の写真の猫のような感じですw

画像1

引用元:https://bokete.jp/odai/1614903

ngrokを使いこなすことで生産性が向上します!

実際に、僕はLINE Bot開発案件でこのサービスを使って開発しています。

 

・Google Cloud Platform

画像文字起こしで使用する「Cloud Vision」
音声・動画文字起こしで使用する「Cloud Speech-to-Text」

これらを使用するために登録します。

本記事内の開発であれば無料枠内で十分しようできます。

ただしクレジットカード登録が必須であるため、登録できない方には申し訳ございませんが、本記事の対象外がとなります。

 

・gcloudコマンド

完成したコードをGoogle App Engine(以下GAEと称す)にデプロイする際に必要です。

「GAEではなくHerokuを使いたい」という方は別のサービスにデプロイしたいという方がいればスキップしてください。

 

・LINE Developer

LINE Botのチャンネルを登録する際の必須項目を、例として以下に示します。

チャネルの種類・・Messaging API
プロバイダー・・・先程作成したプロバイダーを選択
チャネル名・・・・表示するアプリの名前(例:文字起こしアプリ)
チャネル説明・・・LINEの一言のように表示されます
(例:画像、音声、動画を文字起こしするアプリです)
大業種・・・・・・使う用途を選択(例:個人)
小業種・・・・・・使う用途を選択(例:個人(学生))
メールアドレス ・・メールアドレスを記載

Botの応答設定は以下のように、オン・オフを設定してください。

スクリーンショット 2020-05-03 22.20.17 1

参考までに僕の開発環境を以下に示します。

【開発環境のバージョン】
npm・・・6.14.4
node・・・13.13.0
ngrok・・・2.3.35

【開発エディタ】
Visual Studio Code

【gcloudバージョン】
Google Cloud SDK 288.0.0
bq 2.0.56
core 2020.04.03
gsutil 4.49

開発中に行き詰まったり、わからないことがあればGitHubのIssueで質問を受け付けてます。

Issueの対応に応じて、本記事もアップデートしていきます。

注意:本記事の内容に関することに限ります!

それではさっそく開発を始めていきましょう!

オウム返しアプリの作成

まずはLINEにチャットした内容をそのまま返信する、オウム返しアプリを作りましょう!

Bot開発する際、まずオウム返し機能を実装してから仕様書通りのBot開発に切り替えています。

オウム返しアプリはLINE Bot開発の準備運動のようなもので、野球で例えるならばキャッチボールと同じです。

ということで開発を始めていきましょう!

プロジェクトを作成したい場所で以下のコマンドを入力してください。

mkdir mojiokoshi-line-bot
cd mojiokoshi-line-bot
npm init

「mkdir」コマンドで任意のフォルダを作成(今回はmojiokoshi-line-botにしました)

「cd」コマンドでそのフォルダに移動

「npm init」コマンドでNode.jsプロジェクトを作成

対話形式で何度か英語で質問されますが、すべてEnterキーを押して大丈夫です。

➜  mojiokoshi-line-bot npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (mojiokoshi-line-bot) 
version: (1.0.0) 
description: 
entry point: (index.js) 
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 
About to write to /Users/freedom/Development/mojiokoshi-line-bot/package.json:

{
 "name": "mojiokoshi-line-bot",
 "version": "1.0.0",
 "description": "",
 "main": "index.js",
 "scripts": {
   "test": "echo \"Error: no test specified\" && exit 1"
 },
 "author": "",
 "license": "ISC"
}


Is this OK? (yes) 
➜  mojiokoshi-line-bot

詳細を知りたい方は以下のサイトを参考にしてください。

 

終了すると以下の「package.json」ファイルが作成されます。

{
  "name": "mojiokoshi-line-bot",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

これを以下のように変更してください。

{
  "name": "mojiokoshi-line-bot",
  "version": "0.0.0",
  "private" : true,
  "description": "【有料note教材】画像、音声、動画を文字起こしするLINE Botアプリ",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "author": "",
  "license": "MIT"
}

・バージョンの変更
・privateを追加
・descriptionを変更
・scriptsにstart項目の追加
・licenseをMITに変更

scriptsの中にstartという項目を追加しました。

これは、コマンドライン上で「npm start」と入力すると「node index.js」が実行されます。

今回のような小規模アプリで書く利点はあまりありませんが、書く癖をつけとくといいと思います。

他にも様々な項目があり、以下の記事が参考になります。

 

次は必要なライブラリをインストールしましょう。

npm i --save @line/bot-sdk dotenv express

実行すると、「node_modules」フォルダと「package-lock.json」ファイルが作成されます。

node_modules:インストールしたライブラリ格納するフォルダ
package-lock.json:ライブラリのバージョン(依存関係)を管理するファイル

@line/bot-sdk・・LINE Botを作成するために必要なsdk
dotenv・・・・・.envファイルを読み込ませるのに必要
express・・・・・webフレームワーク

expressの参考記事

----- 現在の進捗 -----

 

次に「.env」ファイルを作成します。

touch .env

「.env」ファイルには、LINE Botの「チャンネルアクセストークン」と「チャンネルシークレット」を記述します。

これらは、LINE Botチャンネルによってそれぞれ値が異なります。

各自LINE developer consoleよりメモしてください。

画面が英語表記担っている場合、右下のセレクトボタンで日本語に変更できます。

「チャンネル基本設定」を押す。

スクリーンショット 2020-04-21 18.40.16

「チャンネルシークレット」の内容をメモする。

記載されてない場合は右の再発行ボタンを押す。

スクリーンショット 2020-04-21 18.40.20

「Messaging API設定」を押す。

スクリーンショット 2020-04-21 18.40.24

「チャンネルシークレット」の内容をメモする。

記載されてない場合は右の再発行ボタン

スクリーンショット 2020-04-21 18.40.33

取得した2つのキーを「.env」ファイルに記載する。

CHANNEL_ACCESS_TOKEN=アクセストークン
CHANNEL_SECRET=シークレットキー

「.env」ファイルに記載する情報は、秘匿にすべき情報がほとんどです。
そのため「.gitignore」ファイルに記載して他人に見えないようにするのが当たり前となってます。

 

次に「index.js」と「routes/webhook.js」ファイルを作成します。

touch index.js
mkdir routes
touch routes/webhook.js

「index.js」ファイル内に以下の内容をコピペしてください。

'use strict'

/**
* ライブラリのインポート
*/
const express = require('express')

/**
* 初期設定
*/
require('dotenv').config()
const app = express()
const routes = {
  webhookRouter: require('./routes/webhook.js')
}

/**
* APIルート作成
*/
app.use('/webhook', routes.webhookRouter)

/**
* サーバの起動
*/
const port = process.env.PORT || 3000
app.listen(port, () => {
  console.log(`listening on ${port}`)
})

expressサーバについての参考記事

 

「index.js」ではサーバの起動とルーティングの定義をしてます。

上記のコードでは、「base_url + /webhook」にアクセスがあった時、「routes/webhook.js」に処理

「routes/webhook.js」ファイル内に以下の内容をコピペしてください。

'use strict'

/**
* ライブラリのインポート
*/
const line = require('@line/bot-sdk')
const express = require('express')

/**
* 初期化
*/
const router = express.Router()
const config = {
  channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN,
  channelSecret: process.env.CHANNEL_SECRET
}
const client = new line.Client(config)
const ONE_MINUTES = 60000

/**
* 動作確認用のルート
*/
router.get('/', (req, res) => {
  res.send('Hello World')
  res.status(200).end()
})

/**
* 本番用のルート
*/
router.post('/', line.middleware(config), async (req, res) => {
  Promise.all(req.body.events.map(handlerEvent))
    .then((result) => {
      console.log(result)
      res.status(200).end()
    })
    .catch((err) => {
      console.error(err)
      res.status(500).end()
    })
})

/**
* メイン関数
*/
const handlerEvent = async (event) => {
  // Webhookの検証
  if (event.replyToken && event.replyToken.match(/^(.)\1*$/)) {
    return 'Webhookの検証'
  }

  const replyToken = event.replyToken

  // イベントの処理
  switch (event.type) {
    case 'message':
      const message = event.message
      let text
      switch (message.type) {
        case 'text':
          text = message.text
          await replyText(replyToken, text)
          return 'オウム返し成功'
        default:
          text = 'テキストを送信してください'
          await replyText(replyToken, text)
          return 'その他'
      }
    default:
      return 'その他'
  }
}

/**
* テキストを返信する関数
* @param {String} token
* @param {String[] | String} texts
*/
const replyText = (token, texts) => {
  texts = Array.isArray(texts) ? texts : [texts]
  return client.replyMessage(
    token,
    texts.map((text) => ({ type: 'text', text }))
  )
}

module.exports = router

「router.get」は、getメソッドで「/」にアクセスした時、つまり「base_url + /webhook」にgetでアクセスしたときの処理を記述します。

同様に「router.post」は、postメソッドで「base_url + /webhook」にアクセスしたときの処理を記述します。

今回はLINE Botからのアクセスなので、postメソッドのみの記述で大丈夫なのですが、動作確認用のためgetメソッド用のルーターも作成しました。

ターミナルに「npm start」と入力してサーバを立ち上げて、

http://localhost:3000/webhook

にアクセスし「Hello World」と表示されたらOKです。

「routes/index.js」ファイル内にある、Promise、async、awaitと「Promise.all」の引数に指定しているmapオブジェクトについては以下の記事を参考にしてください。

上記のコードでは、「handleEvent」関数がメイン関数となります。

引数の「event」変数には、LINE Botから送信されたデータが格納されてます。

詳細は公式ドキュメントを参照ください。

それでは実際にLINE Botで試してみましょう。

Node.jsサーバを立ち上げた状態で別ターミナルで以下のコマンドを入力してみましょう

ngrok http 3000

そうすると下のような画面が表示されます。

スクリーンショット 2020-04-21 21.50.47

おそらく皆さんはFree Planで使用中だと思うので、URLはngrokを起動するたびに更新されます。

つまり、ngrokを起動するたびにwebhookのURLを更新しなければいけません。

これを手間だと思うかもしれませんが、herokuやgaeにデプロイしてデバッグするよりは圧倒的に楽だと思います。

もしエラーが起きた場合、おそらくトークン認証を行っていないことが原因だと思います。

以下の記事の「ngrokでトークンを認証する」という項目を参考にしてください。

赤丸で囲ったURLをLINE Botのwebhookに登録します。

スクリーンショット 2020-04-21 21.44.11

ここにngrokで発行されたURLを記載する。

その際に「/webhook」の記載を忘れないようにしてください。

URLが、「https://erg34vfs.ngrok.io」の場合、「https://erg34vfs.ngrok.io/webhook」

最後に、作成したLINE Botをお手持ちのスマートフォンのLINEに友達追加し、テキストを送信してみてください。

友達追加は、Developer画面の「友だち追加」もしくは、「Messaging APIの設定」を押すとQRコードが表示されるので、それをスキャンしてください。

以下のような画面が表示されたらオウム返しアプリは完成です!

画像10

VS Codeで開発されている方であれば、以下の画像のようにターミナルを2つ表示することができます。

スクリーンショット 2020-04-25 18.54.03

左側でnode.jsサーバ、右側でngrokサーバをそれぞれ立ち上げて行います。

ちょっとした動作確認やevent情報を確認したい場合、本来であればHerokuやGAEにデプロイしてログを確認しなければならず、時間がかかります。

しかしngrokを使用することで、手元のPCにログを表示させることができ、非情に生産性が上がります。

----- 現在の進捗 -----

 

以上がオウム返しの実装になります。

ここから有料コンテンツとなってます。

それでは本章に入っていきましょう!

文字起こしアプリの作成

LINEから送信されるメッセージにはいくつかの種類があり、その中で今回使用するものを以下に示します。

・メッセージ送信時
→画像
→音声
→動画
・友達追加時
・ブロック時

詳細は公式ドキュメントを参照ください。

これらを条件分岐させ、それぞれの処理を行うように「routes/webhook.js」のhandleEvent関数内のswich文を以下のように修正します。

const replyToken = event.replyToken

// イベントの処理
switch (event.type) {
  case 'follow':
    return 'フォローされました'

  case 'unfollow':
    return 'フォロー解除されました'

  case 'message':
    const message = event.message
    let text
    switch (message.type) {
      case 'image':
        // 画像を受信した際の処理
        return '画像を文字起こししました'

      case 'audio':
        // 音声を受信した際の処理
        return '音声を文字起こししました'

      case 'video':
        // 動画を受信した際の処理
        return '動画を文字起こししました'

      default:
        // 画像、音声、動画以外を受信した際の処理
        text = '画像、音声、動画を送信していてください'
        await replyText(replyToken, text)
        return '画像、音声、動画以外を受信'
    }
   
  default:
    return 'その他'
}

画像、音声、動画以外のメッセージが来た場合、「画像、音声、動画を送信していてください」というメッセージが送信されます。

GCP設定

まず、GCPでプロジェクトを作成してCloud Vision APIを有効にしましょう!

以下の記事を参考にGCPプロジェクトを作成してください。
プロジェクト名は任意です。

プロジェクトを作成したら、以下にアクセスしCloud Vision APIを有効にします。

 

次に、サービスアカウントを作成し設定しましょう!

APIは有効にするだけでは使用できません。

サービスアカウントキーを作成し、アプリに組み込むることで使用可能になります。

以下のサイトよりサービスアカウントキーを作成してください。
最終的にjsonファイルが生成されます。

先程生成されたjsonファイルを以下で作成したディレクトリに格納してください。

mkdir key

.envファイルにサービスアカウントキーの相対パスを以下のように記述してください。

GOOGLE_APPLICATION_CREDENTIALS=./key/ファイル名.json

以上の設定で、GCPのAPIが使用可能になります。

それでは、画像、音声、動画それぞれの処理を書いていきましょう!

画像文字起こし

画像文字起こしで使用する機能は、Cloud Vision APIです。

Vision APIには、文字起こし以外にラベル検出、分類、オブジェクト検出など、様々な処理が可能です。

今回は、文字起こし(textDetection)機能を使用します。

補足
今回使用する「textDetection」という関数は、ワープロ文字に対して非情に精度が高いのですが、手書き文字の精度は劣ります。
手書きに特化した、「documentTextDetection」という関数をしようすれば手書きの精度が高くなりますが、ワープロ文字への精度は「textDetection」に比べて劣ります。

まず画像を文字起こしするには、LINEサーバから画像を取得し(①)、その画像データをCloud Vision APIに送る(②)ことで文字起こしデータを取得することができます。

スクリーンショット 2020-05-03 17.35.35

まず①のLINEサーバからデータを取得する関数を作成します。

mkdir lib
touch lib/index.js

「lib/index.js」ファイル内に以下のコードを記載してください。

'use strict'

/**
* ライブラリの読み込み
*/
const line = require('@line/bot-sdk')

/**
* 初期設定
*/
const config = {
  channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN,
  channelSecret: process.env.CHANNEL_SECRET
}
const client = new line.Client(config)

/**
* 自作関数群
*/

/**
 * 2000文字を区切りテキストを分割し配列を返す関数
 * @param {String} text
 */
exports.getTextArray = text => {
  const texts = []
  if (text.length > 2000) {
    while (text.length > 2000) {
      texts.push(text.substr(0, 2000))
      text = text.slice(2000, -1)
    }
  }
  texts.push(text)
  return texts
}

/**
* LINEサーバーからファイルをbufferでダウンロード
* @param {Number} messageId
*/
exports.getContentBuffer = messageId => {
  return new Promise((resolve, reject) => {
    client.getMessageContent(messageId).then(stream => {
      const content = []
      stream
        .on('data', chunk => {
          content.push(Buffer.from(chunk))
        })
        .on('error', reject)
        .on('end', () => {
          resolve(Buffer.concat(content))
        })
    })
  })
}

「getTextArray」関数は、2000文字以上のテキストを2000文字ごとに配列に分割します。
LINEは一度のチャットで2000文字までしか送れないためこのような関数を作成してます。

「getContentBuffer」関数は、LINEからおくられてきたメッセージIDを元に、LINEサーバからコンテンツをBuffer形式で取得する関数です。

streamについて深く知りたい方は以下の記事を参考にしてください。

Bufferとは、バイナリデータをNode.jsで取り扱うためのクラスです。

引用:https://techblog.yahoo.co.jp/advent-calendar-2016/node_new_buffer/

 

次に②のLINEサーバから取得した画像データをCloud Vision APIに送り、文字起こしデータを取得する関数を作成します。

以下のコマンドで、画像文字起こしに必要な「@google-cloud/vision」ライブラリを読み込み必要なファイルを作成してください。

npm i --save @google-cloud/vision

touch lib/gcloud-api.js

「lib/gcloud-api.js」に以下のコードを記述してください。

'use strict'

/**
* ライブラリの読み込み
*/
const vision = require('@google-cloud/vision')

/**
* Cloud Vision text detection
* 画像からテキストを抽出する
* @param {buffer} imageBuffer
*/
exports.cloudVisionText = async imageBuffer => {
  const client = new vision.ImageAnnotatorClient()
  const results = await client.textDetection({
    image: { content: imageBuffer }
  })
  if (results[0].fullTextAnnotation === null) {
    const message = 'テキストが抽出できませんでした'
    return message
  } else {
    const message = results[0].fullTextAnnotation.text
    return message
  }
}

以上2つの処理をまとめる以下の「imageToText」関数を「routes/webhook.js」の「replyText」関数の下に記述してください。

/**
* 画像をテキストに変換する関数
* @param {Number} messageId
*/
const imageToText = async (messageId) => {
  const buffer = await func.getContentBuffer(messageId)
  const text = await gcloudApi.cloudVisionText(buffer)
  const texts = await func.getTextArray(text)
  return texts
}

「imageToText」関数内で、先程作成した関数を呼び出していますが、現状ではrequireしていないのでエラーが発生します。

そのため、「routes/webhook.js」内のライブラリインポート欄を以下のようにしてください。

/**
* ライブラリのインポート
*/
const line = require('@line/bot-sdk')
const express = require('express')
const func = require('../lib/index')
const gcloudApi = require('../lib/gcloud-api')

それでは、画像を送信された際の処理を「routes/webhook.js」に記載していきましょう。

case 'image':
  // 画像を受信した際の処理
  return '画像を文字起こししました'

この部分を以下のように修正します。

case 'image':
  // 画像を受信した際の処理
  text = await imageToText(message.id)
  await replyText(replyToken, text)
  return '画像を文字起こししました'

以上で画像が送信された際に、LINEサーバから画像を取得し、Cloud Vision APIで文字起こしする処理が完了しました。

試しに、ngrokとNodeサーバを立ち上げて確認してみましょう!

もし、画像を送信して文字起こしされなかった場合、サービスアカウントキーの設定を確認してください。

----- 現在の進捗 -----

音声文字起こし

音声文字起こしで使うAPIは、「Cloud Speech-to-Text」です。

名前の通りで、音声データを送るとその中の音声をテキストに変換してくれます。

音声文字起こしには以下の点に注意しなければいけません。

・エンコード形式
・サンプリング周波数
・チャンネル数

これらを設定した上でCloud Speech-to-Textを使用しなければいけません。

今回は、エンコード形式を「flac」にすべて統一させました。
サンプリング周波数とチャンネル数は、送信されたデータ形式をそのまましようします。

これを実装するには「FFmpeg」というライブラリを使用します。

「FFmpeg」は音声・動画変換ライブラリの中で最も有名なライブラリです。

言葉で説明するよりコードで示したほうが理解しやすいと思うので、さっそく始めていきましょう!

まず、音声文字起こしで使用する関数を読み込みます。
(動画文字起こしも同様のライブラリを使用します)

npm i --save @google-cloud/speech
npm i --save fluent-ffmpeg @ffmpeg-installer/ffmpeg @ffprobe-installer/ffprobe streamifier

次にFFmpegを使用して、音声のエンコード変換とメタデータの取得(サンプリング周波数とチャンネル数)を処理する関数を作成しましょう!

「lib/index.js」ファイルを以下のコードをそれぞれ追加してください。

・ライブラリ

/**
* ライブラリの読み込み
*/
const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path
const ffprobePath = require('@ffprobe-installer/ffprobe').path
const ffmpeg = require('fluent-ffmpeg')
const streamifier = require('streamifier')

・初期設定

/**
* 初期設定
*/
ffmpeg.setFfmpegPath(ffmpegPath)
ffmpeg.setFfprobePath(ffprobePath)

・自作関数群

/**
* 音声・動画の秒数、サンプリング周波数、チャンネル数を取得
* @param {Buffer} buffer
*/
exports.getAudioMetaData = buffer => {
  return new Promise((resolve, reject) => {
    const inStream = streamifier.createReadStream(buffer)
    ffmpeg(inStream).ffprobe(0, (err, metaData) => {
      if (err) {
        reject(err)
        return
      }

      const stream = metaData.streams.find(chunk => 'sample_rate' in chunk)
      const sampleRateHertz = stream.sample_rate
      const audioChannelCount = stream.channels

      resolve({
        sampleRateHertz,
        audioChannelCount
      })
    })
  })
}

/**
* 音声をflacにエンコード
* @param {Buffer} audioBuffer
*/
exports.audioToFlac = async audioBuffer => {
  return new Promise((resolve, reject) => {
    const inStream = streamifier.createReadStream(audioBuffer)
    const content = []
    ffmpeg(inStream)
      .audioCodec('flac')
      .format('flac')
      .pipe()
      .on('data', chunk => {
        content.push(Buffer.from(chunk))
      })
      .on('error', reject)
      .on('end', () => {
        resolve(Buffer.concat(content))
      })
  })
}

次に、Cloud Speech-to-Textを使用するコードを記述します。

「lib/gcloud-api.js」に以下のコードを追加してください。

/**
* ライブラリの読み込み
*/
const vision = require('@google-cloud/vision')
const speech = require('@google-cloud/speech')
/**
* Cloud Speech-To-Text
* 画像からテキストを抽出する
* @param {Buffer} buffer
* @param {String} encoding
* @param {string} languageCode
* @param {String} model
* @param {Number} sampleRateHertz
* @param {Number} audioChannelCount
*/
exports.cloudSpeechToText = async (
  buffer,
  {
    encoding = 'FLAC',
    model = 'default',
    sampleRateHertz = 16000,
    audioChannelCount = 2,
    languageCode = 'ja-JP'
  }
) => {
  const client = new speech.SpeechClient()

  const config = {
    encoding: encoding,
    sampleRateHertz: sampleRateHertz,
    languageCode: languageCode,
    model: model,
    audioChannelCount: audioChannelCount
  }

  const audio = {
    content: buffer.toString('base64')
  }

  const request = {
    config: config,
    audio: audio
  }

  const [response] = await client.recognize(request)
  const transcription = response.results
    .map(result => {
      return result.alternatives[0].transcript
    })
    .join('\n')

  if (transcription === '') {
    return '音声を抽出できませんでした...\n音を大きくしてみてください!'
  }

  return transcription
}

次に、音声をFFmpegを使用してメタ情報の抽出と「flac」形式にエンコードする処理を記述します。

「lib/index.js」に以下のコードを記述してください。

/**
* 音声・動画のサンプリング周波数とチャンネル数を取得
* @param {Buffer} buffer
*/
exports.getAudioMetaData = buffer => {
  return new Promise((resolve, reject) => {
    const inStream = streamifier.createReadStream(buffer)
    ffmpeg(inStream).ffprobe(0, (err, metaData) => {
      if (err) {
        reject(err)
        return
      }

      const stream = metaData.streams.find(chunk => 'sample_rate' in chunk)
      const sampleRateHertz = stream.sample_rate
      const audioChannelCount = stream.channels
 
      resolve({
        sampleRateHertz,
        audioChannelCount
      })
    })
  })
}

/**
* 音声をflacにエンコード
* @param {Buffer} audioBuffer
*/
exports.audioToFlac = async audioBuffer => {
  return new Promise((resolve, reject) => {
    const inStream = streamifier.createReadStream(audioBuffer)
    const content = []
    ffmpeg(inStream)
      .audioCodec('flac')
      .format('flac')
      .pipe()
      .on('data', chunk => {
        content.push(Buffer.from(chunk))
      })
      .on('error', reject)
      .on('end', () => {
        resolve(Buffer.concat(content))
      })
  })
}

以上2つの処理をまとめる以下の「audioToText」関数を「routes/webhook.js」の「imageToText」関数の下に記述してください。

/**
* 音声をテキストに変換する関数
* @param {Number} messageId
*/
const audioToText = async (messageId, duration) => {
  if (duration >= ONE_MINUTES) return '1分未満の音声を送信してください'
  let buffer = await func.getContentBuffer(messageId)
  buffer = await func.audioToFlac(buffer)

  const metaData = await func.getAudioMetaData(buffer)
  const text = await gcloudApi.cloudSpeechToText(
    buffer,
    {
      sampleRateHertz: metaData.sampleRateHertz,
      audioChannelCount: metaData.audioChannelCount
    }
  )

  const texts = await func.getTextArray(text)

  return texts
}

1分以上の音声を取得した場合、「1分未満の音声を送信してください」と返信されます。

Cloud Speech-to-Textは、1分以上と1分未満の音声でコードの内容が違います。

詳しくは公式ドキュメントを参照ください。

それでは、音声を送信された際の処理を「routes/webhook.js」に記載していきましょう。

case 'audio':
  // 音声を受信した際の処理
  return '音声を文字起こししました'

この部分を以下のように修正します。

case 'audio':
  // 音声を受信した際の処理
  text = await audioToText(message.id, message.duration)
  await replyText(replyToken, text)
  return '音声を文字起こししました'

以上で音声が送信された際に、LINEサーバから音声を取得し、Cloud Speech-to-Textで文字起こしする処理が完了しました。

試しに、ngrokとNodeサーバを立ち上げて確認してみましょう!

----- 現在の進捗 -----

動画文字起こし

動画文字起こしは、音声文字起こしと処理がほとんど同じです。

違いは、FFmpegで動画から音声のみを抽出する処理が加わるだけです。

まず、「lib/index.js」ファイルに以下のコードを自作関数群に追加してください。

/**
* 動画から音声のみを抽出しflacにエンコード
* @param {Buffer} videoBuffer
*/
exports.videoToFlac = videoBuffer => {
  return new Promise((resolve, reject) => {
    const inStream = streamifier.createReadStream(videoBuffer)
    const content = []
    ffmpeg(inStream)
      .withNoVideo()
      .audioCodec('flac')
      .format('flac')
      .pipe()
      .on('data', chunk => {
        content.push(Buffer.from(chunk))
      })
      .on('error', reject)
      .on('end', () => {
        resolve(Buffer.concat(content))
      })
  })
}

「withNoVideo」オプションをつけることで、動画から音声のみを抽出することができます。

 

次に、「routes/webhook.js」に

/**
* 動画をテキストに変換する関数
* @param {Number} messageId
*/
const videoToText = async (messageId, duration) => {
  if (duration >= ONE_MINUTES) return '1分未満の動画を送信してください'
  let buffer = await func.getContentBuffer(messageId)
  buffer = await func.videoToFlac(buffer)

  const metaData = await func.getAudioMetaData(buffer)
  const text = await gcloudApi.cloudSpeechToText(
    buffer,
    {
      sampleRateHertz: metaData.sampleRateHertz,
      audioChannelCount: metaData.audioChannelCount
    }
  )

  const texts = await func.getTextArray(text)

  return texts
}

それでは、動画を送信された際の処理を「routes/webhook.js」に記載していきましょう。

case 'video':
  // 動画を受信した際の処理
  return '動画を文字起こししました'

この部分を以下のように修正します。

case 'video':
  // 動画を受信した際の処理
  text = await videoToText(message.id, message.duration)
  await replyText(replyToken, text)
  return '動画を文字起こししました'

以上で動画が送信された際に、LINEサーバから動画を取得し、Cloud Speech-to-Textで文字起こしする処理が完了しました。

----- 現在の進捗 -----

 

以上で、画像、音声、動画の文字起こしの実装は終了です。

中級者向けということで細かな説明を省略したため、分からないことが多少あったと思います。

本noteで使用したコードは、各自好きに使用してください!
MITライセンスです。

まとめ

今回は、画像・音声・動画の文字起こしアプリをLINE Botで実装する方法について紹介しました。

また、最速でデバッグを行えるngrokというサービスと動画変換に使用される最も有名なライブラリ「FFmpeg」の使い方を紹介しました。

 

最後までご覧いただきありがとうございます。

本noteで皆さんの技術向上に繋がれば幸いです。

今後も、皆さんの役に立てるようなアプリを開発または教材を作成していきます!

それではまたどこかでお会いしましょう!

Twitterでフォローしよう

おすすめの記事