作成背景
とあるゲームのチームメンバー用にDiscordサーバーを作成・運用しています。
ゲーム公式Discordサーバーが定期的に投稿するアップデート情報等が全て英語なため、メンバーが読むのを面倒に思っていたみたいなので、Botで自動翻訳を作って煩わしさを減らしたいと思ったのがきっかけです
なお、備忘録替わりなので粗さは目立つ記事かもです(時間があれば更新します)
実現したかったこと
- 翻訳時に原文をそのまま再現すること
- リンク・画像・絵文字を含む
- 管理コストを極限まで下げること
- サーバー維持費は無料
- 24h/365d 自動稼働
完成物
翻訳前の文章が投稿されているチャンネル(計3つ)のうち投稿があった場合をトリガーにChatGPTで翻訳
準備・設定するもの
- 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
- Builder
- [Save and Deploy]をクリック
- Secretsメニューを開き[Create secret]ボタンを押して以下変数を設定
-
各ファイルにそれぞれ実装
main.pyimport 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.pyimport 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()
DokerfileFROM 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.txtopenai==1.95.1 discord.py python-dotenv uvicorn fastapi httpx
-
実装したらGitHubへPush
-
Koyebでオートデプロイが走り、状態がHealthyになったことを確認
-
10分間隔でヘルスチェックが走っていることを確認
-
翻訳前のチャンネルでテスト用英文&画像を投稿し、Botが翻訳&投稿を行うことを確認できたらOK
終わりに
翻訳用Botなんて今まで数多の方が作成されてきたかと思いますが、個人的には初めてのことだったので良い経験になりました。