Microsoft Translator APIを使ってみた

# はじめに

以前投稿した 「[私的に日英翻訳APIを利用して、個人辞書を作ってみた](https://qiita.com/YasuhiroABE/items/11292381a61282563973)」 の記事では、Microsoft Translatorは検討対象としたものの実際には利用していませんでした。

AzureにIDを登録したので、Translation APIを利用してみることにしました。

DeepL API, GCTと合わせて利用するためのRubyコードを追加したので、その顛末を残しておきます。

## 方針

既にGitHubに公開しているコードにMicrosoft Translatorを利用するためのコードを追加する。

# 準備作業・環境

* [Microsoft Azure Portal](https://azure.microsoft.com/ja-jp/features/azure-portal/) (利用開始はこちらから)

## 参考資料

* [Microsoft Translator 公式トップページ](https://azure.microsoft.com/ja-jp/services/cognitive-services/translator/)
* [Microsoft Translator ドキュメント](https://docs.microsoft.com/ja-jp/azure/cognitive-services/translator/)
* http://hamasyou.com/blog/2014/02/14/microsoft-translator-api/
* [GitHub - Microsoft公式Rubyサンプル](https://github.com/MicrosoftTranslator/Text-Translation-API-V3-Ruby)
* [RubyDoc - HTTPClient](https://rubydoc.info/gems/httpclient/HTTPClient)

## 基本的なMicrosoft Translatorの使用方法

[Azure Portal](https://portal.azure.com/#home)にログインし、検索ウィンドウで、**"Translator"** を検索すると、Martplaceのカテゴリに表示されますのでクリックし、必要事項を入力し、サービスを作成します。


### サンプルコードの確認

公式マニュアルにはcurlコマンドを利用したアクセス例があるので、シェル・スクリプトで動作を確認しておきます。

```bash:test-ts.sh
#!/bin/bash

curl -X POST "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=ja" \
     -H "Ocp-Apim-Subscription-Key:${MSATS_KEY}" \
     -H "Ocp-Apim-Subscription-Region:japaneast" \
     -H "Content-Type: application/json" \
     -d "[{'Text':'Hello, what is your name?'}]" | jq .
```

こんな感じのスクリプトにしました。MSATS_KEY環境変数にAzure Portalに表示されるキー(秘密鍵)を設定して実行します。ここではリージョンに"japaneast"を設定している前提で埋め込んでいます。違うようであれば適宜変更してください。

```bash:test-ts.shの実行
$ env MSATS_KEY="your-secret-key" bash ./test-ts.sh
```

出力はjqを利用しているので、次のようになります。インストールされていないければスクリプトの後段のパイプを削除すれば1行で出力されます。

```json:スクリプトの実行結果
[
  {
    "detectedLanguage": {
      "language": "en",
      "score": 1
    },
    "translations": [
      {
        "text": "こんにちは、あなたの名前は何ですか?",
        "to": "ja"
      }
    ]
  }
]
```

## Prerequisites

* Docker Runtime (Docker Desktop for WSL2/macOS, Docker CE for Linuxなど)
* Ruby 2.7以降

## 準備作業

以下のコマンドで作成したディレクトリの中で作業を行ないます。

```bash:gitコマンドによるコードのpull
$ git clone https://github.com/YasuhiroABE/docker-example-transapi.git
$ cd docker-example-transapi/
$ git branch -b azure
$ make gen-code
$ cd code/
```

## 基本的な動作を確認するためのサンプルスクリプト

code/ディレクトリの環境で動作するか確認のためのスクリプトを作成してみます。

```ruby:test-ts.rbスクリプトファイル
#!/usr/bin/ruby

## lib/ruby/以下のライブラリを利用する
require 'bundler/setup'
Bundler.require
require 'securerandom'

ATS_URI = "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=ja"
TEXT = [ { "Text" => "I'm working at home." } ].to_json

headers = {
  'Ocp-Apim-Subscription-Key' => ENV.has_key?("MSATS_KEY") ? ENV["MSATS_KEY"] : "your-secret-key",
  'Ocp-Apim-Subscription-Region' => ENV.has_key?("MSATS_REGION") ? ENV["MSATS_REGION"] : "japaneast",
  'Content-type' => 'application/json',
  'X-ClientTraceId' => SecureRandom.uuid
}

clnt = HTTPClient.new
res = clnt.post(ATS_URI, { :body => TEXT, :header => headers } )
puts res.body
exit(0)
```

今回は既に存在しているGemfileなどを利用することにします。

```bash:
$ make bundle-install
$ env MSATS_KEY="your-secret-key" ruby ./test-ts.rb | jq .
```

出力結果は、テキストが違うだけで、ほぼbashスクリプトと同じになるはずです。

### HTTPClientの使い方

今回はPOSTメソッドを利用する点が、これまでのDeepLや、Google Cloud Translate(GCT)と異なる点です。
ほぼGCTと同じような動作ですが専用のライブラリが必要になるほどの複雑さもないのが、Microsoft Translateの特徴かなと思います。

RubyとHTTPClientを利用する場合には、postメソッドでヘッダーをどう渡すのか別りにくく、httpclient.rbを直接読むのが早かったです。

JSON形式にデコードしたリクエストを本文(Body)として、ヘッダーを適切に設定すること、postメソッドの第二引数に:bodyと:headerをキーとするハッシュを渡すことで適切に動作します。

## 変更の方針

[Microsoft TranslatorのAPIドキュメント](https://docs.microsoft.com/ja-jp/azure/cognitive-services/translator/text-translation-overview) に目を通すと、GETかPOSTかの違いはありますが、HTTPClientを使うのが良さそうなので、deepl.rbのコードを少し変更する方針で作業を行ないます。

## Microsoft Translateの追加作業

```bash:
$ cp -ip lib/deepl.rb lib/ats.rb
$ emacs lib/ats.rb
```

ここでサンプルで作成したコードを使って、変更したコードは以下のようになりました。
MSATS_KEY、MSATS_REGION環境変数を利用しています。本来はURIなどの他のパラメータも環境変数で渡せるようにするべきだと思いましたが、とりあえず省略しています。

```ruby:lib/ats.rb全体
# coding: utf-8

require 'securerandom'

class AzureTranslate < DataAPI
  def initialize
    @ats_host  = ENV.has_key?('ATS_HOST') ? ENV['ATS_HOST'] : "api.cognitive.microsofttranslator.com"
    @ats_path = ENV.has_key?('ATS_PATH') ? ENV['ATS_PATH'] : "/translate"
    @ats_query = { "api-version" => "3.0" }
    @ats_key  = ENV.has_key?('MSATS_KEY') ? ENV['MSATS_KEY'] : ""
    @ats_region  = ENV.has_key?('MSATS_REGION') ? ENV['MSATS_REGION'] : "japaneast"
  end

  ## query: :to => "ja"
  ##        :from => "en"
  ##        :text => ""
  def query_api(query = {})
    ret = {}

    ## prepare URI
    q = @ats_query
    q = q.merge({ :to => query[:to] }) if query.has_key?(:to)
    q = q.merge({ :from => query[:from] }) if query.has_key?(:from)
    query_string = URI.encode_www_form(q)
    url = URI::HTTPS.build({:host => @ats_host, :path => @ats_path,
                            :query => query_string})
    ## prepare Header
    headers = {
      'Ocp-Apim-Subscription-Key' => @ats_key,
      'Ocp-Apim-Subscription-Region' => @ats_region,
      'Content-type' => 'application/json',
      'X-ClientTraceId' => SecureRandom.uuid
    }

    ## prepare Body
    body = [ { "Text" => query.has_key?(:text) ? query[:text] : "" } ].to_json
    begin
      client = HTTPClient.new
      resp = client.post(url, { :body => body, :header => headers } )
      ret = JSON.parse(resp.body)
    rescue
      puts "[error] query_api: failed to query: #{:host},#{:path},#{:query}."
      ret = {}
    end
    return ret
  end

  def query_trans(source, to_lang, src_lang)
    q = {
      :text => source,
      :to => to_lang,
      :from => src_lang
    }
    query_api(q)
  end

  def trans(string, to_lang: AzureTranslate::EN, src_lang: AzureTranslate::JA)
    s = CGI.unescapeHTML(string)
    ret = { :original_text => s,
            :translate_text => "",
            :to_lang => to_lang }
    trans_result =  query_trans(s, to_lang, src_lang)
    ret[:translate_text] = trans_result[0]["translations"][0]["text"]
    return ret
  end
end
```

これを利用するために変更したコードは次のようになります。
default_api.rbファイルなどを変更しています。

```diff:api/default_api.rbファイルの差分
--- a/_docker/default_api.rb
+++ b/_docker/default_api.rb
@@ -1,4 +1,3 @@
-require 'json'
 
 
 MyApp.add_route('GET', '/dict', {
@@ -111,7 +110,8 @@ MyApp.add_route('GET', '/trans', {
 
   deepl = DeepLAPI.new
   gct = GoogleCloudTranslate.new
-  for result in [["deepl", deepl.trans(param[:q])], ["gct", gct.trans(param[:q])]]
+  ats = AzureTranslate.new
+  for result in [["deepl", deepl.trans(param[:q])], ["gct", gct.trans(param[:q])], ["ats", ats.trans(param[:q])]]
     MyUtil::add_data(result[0], ret[:results], result[1])
   end
   ret.to_json
```

その他にも、DeepLやGCTにアクセスする方法なども見直して、最終的に実行結果は次のようになりました。



```json:出力結果
{
  "version": "1.0",
  "results": [
    {
      "engine": "deepl",
      "original_text": "\"はじめまして\"",
      "translate_text": "\"Nice to meet you.\"",
      "to_lang": "en"
    },
    {
      "engine": "gct",
      "original_text": "\"はじめまして\"",
      "translate_text": "\"&quot;nice to meet you&quot;\"",
      "to_lang": "en"
    },
    {
      "engine": "ats",
      "original_text": "\"はじめまして\"",
      "translate_text": "\"Nice to meet you\"",
      "to_lang": "en"
    }
  ]
}
```

これらの一連の変更は、最新版のGitHubのmainブランチに反映されています。