🤖

Azure AI Translatorを使用してSlack自動翻訳ボットを作ってみた

に公開

はじめに

最近フリーランスになり、副業も始めたしゃまです。
今年の目標として技術ブログを書くというのがあり初めての試みになります。
記事を書くことを前提としていませんでしたので、実際のエラーや不具合の写真が少ないです!
しかし、もう一度実施する際にマストなノウハウとして、自分用にでも記録しておこうと思い筆を走らせています。

背景

副業の案件先に海外の方が数名いてコミュニケーションツールとしてSlackを使っています。
責任者の方からKiraraのような発言に対してそのまま翻訳してくれるSlackBOTを作ってほしいといった依頼を受けたのが事の発端です。↓完成物のイメージ↓
https://www.youtube.com/watch?list=TLGG4l4GUe2ek3sxOTA0MjAyNQ&time_continue=12&v=i5kBVPzeeqo&embeds_referring_euri=https%3A%2F%2Fwww.getkiara.com%2F&embeds_referring_origin=https%3A%2F%2Fwww.getkiara.com&source_ve_path=Mjg2NjY&themeRefresh=1
しかし、Kiraraは価格が高いのでもっとリーズナブルに実装できないかという要望もありました。
そこでAzureが好きな私は、Azure AI TranslatorのAPIを使用して関数アプリを実装すればいいのでは?と思い実装を試みました。

リソースの選定

Azureにはもう一つAzure OpenAI があり、そのAPIでも実装が可能ということで、
まずは特徴を比較するところから実施しました。

Azure AI Translator Azure OpenAI
特徴 簡易的な翻訳に向いている 文脈に基づいた高度な翻訳が可能
価格 月200万文字まで無料の枠有り 無料枠無し。トークンベースの従量課金制

文脈理解が重要な場合はAzure OpenAI、コスト効率を重視する場合はAzure AI Translatorが適しているなと思いました。今回はコスト重視だったのでAzure AI Translatorを選びました。

やりたいこと

ゴールとしてはSlackのチャネルに送信された言語を日本語であれば英語に、英語であれば日本語に翻訳してBotが送信するということです。

今回は必要最低限の設定のみ行います。

事前準備

以下のリソースが準備してある前提で進めていきます。

  • Slackのアカウント
  • Azureのアカウント
  • Visual Studio Code(Azure ToolsやPythonの拡張機能)
  • Copilot(任意)

実装手順

1.Azure リソースの払い出し

AI Transrator

Azureポータルにサインインして検索バーに「翻訳」と入力します。
そのまま「翻訳」をクリックしてリソース作成します。
Azure AI services | 翻訳
リージョンは無料枠を超えない前提で、速度を意識してJapan Eastを選択しました。

その他の設定は触らずに作成しました。
作成出来たら、のちに環境変数として使用するので[キーとエンドポイント]に遷移して、「キーの値」と「場所/地域」とテキスト翻訳の「エンドポイント」を控えておきます。

Azure Functions

Azureポータルにサインインして検索バーに「関数」と入力します。
そのまま「関数アプリ」をクリックしてリソース作成します。
トリガーされた際にのみ動けばいいので今回は「従量課金」を選択します。
ランタイムスタックはPythonを選択し、バージョンは何でもいいです。(今回は3.11)

その他の設定は触らずに作成しました。

2.Slack Botアプリ作成

Slack APIのアプリの管理画面に遷移します。
「Create New App」をクリックし、「From scratch」をクリックします。
作成するアプリの名前を入力し追加したいワークスペースを選択して作成を選択します。
以下の画像に遷移したら完了です。

各必要な設定はAzure Functionにコードをデプロイした後に行います。

3.Visual Studio Codeや必要な開発環境のセットアップ

今回はGETやPOSTなどでリクエストがあった際に起動するHTTPTriggerを作成します。
以下の公式ドキュメントに従って、HTTP によってトリガーされる関数のテンプレートで関数が作成されるところまで実施します。

Azure Functions プロジェクトを作成する

以下のようなファイルが自動で作成されます。

.venvも含まれているのでそのままターミナルを開き、仮想環境を有効化します。(bash)

source .venv/Scripts/activate

必要なライブラリを仮想環境にインストールし、その後requirements.txtに保存します。

pip install slack-sdk requests python-dotenv azure-functions
pip freeze > requirements.txt

4.コード作成

それでは「function_app.py」を編集していきます。
開発エンジニアではないので、copilot先生にリードしてもらいながら記述しました。

メインの処理を「translator_slackbot」関数に記述し、そこで使用する言語を判別する処理や、実際に翻訳する処理、Slackに送り返す処理を下部に記述しています。
後述しますが、Bot自身の送信に対してもトリガーされてしまいSlackにおびただしい量の文章が連投されますので重複防止処理を組み込んでいます。(というかFunctionsを停止しないと止まらない)

参考までに、私が作成したコードを貼付しておきます。

function_app.py
import logging
import requests
import os
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
import azure.functions as func

logging.basicConfig(level=logging.DEBUG)
processed_events = set()

app = func.FunctionApp()

@app.route(route="translator_slackbot", auth_level=func.AuthLevel.FUNCTION)
def translator_slackbot(req: func.HttpRequest) -> func.HttpResponse:
    global processed_events
    try:
        req_body = req.get_json()

        # SlackのURL検証
        if req_body.get("type") == "url_verification":
            return func.HttpResponse(req_body.get("challenge"), status_code=200)

        # イベントデータの取得と必須フィールドチェック
        event = req_body.get("event", {})
        text = event.get("text")
        channel = event.get("channel")
        user_id = event.get("user")
        event_ts = event.get("ts")
        thread_ts = event.get("thread_ts")
        bot_id = event.get("bot_id")

        if not text or not channel or not event_ts:
            return func.HttpResponse("Invalid payload", status_code=400)

        # BOT自身のメッセージまたはイベント重複防止
        bot_user_id = os.getenv("SLACK_BOT_USER_ID", "")
        if user_id == bot_user_id or bot_id or event_ts in processed_events:
            return func.HttpResponse("Message from bot ignored or duplicate event", status_code=200)

        processed_events.add(event_ts)

        # 言語検出と翻訳処理
        detected_lang = detect_language(text)
        if detected_lang == "unknown":
            return func.HttpResponse("Language detection failed", status_code=500)

        translated_text = translate_text(text, detected_lang)
        send_to_slack(translated_text, channel, thread_ts or event_ts)

        return func.HttpResponse("Event processed successfully", status_code=200)
    except Exception as e:
        logging.error(f"Function error: {e}")
        return func.HttpResponse("Internal Server Error", status_code=500)

def detect_language(text: str) -> str:
    """Azure Translator APIで言語検出"""
    endpoint = os.getenv("TRANSLATOR_ENDPOINT", "").rstrip('/')
    headers = {
        "Ocp-Apim-Subscription-Key": os.getenv("TRANSLATOR_KEY", ""),
        "Ocp-Apim-Subscription-Region": os.getenv("TRANSLATOR_REGION", ""),
        "Content-Type": "application/json",
    }
    body = [{"text": text}]
    response = requests.post(f"{endpoint}/detect?api-version=3.0", headers=headers, json=body)
    response.raise_for_status()
    return response.json()[0]["language"]

def translate_text(text: str, detected_lang: str) -> str:
    """Azure Translator APIで翻訳"""
    endpoint = os.getenv("TRANSLATOR_ENDPOINT", "").rstrip('/')
    headers = {
        "Ocp-Apim-Subscription-Key": os.getenv("TRANSLATOR_KEY", ""),
        "Ocp-Apim-Subscription-Region": os.getenv("TRANSLATOR_REGION", ""),
        "Content-Type": "application/json",
    }
    target_lang = "ja" if detected_lang == "en" else "en"
    body = [{"text": text}]
    response = requests.post(f"{endpoint}/translate?api-version=3.0&to={target_lang}", headers=headers, json=body)
    response.raise_for_status()
    return response.json()[0]["translations"][0]["text"]

def send_to_slack(text: str, channel: str, thread_ts: str = None):
    """Slackに翻訳結果を送信"""
    slack_token = os.getenv("SLACK_BOT_TOKEN", "")
    client = WebClient(token=slack_token)
    client.chat_postMessage(channel=channel, text=text, thread_ts=thread_ts)

5.Azure Functionにデプロイ

ローカルで作成したソースコードをAzure上のFunctionsにデプロイします。
VSCodeの左ペインのAzureのロゴを選択します。
(サインインしていない場合はサインインして下さい。)

サインインしている状態になったら以下の順に選択してデプロイを進めます。
[任意のサブスクリプション] > [Function App] > [手順1.で払い出したアプリ]
> 右クリック > [Deploy to Function App...]

以下の画面が出てきたら「Deploy」をクリックします。
デプロイが完了したら、右下にログが出力されるので「View Output」で確認します。

Azureポータルでも確認します。
手順1.で作成したFunctionsの概要の画面に関数名が表示されていたら完了です。

Slack Botアプリと関数アプリと紐づけるため、関数のURLを取得します。
上記画像の関数名をクリックします。
画面上部の「関数のURLの取得」をクリックして控えておきます。(私は_masterを使用しました。)

6.Slack Botアプリ設定

手順2.で作成したBotアプリの各設定を行います。

Bot Token Scopesの設定

アプリ管理ページの左メニューから「OAuth & Permissions」セクションを選択します。

「Bot User OAuth Token」が表示されます。
こちらは次の手順で環境変数として必要ですので控えてください。

<ワークスペース名>にインストールと表示されている場合は、インストールを行ってください。

画面を下に移動し「Scopes」にて作成したチャットBOTに以下の権限を付与します。
(必要な権限に応じて変更してみてください。)

Bot Token Scopes

channels:history
channels:read
chat:write
groups:history
groups:read
groups:write
im:history
users:read

イベントサブスクリプションの設定

アプリ管理ページの左メニューから「Event Subscriptions」セクションを選択します。
Enable EventsをOnにします。

有効化されたら「Request URL」に先ほど控えたAzure関数アプリのURLを入力します。
入力すると自動的に「このURLは本当に正しい場所につながっているの?」を検証します。

「Verified」と表示されれば成功です。

画面下部の「Subscribe to bot events」を設定します。
ここではどのようなイベントが発生したら関数アプリにリクエストを飛ばすかを設定できます。
今回はチャネルにメッセージが投稿されたイベントにトリガーしてほしいので以下を「Add Bot User Event」から追加しました。

Event Name

message.channels
message.groups
message.im

SLACK_BOT_TOKENの取得

次の手順で環境変数として使用するためのSlack BOTのユーザーIDを取得します。
curlコマンドで実際にSlack Botにリクエストを送信して情報を取得します。

ターミナルまたはコマンドプロンプトで以下のコマンドを実行します。

curl -X POST \
-H "Authorization: Bearer (xoxb-から始まるOAuth Tokenを入れてください)" \
https://slack.com/api/auth.test

うまくいけば以下の様な値が返ってきます。

{"ok":true,"url":"https:\/\/w1670728113-gui645900.slack.com\/","team":"選択したワークスペース名","user":"Slack Botアプリ名","team_id":"T0*********","user_id":"U0*********","bot_id":"B08MJ1HB747","is_enterprise_install":false}

下記の値を後述の手順で使用しますので控えてください。
user_id":"U0*********"

7.環境変数の設定

今回使用する環境変数をAzure Functionの環境変数セクションに設定していきます。
手順1.で作成したFunctionsの設定の環境変数の画面に遷移してください。
以下の環境変数を追加します。

環境変数
  • SLACK_BOT_TOKEN: SlackのBot Token(xoxb-...)。
  • SLACK_BOT_USER_ID: Slack APIレスポンスで取得したBOTのユーザーID。(手順6)
  • TRANSLATOR_ENDPOINT: Azure Translator APIのエンドポイント。(手順1)
  • TRANSLATOR_KEY: Azure Translator APIのキー。 (手順1)
  • TRANSLATOR_REGION: Azure Translator APIのリージョン。 (手順1)

すべて正しく入力できたら画面下部の適用をクリックして保存します。

ここまできたら実際に動くか確認するだけです。

8.動作確認

テストを行いたいチャネルに以下のメッセージを送信してBotを招待する。

/invite @<ボット名>

任意のメッセージを送信し、日本語なら英語に、英語なら日本語に翻訳されてBOTがチャット、もしくはスレッドに返信していれば正常に動作されています。


↑スレッドが無ければ自ら立ててそこに翻訳を送信する

つまづいた点(重要)

自分がつまずいた点と解決策を備忘として簡潔に記述しておこうと思います。

1.コードをデプロイ時にHttpTriggersとしてAzure Functionが認識してくれない。
事象:デプロイしたのにもかかわらず、Azureポータル上には表示されていない。
原因:Azureのディレクトリが無料プランだった。無料プランでは一部機能が制限されてるっぽい。
解決法:アップデートしてAzure AD Freeプランに変更したら解消された。

2.SlackのURL検証する処理が必要
事象:Slack APIのイベントサブスクリプションを登録する画面で関数アプリのURLを正しく入力してもエラーになる。
原因:以下のようなURL検証で飛んでくるSlackのリクエストに受け答えをする処理がないと検証が失敗するそうです。

if req_body.get("type") == "url_verification": return func.HttpResponse(req_body.get("challenge"), status_code=200)

解決法:上記処理をhttp_trigger関数内に組み込んだ。

3.なかなか翻訳APIが動いてくれない(プライベートチャネルはグループ扱い)
事象:テストでSlackにメッセージを送信しても関数が動作しない。⇒翻訳されない。
原因:テストや本番運用しているチャネルはプライベートチャネルであり、イベントサブスクリプションの設定不足。
解決法:イベントサブスクリプションにmessage.groupsを、権限にgroups:history、groups:read、groups:writeを付与してアプリの再インストールを実施したら解消された。

4.翻訳無限ループ
事象:テストで1度メッセージを送信した際に、正しく翻訳されるが翻訳ボットが連続で翻訳したメッセージを送信し続ける。

原因:翻訳BOTが送信したメッセージにもイベントサブスクリプションがトリガーしている。
解決法:直前で翻訳BOTがメッセージを送信している場合は、処理を正常終了させるコードを組み込んだ。

        # BOT自身のメッセージまたはイベント重複防止
        bot_user_id = os.getenv("SLACK_BOT_USER_ID", "")
        if user_id == bot_user_id or bot_id or event_ts in processed_events:
            return func.HttpResponse("Message from bot ignored or duplicate event", status_code=200)

        processed_events.add(event_ts)

まとめ

初めてのアプリ実装、初めての技術ブログということで、
かなりてこずりましたが書きまとめてよかったです。

アウトプットをすることで頭の中が整理されてるのが実感できました。
5時間くらいかかったのでもう少し工夫して、アウトプットする前提で作業やハンズオンしようと思いました。

勉強になった!

以上

参考資料

MS Learn | Visual Studio Code を使用して Azure Functions を開発する

Discussion