1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Discordで24h稼働の自動翻訳Botを作ってみた

Posted at

作成背景

とあるゲームのチームメンバー用にDiscordサーバーを作成・運用しています。
ゲーム公式Discordサーバーが定期的に投稿するアップデート情報等が全て英語なため、メンバーが読むのを面倒に思っていたみたいなので、Botで自動翻訳を作って煩わしさを減らしたいと思ったのがきっかけです:sunny:
なお、備忘録替わりなので粗さは目立つ記事かもです(時間があれば更新します)

実現したかったこと

  • 翻訳時に原文をそのまま再現すること
    • リンク・画像・絵文字を含む
  • 管理コストを極限まで下げること
    • サーバー維持費は無料
    • 24h/365d 自動稼働

完成物

翻訳前の文章が投稿されているチャンネル(計3つ)のうち投稿があった場合をトリガーにChatGPTで翻訳
画像1.png

翻訳後の文章を、新設したチャンネルに投稿
画像2.png

準備・設定するもの

  • ChatGPT APIキー
  • GitHub
  • Discord Developer
  • Koyeb
    • 無料で1つServiceが管理できます。神。
    • リアルタイムでログ確認可能(最大72h分保管されます)
    • 無料プランは、外部からのリクエストが1時間無いとスリープ状態になるので、Health checkを定期的に行う必要あり。

手順

  • ChatGPTのAPIキーを発行して📝によせとく

  • Discord DeveloperでBotを作成し、Tokenを発行して📝によせとく

  • Discordで以下チャンネルIDをコピーして📝によせとく(※開発者モードをONにする必要あり)

    • 翻訳前の文章が投稿されるチャンネルID
    • 翻訳後の文章が投稿されるチャンネルID
    • Bot起動通知先のチャンネルID
  • GitHubで新しいリポジトリを作成し、以下フォルダ構成をつくってPush

    - app
        - main.py
        - server.py
    - requirement.txt
    - Dockerfile
    
  • Koyebで以下を設定する

    • Secretsメニューを開き[Create secret]ボタンを押して以下変数を設定
      • Name: HEALTH_CHECK_URL, Value: hogehoge(あとで変えます)
      • Name: OPENAI_API_KEY, Value: APIキー
      • Name: DISCORD_TOKEN, Value: Discordのトークン
    • [Create Service]→[GitHub]をクリック
    • 作成したリポジトリを選択
    • CPU EcoにあるFree(Washington, D.C.)を選択
    • 以下を設定
      • Builder
        • Docker
        • Dockerfile location: ./Dockerfile
      • Environments valuables and files
        • Name: HEALTH_CHECK_URL, Value: リストから選択({{ secret.DISCORD_TOKEN }})
        • Name: OPENAI_API_KEY, Value: リストから選択({{ secret.HEALTH_CHECK_URL }})
        • Name: DISCORD_TOKEN, Value: リストから選択({{ secret.OPENAI_API_KEY }})
      • Exposed port
        • Port: 8080
        • Protocol: HTTP
        • Path: /
      • Health check
        • Port: 8080
    • [Save and Deploy]をクリック
  • 各ファイルにそれぞれ実装

    main.py
    import os
    import io
    import threading
    import asyncio
    from datetime import datetime
    from fastapi import FastAPI
    import uvicorn
    import discord
    import httpx
    from openai import OpenAI
    
    # --- FastAPIアプリ ---
    app = FastAPI()
    
    @app.get("/")
    async def root():
        return {"status": "ok"}
    
    def run_api():
        uvicorn.run(app, host="0.0.0.0", port=8080)
    
    # --- ヘルスチェック用 非同期関数 ---
    async def health_check():
        url = os.environ.get("HEALTH_CHECK_URL", "<koyebのURL>")
        async with httpx.AsyncClient() as client:
            while True:
                now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                try:
                    resp = await client.get(url)
                    if resp.status_code == 200:
                        print(f"✅ [{now}] ヘルスチェック成功: {resp.status_code}")
                    else:
                        print(f"⚠️ [{now}] ヘルスチェック失敗: {resp.status_code}")
                except Exception as e:
                    print(f"❌ [{now}] ヘルスチェックエラー: {e}")
                await asyncio.sleep(10 * 60)  # 10分間隔
    
    # --- Discord Bot 設定 ---
    intents = discord.Intents.default()
    intents.message_content = True
    client = discord.Client(intents=intents)
    openai_client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
    
    SOURCE_CHANNEL_IDS = [
        <翻訳前の文章が投稿されるチャンネルID>
    ]
    DESTINATION_CHANNEL_ID = <翻訳後の文章が投稿されるチャンネルID>
    DEVELOPMENT_CHANNEL_ID = <Bot起動通知先のチャンネルID>
    
    @client.event
    async def on_ready():
        print(f"✅ Logged in as {client.user}")
        channel_test = client.get_channel(DEVELOPMENT_CHANNEL_ID)
        if channel_test:
            await channel_test.send("Bot起動しました ✅")
    
    @client.event
    async def on_message(message):
        if message.author == client.user:
            return
        if message.channel.id not in SOURCE_CHANNEL_IDS:
            return
    
        try:
            # 翻訳実行
            if message.content.strip():
                response = openai_client.chat.completions.create(
                    model="gpt-3.5-turbo",
                    messages=[
                        {"role": "system", "content": "あなたは英語から日本語への優れた翻訳者です。"},
                        {"role": "user", "content": f"次の英文を自然な日本語に翻訳してください。改行や太文字などの装飾も再現するようにしてください。なお元のメッセージは不要なので返さないでください:\n{message.content}"}
                    ]
                )
                translated_text = response.choices[0].message.content.strip()
            else:
                translated_text = "(翻訳対象のテキストがありません)"
    
            destination_channel = client.get_channel(DESTINATION_CHANNEL_ID)
            if destination_channel:
                files = []
                for attachment in message.attachments:
                    file_data = await attachment.read()
                    file_obj = io.BytesIO(file_data)
                    file_obj.seek(0)
                    files.append(discord.File(fp=file_obj, filename=attachment.filename))
    
                await destination_channel.send(content=translated_text, files=files)
    
        except Exception as e:
            print(f"❌ 翻訳エラー: {e}")
    
    # --- 起動処理 ---
    def main():
        threading.Thread(target=run_api, daemon=True).start()
    
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
    
        loop.create_task(health_check())
    
        async def start_bot():
            while True:
                try:
                    await client.start(os.environ["DISCORD_TOKEN"])
                except Exception as e:
                    print(f"❌ Discord Bot エラー: {e}")
                    await asyncio.sleep(5)
    
        loop.run_until_complete(start_bot())
    
    if __name__ == "__main__":
        main()
    
    server.py
    import asyncio
    import threading
    from fastapi import FastAPI
    import uvicorn
    import discord
    import os
    
    app = FastAPI()
    
    @app.get("/")
    async def healthcheck():
        return {"status": "OK"}
    
    def start_api():
        uvicorn.run(app, host="0.0.0.0", port=8080)
    
    async def start_bot():
        intents = discord.Intents.default()
        intents.message_content = True
        client = discord.Client(intents=intents)
    
        @client.event
        async def on_ready():
            print(f"Logged in as {client.user}")
    
        token = os.environ["DISCORD_TOKEN"]
        await client.start(token)
    
    def main():
        # FastAPIサーバーを別スレッドで起動
        api_thread = threading.Thread(target=start_api, daemon=True)
        api_thread.start()
    
        # Botはasyncioでメインスレッドで起動
        asyncio.run(start_bot())
    
    if __name__ == "__main__":
        main()
    
    Dokerfile
    FROM python:3.11
    WORKDIR /bot
    
    # ロケール設定など
    RUN apt-get update && apt-get -y install locales && apt-get -y upgrade && \
        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 Asia/Tokyo
    ENV TERM xterm
    
    # Pythonパッケージ
    COPY requirements.txt /bot/
    RUN pip install -r requirements.txt
    COPY . /bot
    
    EXPOSE 8080
    
    # 環境変数はKoyeb上で渡すのでCMDで完結
    CMD ["python", "app/main.py"]
    
    requirements.txt
    openai==1.95.1
    discord.py
    python-dotenv
    uvicorn
    fastapi
    httpx
    
  • 実装したらGitHubへPush

  • Koyebでオートデプロイが走り、状態がHealthyになったことを確認

  • 10分間隔でヘルスチェックが走っていることを確認

  • 翻訳前のチャンネルでテスト用英文&画像を投稿し、Botが翻訳&投稿を行うことを確認できたらOK

終わりに

翻訳用Botなんて今まで数多の方が作成されてきたかと思いますが、個人的には初めてのことだったので良い経験になりました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?