この記事の目的
- 主旨: 個人・企業問わず、巨大プラットフォームYoutubeの拡散力を、"超低コストで、且つ安全に"、最大限に高める技術ナレッジの提供
- 前提: 今回は GCP の恩恵を最大限に享受する 構成(STT / TTS を自前 GPU で抱えない。従量で配信の長さに寄せる)
- 対象: YouTube / 配信コンテンツ向けの二言語会話レイヤ(会議ツールの代替ではない)
- やること: Master マイク → STT → 翻訳 → Guest 表示/TTS、Guest テキスト → 翻訳 → Master、のパイプラインをメモする
- 残すこと: 構成の骨格、Audio / unlock、CF・GCP・SA、middleware / CSP、localhost=Master、Cloud Run、防御の置き場所、Terms、コスト概算、レイテンシ目安
- 想定読者: 「Zoomでよくね?」と思う人、および実装側の判断材料が欲しい人
- 言語: 多言語可。本メモの実例は英語
会議じゃない。発信を国際化したい。
今回は 音声の文字起こしと読み上げを従量 API に寄せる。
常時 Whisper 箱を温める話ではない。配信が止まったらコストも止まる側に乗る。
ネットは世界につながっている。
なのに配信のコメント欄だけ、言葉の壁で黙るの、もったいない。
同時通訳。なぜ必要か
日本人(アジア圏)以外は、字幕に慣れていない。
母国語が世界標準の世界では——字幕を 煩わしく 感じることが多いのだそう(確かに..)。
海外圏では、会話は 音声で成立する のが当たり前に近い。
だから「翻訳テキストだけ出せば国際化」は、意味がゼロではないが、
相手から見ると、ほぼ 「離脱してください」 と受け取られやすいらしい。
ん~...文字の橋だけでは足りないのか。
読ませるのではなく、聞かせる。 だから STT → 訳 → TTS までが必要になるという訳です。
あと、この記事は、自身(自社)のプロダクトと巨大プラットフォーム YouTube を土台に、配信しながら海外の人と普通に会話する話で、目的が全く異なるので、"Zoom"で事足りる。という層は対象外です
自社アプリ、 Imaju(イマージュという) まわりの音楽制作配信で、「画面の向こうの英語圏と、作業を止めずに話したい」が先にあった。 利用者のディスカッション にも必要だったので機能を追加したお話です。
この構成なら、やろうと思えば何言語でもいけるじゃんと。(今回の実例は英語のみ)。
これ、何が嬉しいか
- なんせ巨大プラットフォーム YouTube を土台にできる(映像・集客・OBS はそのまま)
- 不特定多数という環境下で可能
- 圧倒的低コスト
- 何言語でもカスタマイズ可能(自前だもんね当たり前)
- 海外からの訪問者と 普通に会話できる(配信者は日本語、相手は英語)※要は、chat機能
- アクセスの制御が 手元でできる(Room 寿命・接続切替・発話内容単位でブロックなど)
- 作業しながら会話が成立する(Guest は text、Master は訳+音声)
- DAW を触りながらの音楽制作配信でも、Imaju 側と同じく「手は作業・口と耳は橋」で回せる
- 字幕だけ国際化にせず、音声で聞かせるので動的なアクセスにも離脱しにくい
予約のいらない雑談の国際化。なんて贅沢なんでしょう...![]()
日本てポップカルチャー強いから、アニメの話題なんかでかなり使えそうだな~...
手段の選定
インターネットはすでに世界に開いている。
足りないのは回線ではなく、言葉の橋のほうが大きい。
YouTube配信でZoomインストール、開いて下さいというコストはちょっと障壁高すぎ。
「配信の横で、会話だけ二言語にする」は、いまのクラウドで十分モダンに組める。
そんな構成で実装してみる。
ざっくり構成
localhost = Master(配信者・マイク)
Cloud Run (Docker / Next) = 公開 Front
Cloudflare Worker / DO = Room・扇・揮発
GCP STT / TTS (+ SA) = 音声
Gemini = 翻訳・文体・用語・中傷ゲート
管理者コンソール = 物理的に載せない(tunnel のみ)
- CF Worker / DO … 常時サーバを自前で抱えない
- GCP … 従量。配信の切れ目にコストが寄る
- Redis / WAF … 「動いた」で終わりにしない側の防御
- Admin … 本番に出さない。出さないのが最強の ACL
1. Audio と Buffer(OGG 16k で十分)
配信の横で回すなら、スタジオマスター品質は要らない。
OGG / 16kHz 帯がむしろ最良。STT に渡せれば勝ち。
// 雰囲気: MediaRecorder で ogg を積んで切る
const rec = new MediaRecorder(stream, { mimeType: 'audio/ogg; codecs=opus' });
const chunks: BlobPart[] = [];
rec.ondataavailable = (e) => {
if (e.data.size) chunks.push(e.data);
};
rec.onstop = async () => {
const blob = new Blob(chunks, { type: 'audio/ogg' });
const buf = new Uint8Array(await blob.arrayBuffer());
// → base64 にして Worker へ
ws.send(
JSON.stringify({
type: 'audio',
audio: btoa(String.fromCharCode(...buf)),
encoding: 'OGG_OPUS',
sourceLang: 'ja',
}),
);
};
oggだと5秒で24kくらいになる。激軽!
PCM 直送でもいい。要は Buffer を自分で握って、切った単位で送ること。
「ブラウザにお任せ再生だけ」だと、配信オペに耐えない。
2. 最初のアクセスで AudioContext に触れろ(スイッチがトリガー)
自動再生ポリシー対応
ユーザー操作なしに TTS を鳴らすと死ぬ。
だから「マイク ON / Speaker ON」のスイッチを、意図的に unlock のトリガーにする。
let unlocked = false;
async function unlockPlaybackOnce() {
if (unlocked) return;
const ctx = new AudioContext();
if (ctx.state === 'suspended') await ctx.resume();
const buf = ctx.createBuffer(1, 1, ctx.sampleRate);
const src = ctx.createBufferSource();
src.buffer = buf;
src.connect(ctx.destination);
src.start(0);
await ctx.close().catch(() => undefined);
unlocked = true;
}
// スイッチ(ユーザー操作)でのみ呼ぶ
async function onMicOnClick() {
await unlockPlaybackOnce();
await startMic();
}
3. CF / GCP STT・TTS・SA
Room は Cloudflare。音声は GCP。鍵は Service Account。
# wrangler 雰囲気
name = "yt_caption"
main = "index.ts"
# secrets: GEMINI_API_KEY, など
# STT_DO_URL は既存 STT Worker を呼ぶだけ(鍵を二重管理しない)
// STT 呼び出し雰囲気(Worker → stt_do)
await fetch(`${STT_DO_URL}/stt/${roomId}?region=USE`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
audio: audioBase64,
uid,
locale: 'ja-JP',
Terms: JSON.stringify(termKeys), // boost 用は key だけ
encoding: 'OGG_OPUS',
}),
});
# SA は Secret Manager / wrangler secret。リポジトリに置かない
gcloud iam service-accounts create stt-runner
# 鍵ファイルをチャットに貼った時点で試合終了....
TTS も同系統。Master 向け JA / Guest 向け EN。
従量なので「常時サーバーで自前 Whisper」より配信向き。
4. middleware
「ページ作っただけ」では、bot嵐で吹き飛んでしまうのでそのまま絶対に公開しない。
locale・予約パス・怪しいアクセスは 城手前で落とす。
// Next middleware 雰囲気
export function middleware(req: NextRequest) {
const { pathname } = req.nextUrl;
// speech-master は localhost / 開発面だけ、など
if (pathname.includes('speech-master') && !isLocalMaster(req)) {
return new NextResponse('Nope', { status: 404 });
}
// ban / rate / locale 正規化…
return NextResponse.next();
}
Master 入口を全世界に並べない。これだけで事故率が下がる。
5. CSP
XSS 一発で WebSocket も Audio も持っていかれるのでしっかり充てる。
// ヘッダ雰囲気(厳密値は環境に合わせる)
const csp = [
"default-src 'self'",
"connect-src 'self' https://*.workers.dev wss://*.workers.dev",
"media-src 'self' blob: data:",
"script-src 'self'",
"frame-ancestors 'none'",
].join('; ');
res.headers.set('Content-Security-Policy', csp);
ゆるくしすぎると意味がない。締めすぎると TTS の data: が死ぬ。
6. localhost は Master
公開 Front(Guest)と、配信者オペ(Master)は分ける。
http://localhost:3000/speech-master ← マイク・Block・用語・ニュアンス
https://example.com/{locale}/speech/{roomId} ← 視聴者 Chat
Master を Cloud Run の表に出して「便利!」は、だいたい後で泣く。
発信の司令塔は手元。観客面だけ遠くに出す。
7. Cloud Run × Docker で出す
Cloud Run 泣きそうなレベルで便利なのでありがたくいただく。
Front はコンテナでよい。毎日デプロイする前提ならなおさら。
# 雰囲気
FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-slim
WORKDIR /app
COPY --from=build /app ./
ENV NODE_ENV=production
CMD ["npm", "start"]
gcloud run deploy imaju-front --source . --region asia-northeast1
# 秘密は Secret Manager。イメージに焼かない
「Vercel にぽち」でもいい人がいる。
こっちは GCP 側の STT/TTS/SA と距離を近くする選択。好みというより運用の形。
8. Redis / WAF で防御する側
Room 本体は揮発でいい。
それでも 入口の防御と、必要ならレート・一時状態は別レイヤで持つ。
Edge (Cloudflare WAF / ルール)
→ Front (Cloud Run)
→ Worker / DO (Room)
→ (必要なら) Redis … レート・短命フラグ・共有カウンタ
// 雰囲気: 連打を止める
const n = await redis.incr(`chat:${endUserKey}:${minute}`);
if (n === 1) await redis.expire(`chat:${endUserKey}:${minute}`, 60);
if (n > 30) return new Response('slow down', { status: 429 });
「DO だけで全部」も美しい。
WAF と Redis を知った上で、どこに何を置くか選んでいるのがポイント。
(全部 Redis に永続する配信履歴を作る話ではない)
9. 管理者は物理的に載せない
運用モニター・支援者名簿・決済の生ログ閲覧——
本番 URL に載せない。tunnel のみ。サーバーへデプロイしない。
管理者コンソール = ローカル + tunnel
tunnel を閉じる = 外界から消える
= いちばん安いゼロトラスト
「Admin も同じ Cloud Run にパス切りで」は、便利な顔をした事故待ち。
出さない判断ができるかどうかがキモ、
専門用語(Terms)— ニックネームと DTM 語が誤訳る件
一般翻訳だけだと、配信チャットはすぐ壊れる。
-
ニックネーム …
markが動詞になったり、人名が普通名詞になったりする -
Imaju / 音楽制作 / DTM 語 …
Trackが車道、Busがバス、Plug-inがコンセント、みたいな事故
いちばんハズいのはここ。音楽を知っている相手ほど、誤訳がすぐバレる。
フランスなど、音楽語彙が日常・教育に根づいている圏では、なおさら「笑えない誤訳」になりやすい。
だから専門用語を Terms で寄せるのは必須 で、飾りではない。
Imaju まわりの音楽制作配信は、そういう語を普通に多用する。
Terms を Room に渡して、STT と翻訳の両方に効かせる。 設定しないと「動く翻訳」でも意味が死ぬ。
書き方(1行=3欄)
key|type|description
例:
mark|ニックネーム|人名として扱ってください
Track|DAW・音楽|車関係ではありません
Imaju|プロダクト名|固有名。訳さない
Bus|DAW・音楽|音声バス。交通機関ではない
Master 側は改行区切りで送るだけ(雰囲気):
ws.send(JSON.stringify({
type: 'terms',
terms: termsDraft.split(/\n+/).map((t) => t.trim()).filter(Boolean),
}));
適用方法(二段)
| 層 | 何を渡すか | 何のため |
|---|---|---|
| GCP STT | key だけ |
speechContexts の phrase boost(拾いやすくする) |
| Gemini 翻訳 | {key, type, description} |
意味の固定・「〜ではない」の否定・人名扱い |
// STT へは key のみ
Terms: JSON.stringify(terms.map((t) => t.key))
// 翻訳へは構造化
terms: terms.map((t) => ({
key: t.key,
type: t.type,
description: t.description,
}))
説明文まで STT に投げない。GCP の boost は「語」向きで、小説を読ませても効かない。
prompt の一部(翻訳 system)
短く固定して、Terms と history(誰が何を言ったか)を守らせる。
日本語のプロンプトは、英語の約4倍のtokenを消費するので英語で圧縮は必須。
特にモデルをopus系に差し替えた時に効いてくる。
const SYSTEM = `You translate for a live music-product stream chat (Imaju).
Task:
1) Translate into targetLang. If already targetLang, return unchanged.
2) Apply style (formal|friendly|frank) to wording only.
3) Terms: each item is {key, type, description}.
Honor type/description (e.g. nickname = person name;
domain sense; explicit "not X"). Do not invent jargon.
4) history: prior turns with role and nickname.
Use only for who-said-what continuity. Do not quote history.
5) Clear defamation → reject=true, text="".
Output JSON only: {"text":"...","reject":false}`;
user 側に毎回載せる JSON の雰囲気:
{
"text": "Track の Bus に Imaju の mark が来た",
"sourceLang": "ja",
"targetLang": "en",
"style": "friendly",
"terms": [
{ "key": "Track", "type": "DAW・音楽", "description": "車関係ではありません" },
{ "key": "Bus", "type": "DAW・音楽", "description": "音声バス。交通機関ではない" },
{ "key": "Imaju", "type": "プロダクト名", "description": "固有名。訳さない" },
{ "key": "mark", "type": "ニックネーム", "description": "人名として扱ってください" }
],
"history": [
{ "role": "guest", "nickname": "mark", "text": "..." }
]
}
用語を入れないと、ここが全部一般英語に流れて事故る。
Terms はオプション装飾ではなく、誤訳防止の本線。
「同時通訳」と言いつつ何をしているか
厳密な人間通訳の代替アピールではない。
- 配信者の発話 → 文字起こし → 英訳 → 視聴者へ
- 視聴者のテキスト → 必要なら日本語 → 配信者へ(読み上げも可)
- 文体 / 用語 / 中傷は自動無視
橋を渡す。会議を開かない。
コストの考察
人間の同時通訳を1時間雇う話ではない。
20秒くらいの発話を切れ目なく1時間ぶん回したとき、API側にいくらかの話。
前提(ざっくり・公開単価ベースの概算):
- Master が 約20秒ごとに切って送り続ける → 1時間で 約180本
- 音声合計 60分(GCP STT V2 Standard 目安 $0.016 / 分)
- 英訳を WaveNet TTS で Guest に読む(目安 $4 / 100万文字)
- 翻訳は Gemini Flash 系(短文×180回)
- CF Worker / DO・Cloud Run は、この1時間単体では誤差(無料枠〜数十円イメージ)
| 項目 | ざっくり計算 | 目安 |
|---|---|---|
| STT(60分) | $0.016 × 60 | 約 $0.96 |
| TTS(Master→英・1時間分の読み上げ文字) | 英訳テキストを仮に〜4〜5万文字 | 約 $0.16〜0.20 |
| Gemini(180回・短文+history) | Flash 従量 | 約 $0.05〜0.15 |
| CF / Front | Room 扇・WS | ほぼ無視できる |
| 合計 | だいたい $1.2〜1.5 / 時間(≒ 200円前後) |
Guest のテキスト→Master 日本語 TTS を足しても、チャット量次第で 数十円増えるかどうか。
Guest が Speaker OFF なら Master→英 TTS は $0(文字だけ流す)。
# 頭の中の式(雰囲気)
STT ≈ 0.016 USD/min × 60
TTS ≈ 4 USD / 1e6 chars × (英訳の文字数)
LLM ≈ Flash単価 × (入出力トークン)
CF ≈ 無料枠の中で余裕で収まる
人間通訳・Zoom有料会議室・常時GPUサーバーと比較
配信1時間で数百円以下のレイヤなら、「もったいないから言葉の橋を諦める」のはもったいない。
注意:
- 公式単価は変わる。これは 2026年時点の公開価格からの机上
- リクエストの秒丸め・空transcript・Guest人数×TTS・Studio級ボイスにすると上がる
- 同一文の TTS キャッシュを Room に持てば、さらに下がる
- 無料枠(TTS 月数百万文字など)に収まれば、実験配信は実質もっと安い
要するに 超低コスト運用で足りる。
高いのはクラウドではなく、言葉の壁を放置する機会損失のほうかと思います。
- 実装コスト:色々あるけどそんなに重いものではない
レイテンシ(2.3秒で相手に届く)
遅延の内訳(目安):
| 区間 | だいたい |
|---|---|
| 自然な音切れ検知(無音カット) | +1.0s |
| 文字起こし + 翻訳 | +1.0s |
| WSS 片道 | 最大 約300ms |
| 合計 | おおよそ 2.3s で相手側に届く |
1.0s(無音検知) + 1.0s(STT+訳) + 0.3s(WSS片道) ≈ 2.3s
「同時通訳なのに2秒遅れるのかよ」——わかる。
でも人間の同時通訳も、聞き始めてから口が動くまで 必ず遅れる。これを通訳研究では EVS(ear–voice span) と呼ぶらしい。
遅延許容を6秒に定める
先に正直に書く。
「人の同時通訳の許容範囲は一律6秒以内」という規格文書はない。
あるのは、観測された EVS のレンジと、長すぎると品質が落ちる、という研究側の話のようだ。調べて見た。
| 出典のニュアンス | 中身 |
|---|---|
| Barik ほか多くの報告 | 平均 EVS はだいたい 2〜3秒 前後 |
| Defrancq (2015) などコーパス | 多くが 1〜4秒 に収まる(平均 約2.7秒前後の例) |
| Lederer (1978) | 遅れを 3〜6秒 のあいだで観察、と報告される |
| Lee (2002) など | おおよそ4秒を超えると訳の精度・欠落が悪化しやすい、という指摘 |
結論
人間の同時通訳でも、典型的な EVS は 数秒(だいたい2〜4秒、観察では最大6秒前後まで報告あり)。
うちのパイプライン目安 約2.3秒 は、その帯の内側に十分入る。
ただし、専門用語が嵩む、長文、それらによる考察時間によっては6秒を超える場合もある。
まあ十分では無いかと…。
おわりに
YouTube で発信しているのに、コメントだけ母語の壁で閉じるのは、やっぱりもったいない。
Imaju のような音楽制作の文脈でも、会議ツールを無理に載せなくても、配信の横に言語レイヤは足せる。
AudioBuffer も、AudioContext unlock も、CSP も、SA も、Admin を載せない判断も、
やれる側の出来ることを、配信の横に載せただけ。