はじめに
先日、OpenAIから gpt-realtime-translate という、音声→音声の同時通訳に特化したモデルが公開されました。
入力は70言語以上に対応していて、出力は日本語・英語・中国語・韓国語など13言語。話者の声色や抑揚をそのまま引き継いで翻訳音声を返してくれて、価格は $0.034/分。Realtime APIの一系統として WebRTC / WebSocket で接続できます。
これを試したくて、外国人とリアルタイムに別言語同士で会話できるアプリを作ってみました。
GitHub: https://github.com/gyokuro33/realtime-meeting-translator
どんな感じで動くのか
自分が日本語で話すと、相手のTeamsには英語(または中国語など)で聞こえる。相手が英語で話すと、こちらのヘッドホンには日本語が聞こえる。それだけのアプリです。
字幕も出るので、聞き取りに自信がない場面でも追えます。
動機:くそめんどくさい会社のTeams会議で使えるようにしたかった
仕事柄、海外チームとTeams会議をする機会が時々あります。なので翻訳アプリは欲しかった。
が、会社のTeamsに組み込みのAIアプリを作るのは死ぬほどめんどくさい。
なので、Teams側のAPIには一切触れない方式で作りました。Teamsから見ると「ちょっと変わった名前のマイクとスピーカー」が一つ繋がっているだけ。本アプリ側がその仮想マイク/スピーカーに翻訳音声を流し込んだり、相手の音声を取り込んだりします。
この方式だと、Teamsだろうと、Zoomだろうと、Discordだろうと、Google Meetだろうと、デバイス指定ができるツールならすべて同じように動きます。後付けで仕様が変わって動かなくなる、ということもまず起こりません。
仕組み
仮想オーディオケーブルとして有名な VB-CABLE A+B Pack をルーティングに使います。
Teams / Zoom / Meet 側のデバイス設定はこちらです:
| 項目 | 値 |
|---|---|
| マイク | CABLE-A Output |
| スピーカー | CABLE-B Input |
仮想ケーブルAは「自分の翻訳音声をTeamsに送る」専用、仮想ケーブルBは「Teamsから来た相手音声を本アプリに取り込む」専用。これで音声の流れが完全に分離できます。
そしてアプリ内では、gpt-realtime-translate のWebRTCセッションを2本張ります。
- セッションS(送信側): 実マイク → OpenAI(日本語→英語) → CABLE-A Input
- セッションR(受信側): CABLE-B Output → OpenAI(英語→日本語) → ヘッドホン
「往復で2セッションになる=コストも2倍」というのが地味に重要で、$0.034 × 2 = $0.068/分。1時間の会議で約 $4 = 600円。海外ビジネスが楽になるならこれぐらい誤差でしょう。
実装まわりのポイント
Electron + WebRTC で完結する
OpenAIのSDKは挟まず、Electronのレンダラープロセスでブラウザ標準のWebRTCと fetch を使えばそれだけで完結しました。
ephemeral token(短命なクライアント秘密鍵)はメインプロセス側で発行して、レンダラーには本物のAPIキーが届かないようにしてあります。
// メインプロセス側
const res = await fetch(
"https://api.openai.com/v1/realtime/translations/client_secrets",
{
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
session: {
model: "gpt-realtime-translate",
audio: {
input: {
transcription: { model: "gpt-realtime-whisper" },
noise_reduction: { type: "near_field" },
},
output: { language: targetLanguage },
},
},
}),
},
);
/v1/realtime/translations/client_secrets への POST には session パラメータが必須で、ここで出力言語と入力時の文字起こしモデルを指定します。これに気付かず空ボディで叩いて Missing required parameter: 'session' で詰まりました。
出力先デバイスは setSinkId で切り替え
WebRTCで受け取った翻訳音声は HTMLAudioElement に入れて、setSinkId で出力デバイスを切り替えています。
const audioEl = new Audio();
audioEl.autoplay = true;
pc.ontrack = (e) => {
audioEl.srcObject = e.streams[0];
audioEl.setSinkId(outputDeviceId); // 例: CABLE-A Input の deviceId
};
これがあるから「翻訳音声だけTeamsに流す」「自分の原音は混ぜない」というのが綺麗にできます。
fetch失敗時のpc.close()忘れで詰む
接続テスト中に何度か失敗していたら net::ERR_INSUFFICIENT_RESOURCES が出るようになりました。原因は、SDP交換のfetchが失敗したときに RTCPeerConnection を閉じていなかったことで、失敗のたびに接続オブジェクトが累積していたからです。
WebRTCはブラウザ全体でリソースを持っているので、try/catchで失敗時に必ず pc.close() を呼ぶようにしないと簡単にリークします。
画面側
ReactでシンプルなUIを組みました。言語選択(13言語)と、デバイス選択(4枠:自マイク/自出力/Teams送出/Teams取込)、接続ボタン、字幕、概算コスト。それだけ。
VB-Cableのデバイスは名前で自動推定するようにしてあります。外したらユーザーに手動で選び直してもらう想定。
セットアップ
ざっくりこんな感じです。
-
VB-CABLE A+B Pack をインストール(個人利用は寄付ウェア)
-
リポジトリをclone
git clone https://github.com/gyokuro33/realtime-meeting-translator cd realtime-meeting-translator npm install npm run dev -
アプリ起動後にOpenAI APIキーを入力(OSのCredential Manager経由で暗号化保存されます)
-
言語と4枠のデバイスを選んで [接続]
-
Teams側でマイク=
CABLE-A Output、スピーカー=CABLE-B Inputに設定
使ってみての所感
- レイテンシ: 片道1〜3秒。会話のテンポは少しスローになりますが、同時通訳としては十分実用的でした。
- 抑揚がわりと近い: 他人の声で違う言語だから抑揚も違うのに、なんとなくしゃべるときの感情というかニュアンスが似ているのが面白い。
- 同言語無音: 自分が間違えて英語で話すと無音になります(モデル仕様)。日本語と英語が混ざる人は要注意。
- VB-Cableは入れるだけの価値あり: Teams以外でも仮想オーディオを使った仕組みはいろいろ応用が利くので、入れておいて損はないです。
おわりに
OpenAIの gpt-realtime-translate は、これまでの「文字起こし→翻訳→TTS」の三段組みを丸ごとひとつのモデルに置き換えてくれて、しかも声色を継承してくれる。同時通訳というジャンルが急に身近になった実感があります。
「外国の取引先と話す機会があるけど英語が苦しい」とか、「海外チームとの定例会議で発言量が落ちる」みたいな悩みがある方には結構役立つと思います。
リポジトリはこちらです。よければ試してみてください。

