【Unity】DeepLで文書翻訳してみた

#1. はじめに

ノベルワークスR&Dチームのユウガです。現在は, バーチャルオフィスの開発を行っています。
バーチャルオフィスの機能の1つとして, DeepL文書翻訳をやってみたので紹介します。

主に新しい技術のリサーチをしているため, 最新のドキュメントはだいたい英語だったりします。「このドキュメント日本語にして読みたいなぁ」と思った時に, すぐに翻訳できると便利です。また, 外国人とチャットでやりとりしていて, ファイルを共有された時に, すぐに母国語に翻訳できると便利ですよね。
DeepLを使えば日本語で作った製品案内とかを瞬時に翻訳できるので, 海外への営業・販売など比較的容易に展開できるようになるかもしれませんね。

所見としては, 文書翻訳はベータ版ではありますが, かなり精度も良くて, 全然使えると思いました。また, 画像が添付されてる場合も, レイアウトが崩れる事なく, 翻訳できた印象です。

**環境**
・windows10
・Unity2020.3.2f.1

#2. DeepL文書翻訳

翻訳できるファイル形式は, `Word(.docx)`, `PowerPoint(.pptx)`, `Text(.txt)`, `HTML(.html)` です。

ファイルサイズ&文字数の制限は, `Word(.docx)`, `PowerPoint(.pptx)`が, 10MBまたは100万文字。`Text(.txt)`が, 1MBまたは100万文字。 `HTML(.html)`が, 5MBまたは100万文字です。

DeepL API プランの場合は, 好きなだけ文書翻訳できるそうです。
このプランは従量課金制で, 1か月の基本料金630円に加え, 1文字あたり0.0025円として料金が請求されます。 

**注意点**としては, 文字数が少ない場合も, 1ファイルにつき最低5万文字分が請求されます。5万文字以上の場合は, 文字数分の料金が請求されます。

なので, 1ファイルにつき最低でも125円かかります.
テストしてる時, これに気が付かなくて焦りました(-_-;)

#3. 実装

```DeeplFileTranslator.cs
using UnityEngine;
using UnityEngine.Networking;
using System;
using System.IO;
using System.Collections.Generic;
using System.Threading.Tasks;

public static class DeeplFileTranslator
{
    [Serializable]
    public class ResponseForUpload
    {
        public string document_id;
        public string document_key;
    }

    [Serializable]
    public class ResponseForCheck
    {
        public string document_id;
        public string status;
        public string seconds_remaining;
        public string billed_characters;
    }

    private static readonly string auth_key = "XXX";

    public static async Task<string> Upload(string path, string filename, string target_lang, string content_type)
    {
        // pathにあるファイルを読み込む
        byte[] rawdata = File.ReadAllBytes(path);

        List<IMultipartFormSection> requestData = new List<IMultipartFormSection>();
        requestData.Add(new MultipartFormDataSection("auth_key",auth_key));
        requestData.Add(new MultipartFormDataSection("target_lang",target_lang));
        requestData.Add(new MultipartFormFileSection("file", rawdata, filename, content_type));

        UnityWebRequest request = UnityWebRequest.Post("https://api.deepl.com/v2/document", requestData);
        await request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.ProtocolError || request.result == UnityWebRequest.Result.ConnectionError)
        {
            Debug.LogError(request.error);
            return "error";
        }
        
        return request.downloadHandler.text;
    }

    public static async Task<string> CheckStatus(string document_id, string document_key)
    {
        List<IMultipartFormSection> requestData = new List<IMultipartFormSection>();
        requestData.Add(new MultipartFormDataSection("auth_key",auth_key));
        requestData.Add(new MultipartFormDataSection("document_key",document_key));

        UnityWebRequest request = UnityWebRequest.Post("https://api.deepl.com/v2/document/"+document_id, requestData);
        await request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.ProtocolError || request.result == UnityWebRequest.Result.ConnectionError)
        {
            Debug.LogError(request.error);
            return "error";
        }
        
        return request.downloadHandler.text;
    }

    public static async Task<string> Download(string document_id, string document_key, string output_path)
    {
        List<IMultipartFormSection> requestData = new List<IMultipartFormSection>();
        requestData.Add(new MultipartFormDataSection("auth_key",auth_key));
        requestData.Add(new MultipartFormDataSection("document_key",document_key));

        UnityWebRequest request = UnityWebRequest.Post("https://api.deepl.com/v2/document/"+document_id+"/result", requestData);
        await request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.ProtocolError || request.result == UnityWebRequest.Result.ConnectionError)
        {
            Debug.LogError(request.error);
            return "error";
        }

        // output_pathにデータを書き込む
        byte[] rawdata = request.downloadHandler.data;
        File.WriteAllBytes(output_path, rawdata);
        return "success";
    }
}
```

このコードは見てもらえば分かると思いますが, UnityWebRequestを使って, DeepLとやりとりしてます。
UnityWebRequestでasync/awaitを使いたかったので, こちらの[コード](https://gist.github.com/krzys-h/9062552e33dd7bd7fe4a6c12db109a1a)も使いました。
auth_keyの所は, ダッシュボードにある `Authentication Key` を入力してください。
<br><br>

```FileTranlateManager.cs
using UnityEngine;
using UnityEngine.UI;
using System;
using System.IO;
using System.Threading.Tasks;

public class FileTranslateManager : MonoBehaviour
{
    [SerializeField] Button fileTranslateButton;
    private string document_id;
    private string document_key;

    void Start()
    {
        fileTranslateButton.onClick.AddListener(async () =>
        {
            string path = @"C:\Users\username\Documetns\test.docx";
            string target_lang = "EN-US";
            await DeeplFileTranslate(path, target_lang);
        });
    }

    private async Task DeeplFileTranslate(string path, string target_lang)
    {
        string ext = Path.GetExtension(path);
        string filenameWithoutExt = Path.GetFileNameWithoutExtension(path);
        string output_path = "C:\\Users\\" + Environment.UserName + "\\Downloads\\" + filenameWithoutExt + "_" + target_lang + ext;

        string jsonString = null;
        int millisecond = null;

        // Deeplにアップロード
        jsonString = await DeeplFileTranslator.Upload(path, filenameWithoutExt+ext, target_lang, GetContentType(ext));
        if (jsonString == "error") return;
        var responsForUpload = JsonUtility.FromJson<DeeplFileTranslator.ResponseForUpload>(jsonString);
        document_id = responsForUpload.document_id;
        document_key = responsForUpload.document_key;

        // 翻訳状況をチェック
        while (true)
        {
            jsonString = await DeeplFileTranslator.CheckStatus(document_id, document_key);
            if (jsonString == "error") return;

            // 翻訳完了なら, ループを抜ける
            var responseForCheck = JsonUtility.FromJson<DeeplFileTranslator.ResponseForCheck>(jsonString);
            if (responseForCheck.status == "done") break;

            if (!String.IsNullOrEmpty(responseForCheck.seconds_remaining))
            {
                millisecond = int.Parse(responseForCheck.seconds_remaining) * 1000;
                Debug.Log($"{millisecond / 1000}s remaining.");
            }
            else
            {
                millisecond = 1000;
            }

            await Task.Delay(millisecond); // ミリ秒待つ
        }

        // Deeplからダウンロード
        jsonString = await DeeplFileTranslator.Download(document_id, document_key, output_path);
        if (jsonString == "error") return;

        // 翻訳完了を知らせるために, ダウンロードフォルダを開く
        Application.OpenURL("file:C:\\Users\\" + Environment.UserName + "\\Downloads");

        document_id = null;
        document_key = null;
        return;
    }

    private static string GetContentType(string ext)
    {
        switch(ext)
        {
            case ".docx":
                return "application/vnd.openxmlformats-officedocument.wordprocessingm";
            case ".pptx":
                return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
            case ".txt":
                return "text/plain";
            case ".html":
                return "text/html";
            default:
                return null;
        }
    }

}
```

このコードは, DeeplFileTranslatorのメソッドを呼び出して, 処理を実行してくれます。
翻訳状況をチェックの所で,　翻訳完了したらループを抜けるようにしてますが, あんまり良くないなぁと思ってるので, こう書いたら良いよって方がいたら教えてください！！

#4. おわりに

個人的には, PDFも翻訳してくれたらありがたいのですが, DeeplAPIでは無理です。
なので, PDFを翻訳したければ, CloudConvertAPIを使って, word(.docx)に変換してから, DeepL翻訳すると良いかもしれません。