1. 主要ページへ移動
  2. メニューへ移動
  3. ページ下へ移動

QES ブログ

記事公開日

Azureで実現!リアルタイム音声通話×生成AIチャットを構築してみた

  • このエントリーをはてなブックマークに追加

🎙️ Azureで実現!リアルタイム音声通話×生成AIチャットを構築してみた

こんにちは。今回は、Azure SpeechサービスAzure OpenAIを組み合わせて、リアルタイム音声チャットが可能な生成AIアプリケーションを構築した手順と実装ポイントをご紹介します。

本記事では、実際に作成したアプリケーションの構成からサーバ・クライアント側のコードまでをまとめ、生成AI × 音声UI に興味がある方の参考になることを目指しています。


📌 目次

  1. 目指すゴールとアーキテクチャ

  2. 必要なAzureリソース一覧

  3. 全体構成図と技術スタック

  4. サーバー側実装(FastAPI)

  5. クライアント実装(HTML + JS)

  6. 音声→テキスト→AI→音声 の処理フロー

  7. まとめと感想

 

🎯 目指すゴールとアーキテクチャ

今回目指したのは、

マイクで話しかけると、生成AIが応答し音声で返してくれる。
Webブラウザだけで動作、インストール不要。
複数ユーザーが利用できる。

というシンプルかつ実用的な音声チャットシステムの構築です。

そのために選定した構成は以下です:

  • バックエンド:FastAPI + Azure OpenAI + Azure Speech SDK

  • フロントエンド:HTML + JavaScript + Azure Speech JavaScript SDK

  • 通信:WebSocket(双方向通信)

 

🧱 必要なAzureリソース一覧

本構成で必要になるAzureのサービスは以下の通りです。

サービス名 用途 補足
Azure OpenAI 生成AIモデル(GPT-4など) gpt-4o を利用(stream対応)
Azure Speech Service 音声認識(STT)・音声合成(TTS) 日本語も対応可
Azure App Service または
Docker on VM
アプリケーションホスト Dockerイメージで実行可能

※SpeechとOpenAIは同一リージョン(例: japaneast)にしておくとLatencyが改善されます。

 

🗺️ 全体構成図と技術スタック

全体構成図と技術スタック

使用技術まとめ:

項目 使用技術
フロントエンド HTML / JS / Azure Speech JS SDK
バックエンド FastAPI / Python / Azure SDKs
通信 WebSocket
ビルド環境 Docker (uvicorn, azure SDK)
 

🧠 サーバー側実装(FastAPI)

以下のような main.py を実装しました:

  1. WebSocketで接続されたユーザーごとにセッション履歴を記録

  2. 受信したテキストを Azure OpenAI に送信して応答を取得

  3. 応答を Azure Speech TTS に渡し、WAV音声としてバイナリ送信

工夫した点:

  • sessions[websocket] に会話履歴を格納(OpenAI用のメモリとして)

main.py
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
import azure.cognitiveservices.speech as speechsdk
from azure.cognitiveservices.speech import AudioDataStream
from openai import AzureOpenAI
import os
import tempfile
import traceback

app = FastAPI()

# 環境変数から設定を取得
AZURE_SPEECH_KEY = os.getenv("AZURE_SPEECH_KEY")
AZURE_REGION = os.getenv("AZURE_REGION", "japaneast")
OPENAI_ENDPOINT = os.getenv("OPENAI_ENDPOINT")
OPENAI_KEY = os.getenv("OPENAI_KEY")
OPENAI_DEPLOYMENT = os.getenv("OPENAI_DEPLOYMENT", "gpt-4o")
OPENAI_API_VERSION = os.getenv("OPENAI_API_VERSION", "2024-05-01-preview")

# OpenAI クライアント初期化
client = AzureOpenAI(
    api_key=OPENAI_KEY,
    api_version=OPENAI_API_VERSION,
    azure_endpoint=OPENAI_ENDPOINT,
)

# WebSocketごとのセッション履歴(メモリ保持)
sessions = {}

@app.websocket("/api/chat")
async def chat_ws(websocket: WebSocket):
    await websocket.accept()
    print("✅ WebSocket接続開始")

    # 初期履歴(Systemプロンプト付き)
    sessions[websocket] = [
        {"role": "system", "content": "あなたは親しみやすく丁寧な日本語で応答するAIです。"}
    ]

    try:
        while True:
            try:
                # クライアントから音声認識されたテキストを受信
                user_text = await websocket.receive_text()
                print(f"👤 User: {user_text}")

                if not user_text.strip():
                    await websocket.send_text("⚠️ 入力が空です。")
                    continue

                # 履歴に追加
                sessions[websocket].append({"role": "user", "content": user_text})

                # OpenAI に問い合わせ(履歴込み)
                response = client.chat.completions.create(
                    model=OPENAI_DEPLOYMENT,
                    messages=sessions[websocket]
                )
                ai_text = response.choices[0].message.content
                print(f"🤖 Assistant: {ai_text}")

                # 応答を履歴に追加
                sessions[websocket].append({"role": "assistant", "content": ai_text})

                # クライアントにテキスト送信
                await websocket.send_text(ai_text)

                # 音声合成(Text → Speech)
                speech_config = speechsdk.SpeechConfig(subscription=AZURE_SPEECH_KEY, region=AZURE_REGION)
                speech_config.speech_synthesis_voice_name = "ja-JP-NanamiNeural"
                synthesizer = speechsdk.SpeechSynthesizer(speech_config=speech_config, audio_config=None)
                result = synthesizer.speak_text_async(ai_text).get()

                if result.reason == speechsdk.ResultReason.SynthesizingAudioCompleted:
                    with tempfile.NamedTemporaryFile(suffix=".wav", delete=True) as tmpfile:
                        stream = AudioDataStream(result)
                        stream.save_to_wav_file(tmpfile.name)

                        # 音声データを読み込んで送信
                        tmpfile.seek(0)
                        wav_data = tmpfile.read()
                        await websocket.send_bytes(wav_data)
                        print("🔊 音声送信完了")

                else:
                    print(f"❌ 音声合成エラー: {result.reason}")
                    await websocket.send_text("❌ 音声合成に失敗しました")

            except WebSocketDisconnect:
                print("🔌 WebSocket切断")
                break
            except Exception as e:
                print("⚠️ 処理中エラー:", e)
                traceback.print_exc()
                await websocket.send_text("❌ サーバー内部エラー")

    finally:
        sessions.pop(websocket, None)
        print("🧹 セッション履歴削除完了")

@app.get("/")
def index():
    return HTMLResponse("✅ Server is running")

 

💬 クライアント実装(HTML + JS)

クライアント側は以下の3つの要素で構成されています:

  1. index.html: マイク起動ボタンとログ表示用UI

  2. main.js: Azure Speech JS SDKを用いた音声認識(STT)

  3. WebSocket通信で音声テキストをサーバーに送信し、応答と音声を受信

工夫した点:

  • マイク入力後、自動で認識しWebSocket送信

  • 音声ファイルをBlobとして再生

  • WebSocket切断時の自動リトライ機構を搭載

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Azure Speech Chat</title>
  <script src="https://aka.ms/csspeech/jsbrowserpackageraw"></script>
  <style>
    body { font-family: sans-serif; padding: 2em; }
    button { padding: 10px 20px; font-size: 1em; }
    .box { margin-top: 1em; background: #f0f0f0; padding: 1em; border-radius: 8px; min-height: 3em; }
  </style>
</head>
<body>
  <h1>🎤 Azure Speech Chat</h1>
  <button id="start-btn">マイク開始</button>
  <div id="status">停止中</div>
  <div class="box">
    <div><strong>あなた:</strong></div>
    <div id="user-text"></div>
  </div>
  <div class="box">
    <div><strong>AI:</strong></div>
    <div id="ai-text"></div>
  </div>

  <script type="module" src="main.js?v=8"></script>
</body>
</html>
main.js
const SPEECH_KEY = "xxxxxxxxxxxxx";
const REGION = "japaneast";
const WS_URL = "wss://xxxxxx.azurewebsites.net/api/chat";

// DOM要素取得
const startBtn = document.getElementById("start-btn");
const userTextDiv = document.getElementById("user-text");
const aiTextDiv = document.getElementById("ai-text");
const statusDiv = document.getElementById("status");

// DOM要素チェック
if (!startBtn || !userTextDiv || !aiTextDiv || !statusDiv) {
  console.error("❌ 必要なDOM要素が見つかりません");
}

// WebSocket初期化
let socket = null;
let socketReady = false;

function connectWebSocket() {
  socket = new WebSocket(WS_URL);
  socket.binaryType = "arraybuffer";

  socket.onopen = () => {
    console.log("✅ WebSocket 接続が開かれました ✅");
    socketReady = true;
  };

  socket.onmessage = (event) => {
    if (typeof event.data === "string") {
      console.log("📥 テキスト受信:", event.data);
      aiTextDiv.textContent = event.data;
      return;
    }

    console.log("📥 音声受信(バイナリ):", event.data);
    const blob = new Blob([event.data], { type: "audio/wav" });
    const audio = new Audio(URL.createObjectURL(blob));
    audio.onerror = (e) => {
      console.error("❌ 音声再生エラー", e);
    };
    audio.play();
    aiTextDiv.textContent = "🔊 音声再生中...";
  };

  socket.onclose = () => {
    console.warn("⚠ WebSocket が切断されました。再接続を試みます...");
    socketReady = false;
    setTimeout(connectWebSocket, 1000); // 1秒後に再接続
  };

  socket.onerror = (err) => {
    console.error("❌ WebSocket エラー:", err);
    socketReady = false;
  };
}

// 初回接続
connectWebSocket();

// テキスト送信関数
function sendTextToSocket(text) {
  if (socketReady && socket.readyState === WebSocket.OPEN) {
    socket.send(text);
    console.log("📤 WebSocket に送信:", text);
    aiTextDiv.textContent = "🤖 Azure OpenAI に送信中...";
  } else {
    console.warn("🔁 WebSocket 未接続。500ms後に再試行...");
    setTimeout(() => sendTextToSocket(text), 500);
  }
}

// 音声認識の開始
startBtn.addEventListener("click", () => {
  console.log("🎤 マイクボタンが押されました");
  statusDiv.textContent = "録音中...";

  const speechConfig = SpeechSDK.SpeechConfig.fromSubscription(SPEECH_KEY, REGION);
  console.log("✅ SpeechConfig 初期化");

  speechConfig.speechRecognitionLanguage = "ja-JP";

  const audioConfig = SpeechSDK.AudioConfig.fromDefaultMicrophoneInput();
  console.log("✅ AudioConfig 初期化");

  const recognizer = new SpeechSDK.SpeechRecognizer(speechConfig, audioConfig);
  console.log("✅ SpeechRecognizer 初期化");

  recognizer.recognizeOnceAsync(result => {
    console.log("🎧 音声認識結果:", result);
    if (result.text) {
      userTextDiv.textContent = result.text;
      sendTextToSocket(result.text);
    } else {
      console.warn("⚠ 音声認識に失敗、テキストが取得できません");
      statusDiv.textContent = "⚠ 音声認識に失敗しました";
    }
  }, err => {
    console.error("❌ 音声認識エラー:", err);
    statusDiv.textContent = "音声認識に失敗しました";
  });
});
 

🔁 音声→テキスト→AI→音声 の処理フロー

 音声→テキスト→AI→音声 の処理フロー
 

📝 まとめと感想

今回のプロジェクトでは、

  • Azure Speech × Azure OpenAI を活用した双方向音声チャット

  • WebSocketを活用したリアルタイム対話

  • 会話文脈を保持しつつ人間らしい応答

という、生成AIの魅力を活かしたアプリケーションを構築しました。
ご参考になれば幸いです。

QUICK E-Solutionsでは、「AIチャットボット構築サービス」をはじめとして、各AIサービスを利用したシステム導入のお手伝いをしております。
それ以外でも QESでは様々なアプリケーションの開発・導入を行っております。提供するサービス・ソリューションにつきましては こちら に掲載しております。
システム開発・構築でお困りの問題や弊社が提供するサービス・ソリューションにご興味を抱かれましたら、是非一度 お問い合わせ ください。

※このブログで参照されている会社名、製品名は各社の登録商標または商標です。

  • このエントリーをはてなブックマークに追加

お問い合わせ

Contact

ご質問やご相談、サービスに関する詳細など、何でもお気軽にご連絡ください。下記のお問い合わせフォームよりお気軽に送信ください。

お問い合わせ

資料ダウンロード

Download

当社のサービスに関する詳細情報を掲載した資料を、下記のページよりダウンロードいただけます。より深く理解していただける内容となっております。ぜひご活用ください。

資料ダウンロード