2023.01.03

200の言語を翻訳できるNLLB-200モデルを用いて自動翻訳Slackボットを作ってみた

はじめに

こんにちは。グループ研究開発本部 次世代システム研究室のT.D.Qです。
Metaは米国時間7月6日、200種類もの言語翻訳が可能な人工知能(AI)モデル「NLLB-200」をオープンソース化したと発表した。NLLB-200は、高い翻訳精度で多くの言語サポートが特徴です。自分は毎日仕事でSlackを使ってベトナムの開発チームと頻繁にコミュニケーションを取りながらプロジェクトを進めていきます。開発チームの中に技術力が高いだが日本語できないメンバーに対して言語の壁を超えて作業効率を向上する課題があります。解決方法はいろいろあると思いますが、今回はNLLB-200を使ってSlackbotで簡単あアクションで日本語・ベトナム語翻訳する方法を紹介したいと思います。

1.やりたいこと

  1. Docker Composeで翻訳専用のAPIサーバーを構築したい
  2. SlackbotのEvent Subscription仕組みで翻訳専用APIを実行できるようにしたい

2.環境構築

2-1.NLLB-200翻訳専用のAPIサーバー構築

NLLB-200は200以上の言語の任意の翻訳ができるMetaのNo Language Left Behindモデルです。HuggingFaceのtransformersを使って翻訳APIサーバーを構築します。Githubにすでにこのサーバーのリポジトリを見つけましたのでそのまま使います。
今回はDocker環境を使うので以下のDockerfileを実装しました。
FROM ubuntu:20.04

RUN apt-get update && apt-get install -y software-properties-common \
&& apt-get install -y python3.9 && apt-get install -y python3-pip \
&& pip install --upgrade pip \
&& pip install --upgrade setuptools \
&& apt-get install -y git

RUN apt-get -y install locales && \
    localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
ENV TZ JST-9
ENV TERM xterm

WORKDIR /

EXPOSE 6060

RUN git clone https://github.com/thammegowda/nllb-serve && cd /nllb-serve && pip3 install -e .

ENV PATH "$PATH:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"

CMD ["/bin/bash","-c","cd /nllb-serve && python3 -m nllb_serve"]
翻訳専用APIサーバーは、Python 3.9が実行環境とし、FlaskフレームワークでAPIが実装されます。requirements.txtに以下の内容で準備します。
transformers==4.21
Flask==2.1
torch==1.12

2-2.Slackbot向けのAPPサーバー構築

Slackで翻訳したいメッセージに国旗の絵文字(例:🇯🇵)でリアクションを送ると、その言語に翻訳してスレッドに返信してくれるAPIを開発します。
このAPIはSlackにて発火したイベントで実行されると以下のことを実施する
①APIリクエストのpayloadからチャネル・スレッド・翻訳する必要な言語を取得
②①の情報を基づいてSlack APIで翻訳文章を取得
③翻訳API(NLLB-200)に必要な情報を渡して翻訳結果を取得
④③の結果をSlackの該当スレッドに投稿

ということで、まず以下のDockerfileでAPPサーバーの実行環境を準備します。
FROM ubuntu:20.04

RUN apt-get update && apt-get install -y software-properties-common \
&& apt-get install -y python3.9 && apt-get install -y python3-pip \
&& pip install --upgrade pip \
&& pip install --upgrade setuptools \
&& apt-get install -y git

RUN apt-get -y install locales && \
    localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
ENV TZ JST-9
ENV TERM xterm
ENV SLACK_API_TOKEN xoxb-2523231391122-4568907382871-9is0JMh7wbI8soY9ZRCebJhc

WORKDIR /app
COPY ./translation_app/ /app
RUN pip --no-cache-dir install -r requirements.txt

EXPOSE 5000

CMD ["/bin/bash","-c","cd /app && python3 /app/app.py"]
APIサーバーと同様、Flask及びSlackクライアントでAPIを開発しますので、requirements.txtに以下の内容で準備します。
Flask==2.1
slackclient==2.9.4

2-3.Slack連携の実装

APPサーバーのエントリースクリプト(app.py)を実装します。中身は以下の通りです。
import flask
from flask import request, jsonify, Blueprint
import logging
from slack_client import SlackClient

app = flask.Flask(__name__)
logging.basicConfig(filename='app.log', level=logging.DEBUG)
app.config["DEBUG"] = True

@app.route("/api/v1/translate",  methods = ['POST'])
def api_translate():
    try:
        req = request.get_json()
        app.logger.info(req)
        if (req.get("challenge") is not None) and (len(req.get("challenge")) > 0):
            return jsonify(req.get("challenge"))
        client = SlackClient(logger=app.logger)
        translated=client.onReactionAdded(req)
        return jsonify(translated)
    except Exception as e:
        app.logger.error(e)
    return ""

if __name__ == '__main__':
    app.run(host="0.0.0.0", port=5000)
次は、Slackクライアント(slack_client.py)の実装になります。内容は以下の感じです。
import os
import requests
from slack import WebClient
from slack.errors import SlackApiError

flags_map = {
  "flag-vn": { "translateTo": "vie_Latn", "languageIcon": ":flag-vn:" },
  "flag-jp": { "translateTo": "jpn_Jpan", "languageIcon": ":jp:" },
  "jp": { "translateTo": "jpn_Jpan", "languageIcon": ":jp:" }
}

DEF_SRC_LNG = 'jpn_Jpan'
DEF_TGT_LNG = 'vie_Latn'

class SlackClient:

    def __init__(self, logger):
        slack_token = os.environ["SLACK_API_TOKEN"]
        self.client = WebClient(token=slack_token)
        self.logger = logger

    def postThreadMessage(self, channel, ts, text):
        try:
            response = self.client.chat_postMessage(
                channel=channel,
                thread_ts=ts,
                text=text
            )
        except SlackApiError as e:
            assert e.response["error"]

    def onReactionAdded(self, json):

        event_type = json['event']['type']
        self.logger.info("Start!!" + event_type)
        if (event_type != "reaction_added"):
            self.logger.info("Event type is not reaction_added. Ignored.")
            return False

        ts = json['event']['item']['ts']
        if (len(ts)==0) :
            self.logger.info("thread not found!")
            return False

        reaction = json['event']['reaction']
        if (len(reaction)==0):
            self.logger.info("reaction not found!")
            return False

        if (reaction not in flags_map):
            self.logger.info("Not supported language!")
            return False

        target_lang = flags_map[reaction]['translateTo']
        if (target_lang is None or len(target_lang)==0):
            self.logger.info("target_lang not found! Skip translate")
            return False

        channel = json['event']['item']['channel']
        self.logger.info("channel:" + channel + ", ts:" + ts + ", reaction:" + reaction)
        message = self.getMessages(channel, ts)
        src_lang = DEF_SRC_LNG
        if (src_lang == target_lang):
            src_lang = DEF_TGT_LNG
        translated_message = self.call_translation_api(source_msg=message, src_lang=src_lang, tgt_lang=target_lang)
        self.logger.info("translated Message:")
        self.logger.info(translated_message)
        if (len(translated_message) > 0):
            self.postThreadMessage(channel, ts, flags_map[reaction]['languageIcon'] + " " + translated_message[0]);
        return True

    def getMessages(self, channel, ts):
        message_history = self.client.api_call(api_method='conversations.replies', data={'channel':channel, 'ts': ts})
        self.logger.info(message_history['messages'][0]['text'])
        return message_history['messages'][0]['text']

    def call_translation_api(self, source_msg, src_lang, tgt_lang):
        if isinstance(source_msg, str):
            source_msg = [source_msg]
        r = requests.post('http://nllb200-api:6060/translate', json={"source": source_msg, "src_lang": src_lang, "tgt_lang": tgt_lang})
        return r.json()['translation']

2-4.翻訳APIサーバーとSlack Appサーバの組み合わせ

以下の内容でdocker-compose.ymlファイルを作成し、翻訳専用APIサーバーとSlack Appサーバーを組み合わせます。
version: '3'
services:
  nllb200_app:
    restart: always
    build:
      context: .
      dockerfile: ./nllb_api/Dockerfile
    ports:
      - "6060:6060"
    container_name: 'nllb200-api'
    tty: true
    networks:
      - translator-network
  translation_app:
    restart: always
    build:
      context: .
      dockerfile: ./translation_app/Dockerfile
    ports:
      - "5000:5000"
    container_name: 'translation_app'
    tty: true
    volumes:
      - ./translation_app:/app
    depends_on:
      - nllb200_app
    networks:
      - translator-network
networks:
  translator-network:
    external: true

早速、docker-composeコマンドで各サーバーを起動しましょう。起動時にNLLB-200モデルをダウンロードしますが、このファイルサイズが大きいので数分待つ必要です。
 docker-compose up -d --build

ここまでは翻訳APIサーバー及びSlack連携のAppサーバーの準備ができました。

2-5.インターネットからアクセスための対応

今回の記事は翻訳APIサーバーおよびAPPサーバはLocal PCで構築しました。Slackと連携するため、インタネットからLocal PC(Macbook Pro)のサーバーにアクセスできるようにする必要があるので、ngrokをMacに入れてローカル開発環境を外からアクセスできるようにします。
MacbookにNgrokの設定手順はネットで良い記事が多いのでこの記事は割愛します。設定後に以下のコマンドを実行することで外部からアクセスできるURLが生成されます。このURLを使ってSlackbotのEvent Subscriptionに設定します。
ngrok http 5000

最終的にdocker-compose.ymlファイルの内容はいかになりますね。

3.Slack Appの作成

Slack Appを見ることができるページ(https://api.slack.com/apps)にアクセスし、Event SubscriptionsのSlack botを作成します。Event Subscriptionsは、Slack内で起きたイベントをトリガーに、発生した内容をApp指定のURLに送る仕組みです。インターネット経由でコールバックを受ける仕組みです。Slackが送信元になり、HTTPSで待ち受ける形式です。

3-1.受け取るイベントを設定

左側のメニューからEvent SubscriptionsをクリックしEnable EventsをOnにRequest URLにNgrokで作成したapiのURLを入力します。

次は、Subscribe to bot eventsのAdd User Bot EventにてEvent name: reaction_addedを追加することでメンバーによって絵文字リアクションが追加された時に上記に設定したAPIが実行されます。

3-2.Botに必要な権限設定

作成したSlack Appの「OAuth & Permissions」というページで「User Token Scopes」にて権限設定可能です。
「Add an OAuth Scope」をクリックし、必要な権限のScopeを追加します。以下のようにユーザーのリアクション読み込みやユーザーのスレッドに書き込み権限を設定しましょう。


次は、Slackbotの権限設定になります。左側のメニューからOAuth & Permissionsをクリックし、Bot Token ScopesのAdd an OAuth Scopeをクリックし、必要な権限のScopeを追加します。自分が追加したscopeは以下のものになります。

最後に、作成したSlackbotをSlackのWorkspaceにインストールしましょう。
左メニューのBasic Information > Install your app > Install to Workspaceをクリックし、連携するchannelを選択しAllowを押し連携完了です。また、Slack側からもSlack画面のチャンネル名でチャンネル詳細を表示し、Integrations > Add appsで作成したSlack appをChannelに追加しましょう。

4.動作確認

動作確認するため、検証ようのチャンネルに作成したSlackbotを追加し、翻訳したい文章を記入しましょう。

日本語からベトナム語に翻訳してみましょう


翻訳したベトナム語文章の意味がほぼ理解できるし、日付フォーマットも日本フォーマットからベトナムフォーマットに変換してもらったので、違和感なく分かりやすいですね。

ベトナム語から日本語に翻訳してみましょう



想定の通り、日本語・ベトナム語の翻訳機能が動きました。翻訳の精度はまだ改善する余地があると思いますが、日本語できないベトナム人メンバーに対してNLLB-200の翻訳した文章の意味も理解できるレベルではないでしょうか。

まとめ

以上、SlackでNLLB-200を用いて日本語・ベトナム語を翻訳してくれるBotを作る方法でした。今回の記事は一番パラメーターの少ない600Mモデルを使いましたが、翻訳した内容は悪くないと思いました。NLLB-200は複数のモデルが公開されているので、モデルを切り替えて精度を確認できると思います。

宣伝

次世代システム研究室では、最新のテクノロジーを調査・検証しながらインターネットのいろんなアプリケーションの開発を行うアーキテクトを募集しています。募集職種一覧 からご応募をお待ちしています。

  • Twitter
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事