Teams 会議中にも AI と話したい!
Teams 会議中に「 AI に聞いてその回答を共有したいな、でもチャット打つのはちょっと面倒だな」って思ったことありませんか?
今回はそんなお悩みを解決します。
0. 初めに
0.1. 何をするの?
Teams 会議中の音声を拾い、特定の単語の後の質問を Azure OpenAI へ投げてその回答をチャットに投稿するといった web アプリを作成します。
0.2. どうやって作るの?
本記事では簡単のために React を用いて作成していきます。
作成する web アプリでは以下の様なフローを実行します。
- Azure Communication Services を使用して会議に参加し、音声を取得
- 取得した音声を Azure AI Services の Speech Service を使用してテキストに変換
- 取得したテキストを Azure AI Services の Azure OpenAI に投げて回答を得る
- Azure Communication Services を使用してチャットに投稿
ほぼほぼ Azure に頼り切ってますね。
1. 作成していく
1.0 前提条件
本記事では React を使用するため、 node.js および npm をインストールしています。
$ node --version
v18.17.1
$ npm --version
9.6.7
1.1 環境構築
まず React の環境構築をします。
以下のコマンドで react-app
というプロジェクトが作成されます。
npx create-react-app react-app --template typescript
cd react-app
npm run start
ブラウザから http://localhost:3000 にアクセスし、以下の様に表示されたら OK です。
環境構築の最後に今回使用するライブラリを追加しておきます。
npm install @azure/communication-calling @azure/communication-chat @azure/communication-common @azure/openai microsoft-cognitiveservices-speech-sdk --save
1.2 Azure Communication Services を使用して会議に参加
次は、 Azure Communications Services を使用して会議に参加します。
会議への参加は以下の手順で行います。
- Azure Communication Services のリソースを作成する
- CallClient の作成
- AzureCommunicationTokenCreadential の作成
- CallAgent の作成
実際に会議に参加するのは 4. の CallAgent ですね。
1.2.1 Azure Communication Services のリソース作成
それでは Azure Portal にアクセスしてリソースを作成していきましょう。
"Communication Service" で検索し、空欄を埋め、 [ レビューと作成 ] でリソースを作成します。
作成後、 Azure Communication Services に接続するための 2 つの文字列を保存します。
リソースの [ 概要 ] にあるエンド ポイントと
[ ID およびユーザー アクセス トークン ] より、 [ 音声およびビデオ通話 (VoIP) ] と [ チャット ] にチェックを入れて [ 生成 ] をクリックすると作成される [ ユーザー アクセス トークン ] を保存しておきます。
※ このユーザー アクセス トークンは 24 時間で使用できなくなりますが、今回は簡単のためこちらを使用します。
1.2.2 Azure Communication Services で会議に参加
プロジェクト直下に .env
ファイルを作成し、以下のように書きます。
環境変数に入れるので名前はなんでもいいのですが、先頭に REACT_APP_
を入れないと React 側で読み取ってくれなくなります。
REACT_APP_ACS_TOKEN=<< Azure Communication Service のユーザー アクセス トークン >>
REACT_APP_ACS_ENDPOINT=<< Azure Communication Service のエンド ポイント >>
src/App.tsx
を以下のように変更します。
import React from "react";
import { CallAgent, CallClient } from "@azure/communication-calling";
import { AzureCommunicationTokenCredential } from "@azure/communication-common";
const callClient = new CallClient();
const tokenCredential = new AzureCommunicationTokenCredential(
process.env.REACT_APP_ACS_TOKEN || ""
);
let callAgent: CallAgent | undefined;
function App() {
const [meetingUrl, setMeetingUrl] = React.useState("");
const [submitButtonDisabled, setSubmitButtonDisabled] = React.useState(false);
return (
<div className="App">
<div>
<input
type="text"
placeholder="Teams meeting link"
value={meetingUrl}
onChange={(e) => setMeetingUrl(e.target.value)}
/>
<button
onClick={async () => {
console.log("join");
console.log(!callAgent);
if (!callAgent) {
callAgent = await callClient.createCallAgent(tokenCredential, {
displayName: "Azure Open AI 君",
});
}
callAgent.join({ meetingLink: meetingUrl });
setSubmitButtonDisabled(true);
}}
disabled={submitButtonDisabled}
>
submit
</button>
</div>
</div>
);
}
export default App;
npm run start
後、 Teams 会議のリンクを入力して submit をクリックすると、、、
会議に "Azure OpenAI 君" が入ってきます。
1.3 会議の音声を取得する
次は会議の音声を取得しましょう。
会議の音声は、 callAgent.join()
の返り値の Call
の中, remoteAudioStreams
に MediaStream として格納されています。
この remoteAudioStreams
は会議に接続後すぐは空の配列になってしまっているため、定期的に取得を試みます。
import { Call, CallAgent, CallClient } from "@azure/communication-calling"; // 変更
// ...
const [call, setCall] = React.useState<Call>(); // 変更
// -- 変更 --
React.useEffect(() => {
if (call) {
const intervalId = setInterval(async () => {
if (call.remoteAudioStreams.length === 0) return;
const remoteAudioStream = call.remoteAudioStreams[0];
const mediaStream = await remoteAudioStream.getMediaStream();
console.log(mediaStream);
clearInterval(intervalId);
}, 1000);
}
}, [call]);
// -- 変更 --
//...
<button
onClick={async () => {
console.log("join");
console.log(!callAgent);
if (!callAgent) {
callAgent = await callClient.createCallAgent(tokenCredential, {
displayName: "Azure Open AI 君",
});
}
const _call = callAgent.join({ meetingLink: meetingUrl }); // 変更
setCall(_call); // 変更
setSubmitButtonDisabled(true);
}}
disabled={submitButtonDisabled}
>
// ...
1.4 会議の音声をテキストに変換する
では、取得した音声をテキストに変換しましょう。
ここでは、Azure AI Services の Speech Service を使用します。
1.4.1 Speech Service リソースの作成
ではでは、また Azure Portal にアクセスしてリソースを作成します。
[ Azure AI services ] と検索し、[ 概要 ] から [ 音声サービス ] の [ 作成 ] を選択します。
こちらも空欄を埋めて [ 確認と作成 ] でリソースの作成が可能です。
作成後、 Speech Services に接続するための 2 つの文字列を保存します。
[ キーとエンドポイント ] にある、キー と 場所/地域 を保存しておきます。
※ キーはキー 1 とキー 2 がありますがどちらでも大丈夫です。
1.4.2 Speech Service で音声テキスト変換
.env
内に以下のように追記します。
# ... 以下追記
REACT_APP_SPEECH_KEY=<< Speech Service のキー >>
REACT_APP_SPEECH_REGION=<< Speech Service の場所/地域 >>
src/App.tsx
を以下のように変更します。
import React from "react";
import { Call, CallAgent, CallClient } from "@azure/communication-calling";
import { AzureCommunicationTokenCredential } from "@azure/communication-common";
const sdk = require("microsoft-cognitiveservices-speech-sdk"); // 変更
// ...
// -- 変更 --
const startSpeechToText = async (mediaStream: MediaStream) => {
const speechConfig = sdk.SpeechConfig.fromSubscription(
process.env.REACT_APP_SPEECH_KEY,
process.env.REACT_APP_SPEECH_REGION
);
speechConfig.speechRecognitionLanguage = "ja-JP";
const audioConfig = sdk.AudioConfig.fromStreamInput(mediaStream);
const speechRecognizer = new sdk.SpeechRecognizer(
speechConfig,
audioConfig
);
speechRecognizer.recognizing = (_: any, e: any) => {
console.log("RECOGNIZING: " + e.result.text);
};
speechRecognizer.recognized = (_: any, e: any) => {
console.log("RECOGNIZED: " + e.result.text);
};
await speechRecognizer.startContinuousRecognitionAsync();
};
React.useEffect(() => {
if (call) {
const intervalId = setInterval(async () => {
if (call.remoteAudioStreams.length === 0) return;
const remoteAudioStream = call.remoteAudioStreams[0];
const mediaStream = await remoteAudioStream.getMediaStream();
await startSpeechToText(mediaStream);
clearInterval(intervalId);
}, 1000);
}
}, [call]);
// -- 変更 --
// ...
SpeechRecognizer で音声テキスト変換を実施しているイメージですね。
SpeechRecognizer.recognizing は、変換中の中間結果が返され、 SpeechRecognizer.recognized では変換が完了した結果が返されます。
そのため、 recoginizing と recognied では結果が多少異なる場合がありますね。
1.5 変換した音声を Azure OpenAI に投げて回答をもらう
次に、変換した音声を使って Azure OpenAI に質問して、回答をもらいましょう。
※ 現在 (2023/12/23) Azure OpenAI の使用には事前の申請が必要です。詳しくはこちら
1.5.1 Azure OpenAI リソースの作成
またまた Azure Portal にアクセスしてリソースを作成します。
[ Azure OpenAI ] で検索し、[ 作成 ] から空欄を埋めて [ 次へ ] で複数回進んだのち [ 作成 ] で作成できます。
作成後、 Azure OpenAI に接続するための 2 つの文字列を保存します。
[ キーとエンドポイント ] にある、キー と エンドポイントを保存しておきます。
※ キーはキー 1 とキー 2 がありますがどちらでも大丈夫です。
1.5.2 Azure OpenAI に質問を投げる
.env
内に以下のように追記します。
# ... 以下追記
REACT_APP_AZURE_OPENAI_KEY=<< Azure OpenAI のキー >>
REACT_APP_AZURE_OPENAI_ENDPOINT=<< Azure OpenAI のエンドポイント >>
src/App.tsx
を以下のように変更します。
import React from "react";
import { Call, CallAgent, CallClient } from "@azure/communication-calling";
import { AzureCommunicationTokenCredential } from "@azure/communication-common";
// -- 変更 --
import {
AzureKeyCredential,
ChatRequestMessage,
OpenAIClient,
} from "@azure/openai";
// -- 変更 --
const sdk = require("microsoft-cognitiveservices-speech-sdk");
// ...
// -- 変更 --
const client = new OpenAIClient(
process.env.REACT_APP_AZURE_OPENAI_ENDPOINT || "",
new AzureKeyCredential(process.env.REACT_APP_AZURE_OPENAI_KEY || "")
);
const keyword = "こんにちは"; // 変更
const prompt =
"次の文章は、音声認識システムによって認識された質問文です。 \
正しい文章を推定し、質問に回答してください。 \
回答の際には「推定した正しい質問文」と「回答」の2つを答えてください。 \
また、回答は「推定した正しい質問文」に対しての回答である必要があります。 \
必ず、 1 つの質問文に対して 1 つの回答をし質問を返さないでください。 \
なお、可能な限り自然に回答してください。 \
回答の際には以下のテンプレートに沿って回答してください。: \
「質問文: {推定した正しい質問文}, 回答: {回答}」 \
";
// -- 変更 --
// ...
// -- 変更 --
let isKeywordDetected = false;
speechRecognizer.recognizing = (_: any, e: any) => {
console.log("RECOGNIZING: " + e.result.text);
if (!e.result.text) return;
if (e.result.text.includes(keyword)) {
isKeywordDetected = true;
}
};
speechRecognizer.recognized = async (_: any, e: any) => {
console.log("RECOGNIZED: " + e.result.text);
if (!e.result.text) return;
if (isKeywordDetected && e.result.text.includes(keyword)) {
console.log("keyword detected");
isKeywordDetected = false;
const messages = [
{
role: "user",
content: prompt + e.result.text.replace(keyword, ""),
},
] as ChatRequestMessage[];
const result = await client.getChatCompletions(
"gpt-35-turbo",
messages
);
if (!result.choices) return;
if (!result.choices[0].message) return;
console.log(result.choices[0].message.content);
}
};
// -- 変更 --
// ...
取得した音声全てを Azure OpenAI に渡してしまうのは無駄が多すぎるため、音声アシスタントのように wakeup word の後のみ渡す感じにしています。
(単純に変換後のテキスト内にキーワードがあるか否かを見ているだけなので難しいキーワードだとうまく動かないかもです。)
また、音声テキスト変換での誤差を軽減するため、プロンプト内で正しい質問文の推定を含めています。
これで「こんにちは、五月雨の意味を教えて」というとこんな感じでブラウザ上のコンソールにひょうじされます。
※ ブラウザ コンソールは Ctrl + Shift + C でコンソールを選択すると見れます。
1.6 もらった回答をチャットに投稿する
それでは最後に、 1.5 で得た回答をチャットに投稿しましょう。
最初の方に出てきた Azure Communication Services で実施可能です。
src/App.tsx
を以下のように変更します。
import React from "react";
import { Call, CallAgent, CallClient } from "@azure/communication-calling";
import { AzureCommunicationTokenCredential } from "@azure/communication-common";
import {
AzureKeyCredential,
ChatRequestMessage,
OpenAIClient,
} from "@azure/openai";
import { ChatClient, ChatThreadClient } from "@azure/communication-chat"; // 変更
const sdk = require("microsoft-cognitiveservices-speech-sdk");
// ...
// -- 変更 --
const chatClient = new ChatClient(
process.env.REACT_APP_ACS_ENDPOINT || "",
tokenCredential
);
// -- 変更 --
// ...
const [meetingUrl, setMeetingUrl] = React.useState("");
const [submitButtonDisabled, setSubmitButtonDisabled] = React.useState(false);
const [call, setCall] = React.useState<Call>();
const [chatThreadClient, setChatThreadClient] = React.useState<ChatThreadClient>(); // 変更
// ...
speechRecognizer.recognized = async (_: any, e: any) => {
console.log("RECOGNIZED: " + e.result.text);
if (!e.result.text) return;
if (isKeywordDetected && e.result.text.includes(keyword)) {
console.log("keyword detected");
isKeywordDetected = false;
const messages = [
{
role: "user",
content: prompt + e.result.text.replace(keyword, ""),
},
] as ChatRequestMessage[];
const result = await client.getChatCompletions(
"gpt-35-turbo",
messages
);
// -- 変更 --
if (!result.choices) return;
if (!result.choices[0].message) return;
if (!result.choices[0].message.content) return;
if (!chatThreadClient) return;
await chatThreadClient.sendMessage(
{ content: result.choices[0].message.content },
{ senderDisplayName: "Azure OpenAI" }
);
// -- 変更 --
}
};
// ...
<button
onClick={async () => {
console.log("join");
console.log(!callAgent);
if (!callAgent) {
callAgent = await callClient.createCallAgent(tokenCredential, {
displayName: "Azure Open AI 君",
});
}
const _call = callAgent.join({ meetingLink: meetingUrl });
setCall(_call);
// -- 変更 --
const threadIdInput = meetingUrl.split("/")[5];
if (!chatThreadClient) {
const _chatThreadClient =
chatClient.getChatThreadClient(threadIdInput);
setChatThreadClient(_chatThreadClient);
}
// -- 変更 --
setSubmitButtonDisabled(true);
}}
disabled={submitButtonDisabled}
>
// ...
Teams 会議に入った時に chatThread も一緒に取得している感じですね。
chatThread を作成するときにはチャットのスレッド ID が人うようになります。
しかし、実は Teams 会議のリンク内に会議チャットのスレッド ID が含まれているのでそれを利用しています。
https://teams.microsoft.com/l/meetup-join/<チャットのスレッド ID>/0?context=hoge
これで、 Teams 会議内でした質問の回答を Azure OpenAI からもらえることができました。
2. まとめ
今回は、「Teams 会議中の音声を拾い、特定の単語の後の質問を Azure OpenAI へ投げてその回答をチャットに投稿するといった web アプリ」を作成しました。
とはいえ、実は今回実装したのって、 Teams 会議に参加する web アプリなんですよね。
そのため、実行しているブラウザ側からも音声を入力可能だったりします。
今回作成した web アプリはこちらにあります。(github を公開予定)
Discussion