ガイド: OBS・自作のオーバーレイ

あなたは配信の字幕や、話者ごとの吹き出しを自分で作れます。文字おこしくんは Subscribe の WebSocket につなぐページのオリジンを制限していないので、あなたのページはどこに置いても(OBS のブラウザソースからも)Subscribe につなげます。

方法を選ぶ

方法 手間 できること
Discord の /link の返答のボタンで開く、公式の配信用オーバーレイのリンク(…#token=…&overlay=1)を、そのまま OBS に入れる なし 公式のデザインで字幕を出せる。URL パラメータと OBS の「カスタム CSS」で表示・色・大きさを変えられる(表示ページの URL パラメータと CSS)。view=youtube では、文字おこしくんが YouTube のライブチャットと同じ要素の形で発言を出すので、YouTube のコメント欄向けに公開されている CSS をそのまま使える
自作の HTML + 参照実装(このページ) HTML 1 枚 レイアウト・演出を自由に作れる

自作の HTML(話者ごとに最後の一言を出す字幕)

次の 2 ファイルを同じ場所に置きます。

  1. mojiokoshi-subscribe.js(参照実装。中身を変えずに置く)
  2. overlay.html(このページのコード)

OBS のブラウザソースに https://<置いた場所>/overlay.html#token=<トークン>&server=wss://<host>/v1/subscribe を入れます。トークンの取り方はクイックスタートを見てください。

<!doctype html>
<html lang="ja">
<meta charset="utf-8">
<title>captions</title>
<style>
  html, body { margin: 0; background: transparent; }
  body { font: 700 36px/1.4 "Noto Sans JP", sans-serif; color: #fff; text-shadow: 0 0 6px #000, 0 0 2px #000; }
  #captions { position: fixed; left: 40px; right: 40px; bottom: 40px; display: grid; gap: 8px; }
  .line { transition: opacity .3s; }
  .line.partial .text { opacity: .7; }
  .line.speaking .name::after { content: " ●"; color: #6f6; }
  .name { color: #ffd54f; margin-right: .5em; }
  .status { position: fixed; top: 8px; right: 8px; font-size: 14px; opacity: .6; }
</style>
<div id="captions"></div>
<div class="status" id="status"></div>
<script type="module">
  import { subscribe, speakerName } from "./mojiokoshi-subscribe.js";

  // トークンは URL のフラグメント(#)に入れる。フラグメントは、ページを置いたサーバーのログや Referer に残らない。
  const params = new URLSearchParams(location.hash.slice(1));
  const token = params.get("token");
  const server = params.get("server");

  let frame = 0;
  const render = (state) => {
    // 描画は 1 フレームに 1 回にまとめる(partial は数百 ms ごとに届く)
    if (frame) return;
    frame = requestAnimationFrame(() => {
      frame = 0;
      const lastByKey = new Map(); // 話者(不明ならトラック)→ 最後の発話
      for (const u of state.utterances.values()) {
        const key = u.speaker.kind === "unattributed" ? `track:${u.trackId}` : u.speaker.speakerId;
        const last = lastByKey.get(key);
        if (!last || last.startedAt <= u.startedAt) lastByKey.set(key, u);
      }
      const lines = [...lastByKey.values()]
        .sort((a, b) => a.startedAt - b.startedAt)
        .slice(-3) // 最近話した 3 人まで
        .map((u) => {
          const line = document.createElement("div");
          line.className = `line${u.stage === "partial" ? " partial" : ""}${state.speaking.has(u.trackId) ? " speaking" : ""}`;
          const name = document.createElement("span");
          name.className = "name";
          name.textContent = speakerName(state, u); // HTML として解釈させない
          const text = document.createElement("span");
          text.className = "text";
          text.textContent = u.text;
          line.append(name, text);
          return line;
        });
      document.getElementById("captions").replaceChildren(...lines);
    });
  };

  if (!token || !server) {
    document.getElementById("status").textContent = "URL に #token=…&server=… を付けてください";
  } else {
    subscribe({
      url: server,
      token,
      onChange: render,
      onStatus: (status, detail) => {
        const labels = { connecting: "接続中…", reconnecting: "再接続中…", ended: "終了しました", failed: `接続できません(${detail?.code})` };
        document.getElementById("status").textContent = labels[status] ?? "";
      },
    });
  }
</script>
</html>

セキュリティ

よくある作り方

作りたいもの ヒント
1 行の字幕(テロップ) 全話者の中で startedAt が最新の発話を 1 つだけ出す
話者ごとの定位置の吹き出し 話者の登場順(state.speakers の挿入順)で位置を決める。unattributed の発話は、トラック単位で出す
過去ログのスクロール sortedUtterances(state) を下から積む。partial は最後に来る
アバター付き welcome.speakers[].speaker.avatarUrl と speaker_upserted の speaker.avatarUrl を画像に使う(http / https の URL だけが届く)

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

OBS のブラウザソースで使う、文字おこしくんのリアルタイム字幕オーバーレイ(HTML 1 枚 + 参照実装 1 ファイル)を作ってください。
仕様: https://moji.kaitoranma.com/docs/llms.txt
参照実装: https://moji.kaitoranma.com/docs/examples/mojiokoshi-subscribe.js(そのまま同じフォルダに置いて import する)

- 接続先とトークンは location.hash の server= と token= から読む(クエリには入れない)。API キーは使わない。
- 背景は透明。画面下に、話者ごとに最後の発話を最大 3 行、話者名つきで出す。partial は薄く、final / refined は普通に。
- 発話は utteranceId で置き換わる(参照実装の state.utterances は Map)。行を追記しない。
- 表示名・発言は textContent で入れる。描画は requestAnimationFrame で 1 フレーム 1 回にまとめる。
- 接続状態(再接続中・終了・失敗)を右上に小さく出す。
- 見た目は CSS 変数(文字色・影・大きさ)で変えられるようにする。