ガイド: 記録と書き出し

あなたは文字起こしの記録を、議事録として残したり、検索できるようにしたり、別のシステムに取り込んだりできます。

2 つの取り方

方法 いつ 長所 短所
HTTP で書き出す(GET /v1/sessions/:id/transcript) セッションが終わった後(途中でも可) 全文が確実に取れる。整形後(refined)のテキストと整形前(rawText)の両方を取れる リアルタイムではない
WebSocket で受け取りながら保存する セッション中 発話をリアルタイムに保存できる 切断中の発話は取り戻せない。最後に HTTP で書き出して比べると確実

記録はセッションの終了から保持期間(Discord サーバーのレベルにより 30〜365 日)が過ぎると消えます。 記録が消えた後は、書き出しが 404 session_not_found になり、記録は戻せません。残したい記録は保持期間の内に書き出してください。

HTTP で書き出す

# JSON で書き出す(既定)
curl -s "$BASE/v1/sessions/$SID/transcript" -H "Authorization: Bearer $TOKEN" -o transcript.json

# テキストで書き出す(タイムゾーンは tz で指定する。既定は Asia/Tokyo)
curl -s "$BASE/v1/sessions/$SID/transcript?format=text&tz=UTC" -H "Authorization: Bearer $TOKEN"

# CSV で書き出す(見出しは「日時,話者,発言」。Excel で開けるように、先頭に BOM が付く)
curl -s "$BASE/v1/sessions/$SID/transcript?format=csv" -H "Authorization: Bearer $TOKEN" -o transcript.csv

JSON の形:

{
  "sessionId": "0f0c3c1e-…",
  "label": "#雑談 VC",
  "speakers": [{ "speakerId": "discord:1", "displayName": "たろう", "avatarUrl": "https://cdn.discordapp.com/…" }],
  "utterances": [
    {
      "utteranceId": "utt_01",
      "trackId": "discord:1",
      "speakerId": "discord:1",
      "speakerName": "たろう",
      "stage": "refined",
      "text": "こんにちは、今日はよろしくお願いします。",
      "rawText": "こんにちは今日はよろしくお願いします",
      "startedAt": 1790350826814
    }
  ]
}

WebSocket で受け取りながら保存する

保存は utteranceId をキーにした upsert にし、revision が大きいときだけ上書きします。文字おこしくんは、同じ発話を final → refined と送り、再接続の welcome.recent でも同じ発話をもう一度送ります。

CREATE TABLE utterances (
  session_id   TEXT    NOT NULL,
  utterance_id TEXT    NOT NULL,
  track_id     TEXT    NOT NULL,
  speaker_id   TEXT,               -- unattributed なら NULL
  stage        TEXT    NOT NULL,   -- final / refined(partial は保存しない)
  revision     INTEGER NOT NULL,
  text         TEXT    NOT NULL,
  started_at   INTEGER NOT NULL,   -- エポックミリ秒
  PRIMARY KEY (session_id, utterance_id)
);

-- 新しい改訂のときだけ上書きする(古い・重複した改訂は何もしない)
INSERT INTO utterances VALUES (:session_id, :utterance_id, :track_id, :speaker_id, :stage, :revision, :text, :started_at)
ON CONFLICT (session_id, utterance_id) DO UPDATE SET
  track_id = excluded.track_id, speaker_id = excluded.speaker_id, stage = excluded.stage,
  revision = excluded.revision, text = excluded.text, started_at = excluded.started_at
WHERE excluded.revision > utterances.revision;

セッションを探す

終わったときに書き出す(Webhook)

Discord サーバーの管理者が Webhook の送信先を登録し、送る出来事に「文字起こしの終了」を選ぶと、文字おこしくんはセッションが終わるたびに session.ended をあなたのサーバーへ送ります(Webhook リファレンス)。あなたのサーバーは session.ended を受け取ったら、data.sessionId のセッションの記録を書き出せます。文字おこしくんは確定した発言を保存し終えてから session.ended を送るので、あなたのサーバーが session.ended を受け取ってから書き出す記録には終わりまでの発言が入ります。新しく終わったセッションを探して一覧を読み直す必要はありません。

署名を確かめる依存なしの参照実装 examples/mojiokoshi-webhook.ts と依存なしのクライアント examples/mojiokoshi-http.ts を使う例:

// archive-webhook.mts — node archive-webhook.mts(Node 22.18 以降。2 つの例のファイルを同じフォルダに置く)
import { writeFile } from "node:fs/promises";
import { createServer } from "node:http";
import { createMojiokoshiHttp } from "./mojiokoshi-http.ts";
import { MojiokoshiWebhookError, verifyMojiokoshiWebhook } from "./mojiokoshi-webhook.ts";

const api = createMojiokoshiHttp({ apiKey: process.env.MOJIOKOSHI_API_KEY! }); // transcripts:read
const secret = process.env.MOJIOKOSHI_WEBHOOK_SECRET!; // whsec_…(ソースに書かず、環境変数などから読む)

createServer((request, response) => {
  const chunks: Buffer[] = [];
  request.on("data", (chunk: Buffer) => chunks.push(chunk));
  request.on("end", async () => {
    let event;
    try {
      event = await verifyMojiokoshiWebhook(Buffer.concat(chunks), request.headers, secret); // JSON として読む前の生の本文で確かめる
    } catch (error) {
      response.writeHead(error instanceof MojiokoshiWebhookError ? 400 : 500).end();
      return;
    }
    response.writeHead(204).end(); // 書き出す前に 2xx を返す(文字おこしくんは 15 秒で待つのをやめる)
    if (event.type !== "session.ended") return; // session.started・webhook.test・知らない出来事には何もしない
    try {
      // 同じ出来事が 2 回届いても、同じファイルを書き直すだけで済む
      const transcript = await api.exportTranscript(event.data.sessionId);
      await writeFile(`${event.data.sessionId}.json`, JSON.stringify(transcript, null, 2));
    } catch (error) {
      console.error("書き出せませんでした", event.data.sessionId, error);
    }
  });
}).listen(8787);

決まった時刻に一覧を読む

transcripts:read の API キーがあれば、Discord サーバー(テナント)のセッションを一覧できます。

# 終わったセッションを新しい順に一覧する(続きは、nextCursor を cursor に渡して取る)
curl -s "$BASE/v1/sessions?status=ended&limit=20" -H "Authorization: Bearer $KEY"

毎日決まった時刻に、前回の実行の後に終わったセッションを書き出す、という使い方ができます。依存なしのクライアント examples/mojiokoshi-http.ts を使う例:

// archive.mts — node archive.mts(Node 22.18 以降。mojiokoshi-http.ts を同じフォルダに置く)
import { writeFile } from "node:fs/promises";
import { createMojiokoshiHttp, MojiokoshiHttpError } from "./mojiokoshi-http.ts";

const api = createMojiokoshiHttp({ apiKey: process.env.MOJIOKOSHI_API_KEY! }); // transcripts:read
const since = Number(process.env.SINCE ?? 0); // 前回の実行時刻(エポックミリ秒)

// セッションは新しい順に届くので、前回の実行の 1 日前より前に作られたセッションまで来たら、ループを止める(nextCursor は allSessions がたどる)。
for await (const session of api.allSessions({ status: "ended", limit: 100 })) {
  if (session.createdAt < since - 86_400_000) break;
  if ((session.endedAt ?? 0) < since) continue;
  try {
    const transcript = await api.exportTranscript(session.sessionId); // 429 なら、api が Retry-After の秒数だけ待って、自動でやり直す
    await writeFile(`${session.sessionId}.json`, JSON.stringify(transcript, null, 2));
  } catch (error) {
    if (error instanceof MojiokoshiHttpError && error.code === "session_not_found") continue; // 保持期間を過ぎて、記録が消えた
    throw error;
  }
}

エンドポイントの詳細は、HTTP API リファレンスにあります。

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

文字おこしくんのセッションの文字起こしを、終わった後に書き出して SQLite に保存するスクリプトを書いてください。
仕様: https://moji.kaitoranma.com/docs/guides/archive.md

- 書き出しは GET /v1/sessions/:id/transcript(Authorization: Bearer <transcript:read のトークンか API キー>)。JSON の utterances を保存する。
- 保存は (session_id, utterance_id) を主キーにした upsert で、revision が大きいときだけ上書きする(WebSocket から保存した行と突き合わせても壊れないように)。
- 429 rate_limited なら Retry-After の秒数だけ待ってやり直す。404 session_not_found は保持期間を過ぎたものとして記録して飛ばす。
- 資格情報は環境変数から読み、ログに出さない。
- HTTP は注入した fetch で呼び、偽の応答でテストする。
- 文字おこしくんの Webhook の session.ended を受けて書き出すときは、署名を JSON として読む前の生の本文で確かめ(https://moji.kaitoranma.com/docs/webhooks.md)、書き出す前に 2xx を返す。同じ webhook-id が 2 回届いても記録が壊れないようにする。