ガイド: Discord・他のボットへの中継

Discord のテキストチャンネルに発言を流すだけなら、プログラムは要りません。Discord サーバーの管理者が管理画面で「発言をテキストチャンネルにも流す」を選ぶと、文字おこしくんが、発言を話した人の名前とアイコンでテキストチャンネルに投稿します(使い方の発言をテキストチャンネルにも流す)。

あなたのボットは、文字起こしの結果を、別の Discord チャンネル・Slack・Matrix・翻訳や要約の処理などに流せます。あなたのボットはサーバー側で動くので、API キーを安全に持てます。

形

  1. あなたのボット(サーバー側)が、HTTP(API キー)で動いているセッションを探し、transcript:read のトークンを発行します。Discord サーバーの管理者が文字おこしくんの Webhook の送信先を登録し、送る出来事に「文字起こしの開始」を選んでいれば、ボットはセッションを探す代わりに、文字おこしくんが送る session.started を受け取ってトークンを発行できます(Webhook リファレンス)。
  2. ボットが、WebSocket /v1/subscribe で発話を受け取ります。
  3. ボットが、受け取った発話を転送先(Discord の Webhook・Slack・翻訳 API …)に送ります。

このページの「Discord の Webhook」は Discord のチャンネルにメッセージを投稿するための Discord の仕組みです。Discord の Webhook では、あなたのボットが Discord へメッセージを送ります。文字おこしくんの Webhook(Webhook リファレンス)では、文字おこしくんがあなたのボットへ文字起こしの開始と終了を知らせます。

押さえどころ

  1. partial は流さない。 partial は数百 ms ごとに書き換わるので、チャットに流すと荒らしになります。final から流します。
  2. refined は「前に流した発言の修正」として扱う。 同じ utteranceId の final を既に送っていたら、新しいメッセージではなく編集(Discord なら Edit Message)にします。utteranceId → 転送先のメッセージ ID の表を持ってください。refined は final の数秒後に来ることも、来ないこともあります。
  3. revision を比べる。 再接続すると、welcome.recent で同じ発話がもう一度届きます。送信済みの revision 以下の改訂が届いたら、ボットは何もしません(二重投稿を防ぐ)。
  4. 転送先のレート制限を守る。 Discord の Webhook などには送信の上限があります。発話が多いときは、数秒ぶんの発話をまとめて 1 通にするなど、キューで送る量を平らにしてください。
  5. 期限の前にトークンを取り直す。 再接続の前に、API キーで新しいトークンを発行してから使います(トークンの期限)。
  6. 終わりを検知する。 session_ended が来たら、そのセッションの購読をやめて、次のセッションを探しに戻ります。Discord サーバーの管理者が文字おこしくんの Webhook の送信先を登録し、送る出来事に「文字起こしの開始」を選んでいれば、ボットは次のセッションが始まったことを session.started で知ることができます。

例: Discord の Webhook に中継する(Node 22)

参照実装 mojiokoshi-subscribe.js を使った例です。この例のボットは、Discord の Webhook の ?wait=true で送信したメッセージの ID を受け取り、refined が来たら、そのメッセージを編集します。

import { subscribe, speakerName } from "./mojiokoshi-subscribe.js";

const WEBHOOK = process.env.DISCORD_WEBHOOK_URL; // https://discord.com/api/webhooks/<id>/<token>
const posted = new Map(); // utteranceId → { messageId, revision }
const queue = [];

function relay(state, message) {
  if (message.type !== "utterance") return;
  const u = message.utterance;
  if (u.stage === "partial" || u.text.trim() === "") return;
  const sent = posted.get(u.utteranceId);
  if (sent !== undefined && sent.revision >= u.revision) return; // 再接続の welcome.recent などで重複した
  queue.push({ utteranceId: u.utteranceId, revision: u.revision, content: `**${speakerName(state, u)}**: ${u.text}` });
}

// 転送先のレート制限を守るため、1 秒に 1 件ずつ送る(編集も 1 件と数える)
setInterval(async () => {
  const item = queue.shift();
  if (item === undefined) return;
  const sent = posted.get(item.utteranceId);
  const body = JSON.stringify({ content: item.content, allowed_mentions: { parse: [] } }); // メンションさせない
  const headers = { "content-type": "application/json" };
  if (sent === undefined) {
    const response = await fetch(`${WEBHOOK}?wait=true`, { method: "POST", headers, body });
    if (response.ok) posted.set(item.utteranceId, { messageId: (await response.json()).id, revision: item.revision });
  } else {
    await fetch(`${WEBHOOK}/messages/${sent.messageId}`, { method: "PATCH", headers, body });
    sent.revision = item.revision;
  }
}, 1000);

subscribe({
  url: process.env.MOJIOKOSHI_SUBSCRIBE_URL,
  token: process.env.MOJIOKOSHI_TOKEN,
  onChange: relay,
  onStatus: (status, detail) => console.error(status, detail?.code ?? ""),
});

翻訳・要約に流すとき

コーディングエージェント向けのプロンプト

文字おこしくんの Subscribe WebSocket API から発話を受け取り、Discord の Webhook に中継する Node 22 のボットを書いてください。
仕様: https://moji.kaitoranma.com/docs/llms.txt 、ガイド: https://moji.kaitoranma.com/docs/guides/bot-relay.md
参照実装 examples/mojiokoshi-subscribe.js を使う。

- partial は送らない。final を新しいメッセージとして送り、同じ utteranceId の refined(revision が大きい)は同じメッセージを編集する。utteranceId → { messageId, revision } を持つ。
- 送信済みの revision 以下の改訂は無視する(再接続の welcome.recent で重複して届く)。
- 送信はキューにして 1 秒に 1 件まで。allowed_mentions は空にする。
- 設定(Subscribe の URL、トークン、API キー、Discord の Webhook の URL、文字おこしくんの Webhook の署名のシークレット)は環境変数から読む。API キー・トークン・シークレットをログに出さない。
- session_ended で止まる。failed(unauthorized)になったら新しいトークンを取り直す関数を呼ぶ(取り方は差し替え可能にする)。
- 次のセッションは、文字おこしくんの Webhook の session.started(署名を生の本文で確かめる。https://moji.kaitoranma.com/docs/webhooks.md)で知るか、API キーで GET /v1/sessions?status=active を読んで探す(どちらにするかは差し替え可能にする)。
- 転送の判断(新規 / 編集 / 無視)を純粋関数にして、偽のメッセージ列でテストする。