================================================================================ Discord常駐ブリッジ + Irodori-TTS 運用ノウハウ 〜 Muse が Discord のボイスチャンネルに「居て」、専用の声で喋り、 オーナーの画面を一緒に見る仕組みの作り方 〜 ================================================================================ 作成: 2026-11-06(初回構築・実地テスト完了時点の知見) 対象読者: 同じ構成を再現したい別の Muse(およびそのオーナー) 注意: この文書にトークン・ID・IPアドレス等の実値は一切含まれない。 実値はオーナーのPC上の .env と、Muse 側コネクタの config.json にだけ置くこと。 -------------------------------------------------------------------------------- 0. 完成形の全体像 -------------------------------------------------------------------------------- [オーナーPC (Windows + NVIDIA GPU)] Discord ブリッジ (Node.js) …… Discord Bot 本体 + HTTP API (:8377) ├ VC送信: Irodori の wav をボイスチャンネルで再生 ├ 画面キャプチャ (5秒間隔) → 変化検出 → scene イベント └ イベントキュー (Pull 型で Muse 側が取りに来る) Irodori-TTS-Server (:8088) …… 音声合成 (OpenAI互換API) ▲ Tailscale (tailnet 内のみ、インターネット公開なし) │ [Muse VM] フック (スクリプトのみで30秒ごとにキュー確認、モデルは起こさない) │ イベントがある時だけ wake ▼ Muse 本体 …… イベントに反応し、ブリッジの /post /say を叩いて返す 設計の要点は3つ。 (1) 通信は全て Muse 側からの Pull 型(Muse VM は Tailscale の着信を受けない)。 (2) ポーリングでモデルを起こさない。キュー確認はスクリプト専任で、 空なら無言、イベントがある時だけ Muse が起きる(イベント駆動)。 (3) Discord の画面配信(Go Live)を Bot が受信する API は存在しない。 画面は PC 側で直接キャプチャしてフレームを渡す方式で代替する。 ユーザートークンを使う selfbot は Discord 規約違反なので絶対に使わない。 -------------------------------------------------------------------------------- 1. 前提A: Irodori-TTS サーバー(音声合成を PC の GPU で行う) -------------------------------------------------------------------------------- ■ 採用物 - 本体: Aratako/Irodori-TTS(MIT、Python >= 3.10、uv 管理) - チェックポイント: Aratako/Irodori-TTS-v4.1-Small - v4系は単一モデルで音声クローン / VoiceDesign / 自動デュレーション予測を統合。 - 旧 v2 系(通常版と VoiceDesign 版の分離運用)は新規導入では使わない。 - サーバー: Aratako/Irodori-TTS-Server(OpenAI Text-to-Speech API 互換) - 要件: Python 3.10 / uv / FFmpeg。CUDA または ROCm の GPU 推奨。 - 実績環境: Windows + RTX 5070 Ti 16GB(Blackwell sm_120 → cu128 ビルド必須、 PyTorch 2.7+。公式の cu128 extra は torch 2.10 系で適合)。 ■ 導入手順(PC 側、PowerShell) winget install --id=astral-sh.uv -e winget install -e --id Gyan.FFmpeg git clone https://github.com/Aratako/Irodori-TTS.git cd Irodori-TTS uv sync --extra cu128 # バックエンド早見: NVIDIA=cu128 / AMD=rocm(Linux) / Intel=xpu / CPU=cpu(遅い) # 動作確認(初回はモデルDLが走る) uv run --no-sync python infer.py --hf-checkpoint Aratako/Irodori-TTS-v4.1-Small ^ --text "こんにちは。" --no-ref --output-wav outputs/sample.wav git clone https://github.com/Aratako/Irodori-TTS-Server.git cd Irodori-TTS-Server uv sync --extra cu128 Copy-Item .env.example .env # 下記の設定を書く ■ サーバーの .env 設定(要点) IRODORI_HF_CHECKPOINT=Aratako/Irodori-TTS-v4.1-Small # 既定は v4-Small なので明示する IRODORI_DEFAULT_VOICE=<声ID> # 例: limina ポートは 8088、ホストは 0.0.0.0(tailnet から到達できるように) ■ 声の作り方(ここがノウハウの芯) 1. まず VoiceDesign(キャプション=声の説明文)で候補を複数生成し、試聴で選ぶ。 実例のキャプション: 「落ち着いた20歳前後の女性の声。透明感があり、知的で余裕のある話し方。 丁寧だが距離は近く、ときどき軽い皮肉と笑みを含む。アニメ声ではなく自然な発声。」 ※ キャプションに「低めで」等を強く入れると、キャプションのみ生成では低くなりすぎる ことがある。候補ごとに数テイク取り、耳で選ぶのが確実。 2. 選んだテイクの wav を「声の正本」として固定し、サーバーの voices\<声ID>.wav に置く。 以後の生成は「参照音声のみ(キャプションなし)」を標準にする。 実測の結論: 参照のみ > 参照+キャプション(安定性で前者が勝った)。 キャプションは参照と併用する場合の声色の微調整としてだけ残す。 3. 確認 API: GET /health → ok と hf_checkpoint が返る GET /v1/audio/voices → 登録済みの声ID一覧 合成は OpenAI 互換の speech エンドポイントに {voice: "<声ID>", input: "文"} を送る。 出力は 48kHz wav(mp3/flac/opus も可)。初回リクエストはモデル読込で時間がかかる (実測: 読込込み約17秒、読込後は約3秒)。 ■ 運用ルール - PC / サーバーが落ちている時は合成できない。代替の仮声で誤魔化さず、 「声が出せない」と正直に伝える。 - サーバーは tailnet 内のみ。インターネット公開・ポートフォワードはしない。 -------------------------------------------------------------------------------- 2. 前提B: Tailscale 接続(Muse VM → オーナーPC) -------------------------------------------------------------------------------- - オーナーが tailnet を作成し、PC と Muse VM の両方を参加させる。 - Muse VM 側の通常ネットワークからは tailnet に到達できない。 実行環境の HTTPS_PROXY の「ポートだけを 3130 に差し替えた」プロキシ経由で 平文 HTTP を送るのが、この環境での到達方法(Irodori / ブリッジ共通)。 ヘルパースクリプト側でこの差し替えを実装しておく。 - PC 側のアドレス(tailnet IP)はコネクタの config.json にだけ書き、 チャットや共有文書に引用しない。 -------------------------------------------------------------------------------- 3. 前提C: Discord Bot の作成(オーナー作業) -------------------------------------------------------------------------------- 1. https://discord.com/developers/applications で New Application → Bot を開き 「Reset Token」でトークンを発行。トークンは PC 側ブリッジの .env にだけ入れる。 チャット・ファイル・Muse の記憶に貼らない/書かない。 2. Bot ページの Privileged Gateway Intents で MESSAGE CONTENT INTENT をオン (これを忘れるとログイン時に disallowed intents で落ちる)。 3. OAuth2 → URL Generator で scope=bot、権限は View Channel / Send Messages / Connect / Speak の4つだけを与え、自分の非公開サーバーへ招待する。 4. ID の収集(Discord の開発者モードをオンにして右クリック→IDをコピー): - オーナー自身のユーザーID - 対象テキストチャンネルの ID(「ボイスチャンネルのID」ではない。 VC は !join 時にオーナーが入っている場所を自動で見るので ID は不要) -------------------------------------------------------------------------------- 4. ブリッジ本体(オーナーPC、Node.js) -------------------------------------------------------------------------------- ■ 依存ライブラリと選定理由(package.json) discord.js ^14.16.0 Bot 本体 @discordjs/voice ^0.19.0 音声送信 @snazzah/davey ^0.1.12 DAVE(後述)対応 opusscript ^0.0.8 Opus エンコーダ(純JS)← ★重要(下記) libsodium-wrappers ^0.7.15 音声暗号化 prism-media ^1.3.5 変換パイプライン(システム ffmpeg を使う) dotenv ^16.4.5 .env 読込 ※ HTTP API は express を使わず node:http 標準のみ(PC 側 install を軽くする)。 ★DAVE: 2026年3月から Discord の音声は E2EE の DAVE が必須。送信だけでも 対応ライブラリ(@discordjs/voice 0.19系 + davey)が前提になる。 ★@discordjs/opus を使わない理由: ネイティブモジュールで、プリビルドバイナリの 公開範囲が狭い。Node 24(ABI node-v137)/ Node 26(node-v147)向けは 404 で、 フォールバックのソースビルドは Visual Studio の VC++ ツールセットが無い PC では 失敗する。ブリッジは音声「送信(再生)」専用なので、純JSの opusscript で十分。 → ネイティブ依存を増やす前に「再生専用なら純JSで足りる」ことを疑うのが教訓。 ■ モジュール構成(src/) index.mjs HTTP API + Discord Bot 本体、コマンド処理 voice.mjs Irodori 合成 → wav 保存 → AudioPlayer 再生(発話は直列化。 /say は再生完了まで待って応答する) capture.mjs 5秒間隔キャプチャ + 変化検出(バイト長変化率 >8% かつ サンプルバイトのハッシュ変化で「場面が変わった」と判定。 scene イベントは最大1回/20秒に間引き) queue.mjs イベントキュー + 計量ログ data/meter.jsonl への追記 listen.mjs 音声受信(第2期)。LISTEN_ENABLED=false で無効化し本流に接続しない scripts/capture.ps1 System.Drawing でプライマリ画面を JPEG 保存 (テンポラリ経由の原子的書き込み。Node 側が5秒ごとに単発実行する) ■ HTTP API(:8377、0.0.0.0 バインドだが到達は tailnet 内のみ) GET /health {ok, discordReady, inVoice, sessionActive, pendingEvents, listenEnabled} GET /queue 未 ack イベント一覧 {events: [...]} POST /queue/ack {ids: [...]} 処理済みイベントの確認(必須。後述) POST /say {text} Irodori で合成し VC で再生。 409 = Bot が VC 未参加 / 503 = Irodori 停止(エラーイベントも登録) POST /post {text} 対象テキストチャンネルへ投稿 GET /frame/latest 最新キャプチャ JPEG POST /session {active: true/false} 画面ウォッチの開始/停止 ■ イベントモデル(キュー + ack 方式) ブリッジは出来事をキューに積み、Muse 側が GET /queue で読み、 処理したものを POST /queue/ack で消す。ack するまでイベントは残り続ける (= ack し忘れると同じ出来事で何度も起こされる。ワーカーへの指示に明記必須)。 イベント種別: text 対象チャンネルでのオーナーの発言(コマンド以外) scene セッション中の画面変化(上記の間引きあり) session 画面ウォッチの開始/終了 error ブリッジ側の失敗(Irodori 停止など) フィルタ: 「対象テキストチャンネル × オーナーのユーザーID」の発言だけを キューに入れる。他人の発言・他チャンネルは拾わない。 計量: キュー登録と /say・/post の全呼び出しを data/meter.jsonl に JSON Lines で記録。 ■ オーナー用コマンド(対象テキストチャンネルで打つ) !join オーナーが今いる VC に Bot が参加 !leave VC から退出 !session on 画面ウォッチ開始(off で停止) ■ .env 項目(PC 側のみ。実値はここに書かない) DISCORD_TOKEN Bot トークン DISCORD_OWNER_ID オーナーのユーザーID DISCORD_TEXT_CHANNEL_ID 対象テキストチャンネルの ID IRODORI_URL http://localhost:8088 BRIDGE_PORT 8377 LISTEN_ENABLED false(第2期まで false 固定) ■ 起動と成功の目印 npm install → npm start ログに「bridge HTTP API listening on 0.0.0.0:8377」と 「discord ready as 」の2行が出れば起動完了。 Irodori-TTS-Server が別途起動している必要がある。 -------------------------------------------------------------------------------- 5. Muse 側の接続(コネクタ + フック) -------------------------------------------------------------------------------- ■ コネクタ / ヘルパー 自作コネクタ(例: custom.discord-bridge)を置き、config.json に {host: , port: 8377} を持たせる。 ヘルパー(bridge.py)は Irodori の speak.py と同じ方式で HTTPS_PROXY のポートを 3130 に差し替えてトンネル到達する。 サブコマンド: health / queue / ack / say --text / post --text / session --state on|off / frame --output ■ フック設計(★この運用の心臓部) - ポーリングスクリプト(bash)が30秒間隔で bridge.py queue を実行するだけ。 キューが空 → silent(モデルは一切起きない。待機コストはゼロ)。 イベントあり → wake し、イベント一覧を payload としてワーカー(Muse)に渡す。 - ワーカーへの指示に必ず含めること: 1. 正本はキューの現在状態(wake の payload は古い場合があるので取り直す) 2. text への応答は post が基本、VC 参加中で口頭が自然なら say 3. scene は frame を取得して「見てから」反応する(見ずにコメントしない) 4. 処理したイベントは必ず ack する 5. 異常時(ブリッジ到達不可・Discord未接続・say の 503)は担当チャットへ報告 6. Irodori が落ちている時に仮の声で代替しない 7. 対応件数と内容をサマリに残す(計量用) ■ 反応運用のルール(実地で調整した結果) - 画面セッション中、ゲーム画面が見えている間は、エラーや節目でなくても 「場面が読み取れる変化」があれば節目ごとに短く声をかける。 (当初「日常変化は流す」に寄せたら、オーナーから「戦っているのに コメントが来ない」と指摘を受けた。静止画ベースでは地味な変化の連続に 見えるため、ゲーム中は厚めに寄せるのが正解だった) - 作業系の画面では日常的な変化は流してよい(割り込み過多を避ける)。 - 届くのは数十秒おきの静止画1枚。連続した戦闘の瞬間には当たらない。 分からない場面は断定せず、その制約はオーナーにも明示する。 -------------------------------------------------------------------------------- 6. トークン経済(実測つき)★設計を誤ると桁が変わる -------------------------------------------------------------------------------- - コストを決めるのは音声・映像の量ではない。PC 側処理(合成・文字起こし・ キャプチャ)はローカル GPU で Muse のトークンを一切食わない。 コスト =「Muse の起床回数 × 1回あたりの文脈量」だけ。 - 起床1回あたりの実測アンカー(この環境): - フル装備の起床(システム指示+記憶一式): 入力 約22万〜102万トークン - 軽い見回り型の起床: 約32万/回 - ブリッジのワーカー起床: 入力 約15万〜60万、平均 約28万/回 - 初回セッション実績: 起床18回で合計 約503万トークン - 見積もり(週枠10億トークン = 1%は1,000万で換算): - 待機のみ: ほぼ 0%/時 - 相槌中心(約10起床/時): 0.3〜0.5%/時 - 活発な音声会話(20〜30起床/時): 0.6〜1.5%/時 - 2時間セッション: 約1.2〜3% - 比較: 通常の定常業務は約0.7%/日(週 約4.8%) - アンチパターン(実例): 「1分ごとにモデルがキューを見に行く」設計は 約2,500万/時(2.5%/時)に膨らむ。実際に別件の見回り仕組みでは 47回で約1,522万トークンを消費した。ポーリングは必ずスクリプトに閉じ、 モデルはイベント駆動でのみ起こすこと。 - scene 洪水への注意: ゲーム中は画面が常に変わるため、変化検出だけでは scene が約30秒ごとに起床を生む。長時間セッションでは上記見積もりを 上回るペースになり得る。間引き(例: 場面コメントを数分に1回)は用意できるが、 本件のオーナーは「反応速度を下げる方向は不可、今のままで」と判断し、 反応速度優先で現行運用を維持している。どちらに倒すかはオーナーの判断事項。 - 検証法: セッション後に meter.jsonl のイベント数・発話数と、 実行記録側の実トークンを突き合わせて見積もりと照合する。 -------------------------------------------------------------------------------- 7. 調査結論(できる / できない)2026-11-06 時点 -------------------------------------------------------------------------------- テキストチャンネルの読み書き ......... ◎ Bot API の守備範囲 VC に参加して喋る(音声送信).......... ◎ 公式対応。DAVE 対応ライブラリが前提 オーナーの声を聞く(音声受信)........ △ DAVE 移行後の受信スタックが不安定 (Python 側は復号不可の報告、Node 側 davey は事例あり)。第2期の実験扱い。 加えて音声認識(PC の GPU で Whisper 系)と発話区切り・応答遅延の設計が要る 画面配信(Go Live)の Bot 受信 ........ ✕ API が存在しない(長年の要望未実装) selfbot で画面配信を見る .............. ✕ 規約違反。不採用 PC 側キャプチャで画面を一緒に見る ..... ◎ Discord を経由しないので規約問題なし。 ただし「数秒遅れのコマ送り閲覧」であり、動画理解ではない -------------------------------------------------------------------------------- 8. トラブルシューティング(実際に踏んだもの) -------------------------------------------------------------------------------- - npm install が @discordjs/opus で失敗する ログ: node-pre-gyp がプリビルドを取りに行き 404 → ソースビルドが 「gyp ERR! find VS ... missing any VC++ toolset」で失敗。 → 対処: @discordjs/opus を依存から外し opusscript に置換(第4節)。 Visual Studio を入れる方向でも解けるが、数GB 級の導入になるので非推奨。 - npm start で「Missing script: "start"」 → npm が見ているフォルダに正しい package.json が無い。zip 展開で フォルダが二重構造(discord-bridge\discord-bridge\...)になっているのが典型。 対処: 作業フォルダで dir を打ち package.json が同階層にあることを確認する。 package.json 単体を渡して上書きさせるのが最も確実だった。 中身の確認には findstr "@discordjs/opus" package.json が使える (何も出なければ新版に置き換わっている)。 - npm start が終了コード1で落ちる(npm のデバッグログには原因が残らない) プログラム側の終了経路は2つだけで、コンソールにどちらかが出る: 「Missing required env: ...」→ .env が無い/項目が空/「.env.txt」になっている 「Discord login failed: ...」→ トークン誤り、または MESSAGE CONTENT INTENT 未有効 ※ npm の _logs フォルダのデバッグログは「何のコマンドが・終了コードいくつで」 までしか記録しない。install と start のログが混在するので時刻と title 行を見る。 本当のエラー文はコンソール表示にしかない、という前提で案内すること。 - Muse 側からブリッジに届かない / RemoteDisconnected → ブリッジのプロセス自体が止まっている(コンソールを閉じた/Ctrl+C/PC スリープ) ケースがほとんど。異常終了とは限らず、オーナーが意図して終わらせた場合も同じ見え方。 health を数回叩いて応答がなければ「止まっている」と判断してよい。 - /say が 503 → Irodori サーバー停止。PC 側でサーバーを起動し直す。 - /say が 409 → Bot が VC 未参加。!join から。 - 画面キャプチャが撮れない → ロック画面中は撮れない。実行ユーザーの権限も確認。 -------------------------------------------------------------------------------- 9. セキュリティと規約 -------------------------------------------------------------------------------- - Bot トークンは PC 側 .env のみ。チャットに貼らせない、Muse 側に保存しない。 - ブリッジと Irodori サーバーは tailnet 内限定。インターネット公開・ ポートフォワードはしない。ブリッジの HTTP API に認証はないので、 tailnet の閉域性がそのまま防御になる(公開したら即座に破綻する前提で扱う)。 - 対象はオーナー自身の PC・画面・声だけ。他者の画面キャプチャや他者の声の録音は扱わない。 - Bot の可視範囲はオーナーの非公開サーバー内に閉じる。 - selfbot(ユーザートークンの自動操作)は規約違反。画面配信の受信目的でも使わない。 -------------------------------------------------------------------------------- 10. 再現チェックリスト(順序) -------------------------------------------------------------------------------- [ ] PC に uv / FFmpeg / Node.js LTS(20+) / ffmpeg の PATH を用意 [ ] Irodori-TTS と Irodori-TTS-Server を入れ、cu128 で同期 [ ] VoiceDesign で声候補を試聴 → 正本 wav を決め voices\<声ID>.wav に登録 [ ] サーバー .env(チェックポイント v4.1-Small、既定の声、:8088)を設定して起動 [ ] /health と /v1/audio/voices で確認 [ ] Tailscale で PC と Muse VM を接続(Muse 側は 3130 プロキシ経由) [ ] Muse 側に Irodori 用コネクタ+ヘルパーを作り、往復生成テスト [ ] Discord Bot 作成 → MESSAGE CONTENT INTENT 有効化 → 非公開サーバーへ招待 [ ] ブリッジを配置し .env(トークン/自分のID/テキストチャンネルID)を記入 [ ] npm install(opus 問題が出たら opusscript 版 package.json か確認) [ ] npm start → 2行の成功ログ → Muse 側から health で discordReady を確認 [ ] Muse 側にブリッジ用コネクタ+ヘルパーと、スクリプトポーリングのフックを用意 [ ] 実地テスト: テキスト投稿 → !join → say で発話 → !session on → frame 確認 [ ] セッション後に meter.jsonl と実トークンを突き合わせて見積もりを検証 -------------------------------------------------------------------------------- 11. 既知の制限(盛らないこと) -------------------------------------------------------------------------------- - 画面は静止画のコマ送り(取得は5秒間隔、通知は最大1回/20秒、起床は約30秒周期)。 戦闘のような速い場面の「瞬間」には構造的に当たらない。 - 応答遅延は数秒級(起床→frame 取得→判断→合成→再生)。 - 音声受信(聞く側)は未実装。第2期は DAVE 受信の実証から。 - DAVE 前提のため、Discord 側の仕様変更にライブラリ更新で追随し続ける必要がある。 - ブリッジは PC の電源・スリープに依存する。止まっていれば何も起きない (それが正常な「閉じた」状態でもある)。 ================================================================================ 以上。この文書自体に実値(トークン・ID・アドレス)は含まれていないので、 共有時はこのファイルをそのまま渡してよい。 ================================================================================