0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

YouTube配信で同時通訳する方法

0
Last updated at Posted at 2026-09-16

この記事の目的

  • 主旨: 個人・企業問わず、巨大プラットフォーム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 側と同じく「手は作業・口と耳は橋」で回せる
  • 字幕だけ国際化にせず、音声で聞かせるので動的なアクセスにも離脱しにくい

予約のいらない雑談の国際化。なんて贅沢なんでしょう...:grinning:
日本てポップカルチャー強いから、アニメの話題なんかでかなり使えそうだな~...

手段の選定

インターネットはすでに世界に開いている。
足りないのは回線ではなく、言葉の橋のほうが大きい。
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 を載せない判断も、
やれる側の出来ることを、配信の横に載せただけ。


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?