LINE DEVELOPER DAY 2019 が 11/20-21 で開催されました。
そこで参加者全員に LINE BRAIN OCR API の無料体験が行える体験コードが配布されました。
何か作ってみようということで、 今回画像認識を行い、文章を検出し、英語 → 日本語 / 日本語 → 英語のどちらで翻訳するかをユーザーに選ばせ、翻訳してリプライするという LINEBot を作成したいと思います。
実装について
・Elixir ・Phoenix ・Heroku ・Microsoft Azure の Translator API
LINE BRAIN OCR API について
こんな感じで認識してくれます。すげえです。(語彙力) ・文字認識 ・文字領域認識 を行うことができます。
今回はこのうち文字認識のみを使用します。 画像を送信するとレスポンスでその画像内の文字を返してくれるという流れになっています。
実装
流れとして 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/
このようなボタンをリプライします。
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 にデプロイしました。
動かしてみると。。
こういう画像はもちろんのこと ↓
かなり暗めの画像や ↓
縦書きのものにもしっかりと対応しています。↓
※ボタンの選択があるとスクショしずらかったので言語選択なし&日本語 → 英語の固定にしています。
Bot との友達登録はこの QR コードから行えます! ぜひ試してみてください!!
LINE BRAIN OCR API は LINE DEVELOPER DAY の参加者にお試し版として配布されたものなので、12/25 までしか使うことができません。なのでこの Bot もそれまでの使用ということになりますのでよろしくお願いします。
(気が向いたら別の画像認識 api を使うかなんかして期間延長&多言語対応とかしてみようかと思います。)