クイックスタート

本記事ではサンプルコードをもとに、文字起こし機能が付いた会議アプリの実装方法について説明します。

サンプルコードはskyway-stt-client-jsのリポジトリに同梱しています。

GitHub skyway-stt-client-js

サンプルコードの完成形はこちらからご確認いただけます。

skyway-stt-example

クイックスタートの環境

以下の環境で実施してください。

  • Node: v20 以降

利用方法

ではさっそく動かしてみましょう。

まずはGitHub skyway-stt-client-jsをgit cloneします。

exampleフォルダに移動して、.env.example.envにrenameし、APP_IDとSECRETを記入してください。

依存パッケージをインストールします。

npm install

サーバー側アプリケーションを起動します。

npm run server

その後、別ターミナルでクライアント側アプリケーションを起動します。

npm run client

クライアント側アプリケーションを起動すると、以下のような画面が立ち上がります。

stt-example

任意のroom名とmember名を入力して、Joinボタンを押すと、roomが作成されて入室できます。

その後、Modeを選択しStartボタンを押します。

  • transcriptionモード: 入力音声の言語を自動で判定し、その言語で文字起こしします
  • translationモード: 入力音声の言語を日本語または英語で文字起こしを行い、もう一方の言語での翻訳も併記します

タブを2つ開き、同じroom名を入力してJoinボタンを押せば、両タブで会議に参加し、文字起こし結果の表示を確認することができます。

コード解説

全体像

サンプルコードはクライアントとサーバの2つのアプリケーションに分かれています。

それぞれの概要を説明します。

  • クライアント
    • JavaScript SDKとSTT-Clientを利用しています。
    • Room作成や文字起こし開始についてバックエンドサーバにリクエストし、STT-Client経由で文字起こし結果を受け取ります。
  • サーバ
    • 3つのendpointを持っています
      • POST /rooms/:roomName/create
        • リクエストされたroomNameのRoomを作成し、SkyWayAuthTokenを返却する
      • POST /rooms/:roomName/start
        • リクエストされたroomNameのRoomについて、文字起こしを開始する
      • POST /rooms/:roomName/end
        • リクエストされたroomNameのRoomについて、文字起こしを終了する
        • (以降のコード解説は割愛します)
    • sttApiBaseUrlには利用開始案内メールに記載してある接続先情報をご利用ください

クライアントのコード解説

チャンネル作成

const response = await fetch( `${SERVER_HOST}/rooms/${roomName}/create`, { method: "POST", }, ); const { token } = await response.json(); const context = await SkyWayContext.Create(token); const room = await SkyWayRoom.Find( context, { name: roomName, }, );

チャンネルを作成するサーバAPIへリクエストします。レスポンスにSkyWayAuthTokenが入っているので、これをもとにcontextを作成してroomをfindします。なお、文字起こしする音声をpublishするときのtypeは必ず"sfu"を選択する必要があります。

接続とリッスン

const me = await room.join({ name: memberName }); const sttClient = new SkyWaySTTClient(context, me); sttClient.onSTTResultReceived.add(({ result }) => { const member = room.members.find((m) => m.id === result.memberId); const mode = sttMode.value; const messageElement = createSTTMessage(result, member, mode); sttResults.appendChild(messageElement); sttResults.scrollTop = sttResults.scrollHeight; });

SkyWayContextと、joinの返却値であるMemberを引数にとり、new SkyWaySTTClientでインスタンスを生成します。この時、内部的にSTTサーバへ接続します。

onSTTResultReceivedで、コールバック関数をセットすると、引数として文字起こし結果を得ることができます。サンプルコードではこれをHTMLで表示するようにしています。

文字起こし開始

startSttButton.onclick = async () => { const result = await fetch( `${SERVER_HOST}/rooms/${roomName}/start`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ sttMode: sttMode.value, }), }, ); if (result.status === 200) { sttStatus.textContent = "ON"; console.log("STT started"); } else { console.error("Failed to Start STT"); } };

startエンドポイントにたいしてリクエストを行い、文字起こしを開始します。

サーバのコード解説

トークン生成

// クライアント用のトークン const createSkywayAuthToken = (roomId) => { const token = new SkyWayAuthToken({ jti: uuidV4(), iat: nowInSec(), exp: nowInSec() + 60 * 60 * 24, version: 3, scope: { appId, rooms: [ { id: roomId, methods: ["create", "close", "updateMetadata"], member: { id: "*", // subscribeを許可する methods: ["publish", "subscribe", "updateMetadata"], }, // sttをenabledまたは省略(デフォルトでenabled)する stt: { enabled: true, }, // sfuをenabledまたは省略(デフォルトでenabled)する sfu: { enabled: true, }, }, ], }, }).encode(secret); return token; };

コメントに記載の通り、文字起こし結果の取得を行うためには以下の設定にする必要があります。

  • subscribe methodを追加
  • stt.enabledをtrue, またはsttを省略
  • sfu.enabledをtrue, またはsfuを省略

createエンドポイント

app.post("/rooms/:roomName/create", async (req, res) => { const { roomName } = req.params; console.log("create", { roomName }); // リクエストされたroomNameのRoom(Channel)を作成する const response = await fetch(channelApiUrl, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${createSkyWayAdminAuthToken()}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: uuidV4(), method: "findOrCreateChannel", params: { name: roomName, }, }), }); const { result: { channel: { id: roomId }, }, } = await response.json(); roomNameIdMap[roomName] = roomId; // 入室できるroomIdを制限したトークンを作成する const token = createSkywayAuthToken(roomId); // 今後の文字起こしの開始、終了操作を認証するためにtokenとroomIdの紐付けを行う tokenHashRoomIdMap[sha256(token)] = roomId; res.send({ token }); });

startエンドポイント

app.post("/rooms/:roomName/start", async (req, res) => { const { roomName } = req.params; const { authorization } = req.headers; const roomId = roomNameIdMap[roomName]; console.log("start", { roomName, roomId, body: req.body }); if (roomNameRecordingMap[roomName]) { res.status(200).send({ message: "STT already started" }); return; } // roomIdとtokenの紐付けを確認する const tokenStr = (authorization ?? "").replace(/^Bearer\s*/, ""); if (roomId !== tokenHashRoomIdMap[sha256(tokenStr)]) { res.status(403).send({ message: "Forbidden" }); return; } // 文字起こしを開始する const { sttMode } = req.body; const response = await fetch(`${sttApiBaseUrl}/rooms/${roomId}/sessions`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${createSkyWayAdminAuthToken()}`, }, body: JSON.stringify({ mode: sttMode.toUpperCase(), }), }); const json = await response.json(); if (!response.ok) { console.error(json.error); res.status(500).send({ message: json.error.message }); return; } roomNameRecordingMap[roomName] = json.id; res.status(200).send({ id: json.id }); });

STTサーバのsessionsエンドポイントへリクエストを行い、対象Roomの文字起こしを開始します。modeにクライアントで選択されたTRANSCRIPTIONTRANSLATIONのいずれかを設定しています。