導入
Windows 11のVisual Studio Code環境で動作するオフライン環境のVOICE BOX APIを利用したDiscord専用のテキスト読み上げBOTを開発しました。ユーザーが特定のテキストチャンネルに投稿したメッセージを解析し、オフラインのVOICEVOX API を利用して リアルタイム音声読み上げを行う BOT です。
このプロジェクトでは以下の点を重視しています:
- ✔️ VOICEVOX API を用いた高品質 TTS 生成
- ✔️ Discord API(discord.py)による音声チャンネル制御
- ✔️ 自動接続・退出・再起動まで含めた安定動作
- ✔️ メッセージの保存・ログ化
- ✔️ キャッシュ機能による再読み上げ最適化
内容
音声読み上げロジック
本 BOT の音声機能は、VOICEVOX API を中心に構築しています。
- メッセージ内容の受信
- URL 判定(URL は「URL」と要約読み上げ)
- 長文のときは先頭 30 文字のみを読み上げ
- VOICEVOX の audio_query → synthesis による音声生成
- WAV キャッシュ採用(同じテキストは再生成しない)
- 再生後にキャッシュ削除し、ストレージを適切に管理
Discord 連携
- 指定ボイスチャンネルへの自動参加
- 読み上げ終了後は再生キューを監視
- チャンネルが無人になった場合は BOT を自動再起動
- 接続トラブル時の例外処理・再接続を実装
データ保存
- 特定テキストチャンネルのメッセージを JSON 保存
- timestamp / author / content を記録
- セキュリティ領域はローカルフォルダへ安全に保管
実際に動作している環境
- プラットフォーム:Python(discord.py)
- TTS:VOICEVOX(localhost API)
- 読み上げ API:aiohttp による非同期通信
- 実行サーバー:Windows / Linux どちらでも対応
- ※ 現在はポートフォリオ用に公開を停止
Pythonコード例
以下は実際の開発で使用したコードの一部を、
安全化したうえでポートフォリオ向けに整形したものです。
import discord
from discord.ext import commands
import aiohttp
import asyncio
import os
import json
import datetime
import re
import sys
CONFIG_PATH = "config.json"
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
config = json.load(f)
DICTIONARY_PATH = "data/dictionary.json"
TTS_CACHE_DIR = "data/tts_cache"
SECURITY_MESSAGE_FILE = "data/messages.json"
os.makedirs("data", exist_ok=True)
os.makedirs(TTS_CACHE_DIR, exist_ok=True)
if not os.path.exists(DICTIONARY_PATH):
with open(DICTIONARY_PATH, "w", encoding="utf-8") as f:
json.dump({}, f, ensure_ascii=False, indent=4)
if not os.path.exists(SECURITY_MESSAGE_FILE):
with open(SECURITY_MESSAGE_FILE, "w", encoding="utf-8") as f:
json.dump([], f, ensure_ascii=False, indent=4)
TOKEN = config["discord_token"]
VOICE_CHANNEL_ID = config["voice_channel_id"]
TEXT_CHANNEL_ID = config["text_channel_id"]
VOICEVOX_API_BASE_URL = config.get("voicevox_url", "http://localhost:50021")
SPEAKER_ID = config.get("speaker_id", 1)
FFMPEG_PATH = config.get("ffmpeg_path", "ffmpeg")
intents = discord.Intents.default()
intents.messages = True
intents.guilds = True
intents.voice_states = True
intents.message_content = True
bot = commands.Bot(command_prefix="!", intents=intents)
def load_dictionary():
with open(DICTIONARY_PATH, "r", encoding="utf-8") as f:
return json.load(f)
def save_dictionary(data):
with open(DICTIONARY_PATH, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4)
@bot.event
async def on_ready():
print(f"Logged in as {bot.user}")
@bot.event
async def on_voice_state_update(member, before, after):
if member.bot:
return
channel = bot.get_channel(VOICE_CHANNEL_ID)
if before.channel != after.channel:
if after.channel == channel:
await announce_voice(f"{member.display_name} joined.")
elif before.channel == channel:
non_bot_members = [m for m in before.channel.members if not m.bot]
if non_bot_members:
await announce_voice(f"{member.display_name} left.")
else:
await asyncio.sleep(3)
for vc in bot.voice_clients:
if vc.channel == channel:
await vc.disconnect()
await asyncio.sleep(1)
os.execv(sys.executable, ["python"] + sys.argv)
@bot.event
async def on_message(message):
if message.author.bot:
return
if message.channel.id == TEXT_CHANNEL_ID:
with open(SECURITY_MESSAGE_FILE, "r", encoding="utf-8") as f:
logs = json.load(f)
logs.append({
"timestamp": datetime.datetime.now().isoformat(),
"author": message.author.name,
"content": message.content
})
with open(SECURITY_MESSAGE_FILE, "w", encoding="utf-8") as f:
json.dump(logs, f, ensure_ascii=False, indent=4)
if re.search(r'https?://\S+', message.content):
await read_text("URL")
elif len(message.content) >= 60:
await read_text(message.content[:30] + "…")
else:
await read_text(message.content)
await bot.process_commands(message)
async def get_tts_audio(text):
dictionary = load_dictionary()
for key, value in dictionary.items():
text = text.replace(key, value)
filename = os.path.join(TTS_CACHE_DIR, f"{hash(text)}.wav")
if os.path.exists(filename):
return filename
try:
async with aiohttp.ClientSession() as session:
async with session.post(
f"{VOICEVOX_API_BASE_URL}/audio_query",
params={"text": text, "speaker": SPEAKER_ID}
) as query_resp:
if query_resp.status == 200:
audio_query = await query_resp.json()
async with session.post(
f"{VOICEVOX_API_BASE_URL}/synthesis",
params={"speaker": SPEAKER_ID},
json=audio_query
) as synthesis_resp:
if synthesis_resp.status == 200:
with open(filename, "wb") as f:
f.write(await synthesis_resp.read())
return filename
except Exception as e:
print(f"TTS error: {e}")
return None
async def read_text(text):
channel = bot.get_channel(VOICE_CHANNEL_ID)
if not channel:
return
if bot.voice_clients:
vc = bot.voice_clients[0]
else:
vc = await channel.connect()
audio_file = await get_tts_audio(text)
if not audio_file:
return
try:
vc.play(discord.FFmpegPCMAudio(audio_file, executable=FFMPEG_PATH))
while vc.is_playing():
await asyncio.sleep(0.5)
await asyncio.sleep(1)
if os.path.exists(audio_file):
os.remove(audio_file)
except Exception as e:
print(f"Playback error: {e}")
async def announce_voice(text):
await read_text(text)
@bot.command(name="add_word")
async def add_word(ctx, key: str, value: str):
data = load_dictionary()
data[key] = value
save_dictionary(data)
await ctx.send(f"Added: {key} → {value}")
@bot.command(name="remove_word")
async def remove_word(ctx, key: str):
data = load_dictionary()
if key in data:
del data[key]
save_dictionary(data)
await ctx.send(f"Removed: {key}")
else:
await ctx.send("Not found.")
@bot.command(name="show_dictionary")
async def show_dictionary(ctx):
data = load_dictionary()
if data:
content = "\n".join([f"{k}: {v}" for k, v in data.items()])
await ctx.send(content)
else:
await ctx.send("Dictionary is empty.")
bot.run(TOKEN)
技術構成
| 技術 | 内容 |
|---|---|
| Python | BOT 本体・非同期処理 |
| discord.py | Discord API 操作 |
| VOICEVOX API | 音声合成 |
| aiohttp | 非同期 HTTP 通信 |
| FFmpeg | 音声ファイルの再生処理 |
| JSON | 設定ファイル・ログ保存 |
| Session(簡易) | Bot の再接続制御 |
実際のコード公開について
この BOT はポートフォリオ向けに安全化し、以下の項目は非公開としています。
- Discord Token / Bot Key
- 実際のフォルダ構成・ユーザー名
- 運用環境のログ
- セキュリティ関連の内部処理
- 機密性の高いファイルパス
実際のシステム導入・追加機能開発が必要な場合は、個別に相談可能です。
お問い合わせ
本 BOT の開発や、「Discord × AI × 音声合成」を利用したカスタムシステムの制作について何かご質問がございました、お気軽にお問い合わせください。


