ガイド: 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 ファイルを同じ場所に置きます。
- mojiokoshi-subscribe.js(参照実装。中身を変えずに置く)
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>
file://で開くと、ブラウザが ES モジュールの読み込みを拒むことがあります。 2 つのファイルは、GitHub Pages・Cloudflare Pages などの静的ホスティングか、ローカルの静的サーバー(npx serveなど)に置いてください。1 ファイルにしたいときは、参照実装の中身を<script type="module">に貼り付けて、importの行とexportを消します。overlay.htmlの背景は透明です。OBS のブラウザソースの幅・高さは配信の解像度に合わせ、「カスタム CSS」は空にしておきます。overlay.htmlは、partialを少し薄く表示し、発話が確定したら普通の濃さにしています。同じ発話はutteranceIdで置き換わるので、行が増え続けることはありません(改訂モデル)。overlay.htmlは、話している表示(●)をspeech_started/speech_ended(参照実装ではstate.speaking)から作っています。
セキュリティ
- トークンはフラグメント(
#token=)に入れる。 クエリ(?token=)に入れたトークンは、ページを置いたサーバーのアクセスログやRefererに残ります。 - API キーをページに埋め込まない。 配信画面は他人も見るので、ページには
transcript:readのセッショントークンだけを渡します。 - 表示名と発言は
textContentで入れる(innerHTMLを使わない)。 - 文字おこしくんには、トークンを 1 つずつ取り消す仕組みが無いので、自分で発行するトークンの有効期間は、必要な長さより長くしない(セッショントークンの有効期間)。
- Discord のリンクを配信に映してしまったときは、使い方の表示ページと OBSの手順で、文字起こしを止めて始め直します。
よくある作り方
| 作りたいもの | ヒント |
|---|---|
| 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 変数(文字色・影・大きさ)で変えられるようにする。