Azure Translator ドキュメント翻訳APIとLogicAppsで社内向けサービスを作った話

Azure Translatorの機能である[ドキュメント翻訳API](https://docs.microsoft.com/ja-jp/azure/cognitive-services/translator/document-translation/overview)がGAされたので早速使ってみました（[公式ブログ](https://techcommunity.microsoft.com/t5/azure-ai/document-translation-is-generally-available-now/ba-p/2382171)）
今回はLogicAppsと組み合わせて、ローコードかつサーバレスでさくっと社内向けサービスとして公開しました。

## ざっくり構成と処理の流れ
![image-20210629151638954.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206219/6e33c4a1-b82c-8034-10b7-98ec36d841f3.png)

1. SharePoint上に翻訳インプット用のフォルダを作成し、ユーザーは翻訳したいファイルをアップロードします
1. フォルダにファイルがアップロードされたことをトリガーにLogicAppsが起動し、対象ファイルを取得
1. ファイルをBlobストレージに配置
1. ドキュメント翻訳APIにリクエストを送信
1. 翻訳されたファイルがBlobストレージにできるので取得
1. 取得したファイルをSharePoint上にアップロード
1. Blob上のファイルを削除して終了

## 処理の詳細とポイント
### 前提

- [公式ドキュメント](https://docs.microsoft.com/ja-jp/azure/cognitive-services/translator/document-translation/get-started-with-document-translation?tabs=csharp#create-your-azure-blob-storage-containers)をもとにTranslatorサービスのリソースとBlob Containerを作成しておく
- SharePointの任意の場所にインプットとアウトプットフォルダを作成しておく
    - 今回はMicrosoftサービスで統一していますが、LogicAppsにはBoxやDropBoxのコネクタも用意されています
   
```
├─ドキュメント翻訳
│  ├─アウトプット
│  └─インプット
│      ├─日本語→英語
│      └─英語→日本語
```

### ①トリガーの設定
SharePointコネクタの「ファイルが作成されたとき」トリガーを設定します
トリガーが発火すると作成されたファイルの識別IDが取得できます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206219/8a3bb789-9f7e-6ddd-0285-838e60ccdc29.png)

### ②翻訳ターゲットの言語を指定するために変数を確保しておく
ファイルがアップロードされたフォルダによって翻訳ターゲットの言語を振り分けるため変数を2つ確保しておきます

1つはfolderとし、初期値にファイルが置かれたフォルダ名が入るように式を書きます
式は`last(take(split(triggerBody()?['{Path}'],'/'),4))`とします
※①で取得したファイルへのパスを `/` でスプリットし、配列の最後の値を取得しています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206219/9211ca5b-9ab3-7702-89fd-80b3ced9703f.png)

もう1つはlangとします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206219/223c9b85-2fa1-a1e7-1237-c154c836e1e7.png)

### ③翻訳ターゲットの言語を設定する
Switchアクションを使います
スイッチの比較対象には②で初期化したfolder変数を、各ケースにはフォルダ名（日本語→英語など）を設定します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206219/4a021d42-7d94-38d9-89c6-590aa4058eba.png)
ケースごとのアクションではlang変数に `en`や `ja`などの言語コードを指定します
※Azure Translatorが対応している言語コード一覧は[こちら](https://docs.microsoft.com/ja-jp/azure/cognitive-services/translator/language-support)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206219/33334d6b-aa4c-5af6-c9cf-ff17775da034.png)

### ④ファイルコンテンツの取得
①で取得した識別IDを使って実際のファイルコンテンツを取得します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206219/74b3cce6-6d87-d65e-f82f-0aea47172b14.png)

### ⑤Blobストレージにファイルをアップロード
ドキュメント翻訳APIを実行するには一度コンテンツをBlobに置き、そのパスを指定する必要があります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206219/570e9f27-33c0-dc95-ea9e-a1504a265660.png)

### ⑥SAS(Shared Access Signature)を取得
SASについては[公式ドキュメント](https://docs.microsoft.com/ja-jp/azure/cognitive-services/translator/document-translation/get-started-with-document-translation?tabs=csharp#create-sas-access-tokens-for-document-translation)を参照ください
まずは翻訳前（ソース）ドキュメントのblobからSASを取得
有効期限は式 `addMinutes(utcNow(),10,'yyyy-MM-ddTHH:mm:ss')`を使って10分後を指定
アクセス許可はReadとList
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206219/f7d39c59-aa44-5f60-bac3-832c32d0b5d3.png)

次に翻訳後（ターゲット）ドキュメントのSASを取得
ターゲットにはドキュメントを指定するのではなくBlobコンテナーを指定
有効期限は同様に10分後
アクセス許可はWriteとList
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206219/9e6b6f4d-ece2-2af6-8897-9ce6269d2b2e.png)

### ⑦ドキュメント翻訳APIへのリクエスト
ヘッダーのOcp-Apim-Subscription-KeyにはTranslatorサービスのサブスクリプションキーを指定
URIは`https://<NAME-OF-YOUR-RESOURCE>.cognitiveservices.azure.com/translator/text/batch/v1.0-preview.1`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206219/8714dc73-4822-7a42-f95a-4058015e181e.png)

本文（Body）

- sourceUrl: 翻訳前ファイルのSASを指定
- language: ③で指定した言語コード
- targetUrl：翻訳後のファイルとわかるようにファイル名の先頭にtranslated-を付与し、後半に元のファイル名+ ターゲットSASの？より後ろを指定
    - ```?@{last(split(body('パスを使用して_SAS_URI_を作成する_2')?['WebUrl'],'?'))```
    - SASの?以降にトークン情報が含まれているため


```json
{
  "inputs": [
    {
      "source": {
        "sourceUrl": "@{body('パスを使用して_SAS_URI_を作成する')?['WebUrl']}"
      },
      "storageType": "File",
      "targets": [
        {
          "language": "@{variables('lang')}",
          "targetUrl": "https://<NAME-OF-YOUR-RESOURCE>.blob.core.windows.net/target/translated-@{triggerBody()?['{FilenameWithExtension}']}?@{last(split(body('パスを使用して_SAS_URI_を作成する_2')?['WebUrl'],'?'))}"
        }
      ]
    }
  ]
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206219/47d32615-dad5-7f0f-7cd4-bafeb1eb3675.png)

### ⑧ターゲットのBlobパスで変数を初期化
ドキュメント翻訳APIはバッチで処理されるため成功してもレスポンスにはターゲットBlobのパスは含まれない
そのため文字列と元のファイル名を連結してblob_pathを宣言しておく
```concat('/target/translated-',triggerBody()?['{FilenameWithExtension}'])```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206219/13bb33ae-da17-3a2b-ff4f-e2f2b66acdb4.png)

補足：Blobが作成されるまである程度ラグがあるため1分程度スリープを入れる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206219/9ac8607d-4e34-dc7a-5c6b-29bde78c0371.png)


### ⑨ターゲットBlobのコンテンツを取得
⑧で宣言したblob_pathを指定して、翻訳後のコンテンツを取得します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206219/6d452b22-fa37-ce81-8243-dcbd05c2ef86.png)

### ⑩SharePointに翻訳されたコンテンツを作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206219/d570a666-bdc3-a5ce-b47b-b6106f54ca75.png)

### ⑪blobを削除
Azure上にファイルを残さないようにソース、ターゲット共に削除
※当社のセキュリティ要件
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206219/2ce00a1f-e9ba-2b68-ea78-f58f70b23465.png)

### 完成！
翻訳前（日本語）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206219/bf6648b2-2557-7046-62db-d46057deeb4d.png)
↓
翻訳後（英語）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206219/14f3e5bc-1a20-4459-109d-49997b85b4ad.png)

## まとめ
無事ドキュメントが丸ごと翻訳できました:tada:

ローコードと言いつつも結構LogicAppsの式を多用する結果になってしまいましたが、豊富な関数で柔軟に対応できるのがLogicAppsの強みだと思います。
Microsoft系のサービス（もちろんそれ以外も）とLogicAppsで連携することによって利用者にAPIを意識させることなく、ちょっとしたサービスを作ることができます。

それでは良いローコードライフを！