第1回 個人開発でできるリアルタイム翻訳 - 「沈黙」を打破するIntent-First

4言語の双方向翻訳に対応: 英語・日本語・スペイン語・中国語の任意の組み合わせでリアルタイム双方向翻訳が可能です。ソースコードはGitHubで公開しています。

音声翻訳アプリを使って、海外の方と会話をしたことはありますでしょうか。

Microsoft Translator、Google翻訳、AppleのAirPodsライブ通訳——大手テック企業が提供するリアルタイム音声翻訳は、年々精度が向上しています。対応言語も増え、技術としては非常に成熟してきました。

しかし、実際に使ってみると、ある違和感に気づきます。

翻訳は正確なのに、会話がうまくいかない。


問題は「沈黙」にありました

英語でのビジネス会話で翻訳アプリを使うと、こうした状況が生まれます。

相手が英語で話す → 翻訳を待つ(3〜5秒の沈黙)→ 翻訳を読む → 自分が話す → また相手が待つ

この「翻訳を待つ沈黙」の間、相手は「聞こえていないのかな」と不安になり、こちらは翻訳が出るまで反応することができません。翻訳の品質がどれほど向上しても、この会話のテンポが崩れる問題は構造的に解消されません。

既存の音声翻訳は、いずれも「逐次翻訳」というアーキテクチャを採用しています。相手が話し終わるのを待ち、テキストを確定させてから翻訳する。正確ではありますが、そのぶん必ず「待ちの時間」が発生します。


同時通訳者の方は、なぜ「待たない」のか

プロの同時通訳者の仕事を観察すると、興味深いことに気づきます。

通訳者の方は、文が完結する前に訳し始めています

「We should probably reschedule the meeting to…」と聞いた時点で、「会議の日程変更について話しています」と伝え始めている。“Tuesday afternoon” という具体的な情報が聞こえる前から、まず意図を先に伝えているのです。

聞き手にとって最も重要なのは、最初の数秒で「何についての話なのか」がわかることです。詳細な内容は、その後に補足されれば十分対応できます。

この「意図を先に伝える」というアプローチを、ソフトウェアで再現できないか。そう考えて開発を始めたのが、今回ご紹介するプロトタイプです。


Intent-First Translationという考え方

Intent-First Translationは、従来の翻訳とは処理の順序が異なります。

従来の翻訳(逐次翻訳):

話し始める → 話し終わる → テキスト確定 → 翻訳 → 表示
           (この間、聞き手には何も伝わらない)

Intent-First Translation:

話し始める → 0.5秒後「日程調整の提案」と表示
      → 0.8秒後「火曜の午後に会議を移動させたい」と表示
      → 話し終わる → 確定翻訳に更新

話し始めてから約500ミリ秒で、「この方は今、日程調整の話をしている」という意図が画面に表示されます。翻訳の全文はその数百ミリ秒後に続きます。

聞き手は、相手がまだ話している最中に「何について話しているか」を把握できます。これは逐次翻訳の構造では実現できない体験です。


プロジェクト構成

本システムは、ReactフロントエンドとFastAPIバックエンドがWebSocketで通信する構成です。バックエンドはDeepgramによる音声認識とLLM APIによる翻訳を担当します。

[ブラウザ (React)] ←WebSocket→ [FastAPI バックエンド] → [Deepgram STT] + [LLM API]

動作要件: Python 3.11以上、Node.js 18以上、DeepgramのAPIキー、およびLLM APIキー(以下のいずれか1つ以上)が必要です。

サービス種別長所短所
Google Gemini (Flash Lite)自社モデル低コスト(5時間 約$1.17)。無料枠あり。速度・品質のバランスが良い
OpenAI (GPT-4o-mini)自社モデル翻訳品質が安定。APIドキュメントが充実推論速度は上記2つより遅め(ベンチマークで約2秒)
Groq (Llama等)オープンソースモデル専用の推論プラットフォーム独自LPUチップによる高速推論(ベンチマークで約400ms)オープンソースモデルのみ。継続利用のコストは高め(5時間 約$3.43)

Groqは自社でモデルを開発しているのではなく、LlamaなどのオープンソースモデルをLPU(Language Processing Unit)で高速に推論するクラウドサービスです。OpenAIやGoogleとは性質が異なる点に留意してください。

# backend/requirements.txt
fastapi==0.115.6
uvicorn[standard]==0.34.0
websockets==14.1
deepgram-sdk==3.10.0
python-dotenv==1.0.1
openai==1.58.1
google-generativeai>=0.8.0
# .env
DEEPGRAM_API_KEY=your_deepgram_key
OPENAI_API_KEY=your_openai_key      # GPT-4o-mini用
GOOGLE_API_KEY=your_google_key       # Gemini用
GROQ_API_KEY=your_groq_key          # Groq用(オープンソースモデル推論)

Deepgramとの接続:リアルタイム音声認識

システムの中核は、DeepgramのストリーミングSTT APIとのWebSocket常時接続です。以下はTranscriptionManagerstart()メソッドを簡略化したものです。

async def start(self):
    config = DeepgramClientOptions(options={"keepalive": "true"})
    deepgram = DeepgramClient(DEEPGRAM_API_KEY, config)

    self.dg_connection = deepgram.listen.asyncwebsocket.v("1")

    self.dg_connection.on(LiveTranscriptionEvents.Transcript, self._on_transcript)
    self.dg_connection.on(LiveTranscriptionEvents.Error, self._on_error)
    self.dg_connection.on(LiveTranscriptionEvents.Close, self._on_close)

    options = LiveOptions(
        model="nova-2",
        language="en-US",
        encoding="linear16",
        sample_rate=16000,
        channels=1,
        interim_results=True,     # 発話中に部分結果を取得
        utterance_end_ms=1000,    # 発話終了の検出
        vad_events=True,          # 音声区間検出
    )

    await self.dg_connection.start(options)

Intent-Firstアプローチにおいて、以下の3つの設定が特に重要です。

  • interim_results=True — これが最も重要な設定です。話者がまだ話している途中で部分的なテキストを送信するようDeepgramに指示します。これがなければ、意図推定に早期に入力するデータがありません。
  • utterance_end_ms=1000 — 1秒間の沈黙後、Deepgramがテキストをis_final=Trueとしてマークします。応答性と早すぎる打ち切りの防止のバランスをとる閾値です。
  • vad_events=True — 音声区間検出イベントを有効にし、実際の発話と背景ノイズの区別を支援します。

Deepgramからテキスト化イベントが送られてくると、コールバックがそれを処理し、後続のタスクを起動します。

async def _on_transcript(self, *args, **kwargs):
    result = kwargs.get("result")
    if result is None and args:
        result = args[1] if len(args) > 1 else args[0]

    if result:
        transcript_data = result.channel.alternatives[0]
        transcript = transcript_data.transcript

        if transcript:
            is_final = result.is_final
            speech_final = getattr(result, "speech_final", False)

            # フロントエンドにテキストを送信
            await self.websocket.send_json({
                "type": "transcript",
                "transcript": transcript,
                "is_final": is_final,
                "speech_final": speech_final,
                "confidence": transcript_data.confidence,
            })

            # キーワードヒント(LLM不要、ほぼゼロ遅延)
            await self.keyword_predictor.predict(transcript)

            # 意図推定(非同期・ノンブロッキング)
            asyncio.create_task(
                self.intent_estimator.estimate(transcript, is_final)
            )

ここでasyncio.create_task()を使用しているのには明確な理由があります。意図推定にはLLM APIの呼び出しが伴い、数百ミリ秒かかる場合があります。もしこの呼び出しがテキスト化コールバックをブロックすると、Deepgramのkeepalive接続がタイムアウトして切断されてしまいます。別の非同期タスクとして実行することで、コールバックは即座に返り、WebSocket接続が維持されます。


WebSocketエンドポイント

FastAPIサーバーは、バイナリの音声データとJSONの制御メッセージの両方を処理する単一のWebSocketエンドポイントを公開します。

@app.websocket("/ws/audio")
async def websocket_audio(websocket: WebSocket):
    await websocket.accept()
    manager = TranscriptionManager(websocket)

    if not await manager.start():
        await websocket.close()
        return

    try:
        while True:
            data = await websocket.receive()

            if "bytes" in data:
                # ブラウザのマイクからのバイナリ音声データ
                await manager.send_audio(data["bytes"])
            elif "text" in data:
                # 制御メッセージ(停止、モデル切替など)
                message = json.loads(data["text"])
                if message.get("type") == "stop":
                    break
                elif message.get("type") == "set_model":
                    manager.set_model(message["model"])
    finally:
        await manager.close()

このエンドポイントは2種類の受信データを処理します。バイナリメッセージにはブラウザのマイクが取得した生の音声データが含まれ、これはそのままDeepgramに転送されます。テキストメッセージはJSON形式の制御コマンド——たとえばセッションの停止や、会話中のLLMモデルの切り替え——です。


フロントエンド:音声キャプチャ

ブラウザ側では、Web Audio APIがマイク入力を取得し、Deepgramが期待する形式に変換します。

const startRecording = useCallback(async () => {
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: {
      channelCount: 1,
      sampleRate: 16000,
      echoCancellation: true,
      noiseSuppression: true,
    },
  });

  const audioContext = new AudioContext({ sampleRate: 16000 });
  const source = audioContext.createMediaStreamSource(stream);
  const processor = audioContext.createScriptProcessor(4096, 1, 1);

  processor.onaudioprocess = (event) => {
    const inputData = event.inputBuffer.getChannelData(0);
    // Float32からInt16への変換(DeepgramはLinear16を要求)
    const int16Data = new Int16Array(inputData.length);
    for (let i = 0; i < inputData.length; i++) {
      const s = Math.max(-1, Math.min(1, inputData[i]));
      int16Data[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
    }
    onAudioData(int16Data.buffer);
  };

  source.connect(processor);
  processor.connect(audioContext.destination);
}, []);

onAudioDataコールバックがInt16バッファをWebSocket経由でバックエンドに送信します。16kHzモノラル16ビットの場合、4096サンプルのバッファは約8KB——リアルタイムストリーミングに十分な小ささで、目立ったオーバーヘッドなく送信できます。


実測データ

実際に計測した性能は以下の通りです。

指標Intent-First Translation従来の逐次翻訳(参考)
意図の表示約500ms(該当機能なし)
翻訳の表示開始約500〜800ms約2〜5秒
会話のテンポほぼリアルタイム毎回3〜5秒の中断

約500ミリ秒で意図が伝わるということは、完全な翻訳を数秒間待つ場合と比べて、体感として明確な違いを生みます。


デモ動画

実際にIntent-First Translationが動作している様子をご覧ください。話している最中に意図と翻訳が表示される様子がわかります。


「正確に訳す」から「自然に会話する」へ

このプロトタイプが目指しているのは、翻訳の精度で既存サービスと競うことではなく、リアルタイム翻訳の根本解決を主張するものでもありません。情報を段階的に届けることで体感の待ち時間を短縮するという、UXレベルの改善を検証したものです。

リアルタイム翻訳の本質的な進歩は、モデルアーキテクチャやハードウェアの進化——エッジデバイスでの高速推論、より低遅延なモデル、あるいはまったく新しいパラダイム——によってもたらされるものだと考えています。Intent-First Translationはそれらに代わるものではなく、現時点の技術的制約のなかで機能する一つの実践的なアプローチです。

一つ言えるのは、このアプローチは誰にでも試せるということです。公開されているAPIとオープンソース技術を使えば、エンジニアであれば自宅でリアルタイムに近い翻訳機を構築できます。同じ領域に関心をお持ちの方の参考になれば幸いです。

次回は、LLMストリーミングの実装の核心——速度と品質を両立するデュアルプロンプト、JSONフィールド順序の最適化、そしてフロントエンドでの段階的な翻訳表示——について解説します。


この記事をシェア

この記事についてのLinkedIn投稿でコメントや意見を共有できます。

LinkedInで議論する

関連記事