LINE BRAIN OCR APIを使って画像から文章翻訳を行うLINEBot作ってみた

December 04, 2019

LINE DEVELOPER DAY 2019 が 11/20-21 で開催されました。

そこで参加者全員に LINE BRAIN OCR API の無料体験が行える体験コードが配布されました。

何か作ってみようということで、 今回画像認識を行い、文章を検出し、英語 → 日本語 / 日本語 → 英語のどちらで翻訳するかをユーザーに選ばせ、翻訳してリプライするという LINEBot を作成したいと思います。

実装について

・Elixir ・Phoenix ・Heroku ・Microsoft Azure の Translator API

LINE BRAIN OCR API について

image.png image.png

こんな感じで認識してくれます。すげえです。(語彙力) ・文字認識 ・文字領域認識 を行うことができます。

今回はこのうち文字認識のみを使用します。 画像を送信するとレスポンスでその画像内の文字を返してくれるという流れになっています。

実装

流れとして 1.ユーザから画像が送られてくる 2.日本語 → 英語、英語 → 日本語をユーザーに選択させるボタンをリプライする 3.ユーザーが翻訳する言語を選択する 4.画像を取得する 5.LINE BRAIN OCR API で画像中の文章を認識する 6.Microsoft Azure の Translator API で英語に翻訳 7.元の文章と翻訳後の文章をリプライする

という感じになります。

また、今回かなり部分ごとに説明していくのでソースコードを確認しながら読んでもらったほうが理解しやすいかもしれません。Github で公開しているので確認してみてください。 https://github.com/sanposhiho/mr_honyaku

1.ユーザから画像が送られてくる

def line_callback(conn, %{"events" => events}) do
    IO.inspect events
    event_contents = List.first(events)
    message = Map.get(event_contents, "message")
    type = if is_nil(message), do: nil, else: message["type"]
    events = List.first(events)
    endpoint_uri = "https://api.line.me/v2/bot/message/reply"
    line_auth_token = Application.get_env(:mr_honyaku, :line_auth_token)

    json_data =           #以下送られてきた内容によってリプライする内容の作成
    case type do
      "image" -> #画像が送信された時の処理(後述)
      _ ->
      if Map.has_key?(event_contents, "postback") do
       #postbackだった時の処理(後述)
      else
        %{
            replyToken: events["replyToken"],
            messages: [
              %{
              type: "text",
              text: "画像を送ってね!"
              }
            ]
          } |> Poison.encode!
      end
    end

    headers = %{
      "Content-Type" => "application/json",
      "Authorization" => "Bearer ${#{line_auth_token}}"
    }

    case HTTPoison.post(endpoint_uri, json_data, headers) do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        IO.puts body
      {:ok, %HTTPoison.Response{status_code: 404}} ->
        IO.puts "Not found :("
      {:error, %HTTPoison.Error{reason: reason}} ->
        IO.inspect reason
    end

    send_resp(conn, :no_content, "")
  end

処理としては画像が送られてきていた場合はその後の処理に繋げ、画像でないメッセージが送られてきていた場合は「画像を送ってね!」というメッセージをリプライするようになっています。

2.日本語 → 英語、英語 → 日本語をユーザーに選択させるボタンをリプライする

ユーザーが画像を送ってきたとき、まずはじめにボタンで翻訳する言語を選択させます。

LINE の Flex-message を使用します。 Flex-message に関しては以下のドキュメントを参考にしてください。 https://developers.line.biz/ja/docs/messaging-api/using-flex-messages/

このようなボタンをリプライします。 image.png

body として post される json を見てみましょう

message_id = message["id"]
#以下をbodyとしてpost
        %{"replyToken" => events["replyToken"],
          "messages" =>
          [%{
            "type" => "flex",
            "altText" => "言語を選択してください。",
            "contents" => %{
            "type" => "bubble",
            "body" => %{
              "type" => "box",
              "layout" => "vertical",
              "spacing" => "md",
              "contents" => [
                %{
                  "type" => "button",
                  "style" => "secondary",
                  "action" => %{
                    "type" => "postback",
                    "label" => "日本語→英語",
                    "displayText" => "日本語を英語に翻訳",
                    "data" => "en&"<>message_id
                  }
                },
                %{
                  "type" => "button",
                  "style" => "secondary",
                  "action" => %{
                    "type" => "postback",
                    "label" => "英語→日本語",
                    "displayText" => "英語を日本語に翻訳",
                    "data" => "ja&"<>message_id
                  }
                }
              ]
            }
            }}]}|>Poison.encode!

ユーザーがボタンを選択すると、LINE のトーク上では「OO を OO に翻訳」とユーザーが送信したように表示されます。 これが上記の json の displayText の部分です。

しかし実際にサーバーに送られてくるデータは data で指定されている "翻訳語の言語&"<>message_id となります。

こういうボタンは postback という type に分類されるものです。

翻訳後の言語と一緒に messageid が送信されている理由ははじめに送られた画像を取得するのに最初の messageid が必要となるためです。

3.ユーザーが翻訳する言語を選択する

上記の postback のボタンによって送られたデータは postback という key で格納されています。 postback という key が存在するかどうかで送られてきたメッセージがボタンの選択によるものかを見分けています。

4.画像を取得する

送られてきた画像を取得します。 公式ドキュメントはこちら

if Map.has_key?(event_contents, "postback") do
        reply_contents = event_contents["postback"]["data"]
        target = String.slice(reply_contents, 0..1)
        message_id = String.slice(reply_contents, 3..-1)
        image_url = "https://api.line.me/v2/bot/message/#{message_id}/content"
        header = %{"Authorization" => "Bearer ${#{line_auth_token}}"}
        %HTTPoison.Response{body: body} = HTTPoison.get!(image_url, header)

初めの条件分岐は postback によるデータかどうかを判別しています。

5.LINE BRAIN OCR API で画像中の文章を認識する

流れとしては 1.LINE BRAIN OCR API に先ほど取得した画像を Base64 で encode して渡す

2a.ここでエラーが起こると「エラーが発生しました。。」とリプライします 2b.正常に処理できた場合、得られた文章を全て結合してから translate/3(後述)に投げています。

3ba.翻訳でエラーが起こると「エラーが発生しました。。」とリプライします 3bb.正常に翻訳できた場合、翻訳前の文章と翻訳後の文章をリプライします。

得られた文章を結合するのは LINE BRAIN OCR API によって得られる文章は改行ごとに別の文章として扱われるためです。

 %HTTPoison.Response{body: body} = HTTPoison.get!(image_url, header)
        image = body |> Base.encode64()
        brain_url =  "https://ocr-devday19.linebrain.ai/v1/recognition"
        service_id = Application.get_env(:mr_honyaku, :brain_service_id)
        data = %{
          imageContent: [image],
                 entrance: "detection",
                 scaling: false,
                 segments: false
               }
              |> Poison.encode!
        headers = %{
                    "X-ClovaOCR-Service-ID" => service_id,
                    "Content-Type" => "application/json"
                  }
        case HTTPoison.post(brain_url, data, headers) do
          {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
            IO.puts body
            image_data = Poison.decode!(body)
            words = image_data["words"]
            text = Enum.map(words, fn word -> word["text"] end)
            raw_text =
              case target do
                "en" -> Enum.join(text, "")
                "ja" -> Enum.join(text, " ")
              end
            messages =
              case translate("ja", "en", raw_text) do
                {:ok, translated} ->
                  %{
                    raw: raw_text,
                    translated: translated
                  }
                _ ->
                  %{
                    raw: "エラーが発生しました。。",
                    translated: "エラーが発生しました。。"
                  }
              end
            messages_list=
            [
              %{
                type: "text",
                text: "原文:"<>messages[:raw]
              },
              %{
                type: "text",
                text: "翻訳:"<>messages[:translated]
              }
            ]
            |> List.flatten()
            %{replyToken: events["replyToken"],
                messages: messages_list
            } |> Poison.encode!

          error ->
            IO.inspect error
            %{replyToken: events["replyToken"],
                messages: [
                  %{
                  type: "text",
                  text: "エラーが発生しました。もう一度試してください!"
                  }
                ]
            } |> Poison.encode!
        end

6.Microsoft Azure の Translator API で英語に翻訳

3 で正常に LINE BRAIN OCR API が処理してくれた場合 translate/3 関数で文章が翻訳されます。

ここでは Microsoft Azure の Translator API を使用します。

def translate(source, target, text) do
    url = "https://mr-honyaku.cognitiveservices.azure.com/sts/v1.0/issuetoken"
    translation_auth = Application.get_env(:mr_honyaku, :translation_auth)
    headers = %{"Ocp-Apim-Subscription-Key" => translation_auth }
    with %HTTPoison.Response{status_code: 200, body: body} <- HTTPoison.post!(url, [], headers) do
      IO.puts body
      token = "Bearer "<>body
      transrate_url = "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=#{target}"
      headers = %{
        "Authorization" => token,
        "Content-Type" => "application/json; charset=UTF-8",
        "Content-Length" => String.length(text)
      }
      json = [%{
        "text" => text,
      }]|>Poison.encode!
      case HTTPoison.post(transrate_url, json, headers) do
        {:ok,%HTTPoison.Response{status_code: 200, body: body}}->
          IO.inspect body
          body = body |> Poison.decode!|>Enum.at(0)
          translations = body["translations"]|>Enum.at(0)
          {:ok, translations["text"]}
        error ->
          IO.inspect error
          {:error, error}
      end

    else
      error -> IO.inspect error
        {:error, error}
    end
  end

7.元の文章と翻訳後の文章をリプライする

    headers = %{
      "Content-Type" => "application/json",
      "Authorization" => "Bearer ${#{line_auth_token}}"
    }

    case HTTPoison.post(endpoint_uri, json_data, headers) do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        IO.puts body
      {:ok, %HTTPoison.Response{status_code: 404}} ->
        IO.puts "Not found :("
      {:error, %HTTPoison.Error{reason: reason}} ->
        IO.inspect reason
    end

完成!

これで実装完了です。 これをそのまま Heroku にデプロイしました。

動かしてみると。。

こういう画像はもちろんのこと ↓ image.png

かなり暗めの画像や ↓ image.png

縦書きのものにもしっかりと対応しています。↓ image.png

※ボタンの選択があるとスクショしずらかったので言語選択なし&日本語 → 英語の固定にしています。

Bot との友達登録はこの QR コードから行えます! ぜひ試してみてください!!

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

LINE BRAIN OCR API は LINE DEVELOPER DAY の参加者にお試し版として配布されたものなので、12/25 までしか使うことができません。なのでこの Bot もそれまでの使用ということになりますのでよろしくお願いします。

(気が向いたら別の画像認識 api を使うかなんかして期間延長&多言語対応とかしてみようかと思います。)

このエントリーをはてなブックマークに追加