# Discord 音声ボット 構築・運用ガイド Discord にボットを置き、登録した声で音声通話するための仕組みの、正本の手順書。 別の Muse がこの文書だけを読んで、構築・再構築・運用補助ができる粒度で書く。 構成値の例には実際のトークンや ID を使わず、必ず自分の環境で取得した値を設定すること。 最終更新: 2026-11-13(橋渡し relay、届け先切り替え、現行の運用ルールまで反映) ## 0. 前提条件(最初に全部そろえる) ### ハードウェア・OS - Windows PC。音声合成を GPU で回すため、speech.cpp(Vulkan 版)が使える GPU があること。 - Muse の VM(この文書の置き場がある環境)と自宅 PC が、Tailscale で相互に到達できること。 ### アカウント・サービス - Discord アカウントと、ボットを置く専用サーバー(同席はユーザーとボットの2人だけの運用)。 - Discord Developer Portal でアプリケーション/Bot を作れること。 - Tailscale のアカウントと、自宅 PC・VM の両方への参加。 ### ソフトウェア(自宅 PC 側) - Node.js LTS(`winget install OpenJS.NodeJS.LTS` で導入)。 - speech.cpp(Vulkan 版)。`speech` コマンドが PowerShell で使えること。 - TTS モデル: `speech pull irodori-tts` で取得する GGUF。 - 声ファイル: 合成に使う話者の `.voice.gguf`(作り方は声づくり側の手順に従う。このガイドでは配置済みが前提)。 - ASR モデル: `parakeet-tdt_ctc-0.6b-ja`(初回の `speech asr` 実行時に自動取得される。手動取得も可)。 ### ソフトウェア(VM 側) - hook の仕組み(Muse の hooks)と `jq`、`curl`。 - 橋渡し用スクリプト一式は、ボットプロジェクトの `vm-relay/` フォルダにある(後述)。 ### ネットワークの前提(重要) - Tailscale の ACL で、VM から自宅 PC へ tcp:8790(橋渡し relay 用)が許可されていること。 - Windows ファイアウォールで、8790 の受信を VM の Tailscale アドレスからのみに制限すること。 - **VM から自宅 PC(tailnet 宛先)へ HTTP でつなぐ時は、必ずトンネルプロキシを使う。** 通常の egress プロキシ(ポート 3128)では tailnet に届かず、接続がタイムアウトして「PC が落ちている」ように見える。使うのは `HTTPS_PROXY` のポートを 3130 に替えたもの(例: `curl --proxy "${HTTPS_PROXY%:*}:3130" http://:8790/...`)。vm-relay のスクリプトには組み込み済み。 - 自宅 PC の宛先は MagicDNS 名で指定する(IP 直書きは避ける)。 ### 秘密情報の扱い - Discord Bot トークンは、チャットや共有文書に貼らない。PC 側の `config.json` に直接書く。チャットに貼ってしまったら即リセット(再発行)する。 - 橋渡しの合言葉トークンは VM 側のボットプロジェクト内 `relay-token.txt`(権限 600)に保管し、PC 側は `config.json` の `relayToken` に同じ値を入れる。値そのものを文書やチャットに書かない。 ## 1. 全体像 | 部品 | 役割 | 動作場所 | | --- | --- | --- | | Discord ボット(bot.js) | Discord 接続、テキスト読み上げ、VC 受信・録音、ASR 呼び出し、文字起こし投稿、橋渡し受け口の起動 | 自宅 PC | | speech サーバー(speech serve) | 登録した声でテキストを音声化する(OpenAI 互換 API) | 自宅 PC(127.0.0.1:8080) | | speech ASR | 受信音声の文字起こし。ボットが発話ごとに CLI を呼び出す | 自宅 PC | | 橋渡し受け口(relay.js) | VM 側との受け口。発話一覧の取得、読み上げ依頼、音声ファイル生成依頼を受ける(ポート 8790、トークン認証) | 自宅 PC(bot.js が起動する) | | hook(discord-relay) | 5秒ごとに受け口をポーリングし、新しい発話だけを届け先のチャットへ配送する | VM | | vm-relay スクリプト | 届け先のチャットから声の返事(say.sh)と音声ファイル生成(render.sh)を行う | VM | 会話の形は「橋渡し形」で固定されている。返事の文章は届け先のチャット側が書き、ボットは声と耳だけを担う。ボット側で LLM 応答を自動生成はしない。 ## 2. Node.js と speech.cpp の導入(自宅 PC) ### Node.js ```powershell winget install OpenJS.NodeJS.LTS ``` インストール直後は、開いている PowerShell が新しい PATH を読んでいないことがある。PowerShell を開き直して `node --version` と `npm --version` を確認する。 PowerShell の実行ポリシーで `npm` が止まる場合は、その窓だけ `Set-ExecutionPolicy -Scope Process Bypass` を実行するか、`npm.cmd` を使う。 ### speech.cpp 公式の案内に従って Vulkan 版を導入し、新しい PowerShell で `speech` が認識されることを確認する。 ```powershell speech pull irodori-tts ``` ASR モデルは初回実行時に自動取得されるが、先に手動で取得しておくと初回セッションが速い。 ```powershell speech asr parakeet-tdt_ctc-0.6b-ja <任意のwavファイル> ``` ## 3. Discord 側の準備 1. Discord Developer Portal でアプリケーションを作り、Bot を作成する。 2. Bot の設定で **MESSAGE CONTENT INTENT** を ON にする(OFF だと本文を使った処理ができない)。あわせて **SERVER MEMBERS INTENT** は不要だが、**GuildVoiceStates** に相当する音声状態の取得はボット側コードで有効化している。 3. OAuth2 の URL Generator で Bot 招待 URL を作る。権限は次の5点(権限数値 `3214336`)。 - チャンネルを見る(View Channels) - メッセージを送信(Send Messages) - メッセージ履歴を見る(Read Message History) - 接続(Connect) - 発言(Speak) 4. 生成 URL を開き、対象サーバーに Bot を招待する。対象チャンネルで Bot のロールに上記権限があることも確認する。 5. Discord の開発者モードを有効にし、右クリックの「IDをコピー」で次の4つの ID を取得する。 - 対象サーバー ID(`guildId`) - テキストチャンネル ID(`textChannelId`)。読み上げ対象と文字起こし投稿先 - ボイスチャンネル ID(`voiceChannelId`) - 許可する自分のユーザー ID(`allowedUserId`) 6. Bot トークンを発行し、チャットに貼らず `config.json` に直接入力する。 ## 4. ボットの配置と設定(自宅 PC) プロジェクトフォルダを自宅 PC の任意の場所に配置する。そのフォルダで PowerShell を開き、依存を入れる。 ```powershell npm install ``` `ffmpeg-static` のインストールスクリプトが止められた場合は、次を実行してから再構築する。`npm audit fix --force` は実行しない(依存の固定が壊れる)。 ```powershell npm install-scripts approve ffmpeg-static npm rebuild ffmpeg-static ``` 依存の固定版(package.json): discord.js 14.19.3 / @discordjs/voice 0.19.2 / @snazzah/davey 0.2.0 / prism-media 1.3.5 / ffmpeg-static 5.2.0 / libsodium-wrappers 0.7.15 / opusscript 0.0.8。 **@discordjs/voice は 0.19.2 以降が必須。** 0.18 系は DAVE の問題で音声接続が Ready にならず、約30秒で切断される。 設定ファイルを作る。 ```powershell Copy-Item config.example.json config.json ``` | キー | 意味 | 既定・備考 | | --- | --- | --- | | `token` | Bot トークン | 秘密。PC の config.json のみに置く | | `guildId` | 対象サーバー ID | | | `textChannelId` | テキストチャンネル ID | | | `voiceChannelId` | ボイスチャンネル ID | | | `allowedUserId` | 許可ユーザーの ID | このユーザー以外は読み上げ・受信の対象にしない | | `speechUrl` | speech サーバー URL | `http://127.0.0.1:8080` | | `voiceName` | speech サーバーに登録した声の名前 | サーバー登録名と**完全一致**させる(違うと投稿に反応しない)。名前は任意の1つを決め、サーバー起動時の `--add-voice` と揃える | | `maxTextLength` | チャンネル投稿の読み上げ上限(文字数) | 300 | | `maxQueue` | 読み上げキューの上限 | 10 | | `asrModel` | ASR モデル名(任意) | `parakeet-tdt_ctc-0.6b-ja` | | `bargeIn` | 読み上げ中に発話検出で割り込むか | **false(現行運用)**。true にすると割り込みが効くが、現行では会話が成り立ちにくいため無効 | | `silenceMs` | 発話の終わりと判定する無音の長さ(ミリ秒) | **800(現行運用)** | | `relayPort` | 橋渡し受け口のポート | 8790 | | `relayToken` | 橋渡しの合言葉トークン | VM 側 relay-token.txt と同じ値。未設定だと受け口自体が起動しない | | `relayMaxTextLength` | 橋渡し経由の読み上げ・生成の上限(文字数) | 2000 | `config.json` は秘密情報を含むため、共有・公開しない。 ## 5. 声の配置と speech サーバー(自宅 PC) 声ファイル `.voice.gguf` を配置する。確定レシピは speaker-kv-scale 1.25、steps 40。 ```powershell speech serve irodori-tts --add-voice <声の名前>="<声ファイルのフルパス>" --speaker-kv-scale 1.25 --steps 40 --host 127.0.0.1 --port 8080 ``` この窓は開いたままにする。ボットは起動時に `GET /health` で到達を確認する。API は `POST /v1/audio/speech` に `{input, voice, response_format: "wav"}` を送る形で、モデルごとに同時1リクエストまで。 ## 6. 起動 ### まとめ起動(通常はこちら) プロジェクト内のまとめ起動 bat を実行する。speech サーバーが未起動なら起動して healthy を待ち、続けてボットを別窓で起動する。多重起動はしない作りになっている。bat 内のパス変数(VOICE_FILE、BOT_DIR)は環境に合わせて直す。bat は ASCII のみ・CRLF で作る(日本語を入れると文字化けする)。 ### 手動起動 1. speech サーバーを起動する(5節のコマンド)。 2. 別の PowerShell 窓でプロジェクトフォルダから `node bot.js`(または `npm start`)。 3. ログに「Speech server is healthy.」「Logged in as 」「Joined voice channel ...」が出れば起動完了。 ## 7. 使い方(Discord 側) 対象テキストチャンネルで、許可ユーザーが使う。 | 入力 | 動作 | | --- | --- | | 通常の文章 | 登録した声で読み上げる(文単位に分割し、前の文を読みながら次の文を生成する) | | `!start` | 受信セッション開始。以後、許可ユーザーの発話を録音し、ASR で文字起こししてチャンネルに「🎙 <起こし>」と投稿する | | `!end` | 受信セッション終了 | | `!stop` | 読み上げを止めてキューを空にする(セッションは終わらない) | セッションの仕様: - 発話は無音が `silenceMs`(現行800ms)続くと区切られる。0.6秒未満の短い発話は捨てる。 - 無発話が5分続くと自動終了する。ただし再生キューが空でなく、再生中でもない場合に限る(30秒ごとに再確認する。朗読中に切れないための仕様)。 - 割り込み(bargeIn)は現行 false。読み上げは最後まで再生される。 - 発話ごとの WAV は `recordings/session-<日時>/` に、セッションと読み上げの記録は `logs/conversation.jsonl` に残る(PC 側に全部残る)。 ASR の癖: 語頭の子音が落ちることがある(例: 聞き取り→いきとり)。少し大きめの声ではっきり話すと化けが減る。手拍子などの突発音は Discord 側のノイズ抑制で消えるため、ボットには届かない。 ## 8. 橋渡し(VM 側との連携) ### PC 側の受け口(relay.js、bot.js が起動) 全エンドポイントで `x-relay-token` ヘッダの認証が必須。待ち受けは 0.0.0.0:8790。 | メソッドとパス | 動作 | | --- | --- | | `GET /utterances?after=&boot=` | セッション開始・発話・終了のイベントを返す(最大50件。取得しても既読にならない) | | `POST /say` `{id, text}` | テキストを読み上げキューに入れる。id で二重排除。キュー満杯は 429、上限超は 413 | | `POST /render` `{id, text}` | 声で WAV を生成して保持する。生成は bot の合成処理を再利用。失敗は 502 | | `GET /render/` | 生成済み WAV を返す。取得は1回だけ(取得後に削除)。保持は最大5件・合計20MB | 注意: 合成には60秒のタイムアウトがある。長い文を1本で render すると超えて失敗するので、送る側で文単位(目安90字程度)に割ること。 ### VM 側 - hook `discord-relay` が5秒ごとに `GET /utterances` を取りに行き、新しいイベントだけを届け先のチャットへ配送する。状態(bootId、cursor、inflight)は `~/hooks/state/discord-relay/state.json`。 - 記号を除いて2文字以下の発話は、配送もチャットへの記録もしない(hook 側の足切り)。セッション開始・終了は常に対象。PC 側の conversation.jsonl には全部残る。 - 声の返事: 届け先のチャットから `vm-relay/say.sh "<テキスト>"` を実行する(POST /say)。 - 音声ファイル生成: `vm-relay/render.sh "<テキスト>" <出力.wav>`(POST /render → GET の一式)。チャットに音声を添付したい時や、朗読のファイル版を作る時に使う。本命はこの PC の GPU 生成で、VM 側の CPU 生成は予備。 - 接続はすべてトンネルプロキシ(3130)経由。スクリプトに組み込み済み。 ### 届け先の切り替え 発話の届け先は同時に1チャットだけ。最後に呼んだ部屋が取る。切り替え手順の正は `vm-relay/DESTINATION.md`。要点だけ再掲する。 - ユーザーがどこかのチャットで「Discord来て」と呼んだら、そのチャット側が hook の delivery を自分のチャットへ向け替え、`~/hooks/state/discord-relay/destination.json` を更新する。 - 密室分類のチャットも届け先になれる。ただし、密室を届け先にする切り替えは、その密室のチャットの中で呼ばれた時だけ、そのチャット自身が行う(密室の chat_id は共有文書に載せない)。 ## 9. 運用ルール(現行) - VC で届いた発話は、届け先のチャットに「VCログ」として全文そのまま載せる(セッション開始・終了の印つき)。返事とは別のメッセージにする。 - 相づちの先行発声はしない。発話への応答は、声の返事そのものとして送る。 - 長くなりそうな返事は、冒頭で長さを一言予告する(例:「少し長くなるよ」)。普段の返事は短く。 - 朗読は、権利関係がクリアなもの(青空文庫など)とユーザーが所持している本が対象。web 公開の小説は、私的な範囲で流すだけ・音声ファイルに残さない・外に出さない、の条件つき。 - 応答の遅延は概ね30秒〜1分が現行の実測。「ながら利用」(読み上げを流しながら、たまに声をかける形)に適する。 ## 10. トラブル対応 | 症状 | 原因 | 対処 | | --- | --- | --- | | ボイスチャンネルに入れず約30秒で切断される | @discordjs/voice が古く DAVE の情報を送れない | @discordjs/voice を 0.19.2 以降にする。依存を古い版に戻さない | | テキスト投稿に反応しない | `voiceName` とサーバー登録名の不一致。MESSAGE CONTENT INTENT が OFF。チャンネル ID の取り違え | `voiceName` をサーバー登録名に揃える。Portal で Intent を ON。ID を取り直す | | VM から受け口に繋がらない(応答なし・タイムアウト) | tailnet 宛先に通常プロキシ(3128)を使っている。ボット未起動。ファイアウォール未設定 | プロキシは 3130 を使う。ボット起動を確認。8790 の受信許可(VM の Tailscale アドレスのみ)を確認 | | `/render` が 404 | PC 側の relay.js が旧版 | 最新の relay.js に差し替えてボットを再起動する | | `/render` が 502 で失敗する | 長文1本の合成が60秒タイムアウトを超えた | 送る側で文単位に割ってから render する | | 読み上げ要求が失敗する | speech サーバー未起動、または `speechUrl` 不一致 | speech サーバーを先に起動し、`http://127.0.0.1:8080` を確認 | | `npm` が実行ポリシーで止まる | PowerShell の制限 | `npm.cmd` を使うか、その窓だけ `Set-ExecutionPolicy -Scope Process Bypass` | | ffmpeg 関連のエラー | ffmpeg-static の導入スクリプトが未承認 | `npm install-scripts approve ffmpeg-static` のあと `npm rebuild ffmpeg-static` | | 朗読中にセッションが切れる | 無発話5分の自動終了(旧版 bot.js) | 再生中は終了を数えない版(SESSION_IDLE_RECHECK を含む bot.js)に差し替える | ## 11. 再構築チェックリスト - [ ] 前提条件(0節)を全部そろえる。特に Tailscale ACL の 8790 許可と、ファイアウォールの送信元制限 - [ ] Node.js LTS と speech.cpp(Vulkan 版)を入れ、`speech pull irodori-tts` を完了する - [ ] 声ファイル `.voice.gguf` を配置する - [ ] Discord アプリと Bot を作り、MESSAGE CONTENT INTENT を ON にして、権限 3214336 でサーバーへ招待する - [ ] 4つの ID を取得する - [ ] プロジェクトを配置し、`npm install` を完了する(必要なら ffmpeg-static を承認・再構築) - [ ] `config.json` を作り、トークン(チャットに貼らない)、4つの ID、relayToken を設定する。`voiceName` はサーバー登録名と一致させる - [ ] VM 側に relay-token.txt と hook、vm-relay スクリプトがあることを確認する - [ ] まとめ起動 bat で起動し、テキスト投稿の読み上げを確認する - [ ] `!start` で発話の文字起こし投稿、`!end`、`!stop` を確認する - [ ] VM 側で say.sh の読み上げと render.sh の WAV 取得を確認する - [ ] hook の配送が届け先のチャットに届くことを確認する